Skip to content

Commit dcd239e

Browse files
authored
feat(ipc): support exec command in restful api (#52)
Signed-off-by: Kevin Cui <[email protected]>
1 parent 3314b94 commit dcd239e

36 files changed

+1659
-444
lines changed

go.mod

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/pkg/errors v0.9.1
1212
github.com/prashantgupta24/mac-sleep-notifier v1.0.1
1313
github.com/shirou/gopsutil/v3 v3.23.12
14+
golang.org/x/crypto v0.18.0
1415
golang.org/x/net v0.20.0
1516
golang.org/x/sync v0.5.0
1617
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
@@ -20,7 +21,7 @@ require (
2021
github.com/Microsoft/go-winio v0.6.1 // indirect
2122
github.com/apparentlymart/go-cidr v1.1.0 // indirect
2223
github.com/go-ole/go-ole v1.3.0 // indirect
23-
github.com/google/btree v1.0.1 // indirect
24+
github.com/google/btree v1.1.2 // indirect
2425
github.com/google/gopacket v1.1.19 // indirect
2526
github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect
2627
github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect
@@ -35,10 +36,11 @@ require (
3536
github.com/tklauser/numcpus v0.7.0 // indirect
3637
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
3738
github.com/yusufpapurcu/wmi v1.2.3 // indirect
38-
golang.org/x/crypto v0.18.0 // indirect
3939
golang.org/x/mod v0.13.0 // indirect
4040
golang.org/x/sys v0.16.0 // indirect
4141
golang.org/x/time v0.5.0 // indirect
42-
golang.org/x/tools v0.13.0 // indirect
42+
golang.org/x/tools v0.14.0 // indirect
4343
gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db // indirect
4444
)
45+
46+
replace gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db => gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf

go.sum

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
2020
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
2121
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
2222
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
23-
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
24-
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
23+
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
24+
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
2525
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
2626
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
2727
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -166,8 +166,8 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
166166
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
167167
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
168168
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
169-
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
170-
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
169+
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
170+
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
171171
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
172172
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
173173
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -176,7 +176,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
176176
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
177177
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
178178
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
179-
gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db h1:WZSmkyu/hep9YhWIlBZefwGVBrnGE5yW8JPD56YRsXs=
180-
gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g=
179+
gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf h1:0A28IFBR6VcMacM0m6Rn5/nr8pk8xa2TyIkjSaFAOPc=
180+
gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8=
181181
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfzkgyBrHzx2nW28Yg6nc=
182182
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=

pkg/cli/setup.go

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515

1616
"github.com/oomol-lab/ovm/pkg/utils"
17+
"golang.org/x/crypto/ssh"
1718
"golang.org/x/sync/errgroup"
1819
)
1920

@@ -38,6 +39,7 @@ type Context struct {
3839
SSHPublicKeyPath string
3940
SSHPrivateKey string
4041
SSHPublicKey string
42+
SSHSigner ssh.Signer
4143

4244
ForwardSocketPath string
4345
SocketNetworkPath string
@@ -224,6 +226,9 @@ func (c *Context) ssh() error {
224226
}
225227

226228
c.SSHPrivateKey = strings.TrimSpace(string(b))
229+
if c.SSHSigner, err = ssh.ParsePrivateKey(b); err != nil {
230+
return fmt.Errorf("parse private key error: %w", err)
231+
}
227232
}
228233

229234
return nil

pkg/ipc/restful/restful.go

+156
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ import (
77
"context"
88
"encoding/json"
99
"fmt"
10+
"io"
1011
"net"
1112
"net/http"
13+
"strings"
14+
"sync"
15+
"time"
1216

17+
"github.com/Code-Hex/go-infinity-channel"
1318
"github.com/Code-Hex/vz/v3"
1419
"github.com/crc-org/vfkit/pkg/config"
1520
"github.com/oomol-lab/ovm/pkg/cli"
1621
"github.com/oomol-lab/ovm/pkg/logger"
22+
"golang.org/x/crypto/ssh"
1723
"golang.org/x/sync/errgroup"
1824
)
1925

@@ -56,6 +62,10 @@ type powerSaveModeBody struct {
5662
Enable bool `json:"enable"`
5763
}
5864

65+
type execBody struct {
66+
Command string `json:"command"`
67+
}
68+
5969
func (s *Restful) mux() *http.ServeMux {
6070
mux := http.NewServeMux()
6171
mux.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {
@@ -129,6 +139,71 @@ func (s *Restful) mux() *http.ServeMux {
129139

130140
s.powerSaveMode(body.Enable)
131141
})
142+
mux.HandleFunc("/exec", func(w http.ResponseWriter, r *http.Request) {
143+
if r.Method != http.MethodPost {
144+
http.Error(w, "post only", http.StatusBadRequest)
145+
return
146+
}
147+
148+
var body execBody
149+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
150+
s.log.Warnf("Failed to decode request body: %v", err)
151+
http.Error(w, "failed to decode request body", http.StatusBadRequest)
152+
return
153+
}
154+
155+
w.Header().Set("Content-Type", "text/event-stream")
156+
w.Header().Set("Cache-Control", "no-cache")
157+
w.Header().Set("Connection", "keep-alive")
158+
159+
if _, ok := w.(http.Flusher); !ok {
160+
s.log.Warnf("Bowser does not support server-sent events")
161+
return
162+
}
163+
164+
outCh := infinity.NewChannel[string]()
165+
errCh := make(chan string)
166+
doneCh := make(chan struct{})
167+
168+
go func() {
169+
if err := s.exec(body.Command, outCh, errCh); err != nil {
170+
s.log.Warnf("Failed to execute command: %v", err)
171+
}
172+
173+
_, _ = fmt.Fprintf(w, "event: done\n")
174+
_, _ = fmt.Fprintf(w, "data: done\n\n")
175+
w.(http.Flusher).Flush()
176+
177+
doneCh <- struct{}{}
178+
outCh.Close()
179+
close(errCh)
180+
}()
181+
182+
for {
183+
select {
184+
case <-doneCh:
185+
s.log.Warnf("Command execution finished")
186+
return
187+
case err := <-errCh:
188+
_, _ = fmt.Fprintf(w, "event: error\n")
189+
_, _ = fmt.Fprintf(w, "data: %s\n\n", encodeSSE(err))
190+
w.(http.Flusher).Flush()
191+
continue
192+
case out := <-outCh.Out():
193+
_, _ = fmt.Fprintf(w, "event: out\n")
194+
_, _ = fmt.Fprintf(w, "data: %s\n\n", encodeSSE(out))
195+
w.(http.Flusher).Flush()
196+
continue
197+
case <-r.Context().Done():
198+
s.log.Warnf("Client closed connection")
199+
return
200+
case <-time.After(3 * time.Second):
201+
_, _ = fmt.Fprintf(w, ": ping\n\n")
202+
w.(http.Flusher).Flush()
203+
continue
204+
}
205+
}
206+
})
132207

133208
return mux
134209
}
@@ -219,3 +294,84 @@ func (s *Restful) powerSaveMode(enable bool) {
219294
s.log.Info("request /powerSaveMode")
220295
s.opt.PowerSaveMode = enable
221296
}
297+
298+
func (s *Restful) exec(command string, outCh *infinity.Channel[string], errCh chan string) error {
299+
s.log.Info("request /exec")
300+
301+
conf := &ssh.ClientConfig{
302+
User: "root",
303+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
304+
Auth: []ssh.AuthMethod{
305+
ssh.PublicKeys(s.opt.SSHSigner),
306+
},
307+
}
308+
309+
conn, err := ssh.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.opt.SSHPort), conf)
310+
if err != nil {
311+
errCh <- fmt.Sprintf("dial ssh error: %v", err)
312+
return fmt.Errorf("dial ssh error: %w", err)
313+
}
314+
defer conn.Close()
315+
316+
session, err := conn.NewSession()
317+
if err != nil {
318+
errCh <- fmt.Sprintf("new ssh session error: %v", err)
319+
return fmt.Errorf("new ssh session error: %w", err)
320+
}
321+
defer session.Close()
322+
323+
w := ch2Writer(outCh)
324+
session.Stdout = w
325+
stderr := recordWriter(w)
326+
session.Stderr = stderr
327+
328+
if err := session.Run(command); err != nil {
329+
newErr := fmt.Errorf("%s\n%s", stderr.LastRecord(), err)
330+
errCh <- fmt.Sprintf(newErr.Error())
331+
return fmt.Errorf("run ssh command error: %w", newErr)
332+
}
333+
334+
return nil
335+
}
336+
337+
type chWriter struct {
338+
ch *infinity.Channel[string]
339+
mu sync.Mutex
340+
}
341+
342+
func (w *chWriter) Write(p []byte) (n int, err error) {
343+
w.mu.Lock()
344+
defer w.mu.Unlock()
345+
w.ch.In() <- string(p)
346+
return len(p), nil
347+
}
348+
349+
func ch2Writer(ch *infinity.Channel[string]) io.Writer {
350+
return &chWriter{
351+
ch: ch,
352+
}
353+
}
354+
355+
type writer struct {
356+
w io.Writer
357+
last []byte
358+
}
359+
360+
func (w *writer) Write(p []byte) (n int, err error) {
361+
w.last = p
362+
return w.w.Write(p)
363+
}
364+
365+
func (w *writer) LastRecord() string {
366+
return string(w.last)
367+
}
368+
369+
func recordWriter(w io.Writer) *writer {
370+
return &writer{
371+
w: w,
372+
}
373+
}
374+
375+
func encodeSSE(str string) string {
376+
return strings.ReplaceAll(strings.TrimSpace(str), "\n", "\ndata: ")
377+
}

vendor/github.com/google/btree/.travis.yml

-1
This file was deleted.

vendor/github.com/google/btree/README.md

-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/google/btree/btree.go

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)