2020-09-26 08:41:54 +00:00
|
|
|
package server
|
2020-09-26 07:22:14 +00:00
|
|
|
|
|
|
|
import (
|
2022-11-22 20:09:40 +00:00
|
|
|
"encoding/json"
|
2020-09-26 21:01:33 +00:00
|
|
|
"fmt"
|
2020-09-26 07:22:14 +00:00
|
|
|
"html/template"
|
2022-11-22 20:09:40 +00:00
|
|
|
"io"
|
2020-09-26 07:22:14 +00:00
|
|
|
"mime"
|
2022-11-22 20:09:40 +00:00
|
|
|
"net"
|
2020-09-26 07:22:14 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/websocket"
|
2022-11-22 20:09:40 +00:00
|
|
|
"github.com/hashicorp/yamux"
|
2020-09-26 08:41:54 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2020-09-26 07:22:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-09-26 08:41:54 +00:00
|
|
|
errorNotFound = iota
|
|
|
|
errorNotAllowed = iota
|
2020-09-26 07:22:14 +00:00
|
|
|
)
|
|
|
|
|
2020-10-10 19:42:01 +00:00
|
|
|
type PTYHandler interface {
|
|
|
|
Write(data []byte) (int, error)
|
|
|
|
Refresh()
|
|
|
|
}
|
2020-09-26 07:22:14 +00:00
|
|
|
|
|
|
|
// SessionTemplateModel used for templating
|
2020-09-26 21:01:33 +00:00
|
|
|
type AASessionTemplateModel struct {
|
2020-09-26 07:22:14 +00:00
|
|
|
SessionID string
|
|
|
|
Salt string
|
|
|
|
WSPath string
|
|
|
|
}
|
|
|
|
|
|
|
|
// TTYServerConfig is used to configure the tty server before it is started
|
|
|
|
type TTYServerConfig struct {
|
2020-09-26 21:01:33 +00:00
|
|
|
FrontListenAddress string
|
|
|
|
FrontendPath string
|
2020-10-10 19:42:01 +00:00
|
|
|
PTY PTYHandler
|
2020-09-26 21:01:33 +00:00
|
|
|
SessionID string
|
2022-11-22 20:09:40 +00:00
|
|
|
AllowTunneling bool
|
2023-04-26 06:03:41 +00:00
|
|
|
CrossOrigin bool
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TTYServer represents the instance of a tty server
|
|
|
|
type TTYServer struct {
|
2022-11-22 20:09:40 +00:00
|
|
|
httpServer *http.Server
|
|
|
|
config TTYServerConfig
|
|
|
|
session *ttyShareSession
|
|
|
|
muxTunnelSession *yamux.Session
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (server *TTYServer) serveContent(w http.ResponseWriter, r *http.Request, name string) {
|
|
|
|
// If a path to the frontend resources was passed, serve from there, otherwise, serve from the
|
|
|
|
// builtin bundle
|
|
|
|
if server.config.FrontendPath == "" {
|
|
|
|
file, err := Asset(name)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctype := mime.TypeByExtension(filepath.Ext(name))
|
|
|
|
if ctype == "" {
|
|
|
|
ctype = http.DetectContentType(file)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", ctype)
|
|
|
|
w.Write(file)
|
|
|
|
} else {
|
|
|
|
filePath := server.config.FrontendPath + string(os.PathSeparator) + name
|
|
|
|
_, err := os.Open(filePath)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Couldn't find resource: %s at %s", name, filePath)
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
log.Debugf("Serving %s from %s", name, filePath)
|
|
|
|
|
|
|
|
http.ServeFile(w, r, filePath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTTYServer creates a new instance
|
|
|
|
func NewTTYServer(config TTYServerConfig) (server *TTYServer) {
|
|
|
|
server = &TTYServer{
|
|
|
|
config: config,
|
|
|
|
}
|
|
|
|
server.httpServer = &http.Server{
|
2020-09-26 08:41:54 +00:00
|
|
|
Addr: config.FrontListenAddress,
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
routesHandler := mux.NewRouter()
|
|
|
|
|
2020-09-26 21:01:33 +00:00
|
|
|
installHandlers := func(session string) {
|
2020-11-04 17:14:23 +00:00
|
|
|
// This function installs handlers for paths that contain the "session" passed as a
|
|
|
|
// parameter. The paths are for the static files, websockets, and other.
|
2022-11-22 20:09:40 +00:00
|
|
|
staticPath := "/s/" + session + "/static/"
|
|
|
|
ttyWsPath := "/s/" + session + "/ws"
|
|
|
|
tunnelWsPath := "/s/" + session + "/tws"
|
|
|
|
pathPrefix := "/s/" + session
|
|
|
|
|
|
|
|
routesHandler.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath,
|
2020-09-26 21:01:33 +00:00
|
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
server.serveContent(w, r, r.URL.Path)
|
|
|
|
})))
|
|
|
|
|
2022-11-22 20:09:40 +00:00
|
|
|
routesHandler.HandleFunc(pathPrefix+"/", func(w http.ResponseWriter, r *http.Request) {
|
2020-10-03 11:22:25 +00:00
|
|
|
// Check the frontend/templates/tty-share.in.html file to see where the template applies
|
2020-09-26 21:01:33 +00:00
|
|
|
templateModel := struct {
|
|
|
|
PathPrefix string
|
2020-10-10 19:42:01 +00:00
|
|
|
WSPath string
|
2022-11-22 20:09:40 +00:00
|
|
|
}{pathPrefix, ttyWsPath}
|
2020-10-03 11:22:25 +00:00
|
|
|
|
|
|
|
// TODO Extract these in constants
|
2022-11-22 20:09:40 +00:00
|
|
|
w.Header().Add("TTYSHARE-VERSION", "2")
|
|
|
|
|
|
|
|
// Deprecated HEADER (from prev version)
|
|
|
|
// TODO: Find a proper way to stop handling backward versions
|
|
|
|
w.Header().Add("TTYSHARE-WSPATH", ttyWsPath)
|
|
|
|
|
|
|
|
w.Header().Add("TTYSHARE-TTY-WSPATH", ttyWsPath)
|
|
|
|
w.Header().Add("TTYSHARE-TUNNEL-WSPATH", tunnelWsPath)
|
2020-09-26 21:01:33 +00:00
|
|
|
|
|
|
|
server.handleWithTemplateHtml(w, r, "tty-share.in.html", templateModel)
|
|
|
|
})
|
2022-11-22 20:09:40 +00:00
|
|
|
routesHandler.HandleFunc(ttyWsPath, func(w http.ResponseWriter, r *http.Request) {
|
2023-04-26 06:03:41 +00:00
|
|
|
server.handleTTYWebsocket(w, r, config.CrossOrigin)
|
2020-09-26 21:01:33 +00:00
|
|
|
})
|
2022-11-22 20:09:40 +00:00
|
|
|
if server.config.AllowTunneling {
|
|
|
|
// tunnel websockets connection
|
|
|
|
routesHandler.HandleFunc(tunnelWsPath, func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
server.handleTunnelWebsocket(w, r)
|
|
|
|
})
|
|
|
|
}
|
2020-09-26 21:01:33 +00:00
|
|
|
routesHandler.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2020-11-04 17:14:23 +00:00
|
|
|
templateModel := struct{ PathPrefix string }{fmt.Sprintf("/s/%s", session)}
|
2020-09-26 21:01:33 +00:00
|
|
|
server.handleWithTemplateHtml(w, r, "404.in.html", templateModel)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Install the same routes on both the /local/ and /<SessionID>/. The session ID is received
|
|
|
|
// from the tty-proxy server, if a public session is involved.
|
|
|
|
installHandlers("local")
|
|
|
|
installHandlers(config.SessionID)
|
2020-09-26 07:22:14 +00:00
|
|
|
|
|
|
|
server.httpServer.Handler = routesHandler
|
2020-10-10 19:42:01 +00:00
|
|
|
server.session = newTTYShareSession(config.PTY)
|
2020-09-26 08:41:54 +00:00
|
|
|
|
2020-09-26 07:22:14 +00:00
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2023-04-26 06:03:41 +00:00
|
|
|
func (server *TTYServer) handleTTYWebsocket(w http.ResponseWriter, r *http.Request, crossOrigin bool) {
|
2020-09-26 07:22:14 +00:00
|
|
|
if r.Method != "GET" {
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
upgrader := websocket.Upgrader{
|
|
|
|
ReadBufferSize: 1024,
|
|
|
|
WriteBufferSize: 1024,
|
|
|
|
}
|
2023-04-26 06:03:41 +00:00
|
|
|
if crossOrigin {
|
|
|
|
upgrader = websocket.Upgrader{
|
|
|
|
ReadBufferSize: 1024,
|
|
|
|
WriteBufferSize: 1024,
|
|
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-26 07:22:14 +00:00
|
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
2020-09-26 21:01:33 +00:00
|
|
|
log.Error("Cannot create the WS connection: ", err.Error())
|
2020-09-26 07:22:14 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-10 19:42:01 +00:00
|
|
|
// On a new connection, ask for a refresh/redraw of the terminal app
|
|
|
|
server.config.PTY.Refresh()
|
2020-10-10 09:10:17 +00:00
|
|
|
server.session.HandleWSConnection(conn)
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
|
2022-11-22 20:09:40 +00:00
|
|
|
func (server *TTYServer) handleTunnelWebsocket(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Method != "GET" {
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
upgrader := websocket.Upgrader{
|
|
|
|
ReadBufferSize: 1024,
|
|
|
|
WriteBufferSize: 1024,
|
|
|
|
}
|
|
|
|
wsConn, err := upgrader.Upgrade(w, r, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Cannot upgrade to WS for tunnel route connection: ", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer wsConn.Close()
|
|
|
|
|
|
|
|
// Read the first message on this ws route, and expect it to be a json containing the address
|
|
|
|
// to tunnel to. After that first message, will follow the raw connection data
|
|
|
|
_, wsReader, err := wsConn.NextReader()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Cannot read from the tunnel WS connection ", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var tunInitMsg TunInitMsg
|
|
|
|
err = json.NewDecoder(wsReader).Decode(&tunInitMsg)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Cannot decode the tunnel init message ", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
wsRW := &WSConnReadWriteCloser{
|
|
|
|
WsConn: wsConn,
|
|
|
|
}
|
|
|
|
|
|
|
|
server.muxTunnelSession, err = yamux.Server(wsRW, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could not open a mux server: ", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
muxStream, err := server.muxTunnelSession.Accept()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if err != io.EOF {
|
|
|
|
log.Warnf("Mux cannot accept new connections: %s", err.Error())
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
localConn, err := net.Dial("tcp", tunInitMsg.Address)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Cannot create local connection ", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
io.Copy(muxStream, localConn)
|
|
|
|
// Not sure yet which of the two io.Copy finishes first, so just close everything in both cases
|
|
|
|
defer localConn.Close()
|
|
|
|
defer muxStream.Close()
|
|
|
|
}()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
io.Copy(localConn, muxStream)
|
|
|
|
// Not sure yet which of the two io.Copy finishes first, so just close everything in both cases
|
|
|
|
defer muxStream.Close()
|
|
|
|
defer localConn.Close()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-09-26 21:01:33 +00:00
|
|
|
func panicIfErr(err error) {
|
|
|
|
if err != nil {
|
|
|
|
panic(err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (server *TTYServer) handleWithTemplateHtml(responseWriter http.ResponseWriter, r *http.Request, templateFile string, templateInterface interface{}) {
|
2020-09-26 07:22:14 +00:00
|
|
|
var t *template.Template
|
|
|
|
var err error
|
|
|
|
if server.config.FrontendPath == "" {
|
2020-09-26 21:01:33 +00:00
|
|
|
templateDta, err := Asset(templateFile)
|
|
|
|
panicIfErr(err)
|
|
|
|
t = template.New(templateFile)
|
2020-09-26 07:22:14 +00:00
|
|
|
_, err = t.Parse(string(templateDta))
|
|
|
|
} else {
|
2020-09-26 21:01:33 +00:00
|
|
|
t, err = template.ParseFiles(server.config.FrontendPath + string(os.PathSeparator) + templateFile)
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
2020-09-26 21:01:33 +00:00
|
|
|
panicIfErr(err)
|
2020-09-26 07:22:14 +00:00
|
|
|
|
2020-09-26 21:01:33 +00:00
|
|
|
err = t.Execute(responseWriter, templateInterface)
|
|
|
|
panicIfErr(err)
|
2020-09-26 07:22:14 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-10-10 19:42:01 +00:00
|
|
|
func (server *TTYServer) Run() (err error) {
|
2020-09-26 08:41:54 +00:00
|
|
|
err = server.httpServer.ListenAndServe()
|
|
|
|
log.Debug("Server finished")
|
2020-09-26 07:22:14 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
func (server *TTYServer) Write(buff []byte) (written int, err error) {
|
|
|
|
return server.session.Write(buff)
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
|
2020-09-26 08:41:54 +00:00
|
|
|
func (server *TTYServer) WindowSize(cols, rows int) (err error) {
|
|
|
|
return server.session.WindowSize(cols, rows)
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (server *TTYServer) Stop() error {
|
|
|
|
log.Debug("Stopping the server")
|
2022-11-22 20:09:40 +00:00
|
|
|
if server.muxTunnelSession != nil {
|
|
|
|
server.muxTunnelSession.Close()
|
|
|
|
}
|
2020-09-26 08:41:54 +00:00
|
|
|
return server.httpServer.Close()
|
2020-09-26 07:22:14 +00:00
|
|
|
}
|