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:
|
||||
curl -L -o "$@" https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.min.css
|
||||
sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@"
|
||||
|
||||
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"
|
||||
|
265
server-proxy.go
265
server-proxy.go
@ -11,7 +11,9 @@ import "net/http"
|
||||
import "net/url"
|
||||
import "strconv"
|
||||
import "strings"
|
||||
import "sync"
|
||||
import "text/template"
|
||||
import "time"
|
||||
import "unsafe"
|
||||
|
||||
import "golang.org/x/crypto/ssh"
|
||||
@ -367,6 +369,7 @@ type server_proxy_xterm_file struct {
|
||||
}
|
||||
|
||||
type server_proxy_xterm_session_info struct {
|
||||
RouteName string
|
||||
ConnId string
|
||||
RouteId string
|
||||
}
|
||||
@ -404,8 +407,9 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
||||
w.WriteHeader(http.StatusOK)
|
||||
tmpl.Execute(w,
|
||||
&server_proxy_xterm_session_info{
|
||||
req.PathValue("conn_id"),
|
||||
req.PathValue("route_id"),
|
||||
RouteName: "Terminal",
|
||||
ConnId: req.PathValue("conn_id"),
|
||||
RouteId: req.PathValue("route_id"),
|
||||
})
|
||||
}
|
||||
case "_forbidden":
|
||||
@ -413,39 +417,122 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// TODO: logging..
|
||||
}
|
||||
// ------------------------------------
|
||||
|
||||
type server_proxy_ssh_ws struct {
|
||||
s *Server
|
||||
h websocket.Handler
|
||||
ws *websocket.Conn
|
||||
}
|
||||
|
||||
type json_ssh_ws_event struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
// TODO: put this task to sync group.
|
||||
// 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 conn_id string
|
||||
var conn_nid uint64
|
||||
var route_id string
|
||||
var route_nid uint64
|
||||
var r *ServerRoute
|
||||
var addr net.TCPAddr
|
||||
var cc *ssh.ClientConfig
|
||||
var username string
|
||||
var password string
|
||||
var c *ssh.Client
|
||||
var sess *ssh.Session
|
||||
var in io.Writer
|
||||
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
|
||||
|
||||
s = pxy.s
|
||||
req = ws.Request()
|
||||
conn_ready_chan = make(chan bool, 3)
|
||||
|
||||
defer func() {
|
||||
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))
|
||||
if err != nil {
|
||||
return
|
||||
// TODO:
|
||||
goto done
|
||||
}
|
||||
route_nid, err = strconv.ParseUint(route_id, 10, int(unsafe.Sizeof(route_nid) * 8))
|
||||
if err != nil {
|
||||
return
|
||||
// TODO:
|
||||
goto done
|
||||
}
|
||||
|
||||
r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid))
|
||||
if r == nil {
|
||||
// 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)
|
||||
return
|
||||
goto done
|
||||
}
|
||||
|
||||
cc = &ssh.ClientConfig{
|
||||
User: "hyung-hwan",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password("evianilie99"),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
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
|
||||
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 buf []byte
|
||||
var n int
|
||||
var err error
|
||||
|
||||
defer sess.Close()
|
||||
buf = make([]byte, 1024)
|
||||
buf = make([]byte, 2048)
|
||||
for {
|
||||
n, err = out.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
s.log.Write("", LOG_ERROR, "Read from SSH stdout error:", err)
|
||||
s.log.Write("", LOG_ERROR, "Read from SSH stdout error - %s", err.Error())
|
||||
}
|
||||
return
|
||||
break
|
||||
}
|
||||
if n > 0 {
|
||||
_, err = ws.Write(buf[:n])
|
||||
err = pxy.send_ws_data(ws, "iov", string(buf[:n]))
|
||||
if err != nil {
|
||||
s.log.Write("", LOG_ERROR, "Write to WebSocket error:", err)
|
||||
return
|
||||
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 {
|
||||
var msg []byte
|
||||
err = websocket.Message.Receive(ws, &msg)
|
||||
if err != nil {
|
||||
// TODO: check if EOF
|
||||
s.log.Write("", LOG_ERROR, "Failed to read from websocket - %s", err.Error())
|
||||
break
|
||||
goto done
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
var ev json_ssh_ws_event
|
||||
err = json.Unmarshal(msg, &ev)
|
||||
if err == nil {
|
||||
switch ev.Type {
|
||||
case "key":
|
||||
in.Write([]byte(ev.Data))
|
||||
case "resize":
|
||||
var sz []string
|
||||
sz = strings.Fields(ev.Data)
|
||||
if (len(sz) == 2) {
|
||||
case "open":
|
||||
if sess == nil && len(ev.Data) == 2 {
|
||||
username = string(ev.Data[0])
|
||||
password = string(ev.Data[1])
|
||||
|
||||
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 cols int
|
||||
rows, _ = strconv.Atoi(sz[0]);
|
||||
cols, _ = strconv.Atoi(sz[1]);
|
||||
rows, _ = strconv.Atoi(ev.Data[0])
|
||||
cols, _ = strconv.Atoi(ev.Data[1])
|
||||
sess.WindowChange(rows, cols)
|
||||
s.log.Write("", LOG_DEBUG, "Resized terminal to %d,%d", rows, cols)
|
||||
// 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
|
||||
|
||||
pxy_addr []string
|
||||
pxy_ws *server_proxy_ssh_ws
|
||||
pxy_mux *http.ServeMux
|
||||
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.Handle("/_ssh-ws/{conn_id}/{route_id}",
|
||||
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/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
|
||||
*/
|
||||
.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 */
|
209
xterm.html
209
xterm.html
@ -4,73 +4,228 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Terminal</title>
|
||||
<title>{{ .RouteName }}</title>
|
||||
<link rel="stylesheet" href="/_ssh/xterm.css" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
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>
|
||||
<script src="/_ssh/xterm.js"></script>
|
||||
<script src="/_ssh/xterm-addon-fit.js"></script>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", function(event) {
|
||||
const term = new window.Terminal();
|
||||
const fit = new window.FitAddon.FitAddon();
|
||||
const textDecoder = new TextDecoder();
|
||||
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 fit_addon = new window.FitAddon.FitAddon();
|
||||
const text_decoder = new TextDecoder();
|
||||
|
||||
term.loadAddon(fit_addon)
|
||||
term.open(terminal_view_container);
|
||||
|
||||
let set_terminal_status = function(msg, errmsg) {
|
||||
if (msg != null) terminal_status.innerHTML = msg;
|
||||
if (errmsg != null) terminal_errmsg.innerHTML = errmsg
|
||||
}
|
||||
|
||||
let toggle_login_form = function(visible) {
|
||||
login_container.style.visibility = (visible? 'visible': 'hidden');
|
||||
terminal_disconnect.style.visibility = (visible? 'hidden': 'visible');
|
||||
if (visible) username_field.focus();
|
||||
else term.focus();
|
||||
}
|
||||
|
||||
let resize_term_unconnected = function() {
|
||||
fit_addon.fit();
|
||||
}
|
||||
|
||||
toggle_login_form(true);
|
||||
window.onresize = resize_term_unconnected;
|
||||
resize_term_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();
|
||||
|
||||
let currentLocation = window.location.host;
|
||||
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const socket = new WebSocket(prefix + currentLocation + '/_ssh-ws/{{ .ConnId }}/{{ .RouteId }}');
|
||||
const socket = new WebSocket(prefix + window.location.host + '/_ssh-ws/{{ .ConnId }}/{{ .RouteId }}');
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
term.loadAddon(fit)
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
term.writeln('Connecting...');
|
||||
set_terminal_status('Connecting...', '');
|
||||
|
||||
const fit_term = function() {
|
||||
fit.fit();
|
||||
socket.send(JSON.stringify({ type: 'resize', data: term.rows.toString() + " " + term.cols.toString() }));
|
||||
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 () {
|
||||
term.writeln('Connected');
|
||||
fit_term();
|
||||
socket.send(JSON.stringify({ type: 'open', data: [username, password]}));
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
if (typeof event.data === 'string') {
|
||||
term.write(event.data);
|
||||
} else {
|
||||
const text = textDecoder.decode(new Uint8Array(event.data));
|
||||
term.write(text);
|
||||
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) {
|
||||
console.error('WebSocket error:', event);
|
||||
term.writeln('WebSocket error. See console for details.');
|
||||
set_terminal_status('Disconnected', event);
|
||||
toggle_login_form(true)
|
||||
window.onresize = resize_term_unconnected;
|
||||
term.write("\r\n")
|
||||
};
|
||||
|
||||
socket.onclose = function() {
|
||||
term.writeln('Disconnected');
|
||||
set_terminal_status('Disconnected', null);
|
||||
toggle_login_form(true)
|
||||
window.onresize = resize_term_unconnected;
|
||||
term.write("\r\n")
|
||||
};
|
||||
|
||||
term.onData(function(data) {
|
||||
socket.send(JSON.stringify({ type: 'key', data: data }));
|
||||
if (socket.readyState == 1) // if open
|
||||
socket.send(JSON.stringify({ type: 'iov', data: [data] }));
|
||||
});
|
||||
|
||||
window.addEventListener('resize', fit_term);
|
||||
});
|
||||
window.onresize = resize_term_connected;
|
||||
terminal_disconnect.onclick = function(event) {
|
||||
socket.send(JSON.stringify({ type: 'close', data: [""] }));
|
||||
//socket.close()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user