/* * This plugin provide a full fledge terminal application. The code was * adapted from https://github.com/freman/goterm */ package plg_handler_console import ( _ "embed" "encoding/base64" "encoding/json" "io" "net/http" "os" "os/exec" "strings" "syscall" "time" "unsafe" . "github.com/mickael-kerjean/filestash/server/common" "github.com/creack/pty" "github.com/gorilla/mux" "github.com/gorilla/websocket" "golang.org/x/crypto/bcrypt" ) //go:embed src/app.css var AppStyle []byte //go:embed src/xterm.js var VendorScript []byte // made of xterm.js (https://cdnjs.cloudflare.com/ajax/libs/xterm/3.12.2/xterm.js) and the fit addon(https://cdnjs.cloudflare.com/ajax/libs/xterm/3.12.2/addons/fit/fit.js) //go:embed src/xterm.css var VendorStyle []byte var console_enable = func() bool { return Config.Get("features.server.console_enable").Schema(func(f *FormElement) *FormElement { if f == nil { f = &FormElement{} } f.Default = false f.Name = "console_enable" f.Type = "boolean" f.Description = "Enable/Disable the interactive web console on your instance. It will be available under `/admin/tty/` where username is 'admin' and password is your admin console" f.Placeholder = "Default: false" return f }).Bool() } func init() { Hooks.Register.Onload(func() { if console_enable() == false { return } Hooks.Register.HttpEndpoint(func(r *mux.Router, _ *App) error { r.PathPrefix("/admin/tty/").Handler( AuthBasic( func() (string, string) { return "admin", Config.Get("auth.admin").String() }, TTYHandler("/admin/tty/"), ), ) return nil }) }) } var notAuthorised = func(res http.ResponseWriter, req *http.Request) { time.Sleep(1 * time.Second) res.Header().Set("WWW-Authenticate", `Basic realm="User protect", charset="UTF-8"`) res.WriteHeader(http.StatusUnauthorized) res.Write([]byte("Not Authorised")) return } func AuthBasic(credentials func() (string, string), fn http.Handler) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { if strings.HasSuffix(Config.Get("general.host").String(), "filestash.app") { http.NotFoundHandler().ServeHTTP(res, req) return } else if console_enable() == false { http.NotFoundHandler().ServeHTTP(res, req) return } auth := req.Header.Get("Authorization") if strings.HasPrefix(auth, "Basic ") == false { notAuthorised(res, req) return } auth = strings.TrimPrefix(auth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(auth) if err != nil { notAuthorised(res, req) return } auth = string(decoded) stuffs := strings.Split(auth, ":") if len(stuffs) < 2 { notAuthorised(res, req) return } username := stuffs[0] password := strings.Join(stuffs[1:], ":") refUsername, refPassword := credentials() if refUsername != username { Log.Info("[tty] username is 'admin'") notAuthorised(res, req) return } else if len(strings.TrimSpace(password)) < 5 { Log.Info("[tty] password is too short") notAuthorised(res, req) return } else if err = bcrypt.CompareHashAndPassword([]byte(refPassword), []byte(password)); err != nil { notAuthorised(res, req) return } fn.ServeHTTP(res, req) return } } func TTYHandler(pathPrefix string) http.Handler { if strings.HasSuffix(pathPrefix, "/") == false { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusInternalServerError) res.Write([]byte("unsafe path prefix, use a '/'")) return }) } return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { req.URL.Path = "/" + strings.TrimPrefix(req.URL.Path, pathPrefix) if req.Method == "GET" { if req.URL.Path == "/" { res.Header().Set("Content-Type", "text/html") res.Write(htmlIndex(pathPrefix)) return } } if req.URL.Path == "/socket" { handleSocket(res, req) return } res.WriteHeader(http.StatusNotFound) res.Write([]byte("NOT FOUND")) }) } var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } var resizeMessage = struct { Rows uint16 `json:"rows"` Cols uint16 `json:"cols"` X uint16 Y uint16 }{} func handleSocket(res http.ResponseWriter, req *http.Request) { conn, err := upgrader.Upgrade(res, req, nil) if err != nil { res.WriteHeader(http.StatusInternalServerError) res.Write([]byte("upgrade error")) return } defer conn.Close() var cmd *exec.Cmd if _, err = exec.LookPath("/bin/bash"); err == nil { bashCommand := `bash --noprofile --init-file <(cat <
`) } func AppScript(pathPrefix string) string { return ` (function() { Terminal.applyAddon(fit); var term; function Boot() { term = new Terminal({ cursorBlink: true, theme: { background: "#1d1f21", foreground: "#c5c8c6", cursor: "#c5c8c6", black: "#282a2e", brightBlack: "#373b41", red: "#cc645a", brightRed: "#cc6666", green: "#5fa88d", brightGreen: "#aebd66", yellow: "#f0c666", brightYellow: "#f0c673", blue: "#709dbe", brightBlue: "#81a2be", magenta: "#b394ba", brightMagenta: "#b394ba", cyan: "#88beb3", brightCyan: "#8bbfb6", white: "#707880" } }); var websocket = new WebSocket( (location.protocol === "https:" ? "wss://" : "ws://") + location.hostname + ((location.port) ? (":" + location.port) : "") + "` + pathPrefix + `socket" ); websocket.binaryType = "arraybuffer"; websocket.onopen = function(e) { term.open(document.getElementById("terminal")); term.fit(); term.on("data", function(data) { websocket.send(new TextEncoder().encode("\x00" + data)); websocket.send(new TextEncoder().encode("\x01" + JSON.stringify({cols: term.cols, rows: term.rows}))) }); term.on('resize', function(evt) { term.fit(); websocket.send(new TextEncoder().encode("\x01" + JSON.stringify({cols: evt.cols, rows: evt.rows}))) }); window.onresize = function() { term.fit(); } term.on('title', function(title) { document.title = title; }); } websocket.onmessage = function(e) { if (e.data instanceof ArrayBuffer) { term.write(String.fromCharCode.apply(null, new Uint8Array(e.data))); return; } websocket.close() term.destroy(); alert("Something went wrong"); } websocket.onclose = function(){ term.write("Session terminated"); term.destroy(); } websocket.onerror = function(e){ var $term = document.getElementById("terminal"); if($term) $term.remove(); document.getElementById("terminal").remove() document.getElementById("error-message").innerText = "Websocket Error"; } } Boot(); })()` }