added pty terminal support to the client side
This commit is contained in:
parent
01eb2edd6e
commit
6baf3b2b53
4
Makefile
4
Makefile
@ -15,6 +15,7 @@ SRCS=\
|
|||||||
client-ctl.go \
|
client-ctl.go \
|
||||||
client-metrics.go \
|
client-metrics.go \
|
||||||
client-peer.go \
|
client-peer.go \
|
||||||
|
client-pts.go \
|
||||||
hodu.go \
|
hodu.go \
|
||||||
hodu.pb.go \
|
hodu.pb.go \
|
||||||
hodu_grpc.pb.go \
|
hodu_grpc.pb.go \
|
||||||
@ -32,7 +33,8 @@ DATA = \
|
|||||||
xterm.css \
|
xterm.css \
|
||||||
xterm.js \
|
xterm.js \
|
||||||
xterm-addon-fit.js \
|
xterm-addon-fit.js \
|
||||||
xterm.html
|
xterm.html \
|
||||||
|
xterm-pts.html
|
||||||
|
|
||||||
CMD_DATA=\
|
CMD_DATA=\
|
||||||
cmd/rsa.key \
|
cmd/rsa.key \
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
## normal operation
|
## normal operation
|
||||||
- ./hodu server --rpc-on=0.0.0.0:9999 --ctl-on=0.0.0.0:8888 --pxy-on=0.0.0.0:9998 --wpx-on=0.0.0.0:9997
|
- ./hodu server --rpc-on=0.0.0.0:9999 --ctl-on=0.0.0.0:8888 --pxy-on=0.0.0.0:9998 --wpx-on=0.0.0.0:9997
|
||||||
- ./hodu client --rpc-to=127.0.0.1:9999 --ctl-on=127.0.0.1:7777 "127.0.0.2:22,0.0.0.0:12345,ssh,Access SSH Server"
|
- ./hodu client --rpc-to=127.0.0.1:9999 --ctl-on=127.0.0.1:1107 "127.0.0.2:22,0.0.0.0:12345,ssh,Access SSH Server"
|
||||||
|
|
||||||
## server.json
|
## server.json
|
||||||
```
|
```
|
||||||
@ -37,5 +37,5 @@
|
|||||||
|
|
||||||
Run this command:
|
Run this command:
|
||||||
```
|
```
|
||||||
curl -X POST --data-binary @client-route.json http://127.0.0.1:7777/_ctl/client-conns/1/routes
|
curl -X POST --data-binary @client-route.json http://127.0.0.1:1107/_ctl/client-conns/1/routes
|
||||||
```
|
```
|
||||||
|
@ -109,6 +109,7 @@ type json_out_client_stats struct {
|
|||||||
ClientConns int64 `json:"client-conns"`
|
ClientConns int64 `json:"client-conns"`
|
||||||
ClientRoutes int64 `json:"client-routes"`
|
ClientRoutes int64 `json:"client-routes"`
|
||||||
ClientPeers int64 `json:"client-peers"`
|
ClientPeers int64 `json:"client-peers"`
|
||||||
|
ClientPtsSessions int64 `json:"client-pts-sessions"`
|
||||||
}
|
}
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
@ -1136,6 +1137,7 @@ func (ctl *client_ctl_stats) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
stats.ClientConns = c.stats.conns.Load()
|
stats.ClientConns = c.stats.conns.Load()
|
||||||
stats.ClientRoutes = c.stats.routes.Load()
|
stats.ClientRoutes = c.stats.routes.Load()
|
||||||
stats.ClientPeers = c.stats.peers.Load()
|
stats.ClientPeers = c.stats.peers.Load()
|
||||||
|
stats.ClientPtsSessions = c.stats.pts_sessions.Load()
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
||||||
if err = je.Encode(stats); err != nil { goto oops }
|
if err = je.Encode(stats); err != nil { goto oops }
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ type ClientCollector struct {
|
|||||||
ClientConns *prometheus.Desc
|
ClientConns *prometheus.Desc
|
||||||
ClientRoutes *prometheus.Desc
|
ClientRoutes *prometheus.Desc
|
||||||
ClientPeers *prometheus.Desc
|
ClientPeers *prometheus.Desc
|
||||||
|
PtsSessions *prometheus.Desc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientCollector returns a new ClientCollector with all prometheus.Desc initialized
|
// NewClientCollector returns a new ClientCollector with all prometheus.Desc initialized
|
||||||
@ -46,6 +47,11 @@ func NewClientCollector(client *Client) ClientCollector {
|
|||||||
"Number of client-side peers",
|
"Number of client-side peers",
|
||||||
nil, nil,
|
nil, nil,
|
||||||
),
|
),
|
||||||
|
PtsSessions: prometheus.NewDesc(
|
||||||
|
prefix + "pts_sessions",
|
||||||
|
"Number of pts sessions",
|
||||||
|
nil, nil,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,4 +90,10 @@ func (c ClientCollector) Collect(ch chan<- prometheus.Metric) {
|
|||||||
prometheus.GaugeValue,
|
prometheus.GaugeValue,
|
||||||
float64(c.client.stats.peers.Load()),
|
float64(c.client.stats.peers.Load()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.PtsSessions,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(c.client.stats.pts_sessions.Load()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
316
client-pts.go
Normal file
316
client-pts.go
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
package hodu
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
import "errors"
|
||||||
|
import "fmt"
|
||||||
|
import "io"
|
||||||
|
import "net/http"
|
||||||
|
import "os"
|
||||||
|
import "os/exec"
|
||||||
|
import "os/user"
|
||||||
|
import "strconv"
|
||||||
|
import "sync"
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
import "github.com/creack/pty"
|
||||||
|
import "golang.org/x/net/websocket"
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
type client_pts_ws struct {
|
||||||
|
C *Client
|
||||||
|
Id string
|
||||||
|
ws *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type client_pts_xterm_file struct {
|
||||||
|
client_ctl
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
type json_ssh_ws_event struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
func (pts *client_pts_ws) Identity() string {
|
||||||
|
return pts.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pts *client_pts_ws) send_ws_data(ws *websocket.Conn, type_val string, data string) error {
|
||||||
|
var msg []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
msg, err = json.Marshal(json_ssh_ws_event{Type: type_val, Data: []string{ data } })
|
||||||
|
if err == nil { err = websocket.Message.Send(ws, msg) }
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (pts *client_pts_ws) connect_pts(username string, password string) (*exec.Cmd, *os.File, error) {
|
||||||
|
var c *Client
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
var tty *os.File
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// username and password are not used yet.
|
||||||
|
c = pts.C
|
||||||
|
|
||||||
|
if c.pts_shell == "" {
|
||||||
|
return nil, nil, fmt.Errorf("blank pts shell")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command(c.pts_shell);
|
||||||
|
if c.pts_user != "" {
|
||||||
|
var uid int
|
||||||
|
var gid int
|
||||||
|
var u *user.User
|
||||||
|
|
||||||
|
u, err = user.Lookup(c.pts_user)
|
||||||
|
if err != nil { return nil, nil, err }
|
||||||
|
|
||||||
|
uid, _ = strconv.Atoi(u.Uid)
|
||||||
|
gid, _ = strconv.Atoi(u.Gid)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Credential: &syscall.Credential{
|
||||||
|
Uid: uint32(uid),
|
||||||
|
Gid: uint32(gid),
|
||||||
|
},
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
cmd.Dir = u.HomeDir
|
||||||
|
cmd.Env = append(cmd.Env,
|
||||||
|
"HOME=" + u.HomeDir,
|
||||||
|
"LOGNAME=" + u.Username,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"SHELL=" + c.pts_shell,
|
||||||
|
"TERM=xterm",
|
||||||
|
"USER=" + u.Username,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tty, err = pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//syscall.SetNonblock(int(tty.Fd()), true);
|
||||||
|
unix.SetNonblock(int(tty.Fd()), true);
|
||||||
|
|
||||||
|
return cmd, tty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pts *client_pts_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
||||||
|
var c *Client
|
||||||
|
var req *http.Request
|
||||||
|
var username string
|
||||||
|
var password string
|
||||||
|
var in *os.File
|
||||||
|
var out *os.File
|
||||||
|
var tty *os.File
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var conn_ready_chan chan bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
c = pts.C
|
||||||
|
req = ws.Request()
|
||||||
|
conn_ready_chan = make(chan bool, 3)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
var conn_ready bool
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
defer ws.Close() // dirty way to break the main loop
|
||||||
|
|
||||||
|
conn_ready = <-conn_ready_chan
|
||||||
|
if conn_ready { // connected
|
||||||
|
var poll_fds []unix.PollFd;
|
||||||
|
var buf []byte
|
||||||
|
var n int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
|
||||||
|
poll_fds = []unix.PollFd{
|
||||||
|
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.stats.pts_sessions.Add(1)
|
||||||
|
buf = make([]byte, 2048)
|
||||||
|
for {
|
||||||
|
n, err = unix.Poll(poll_fds, -1) // -1 means wait indefinitely
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, unix.EINTR) { continue }
|
||||||
|
c.log.Write("", LOG_ERROR, "[%s] Failed to poll pts stdout - %s", req.RemoteAddr, err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n == 0 { // timed out
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll_fds[0].Revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
|
||||||
|
c.log.Write(pts.Id, LOG_DEBUG, "[%s] EOF detected on pts stdout", req.RemoteAddr)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll_fds[0].Revents & unix.POLLIN) != 0 {
|
||||||
|
n, err = out.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
c.log.Write(pts.Id, LOG_ERROR, "[%s] Failed to read pts stdout - %s", req.RemoteAddr, err.Error())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
err = pts.send_ws_data(ws, "iov", string(buf[:n]))
|
||||||
|
if err != nil {
|
||||||
|
c.log.Write(pts.Id, LOG_ERROR, "[%s] Failed to send to websocket - %s", req.RemoteAddr, err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.stats.pts_sessions.Add(-1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ws_recv_loop:
|
||||||
|
for {
|
||||||
|
var msg []byte
|
||||||
|
err = websocket.Message.Receive(ws, &msg)
|
||||||
|
if err != nil { goto done }
|
||||||
|
|
||||||
|
if len(msg) > 0 {
|
||||||
|
var ev json_ssh_ws_event
|
||||||
|
err = json.Unmarshal(msg, &ev)
|
||||||
|
if err == nil {
|
||||||
|
switch ev.Type {
|
||||||
|
case "open":
|
||||||
|
if tty == nil && len(ev.Data) == 2 {
|
||||||
|
username = string(ev.Data[0])
|
||||||
|
password = string(ev.Data[1])
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
cmd, tty, err = pts.connect_pts(username, password)
|
||||||
|
if err != nil {
|
||||||
|
c.log.Write(pts.Id, LOG_ERROR, "[%s] Failed to connect pts - %s", req.RemoteAddr, err.Error())
|
||||||
|
pts.send_ws_data(ws, "error", err.Error())
|
||||||
|
ws.Close() // dirty way to flag out the error
|
||||||
|
} else {
|
||||||
|
err = pts.send_ws_data(ws, "status", "opened")
|
||||||
|
if err != nil {
|
||||||
|
c.log.Write(pts.Id, LOG_ERROR, "[%s] Failed to write opened event to websocket - %s", req.RemoteAddr, err.Error())
|
||||||
|
ws.Close() // dirty way to flag out the error
|
||||||
|
} else {
|
||||||
|
c.log.Write(pts.Id, LOG_DEBUG, "[%s] Opened pts session", req.RemoteAddr)
|
||||||
|
out = tty
|
||||||
|
in = tty
|
||||||
|
conn_ready_chan <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "close":
|
||||||
|
if tty != nil {
|
||||||
|
tty.Close()
|
||||||
|
tty = nil
|
||||||
|
}
|
||||||
|
break ws_recv_loop
|
||||||
|
|
||||||
|
case "iov":
|
||||||
|
if tty != nil {
|
||||||
|
var i int
|
||||||
|
for i, _ = range ev.Data {
|
||||||
|
in.Write([]byte(ev.Data[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "size":
|
||||||
|
if tty != nil && len(ev.Data) == 2 {
|
||||||
|
var rows int
|
||||||
|
var cols int
|
||||||
|
rows, _ = strconv.Atoi(ev.Data[0])
|
||||||
|
cols, _ = strconv.Atoi(ev.Data[1])
|
||||||
|
pty.Setsize(tty, &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
|
||||||
|
c.log.Write(pts.Id, LOG_DEBUG, "[%s] Resized terminal to %d,%d", req.RemoteAddr, rows, cols)
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tty != nil {
|
||||||
|
err = pts.send_ws_data(ws, "status", "closed")
|
||||||
|
if err != nil { goto done }
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
conn_ready_chan <- false
|
||||||
|
ws.Close()
|
||||||
|
if cmd != nil {
|
||||||
|
// kill the child process underneath to close ptym(the master pty).
|
||||||
|
//cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
if tty != nil { tty.Close() }
|
||||||
|
if cmd != nil { cmd.Wait() }
|
||||||
|
wg.Wait()
|
||||||
|
c.log.Write(pts.Id, LOG_DEBUG, "[%s] Ended pts session", req.RemoteAddr)
|
||||||
|
|
||||||
|
return http.StatusOK, err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
func (pts *client_pts_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||||
|
var c *Client
|
||||||
|
var status_code int
|
||||||
|
// var err error
|
||||||
|
|
||||||
|
c = pts.c
|
||||||
|
|
||||||
|
switch pts.file {
|
||||||
|
case "xterm.js":
|
||||||
|
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||||
|
w.Write(xterm_js)
|
||||||
|
case "xterm-addon-fit.js":
|
||||||
|
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||||
|
w.Write(xterm_addon_fit_js)
|
||||||
|
case "xterm.css":
|
||||||
|
status_code = WriteCssRespHeader(w, http.StatusOK)
|
||||||
|
w.Write(xterm_css)
|
||||||
|
case "xterm-pts.html":
|
||||||
|
status_code = WriteHtmlRespHeader(w, http.StatusOK)
|
||||||
|
if c.xterm_pts_html != "" {
|
||||||
|
w.Write([]byte(c.xterm_pts_html))
|
||||||
|
} else {
|
||||||
|
w.Write(xterm_pts_html)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "_forbidden":
|
||||||
|
status_code = WriteEmptyRespHeader(w, http.StatusForbidden)
|
||||||
|
|
||||||
|
case "_notfound":
|
||||||
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
|
|
||||||
|
default:
|
||||||
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
//done:
|
||||||
|
return status_code, nil
|
||||||
|
|
||||||
|
//oops:
|
||||||
|
// return status_code, err
|
||||||
|
}
|
54
client.go
54
client.go
@ -145,7 +145,12 @@ type Client struct {
|
|||||||
conns atomic.Int64
|
conns atomic.Int64
|
||||||
routes atomic.Int64
|
routes atomic.Int64
|
||||||
peers atomic.Int64
|
peers atomic.Int64
|
||||||
|
pts_sessions atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pts_user string
|
||||||
|
pts_shell string
|
||||||
|
xterm_pts_html string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientConnState = int32
|
type ClientConnState = int32
|
||||||
@ -1624,9 +1629,31 @@ func NewClient(ctx context.Context, name string, logger Logger, cfg *ClientConfi
|
|||||||
c.ctl_mux.Handle(c.ctl_prefix + "/_ctl/metrics",
|
c.ctl_mux.Handle(c.ctl_prefix + "/_ctl/metrics",
|
||||||
promhttp.HandlerFor(c.promreg, promhttp.HandlerOpts{ EnableOpenMetrics: true }))
|
promhttp.HandlerFor(c.promreg, promhttp.HandlerOpts{ EnableOpenMetrics: true }))
|
||||||
|
|
||||||
c.ctl_mux.Handle("/_ctl/events",
|
c.ctl_mux.Handle("/_ctl/events",
|
||||||
c.WrapWebsocketHandler(&client_ctl_ws{client_ctl{c: &c, id: HS_ID_CTL}}))
|
c.WrapWebsocketHandler(&client_ctl_ws{client_ctl{c: &c, id: HS_ID_CTL}}))
|
||||||
|
|
||||||
|
|
||||||
|
c.ctl_mux.Handle("/_pts/ws", c.WrapWebsocketHandler(&client_pts_ws{C: &c, Id: HS_ID_CTL}))
|
||||||
|
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm.js",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.js"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm.js.map",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_notfound"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm-addon-fit.js",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm-addon-fit.js"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm-addon-fit.js.map",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_notfound"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm.css",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.css"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/xterm-pts.html",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm-pts.html"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/favicon.ico",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
|
c.ctl_mux.Handle("/_pts/favicon.ico/",
|
||||||
|
c.WrapHttpHandler(&client_pts_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
|
|
||||||
c.ctl_addr = make([]string, len(cfg.CtlAddrs))
|
c.ctl_addr = make([]string, len(cfg.CtlAddrs))
|
||||||
c.ctl = make([]*http.Server, len(cfg.CtlAddrs))
|
c.ctl = make([]*http.Server, len(cfg.CtlAddrs))
|
||||||
copy(c.ctl_addr, cfg.CtlAddrs)
|
copy(c.ctl_addr, cfg.CtlAddrs)
|
||||||
@ -1646,6 +1673,7 @@ func NewClient(ctx context.Context, name string, logger Logger, cfg *ClientConfi
|
|||||||
c.stats.conns.Store(0)
|
c.stats.conns.Store(0)
|
||||||
c.stats.routes.Store(0)
|
c.stats.routes.Store(0)
|
||||||
c.stats.peers.Store(0)
|
c.stats.peers.Store(0)
|
||||||
|
c.stats.pts_sessions.Store(0)
|
||||||
|
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
@ -1934,6 +1962,30 @@ func (c *Client) ReqStop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetXtermPtsHtml(html string) {
|
||||||
|
c.xterm_pts_html = html
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetXtermPtsHtml() string {
|
||||||
|
return c.xterm_pts_html
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetPtsUser(user string) {
|
||||||
|
c.pts_user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPtsUser() string {
|
||||||
|
return c.pts_user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetPtsShell(user string) {
|
||||||
|
c.pts_shell = user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPtsShell() string {
|
||||||
|
return c.pts_shell
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) RunCtlTask(wg *sync.WaitGroup) {
|
func (c *Client) RunCtlTask(wg *sync.WaitGroup) {
|
||||||
var err error
|
var err error
|
||||||
var ctl *http.Server
|
var ctl *http.Server
|
||||||
|
@ -105,6 +105,9 @@ type ClientAppConfig struct {
|
|||||||
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
|
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
|
||||||
MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections
|
MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections
|
||||||
PeerConnTmout time.Duration `yaml:"peer-conn-timeout"`
|
PeerConnTmout time.Duration `yaml:"peer-conn-timeout"`
|
||||||
|
PtsUser string `yaml:"pts-user"`
|
||||||
|
PtsShell string `yaml:"pts-shell"`
|
||||||
|
XtermPtsHtmlFile string `yaml:"xterm-pts-html-file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
|
21
cmd/main.go
21
cmd/main.go
@ -252,6 +252,10 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
|
|||||||
var logmask hodu.LogMask
|
var logmask hodu.LogMask
|
||||||
var logfile_maxsize int64
|
var logfile_maxsize int64
|
||||||
var logfile_rotate int
|
var logfile_rotate int
|
||||||
|
var pts_user string
|
||||||
|
var pts_shell string
|
||||||
|
var xterm_pts_html_file string
|
||||||
|
var xterm_pts_html string
|
||||||
var i int
|
var i int
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -281,6 +285,9 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
|
|||||||
if logfile == "" { logfile = cfg.APP.LogFile }
|
if logfile == "" { logfile = cfg.APP.LogFile }
|
||||||
logfile_maxsize = cfg.APP.LogMaxSize
|
logfile_maxsize = cfg.APP.LogMaxSize
|
||||||
logfile_rotate = cfg.APP.LogRotate
|
logfile_rotate = cfg.APP.LogRotate
|
||||||
|
pts_user = cfg.APP.PtsUser
|
||||||
|
pts_shell = cfg.APP.PtsShell
|
||||||
|
xterm_pts_html_file = cfg.APP.XtermPtsHtmlFile
|
||||||
config.RpcConnMax = cfg.APP.MaxRpcConns
|
config.RpcConnMax = cfg.APP.MaxRpcConns
|
||||||
config.PeerConnMax = cfg.APP.MaxPeers
|
config.PeerConnMax = cfg.APP.MaxPeers
|
||||||
config.PeerConnTmout = cfg.APP.PeerConnTmout
|
config.PeerConnTmout = cfg.APP.PeerConnTmout
|
||||||
@ -305,8 +312,22 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
|
|||||||
return fmt.Errorf("failed to initialize logger - %s", err.Error())
|
return fmt.Errorf("failed to initialize logger - %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if xterm_pts_html_file != "" {
|
||||||
|
var tmp []byte
|
||||||
|
tmp, err = os.ReadFile(xterm_pts_html_file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s - %s", xterm_pts_html_file, err.Error())
|
||||||
|
}
|
||||||
|
xterm_pts_html = string(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
c = hodu.NewClient(context.Background(), HODU_NAME, logger, config)
|
c = hodu.NewClient(context.Background(), HODU_NAME, logger, config)
|
||||||
|
|
||||||
|
if pts_user != "" { c.SetPtsUser(pts_user) }
|
||||||
|
if pts_shell != "" { c.SetPtsShell(pts_shell) }
|
||||||
|
if xterm_pts_html != "" { c.SetXtermPtsHtml(xterm_pts_html) }
|
||||||
|
|
||||||
c.StartService(&cc)
|
c.StartService(&cc)
|
||||||
c.StartCtlService() // control channel
|
c.StartCtlService() // control channel
|
||||||
c.StartExtService(&signal_handler{svc:c}, nil) // signal handler task
|
c.StartExtService(&signal_handler{svc:c}, nil) // signal handler task
|
||||||
|
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module hodu
|
|||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/creack/pty v1.1.24
|
||||||
github.com/goccy/go-yaml v1.17.1
|
github.com/goccy/go-yaml v1.17.1
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.20.5
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
|
2
go.sum
2
go.sum
@ -2,6 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
17
hodu.go
17
hodu.go
@ -1,6 +1,7 @@
|
|||||||
package hodu
|
package hodu
|
||||||
|
|
||||||
import "crypto/rsa"
|
import "crypto/rsa"
|
||||||
|
import _ "embed"
|
||||||
import "encoding/base64"
|
import "encoding/base64"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "net"
|
import "net"
|
||||||
@ -116,6 +117,22 @@ type json_out_go_stats struct {
|
|||||||
OtherSysBytes uint64 `json:"memory-other-sys-bytes"`
|
OtherSysBytes uint64 `json:"memory-other-sys-bytes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
//go:embed xterm.js
|
||||||
|
var xterm_js []byte
|
||||||
|
//go:embed xterm-addon-fit.js
|
||||||
|
var xterm_addon_fit_js []byte
|
||||||
|
//go:embed xterm.css
|
||||||
|
var xterm_css []byte
|
||||||
|
//go:embed xterm.html
|
||||||
|
var xterm_html string
|
||||||
|
//go:embed xterm-pts.html
|
||||||
|
var xterm_pts_html []byte
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
func (n *Named) SetName(name string) {
|
func (n *Named) SetName(name string) {
|
||||||
n.name = name
|
n.name = name
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package hodu
|
|||||||
import "bufio"
|
import "bufio"
|
||||||
import "context"
|
import "context"
|
||||||
import "crypto/tls"
|
import "crypto/tls"
|
||||||
import _ "embed"
|
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
import "errors"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "io"
|
import "io"
|
||||||
import "net"
|
import "net"
|
||||||
@ -21,15 +21,6 @@ import "golang.org/x/crypto/ssh"
|
|||||||
import "golang.org/x/net/http/httpguts"
|
import "golang.org/x/net/http/httpguts"
|
||||||
import "golang.org/x/net/websocket"
|
import "golang.org/x/net/websocket"
|
||||||
|
|
||||||
//go:embed xterm.js
|
|
||||||
var xterm_js []byte
|
|
||||||
//go:embed xterm-addon-fit.js
|
|
||||||
var xterm_addon_fit_js []byte
|
|
||||||
//go:embed xterm.css
|
|
||||||
var xterm_css []byte
|
|
||||||
//go:embed xterm.html
|
|
||||||
var xterm_html string
|
|
||||||
|
|
||||||
type server_pxy struct {
|
type server_pxy struct {
|
||||||
S *Server
|
S *Server
|
||||||
Id string
|
Id string
|
||||||
@ -584,7 +575,7 @@ func (pxy *server_pxy_ssh_ws) send_ws_data(ws *websocket.Conn, type_val string,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *server_pxy_ssh_ws) connect_ssh (ctx context.Context, username string, password string, r *ServerRoute) ( *ssh.Client, *ssh.Session, io.Writer, io.Reader, error) {
|
func (pxy *server_pxy_ssh_ws) connect_ssh (ctx context.Context, username string, password string, r *ServerRoute) (*ssh.Client, *ssh.Session, io.Writer, io.Reader, error) {
|
||||||
var cc *ssh.ClientConfig
|
var cc *ssh.ClientConfig
|
||||||
var addr *net.TCPAddr
|
var addr *net.TCPAddr
|
||||||
var dialer *net.Dialer
|
var dialer *net.Dialer
|
||||||
@ -712,15 +703,15 @@ func (pxy *server_pxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
for {
|
for {
|
||||||
n, err = out.Read(buf)
|
n, err = out.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if !errors.Is(err, io.EOF) {
|
||||||
s.log.Write(pxy.Id, LOG_ERROR, "Read from SSH stdout error - %s", err.Error())
|
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to read from SSH stdout - %s", req.RemoteAddr, err.Error())
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
err = pxy.send_ws_data(ws, "iov", string(buf[:n]))
|
err = pxy.send_ws_data(ws, "iov", string(buf[:n]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Write(pxy.Id, LOG_ERROR, "Failed to send to websocket - %s", err.Error())
|
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to send to websocket - %s", req.RemoteAddr, err.Error())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -757,26 +748,27 @@ ws_recv_loop:
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r)
|
c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Write(pxy.Id, LOG_ERROR, "failed to connect ssh - %s", err.Error())
|
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to connect ssh - %s", req.RemoteAddr, err.Error())
|
||||||
pxy.send_ws_data(ws, "error", err.Error())
|
pxy.send_ws_data(ws, "error", err.Error())
|
||||||
ws.Close() // dirty way to flag out the error
|
ws.Close() // dirty way to flag out the error
|
||||||
} else {
|
} else {
|
||||||
err = pxy.send_ws_data(ws, "status", "opened")
|
err = pxy.send_ws_data(ws, "status", "opened")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Write(pxy.Id, LOG_ERROR, "Failed to write opened event to websocket - %s", err.Error())
|
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to write opened event to websocket - %s", req.RemoteAddr, err.Error())
|
||||||
ws.Close() // dirty way to flag out the error
|
ws.Close() // dirty way to flag out the error
|
||||||
} else {
|
} else {
|
||||||
|
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Opened SSH session - %s", req.RemoteAddr, err.Error())
|
||||||
conn_ready_chan <- true
|
conn_ready_chan <- true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(connect_ssh_cancel.Get())()
|
(connect_ssh_cancel.Get())()
|
||||||
connect_ssh_cancel.Set(nil)
|
connect_ssh_cancel.Set(nil) // @@@ use atomic
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
case "close":
|
case "close":
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
cancel = connect_ssh_cancel.Get() // is it a good way to avoid mutex?
|
cancel = connect_ssh_cancel.Get() // is it a good way to avoid mutex against Set() marked with @@@ above?
|
||||||
if cancel != nil { cancel() }
|
if cancel != nil { cancel() }
|
||||||
break ws_recv_loop
|
break ws_recv_loop
|
||||||
|
|
||||||
@ -795,7 +787,7 @@ ws_recv_loop:
|
|||||||
rows, _ = strconv.Atoi(ev.Data[0])
|
rows, _ = strconv.Atoi(ev.Data[0])
|
||||||
cols, _ = strconv.Atoi(ev.Data[1])
|
cols, _ = strconv.Atoi(ev.Data[1])
|
||||||
sess.WindowChange(rows, cols)
|
sess.WindowChange(rows, cols)
|
||||||
s.log.Write(pxy.Id, LOG_DEBUG, "Resized terminal to %d,%d", rows, cols)
|
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Resized terminal to %d,%d", req.RemoteAddr, rows, cols)
|
||||||
// ignore error
|
// ignore error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -814,6 +806,7 @@ done:
|
|||||||
if sess != nil { sess.Close() }
|
if sess != nil { sess.Close() }
|
||||||
if c != nil { c.Close() }
|
if c != nil { c.Close() }
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Ended SSH Session", req.RemoteAddr)
|
||||||
|
|
||||||
return http.StatusOK, err
|
return http.StatusOK, err
|
||||||
}
|
}
|
||||||
|
10
server.go
10
server.go
@ -1364,7 +1364,7 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
// this part is duplcate of pxy_mux.
|
// this part is duplcate of pxy_mux.
|
||||||
s.ctl_mux.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
s.ctl_mux.Handle("/_ssh/ws/{conn_id}/{route_id}",
|
||||||
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY_WS}))
|
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY_WS}))
|
||||||
s.ctl_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
s.ctl_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_CTL, NoAuth: true}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_CTL, NoAuth: true}}))
|
||||||
@ -1406,8 +1406,8 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
s.pxy_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh_ws,_http configurable...
|
s.pxy_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
||||||
s.pxy_mux.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
s.pxy_mux.Handle("/_ssh/ws/{conn_id}/{route_id}",
|
||||||
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY_WS}))
|
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY_WS}))
|
||||||
s.pxy_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
s.pxy_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_PXY, NoAuth: true}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_PXY, NoAuth: true}}))
|
||||||
@ -1451,8 +1451,8 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
|
|
||||||
s.wpx_mux = http.NewServeMux()
|
s.wpx_mux = http.NewServeMux()
|
||||||
|
|
||||||
s.wpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh_ws,_http configurable...
|
s.wpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
||||||
s.wpx_mux.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
s.wpx_mux.Handle("/_ssh/ws/{conn_id}/{route_id}",
|
||||||
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: "wpx-ssh"}))
|
s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: "wpx-ssh"}))
|
||||||
|
|
||||||
s.wpx_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
s.wpx_mux.Handle("/_ssh/server-conns/{conn_id}/routes/{route_id}",
|
||||||
|
285
xterm-pts.html
Normal file
285
xterm-pts.html
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Terminal</title>
|
||||||
|
<link rel="stylesheet" href="xterm.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-info-container {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-target {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-status {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-errmsg {
|
||||||
|
display: inline-block;
|
||||||
|
color: red;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-control {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-control button {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-view-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
/*height: 100%;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
#login-form-container {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#login-form-title {
|
||||||
|
line-height: 2em;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="xterm.js"></script>
|
||||||
|
<script src="xterm-addon-fit.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.onload = function(event) {
|
||||||
|
const terminal_container = document.getElementById('terminal-container');
|
||||||
|
const terminal_target = document.getElementById('terminal-target');
|
||||||
|
const terminal_status = document.getElementById('terminal-status');
|
||||||
|
const terminal_errmsg = document.getElementById('terminal-errmsg');
|
||||||
|
const terminal_view_container = document.getElementById('terminal-view-container');
|
||||||
|
const terminal_connect = document.getElementById('terminal-connect');
|
||||||
|
const terminal_disconnect = document.getElementById('terminal-disconnect');
|
||||||
|
const login_container = document.getElementById('login-container');
|
||||||
|
const login_form_title = document.getElementById('login-form-title');
|
||||||
|
const login_form = document.getElementById('login-form');
|
||||||
|
//const username_field = document.getElementById('username');
|
||||||
|
//const password_field= document.getElementById('password');
|
||||||
|
|
||||||
|
const term = new window.Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
//fontSize: 14,
|
||||||
|
fontFamily: 'Ubuntu Mono, Consolas, SF Mono, courier-new, courier, monospace'
|
||||||
|
});
|
||||||
|
const fit_addon = new window.FitAddon.FitAddon();
|
||||||
|
const text_decoder = new TextDecoder();
|
||||||
|
|
||||||
|
term.loadAddon(fit_addon)
|
||||||
|
term.open(terminal_view_container);
|
||||||
|
|
||||||
|
let set_terminal_target = function(name) {
|
||||||
|
terminal_target.innerText = name;
|
||||||
|
login_form_title.innerText = name
|
||||||
|
}
|
||||||
|
|
||||||
|
let set_terminal_status = function(msg, errmsg) {
|
||||||
|
if (msg != null) terminal_status.innerText = msg;
|
||||||
|
if (errmsg != null) terminal_errmsg.innerText = errmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjust_terminal_size_unconnected = function() {
|
||||||
|
fit_addon.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetch_session_info = async function() {
|
||||||
|
let url = window.location.protocol + '//' + window.location.host;
|
||||||
|
let pathname = window.location.pathname;
|
||||||
|
pathname = pathname.replace(/\/$/, '');
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
|
||||||
|
url += pathname + `/session-info`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) throw new Error(`HTTP error in getting session info - status ${resp.status}`);
|
||||||
|
const route = await resp.json()
|
||||||
|
if ('client-peer-name' in route) {
|
||||||
|
set_terminal_target(route['client-peer-name']) // change to the name
|
||||||
|
document.title = route['client-peer-name']
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
set_terminal_target('');
|
||||||
|
document.title = '';
|
||||||
|
set_terminal_status (null, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toggle_login_form = function(visible) {
|
||||||
|
//if (visible) fetch_session_info();
|
||||||
|
login_container.style.visibility = (visible? 'visible': 'hidden');
|
||||||
|
terminal_disconnect.style.visibility = (visible? 'hidden': 'visible');
|
||||||
|
//if (visible) username_field.focus();
|
||||||
|
//else term.focus();
|
||||||
|
if (visible) terminal_connect.focus();
|
||||||
|
else term.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_login_form(true);
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
adjust_terminal_size_unconnected()
|
||||||
|
|
||||||
|
login_form.onsubmit = async function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggle_login_form(false)
|
||||||
|
|
||||||
|
const username = "" //username_field.value.trim();
|
||||||
|
const password = "" //password_field.value.trim();
|
||||||
|
|
||||||
|
const prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
let pathname = window.location.pathname;
|
||||||
|
pathname = pathname.replace(/\/$/, '');
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
|
||||||
|
let url = prefix + window.location.host + pathname + `/ws`;
|
||||||
|
|
||||||
|
const socket = new WebSocket(url);
|
||||||
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
set_terminal_status('Connecting...', '');
|
||||||
|
|
||||||
|
const adjust_terminal_size_connected = function() {
|
||||||
|
fit_addon.fit();
|
||||||
|
if (socket.readyState == 1) // if open
|
||||||
|
socket.send(JSON.stringify({ type: 'size', data: [term.rows.toString(), term.cols.toString()] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onopen = function () {
|
||||||
|
socket.send(JSON.stringify({ type: 'open', data: [username, password]}));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
let event_text;
|
||||||
|
event_text = (typeof event.data === 'string')? event.data: text_decoder.decode(new Uint8Array(event.data));
|
||||||
|
const msg = JSON.parse(event_text);
|
||||||
|
if (msg.type == "iov") {
|
||||||
|
for (const data of msg.data) term.write(data);
|
||||||
|
} else if (msg.type == "status") {
|
||||||
|
if (msg.data.length >= 1) {
|
||||||
|
if (msg.data[0] == 'opened') {
|
||||||
|
set_terminal_status('Connected', '');
|
||||||
|
adjust_terminal_size_connected()
|
||||||
|
term.clear()
|
||||||
|
} else if (msg.data[0] == 'closed') {
|
||||||
|
// doesn't really matter
|
||||||
|
// socket.onclose() will be executed anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type == "error") {
|
||||||
|
toggle_login_form(true);
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
set_terminal_status(null, msg.data.join(' '))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
set_terminal_status('Disconnected', e);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(event) {
|
||||||
|
set_terminal_status('Disconnected', event);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function() {
|
||||||
|
set_terminal_status('Disconnected', null);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
term.onData(function(data) {
|
||||||
|
if (socket.readyState == 1) // if open
|
||||||
|
socket.send(JSON.stringify({ type: 'iov', data: [data] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onresize = adjust_terminal_size_connected;
|
||||||
|
terminal_disconnect.onclick = function(event) {
|
||||||
|
socket.send(JSON.stringify({ type: 'close', data: [""] }));
|
||||||
|
//socket.close()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="login-container">
|
||||||
|
<div id="login-form-container">
|
||||||
|
<div id="login-form-title"></div>
|
||||||
|
<form id="login-form">
|
||||||
|
<!-- <label>
|
||||||
|
Username: <input type="text" id="username" required />
|
||||||
|
</label>
|
||||||
|
<br /><br />
|
||||||
|
<label>
|
||||||
|
Password: <input type="password" id="password" required />
|
||||||
|
</label> -->
|
||||||
|
Click Connect below to start your secure terminal session
|
||||||
|
<br /><br />
|
||||||
|
<button type="submit" id="terminal-connect">Connect</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="terminal-container">
|
||||||
|
<div id="terminal-info-container">
|
||||||
|
<div id="terminal-target"></div>
|
||||||
|
<div id="terminal-status"></div>
|
||||||
|
<div id="terminal-errmsg"></div>
|
||||||
|
<div id="terminal-control">
|
||||||
|
<button id="terminal-disconnect" type="button">Disconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="terminal-view-container"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
xterm.html
21
xterm.html
@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<title>Terminal</title>
|
<title>Terminal</title>
|
||||||
<link rel="stylesheet" href="/_ssh/xterm.css" />
|
<link rel="stylesheet" href="xterm.css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -86,8 +86,8 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="/_ssh/xterm.js"></script>
|
<script src="xterm.js"></script>
|
||||||
<script src="/_ssh/xterm-addon-fit.js"></script>
|
<script src="xterm-addon-fit.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const conn_id = '{{ .ConnId }}';
|
const conn_id = '{{ .ConnId }}';
|
||||||
@ -133,7 +133,10 @@ window.onload = function(event) {
|
|||||||
|
|
||||||
let fetch_session_info = async function() {
|
let fetch_session_info = async function() {
|
||||||
let url = window.location.protocol + '//' + window.location.host;
|
let url = window.location.protocol + '//' + window.location.host;
|
||||||
url += `/_ssh/server-conns/${conn_id}/routes/${route_id}`;
|
let pathname = window.location.pathname;
|
||||||
|
pathname = pathname.replace(/\/$/, '');
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
|
||||||
|
url += pathname + `/server-conns/${conn_id}/routes/${route_id}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
@ -169,9 +172,13 @@ window.onload = function(event) {
|
|||||||
const username = username_field.value.trim();
|
const username = username_field.value.trim();
|
||||||
const password = password_field.value.trim();
|
const password = password_field.value.trim();
|
||||||
|
|
||||||
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
let url = prefix + window.location.host+ `/_ssh-ws/${conn_id}/${route_id}`;
|
let pathname = window.location.pathname;
|
||||||
const socket = new WebSocket(url)
|
pathname = pathname.replace(/\/$/, '');
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
|
||||||
|
let url = prefix + window.location.host + pathname + `/ws/${conn_id}/${route_id}`;
|
||||||
|
|
||||||
|
const socket = new WebSocket(url);
|
||||||
socket.binaryType = 'arraybuffer';
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
set_terminal_status('Connecting...', '');
|
set_terminal_status('Connecting...', '');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user