many enhancements to the ssh terminal support
This commit is contained in:
parent
61c13bd4e8
commit
ea601f1011
1
Makefile
1
Makefile
@ -57,6 +57,7 @@ xterm-addon-fit.js:
|
|||||||
|
|
||||||
xterm.css:
|
xterm.css:
|
||||||
curl -L -o "$@" https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.min.css
|
curl -L -o "$@" https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.min.css
|
||||||
|
sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@"
|
||||||
|
|
||||||
cmd/tls.crt:
|
cmd/tls.crt:
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:10.0.0.1,IP:::1"
|
openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:10.0.0.1,IP:::1"
|
||||||
|
285
server-proxy.go
285
server-proxy.go
@ -11,7 +11,9 @@ import "net/http"
|
|||||||
import "net/url"
|
import "net/url"
|
||||||
import "strconv"
|
import "strconv"
|
||||||
import "strings"
|
import "strings"
|
||||||
|
import "sync"
|
||||||
import "text/template"
|
import "text/template"
|
||||||
|
import "time"
|
||||||
import "unsafe"
|
import "unsafe"
|
||||||
|
|
||||||
import "golang.org/x/crypto/ssh"
|
import "golang.org/x/crypto/ssh"
|
||||||
@ -367,6 +369,7 @@ type server_proxy_xterm_file struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type server_proxy_xterm_session_info struct {
|
type server_proxy_xterm_session_info struct {
|
||||||
|
RouteName string
|
||||||
ConnId string
|
ConnId string
|
||||||
RouteId string
|
RouteId string
|
||||||
}
|
}
|
||||||
@ -404,8 +407,9 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
tmpl.Execute(w,
|
tmpl.Execute(w,
|
||||||
&server_proxy_xterm_session_info{
|
&server_proxy_xterm_session_info{
|
||||||
req.PathValue("conn_id"),
|
RouteName: "Terminal",
|
||||||
req.PathValue("route_id"),
|
ConnId: req.PathValue("conn_id"),
|
||||||
|
RouteId: req.PathValue("route_id"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "_forbidden":
|
case "_forbidden":
|
||||||
@ -413,39 +417,122 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
|||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: logging..
|
||||||
}
|
}
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
type server_proxy_ssh_ws struct {
|
type server_proxy_ssh_ws struct {
|
||||||
s *Server
|
s *Server
|
||||||
h websocket.Handler
|
ws *websocket.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
type json_ssh_ws_event struct {
|
type json_ssh_ws_event struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data string `json:"data"`
|
Data []string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: put this task to sync group.
|
// TODO: put this task to sync group.
|
||||||
// TODO: put the above proxy task to sync group too.
|
// TODO: put the above proxy task to sync group too.
|
||||||
|
|
||||||
|
func (pxy *server_proxy_ssh_ws) send_ws_data(ws *websocket.Conn, type_val string, data string) error {
|
||||||
|
var msg []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
func server_proxy_serve_ssh_ws(ws *websocket.Conn, s *Server) {
|
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 (pxy *server_proxy_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 addr net.TCPAddr
|
||||||
|
var dialer *net.Dialer
|
||||||
|
var conn net.Conn
|
||||||
|
var ssh_conn ssh.Conn
|
||||||
|
var chans <-chan ssh.NewChannel
|
||||||
|
var reqs <-chan *ssh.Request
|
||||||
|
var c *ssh.Client
|
||||||
|
var sess *ssh.Session
|
||||||
|
var in io.Writer // input to target
|
||||||
|
var out io.Reader // ooutput from target
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cc = &ssh.ClientConfig{
|
||||||
|
User: username,
|
||||||
|
Auth: []ssh.AuthMethod{ ssh.Password(password) },
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
// Timeout: 2 * time.Second ,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHECK OPTIONS
|
||||||
|
// if r.svc_option & RouteOption(ROUTE_OPTION_SSH) == 0 {
|
||||||
|
// REJECT??
|
||||||
|
//}
|
||||||
|
// TODO: timeout...
|
||||||
|
|
||||||
|
addr = *r.svc_addr;
|
||||||
|
if addr.IP.To4() != nil {
|
||||||
|
addr.IP = net.IPv4(127, 0, 0, 1) // net.IPv4loopback is not defined. so use net.IPv4()
|
||||||
|
} else {
|
||||||
|
addr.IP = net.IPv6loopback // net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer = &net.Dialer{}
|
||||||
|
conn, err = dialer.DialContext(ctx, "tcp", addr.String())
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
ssh_conn, chans, reqs, err = ssh.NewClientConn(conn, addr.String(), cc)
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
c = ssh.NewClient(ssh_conn, chans, reqs)
|
||||||
|
|
||||||
|
sess, err = c.NewSession()
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
out, err = sess.StdoutPipe()
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
in, err = sess.StdinPipe()
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
err = sess.RequestPty("xterm", 25, 80, ssh.TerminalModes{})
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
err = sess.Shell()
|
||||||
|
if err != nil { goto oops }
|
||||||
|
|
||||||
|
return c, sess, in, out, nil
|
||||||
|
|
||||||
|
oops:
|
||||||
|
if sess != nil { sess.Close() }
|
||||||
|
if c != nil { c.Close() }
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
|
||||||
|
var s *Server
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var conn_id string
|
var conn_id string
|
||||||
var conn_nid uint64
|
var conn_nid uint64
|
||||||
var route_id string
|
var route_id string
|
||||||
var route_nid uint64
|
var route_nid uint64
|
||||||
var r *ServerRoute
|
var r *ServerRoute
|
||||||
var addr net.TCPAddr
|
var username string
|
||||||
var cc *ssh.ClientConfig
|
var password string
|
||||||
var c *ssh.Client
|
var c *ssh.Client
|
||||||
var sess *ssh.Session
|
var sess *ssh.Session
|
||||||
var in io.Writer
|
var in io.Writer
|
||||||
var out io.Reader
|
var out io.Reader
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var conn_ready_chan chan bool
|
||||||
|
var connect_ssh_ctx context.Context
|
||||||
|
var connect_ssh_cancel context.CancelFunc
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
s = pxy.s
|
||||||
req = ws.Request()
|
req = ws.Request()
|
||||||
|
conn_ready_chan = make(chan bool, 3)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
var err interface{} = recover()
|
var err interface{} = recover()
|
||||||
@ -457,127 +544,120 @@ func server_proxy_serve_ssh_ws(ws *websocket.Conn, s *Server) {
|
|||||||
|
|
||||||
conn_nid, err = strconv.ParseUint(conn_id, 10, int(unsafe.Sizeof(conn_nid) * 8))
|
conn_nid, err = strconv.ParseUint(conn_id, 10, int(unsafe.Sizeof(conn_nid) * 8))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
// TODO:
|
||||||
|
goto done
|
||||||
}
|
}
|
||||||
route_nid, err = strconv.ParseUint(route_id, 10, int(unsafe.Sizeof(route_nid) * 8))
|
route_nid, err = strconv.ParseUint(route_id, 10, int(unsafe.Sizeof(route_nid) * 8))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
// TODO:
|
||||||
|
goto done
|
||||||
}
|
}
|
||||||
|
|
||||||
r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid))
|
r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid))
|
||||||
if r == nil {
|
if r == nil {
|
||||||
// TODO: enhance logging. original request, conn_nid, route_nid
|
// TODO: enhance logging. original request, conn_nid, route_nid
|
||||||
|
pxy.send_ws_data(ws, "error", fmt.Sprintf("route(%d,%d) not found", conn_nid, route_nid))
|
||||||
s.log.Write("", LOG_ERROR, "No server route(%d,%d) found", conn_nid, route_nid)
|
s.log.Write("", LOG_ERROR, "No server route(%d,%d) found", conn_nid, route_nid)
|
||||||
return
|
goto done
|
||||||
}
|
|
||||||
|
|
||||||
cc = &ssh.ClientConfig{
|
|
||||||
User: "hyung-hwan",
|
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password("evianilie99"),
|
|
||||||
},
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK OPTIONS
|
wg.Add(1)
|
||||||
// if r.svc_option & RouteOption(ROUTE_OPTION_SSH) == 0 {
|
|
||||||
// REJECT??
|
|
||||||
//}
|
|
||||||
// TODO: timeout...
|
|
||||||
addr = *r.svc_addr;
|
|
||||||
if addr.IP.To4() != nil {
|
|
||||||
addr.IP = net.IPv4(127, 0, 0, 1) // net.IPv4loopback is not defined. so use net.IPv4()
|
|
||||||
} else {
|
|
||||||
addr.IP = net.IPv6loopback // net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
|
||||||
}
|
|
||||||
c, err = ssh.Dial("tcp", addr.String(), cc)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "SSH dial error - %s", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
sess, err = c.NewSession()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "SSH session error - %s", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer sess.Close()
|
|
||||||
|
|
||||||
out, err = sess.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "STDOUT pipe error - ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
in, err = sess.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "STDIN pipe error - ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sess.RequestPty("xterm", 40, 80, ssh.TerminalModes{})
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "Request PTY error - ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sess.Shell()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Write("", LOG_ERROR, "Start shell error - ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async reader and writer to websocket
|
|
||||||
go func() {
|
go func() {
|
||||||
var buf []byte
|
var conn_ready bool
|
||||||
var n int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
defer sess.Close()
|
defer wg.Done()
|
||||||
buf = make([]byte, 1024)
|
defer ws.Close() // dirty way to break the main loop
|
||||||
for {
|
|
||||||
n, err = out.Read(buf)
|
conn_ready = <-conn_ready_chan
|
||||||
if err != nil {
|
if conn_ready { // connected
|
||||||
if err != io.EOF {
|
var buf []byte
|
||||||
s.log.Write("", LOG_ERROR, "Read from SSH stdout error:", err)
|
var n int
|
||||||
}
|
var err error
|
||||||
return
|
|
||||||
}
|
buf = make([]byte, 2048)
|
||||||
if n > 0 {
|
for {
|
||||||
_, err = ws.Write(buf[:n])
|
n, err = out.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Write("", LOG_ERROR, "Write to WebSocket error:", err)
|
if err != io.EOF {
|
||||||
return
|
s.log.Write("", LOG_ERROR, "Read from SSH stdout error - %s", err.Error())
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
err = pxy.send_ws_data(ws, "iov", string(buf[:n]))
|
||||||
|
if err != nil {
|
||||||
|
s.log.Write("", LOG_ERROR, "Failed to send to websocket - %s", err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Sync websocket reader and writer to sshIn
|
ws_recv_loop:
|
||||||
for {
|
for {
|
||||||
var msg []byte
|
var msg []byte
|
||||||
err = websocket.Message.Receive(ws, &msg)
|
err = websocket.Message.Receive(ws, &msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: check if EOF
|
// TODO: check if EOF
|
||||||
s.log.Write("", LOG_ERROR, "Failed to read from websocket - %s", err.Error())
|
s.log.Write("", LOG_ERROR, "Failed to read from websocket - %s", err.Error())
|
||||||
break
|
goto done
|
||||||
}
|
}
|
||||||
if len(msg) > 0 {
|
if len(msg) > 0 {
|
||||||
var ev json_ssh_ws_event
|
var ev json_ssh_ws_event
|
||||||
err = json.Unmarshal(msg, &ev)
|
err = json.Unmarshal(msg, &ev)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case "key":
|
case "open":
|
||||||
in.Write([]byte(ev.Data))
|
if sess == nil && len(ev.Data) == 2 {
|
||||||
case "resize":
|
username = string(ev.Data[0])
|
||||||
var sz []string
|
password = string(ev.Data[1])
|
||||||
sz = strings.Fields(ev.Data)
|
|
||||||
if (len(sz) == 2) {
|
connect_ssh_ctx, connect_ssh_cancel = context.WithTimeout(req.Context(), 10 * time.Second) // TODO: configurable timeout
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Write("", LOG_ERROR, "failed to connect ssh - %s", err.Error())
|
||||||
|
pxy.send_ws_data(ws, "error", err.Error())
|
||||||
|
ws.Close() // dirty way to flag out the error
|
||||||
|
} else {
|
||||||
|
err = pxy.send_ws_data(ws, "status", "opened")
|
||||||
|
if err != nil {
|
||||||
|
s.log.Write("", LOG_ERROR, "Failed to write opened event to websocket - %s", err.Error())
|
||||||
|
ws.Close() // dirty way to flag out the error
|
||||||
|
} else {
|
||||||
|
conn_ready_chan <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect_ssh_cancel = nil
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "close":
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
cancel = connect_ssh_cancel // is it a good way to avoid mutex?
|
||||||
|
if cancel != nil { cancel() }
|
||||||
|
break ws_recv_loop
|
||||||
|
|
||||||
|
case "iov":
|
||||||
|
if sess != nil {
|
||||||
|
var i int
|
||||||
|
for i, _ = range ev.Data {
|
||||||
|
in.Write([]byte(ev.Data[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "size":
|
||||||
|
if sess != nil && len(ev.Data) == 2 {
|
||||||
var rows int
|
var rows int
|
||||||
var cols int
|
var cols int
|
||||||
rows, _ = strconv.Atoi(sz[0]);
|
rows, _ = strconv.Atoi(ev.Data[0])
|
||||||
cols, _ = strconv.Atoi(sz[1]);
|
cols, _ = strconv.Atoi(ev.Data[1])
|
||||||
sess.WindowChange(rows, cols)
|
sess.WindowChange(rows, cols)
|
||||||
s.log.Write("", LOG_DEBUG, "Resized terminal to %d,%d", rows, cols)
|
s.log.Write("", LOG_DEBUG, "Resized terminal to %d,%d", rows, cols)
|
||||||
// ignore error
|
// ignore error
|
||||||
@ -586,4 +666,19 @@ func server_proxy_serve_ssh_ws(ws *websocket.Conn, s *Server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sess != nil {
|
||||||
|
err = pxy.send_ws_data(ws, "status", "closed")
|
||||||
|
if err != nil {
|
||||||
|
s.log.Write("", LOG_ERROR, "Failed to write closed event to websocket - %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
conn_ready_chan <- false
|
||||||
|
ws.Close()
|
||||||
|
if sess != nil { sess.Close() }
|
||||||
|
if c != nil { c.Close() }
|
||||||
|
wg.Wait()
|
||||||
|
s.log.Write("", LOG_DEBUG, "[%s] %s %s - ended", req.RemoteAddr, req.Method, req.URL.String())
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ type Server struct {
|
|||||||
ext_svcs []Service
|
ext_svcs []Service
|
||||||
|
|
||||||
pxy_addr []string
|
pxy_addr []string
|
||||||
|
pxy_ws *server_proxy_ssh_ws
|
||||||
pxy_mux *http.ServeMux
|
pxy_mux *http.ServeMux
|
||||||
pxy []*http.Server // proxy server
|
pxy []*http.Server // proxy server
|
||||||
|
|
||||||
@ -955,10 +956,11 @@ func NewServer(ctx context.Context, logger Logger, ctl_addrs []string, rpc_addrs
|
|||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
s.pxy_ws = &server_proxy_ssh_ws{s: &s}
|
||||||
s.pxy_mux = http.NewServeMux() // TODO: make /_init configurable...
|
s.pxy_mux = http.NewServeMux() // TODO: make /_init configurable...
|
||||||
s.pxy_mux.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
s.pxy_mux.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
||||||
websocket.Handler(func(ws *websocket.Conn) {
|
websocket.Handler(func(ws *websocket.Conn) {
|
||||||
server_proxy_serve_ssh_ws(ws, &s)
|
s.pxy_ws.ServeWebsocket(ws)
|
||||||
}))
|
}))
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/", &server_proxy_xterm_file{s: &s, file: "xterm.html"})
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/", &server_proxy_xterm_file{s: &s, file: "xterm.html"})
|
||||||
s.pxy_mux.Handle("/_ssh/xterm.js", &server_proxy_xterm_file{s: &s, file: "xterm.js"})
|
s.pxy_mux.Handle("/_ssh/xterm.js", &server_proxy_xterm_file{s: &s, file: "xterm.js"})
|
||||||
|
@ -5,4 +5,3 @@
|
|||||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
*/
|
*/
|
||||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||||
/*# sourceMappingURL=/sm/97377c0c258e109358121823f5790146c714989366481f90e554c42277efb500.map */
|
|
235
xterm.html
235
xterm.html
@ -4,73 +4,228 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<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>{{ .RouteName }}</title>
|
||||||
<link rel="stylesheet" href="/_ssh/xterm.css" />
|
<link rel="stylesheet" href="/_ssh/xterm.css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal-container {
|
#terminal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#terminal-status-container {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="/_ssh/xterm.js"></script>
|
<script src="/_ssh/xterm.js"></script>
|
||||||
<script src="/_ssh/xterm-addon-fit.js"></script>
|
<script src="/_ssh/xterm-addon-fit.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", function(event) {
|
window.onload = function(event) {
|
||||||
|
const terminal_container = document.getElementById('terminal-container');
|
||||||
|
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_disconnect = document.getElementById('terminal-disconnect');
|
||||||
|
const login_container = document.getElementById('login-container');
|
||||||
|
const login_form = document.getElementById('login-form');
|
||||||
|
const username_field = document.getElementById('username');
|
||||||
|
const password_field= document.getElementById('password');
|
||||||
|
|
||||||
const term = new window.Terminal();
|
const term = new window.Terminal();
|
||||||
const fit = new window.FitAddon.FitAddon();
|
const fit_addon = new window.FitAddon.FitAddon();
|
||||||
const textDecoder = new TextDecoder();
|
const text_decoder = new TextDecoder();
|
||||||
|
|
||||||
let currentLocation = window.location.host;
|
term.loadAddon(fit_addon)
|
||||||
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
term.open(terminal_view_container);
|
||||||
const socket = new WebSocket(prefix + currentLocation + '/_ssh-ws/{{ .ConnId }}/{{ .RouteId }}');
|
|
||||||
socket.binaryType = 'arraybuffer';
|
|
||||||
|
|
||||||
term.loadAddon(fit)
|
let set_terminal_status = function(msg, errmsg) {
|
||||||
term.open(document.getElementById('terminal-container'));
|
if (msg != null) terminal_status.innerHTML = msg;
|
||||||
term.writeln('Connecting...');
|
if (errmsg != null) terminal_errmsg.innerHTML = errmsg
|
||||||
|
}
|
||||||
|
|
||||||
const fit_term = function() {
|
let toggle_login_form = function(visible) {
|
||||||
fit.fit();
|
login_container.style.visibility = (visible? 'visible': 'hidden');
|
||||||
socket.send(JSON.stringify({ type: 'resize', data: term.rows.toString() + " " + term.cols.toString() }));
|
terminal_disconnect.style.visibility = (visible? 'hidden': 'visible');
|
||||||
};
|
if (visible) username_field.focus();
|
||||||
|
else term.focus();
|
||||||
|
}
|
||||||
|
|
||||||
socket.onopen = function () {
|
let resize_term_unconnected = function() {
|
||||||
term.writeln('Connected');
|
fit_addon.fit();
|
||||||
fit_term();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
socket.onmessage = function(event) {
|
toggle_login_form(true);
|
||||||
if (typeof event.data === 'string') {
|
window.onresize = resize_term_unconnected;
|
||||||
term.write(event.data);
|
resize_term_unconnected()
|
||||||
} else {
|
|
||||||
const text = textDecoder.decode(new Uint8Array(event.data));
|
|
||||||
term.write(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = function (event) {
|
login_form.onsubmit = async function(event) {
|
||||||
console.error('WebSocket error:', event);
|
event.preventDefault();
|
||||||
term.writeln('WebSocket error. See console for details.');
|
toggle_login_form(false)
|
||||||
};
|
|
||||||
|
|
||||||
socket.onclose = function () {
|
const username = username_field.value.trim();
|
||||||
term.writeln('Disconnected');
|
const password = password_field.value.trim();
|
||||||
};
|
|
||||||
|
|
||||||
term.onData(function(data) {
|
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
socket.send(JSON.stringify({ type: 'key', data: data }));
|
const socket = new WebSocket(prefix + window.location.host + '/_ssh-ws/{{ .ConnId }}/{{ .RouteId }}');
|
||||||
});
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
window.addEventListener('resize', fit_term);
|
set_terminal_status('Connecting...', '');
|
||||||
});
|
|
||||||
|
const resize_term_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', '');
|
||||||
|
resize_term_connected()
|
||||||
|
} 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 = resize_term_unconnected;
|
||||||
|
set_terminal_status(null, msg.data.join(' '))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
set_terminal_status('Disconnected', e);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = resize_term_unconnected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(event) {
|
||||||
|
set_terminal_status('Disconnected', event);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = resize_term_unconnected;
|
||||||
|
term.write("\r\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function() {
|
||||||
|
set_terminal_status('Disconnected', null);
|
||||||
|
toggle_login_form(true)
|
||||||
|
window.onresize = resize_term_unconnected;
|
||||||
|
term.write("\r\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
term.onData(function(data) {
|
||||||
|
if (socket.readyState == 1) // if open
|
||||||
|
socket.send(JSON.stringify({ type: 'iov', data: [data] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onresize = resize_term_connected;
|
||||||
|
terminal_disconnect.onclick = function(event) {
|
||||||
|
socket.send(JSON.stringify({ type: 'close', data: [""] }));
|
||||||
|
//socket.close()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="terminal-container"></div>
|
|
||||||
|
<div id="login-container">
|
||||||
|
<div id="login-form-container">
|
||||||
|
<h2>{{ .RouteName }}</h2>
|
||||||
|
<form id="login-form">
|
||||||
|
<label>
|
||||||
|
Username: <input type="text" id="username" required />
|
||||||
|
</label>
|
||||||
|
<br /><br />
|
||||||
|
<label>
|
||||||
|
Password: <input type="password" id="password" required />
|
||||||
|
</label>
|
||||||
|
<br /><br />
|
||||||
|
<button type="submit">Connect</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="terminal-container">
|
||||||
|
<div id="terminal-status-container">
|
||||||
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user