Skip to content

Commit d7e2476

Browse files
authored
Merge pull request #45 from wrfly/develop
feature: collaborate tty
2 parents c4bae92 + f150298 commit d7e2476

File tree

11 files changed

+379
-331
lines changed

11 files changed

+379
-331
lines changed

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type ServerConfig struct {
5252
Term string `default:"xterm"`
5353
ShowLocation bool
5454
EnableShare bool
55+
Collaborate bool
5556

5657
// audit
5758
EnableAudit bool

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
github.com/spf13/pflag v1.0.3 // indirect
3232
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect
3333
github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0
34+
github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4
3435
github.com/yudai/gotty v2.0.0-alpha.3+incompatible
3536
golang.org/x/net v0.0.0-20190326090315-15845e8f865b
3637
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04
7474
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
7575
github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0 h1:Zy3Chk4CvwAqJ6YgQym6hJQEk4JE7iPbHRfxkru6Zn0=
7676
github.com/wrfly/ecp v0.1.1-0.20190725160759-97269b9e95f0/go.mod h1:cmmFTD+MLlrDa3/EO3gjeKLKUhHiYP3cgaaJamIS1NU=
77+
github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4 h1:G8zs08Ln8gek2TpeM1dgYFlUepEK6JNCM966Sf1kT+8=
78+
github.com/wrfly/pubsub v0.0.0-20200307185349-b35c047681a4/go.mod h1:WFtPVb6GumrLEVcAHZPSbjZmvWgenDSi4ceFW7k1r30=
7779
github.com/yudai/gotty v2.0.0-alpha.3+incompatible h1:eUFSuV4B2g+Rj+PS3HxhvOGEu2klWRzsl/7z7T/NUJQ=
7880
github.com/yudai/gotty v2.0.0-alpha.3+incompatible/go.mod h1:QBg0hL6VTVdqQk0qoBYk631EHLRH+XtR4wtbVi64UJ4=
7981
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ func main() {
135135
Usage: "enable share the container's terminal",
136136
Destination: &conf.Server.EnableShare,
137137
},
138+
&cli.BoolFlag{
139+
Name: "enable-collaborate",
140+
Aliases: []string{"collaborate"},
141+
EnvVars: util.EnvVars("collaborate"),
142+
Usage: "shared terminal can write to the same TTY",
143+
Destination: &conf.Server.Collaborate,
144+
},
138145
&cli.BoolFlag{
139146
Name: "enable-audit",
140147
Aliases: []string{"audit"},

route/exec.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package route
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"time"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/gorilla/websocket"
12+
log "github.com/sirupsen/logrus"
13+
"github.com/wrfly/container-web-tty/audit"
14+
"github.com/wrfly/container-web-tty/types"
15+
"github.com/yudai/gotty/webtty"
16+
)
17+
18+
func (server *Server) handleExec(c *gin.Context, counter *counter) {
19+
cInfo := server.containerCli.GetInfo(c.Request.Context(), c.Param("id"))
20+
server.generateHandleWS(c.Request.Context(), counter, cInfo).
21+
ServeHTTP(c.Writer, c.Request)
22+
}
23+
24+
func (server *Server) generateHandleWS(ctx context.Context, counter *counter, container types.Container) http.HandlerFunc {
25+
return func(w http.ResponseWriter, r *http.Request) {
26+
if container.Shell == "" {
27+
log.Errorf("cannot find a valid shell in container [%s]", container.ID)
28+
return
29+
}
30+
31+
num := counter.add(1)
32+
closeReason := "unknown reason"
33+
34+
defer func() {
35+
num := counter.done()
36+
if strings.Contains(closeReason, "error") {
37+
log.Errorf("Connection closed by %s: %s, connections: %d",
38+
closeReason, r.RemoteAddr, num)
39+
}
40+
log.Infof("Connection closed by %s: %s, connections: %d",
41+
closeReason, r.RemoteAddr, num)
42+
}()
43+
44+
if int64(server.options.MaxConnection) != 0 {
45+
if num > server.options.MaxConnection {
46+
closeReason = "exceeding max number of connections"
47+
return
48+
}
49+
}
50+
51+
log.Infof("New client connected: %s, connections: %d", r.RemoteAddr, num)
52+
53+
conn, err := server.upgrader.Upgrade(w, r, nil)
54+
if err != nil {
55+
closeReason = err.Error()
56+
return
57+
}
58+
defer conn.Close()
59+
60+
cctx, timeoutCancel := context.WithCancel(ctx)
61+
defer timeoutCancel()
62+
63+
err = server.processTTY(cctx, timeoutCancel, conn, container)
64+
switch err {
65+
case ctx.Err():
66+
closeReason = "cancelation"
67+
case cctx.Err():
68+
closeReason = "time out"
69+
case webtty.ErrSlaveClosed:
70+
closeReason = "backend closed"
71+
case webtty.ErrMasterClosed:
72+
closeReason = "tab closed"
73+
default:
74+
closeReason = fmt.Sprintf("an error: %s", err)
75+
}
76+
}
77+
}
78+
79+
func (server *Server) processTTY(ctx context.Context, timeoutCancel context.CancelFunc,
80+
conn *websocket.Conn, container types.Container) error {
81+
arguments, err := server.readInitMessage(conn)
82+
if err != nil {
83+
return err
84+
}
85+
log.Debugf("exec container: %s, params: %s", container.ID, arguments)
86+
87+
q, err := parseQuery(strings.TrimSpace(arguments))
88+
if err != nil {
89+
return err
90+
}
91+
container.Exec = types.ExecOptions{
92+
Cmd: q.Get("cmd"),
93+
Env: q.Get("env"),
94+
User: q.Get("user"),
95+
Privileged: q.Get("p") != "",
96+
}
97+
98+
containerTTY, err := server.containerCli.Exec(ctx, container)
99+
if err != nil {
100+
return fmt.Errorf("exec container error: %s", err)
101+
}
102+
defer containerTTY.Exit()
103+
104+
// handle timeout
105+
tout := server.options.IdleTime
106+
if tout.Seconds() != 0 {
107+
go func() {
108+
timer := time.NewTimer(tout)
109+
activeChan := containerTTY.ActiveChan()
110+
for {
111+
select {
112+
case <-timer.C:
113+
timer.Stop()
114+
timeoutCancel()
115+
return
116+
case <-activeChan:
117+
// the connection is active, reset the timer
118+
timer.Reset(tout)
119+
}
120+
}
121+
122+
}()
123+
}
124+
125+
titleBuf, err := server.makeTitleBuff(container)
126+
if err != nil {
127+
return fmt.Errorf("failed to fill window title template: %s", err)
128+
}
129+
130+
opts := []webtty.Option{
131+
webtty.WithWindowTitle(titleBuf),
132+
webtty.WithPermitWrite(),
133+
// webtty.WithReconnect(10), // not work....
134+
}
135+
136+
wrapper := &wsWrapper{conn}
137+
masterTTY, err := types.NewMasterTTY(ctx, containerTTY, container.ID)
138+
if err != nil {
139+
return err
140+
}
141+
server.mMux.Lock()
142+
if _, ok := server.masters[container.ID]; !ok {
143+
server.masters[container.ID] = masterTTY
144+
}
145+
server.mMux.Unlock()
146+
defer func() {
147+
server.mMux.Lock()
148+
delete(server.masters, container.ID)
149+
server.mMux.Unlock()
150+
}()
151+
152+
if server.options.EnableAudit {
153+
go audit.LogTo(ctx, masterTTY.Fork(ctx, false), audit.LogOpts{
154+
Dir: server.options.AuditLogDir,
155+
ContainerID: container.ID,
156+
ClientIP: conn.RemoteAddr().String(),
157+
})
158+
}
159+
160+
log.Infof("new web tty for container: %s", container.ID)
161+
tty, err := webtty.New(wrapper, masterTTY, opts...)
162+
if err != nil {
163+
return fmt.Errorf("failed to create webtty: %s", err)
164+
}
165+
166+
return tty.Run(ctx)
167+
}

0 commit comments

Comments
 (0)