Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -38,16 +39,18 @@ type ttyShareClient struct {
}
winSizesMutex sync.Mutex
tunnelMuxSession *yamux.Session
insecure bool
}

func newTtyShareClient(url string, detachKeys string, tunnelConfig *string) *ttyShareClient {
func newTtyShareClient(url string, detachKeys string, tunnelConfig *string, insecure bool) *ttyShareClient {
return &ttyShareClient{
url: url,
ttyWsConn: nil,
detachKeys: detachKeys,
wcChan: make(chan os.Signal, 1),
ioFlagAtomic: 1,
tunnelAddresses: tunnelConfig,
insecure: insecure,
}
}

Expand Down Expand Up @@ -105,33 +108,52 @@ func (c *ttyShareClient) updateThisWinSize() {
func (c *ttyShareClient) Run() (err error) {
log.Debugf("Connecting as a client to %s ..", c.url)

resp, err := http.Get(c.url)

httpURL, err := url.Parse(c.url)
if err != nil {
return
}

// Get the path of the websockts route from the header
ttyWsPath := resp.Header.Get("TTYSHARE-TTY-WSPATH")
ttyWSProtocol := resp.Header.Get("TTYSHARE-VERSION")
client := &http.Client{}

ttyTunnelPath := resp.Header.Get("TTYSHARE-TUNNEL-WSPATH")
if httpURL.Scheme == "https" {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.insecure,
},
}
}
resp, err := client.Get(c.url)

// Build the WS URL from the host part of the given http URL and the wsPath
httpURL, err := url.Parse(c.url)
if err != nil {
return
}

// Build the WS URL from the host part of the given http URL and the wsPath
wsScheme := "ws"
if httpURL.Scheme == "https" {
wsScheme = "wss"
}

// Get the path of the websockts route from the header
ttyWsPath := resp.Header.Get("TTYSHARE-TTY-WSPATH")
ttyWSProtocol := resp.Header.Get("TTYSHARE-VERSION")

ttyTunnelPath := resp.Header.Get("TTYSHARE-TUNNEL-WSPATH")

ttyWsURL := wsScheme + "://" + httpURL.Host + ttyWsPath
ttyTunnelURL := wsScheme + "://" + httpURL.Host + ttyTunnelPath

log.Debugf("Built the WS URL from the headers: %s", ttyWsURL)

c.ttyWsConn, _, err = websocket.DefaultDialer.Dial(ttyWsURL, nil)
dialer := websocket.DefaultDialer
if httpURL.Scheme == "https" && c.insecure {
dialer.TLSClientConfig = &tls.Config{
// Set InsecureSkipVerify to true to disable certificate validation
InsecureSkipVerify: true,
}
}

c.ttyWsConn, _, err = dialer.Dial(ttyWsURL, nil)
if err != nil {
return
}
Expand Down
64 changes: 48 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@ package main

import (
"bufio"
"crypto/x509"
"errors"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"time"

log "github.com/sirupsen/logrus"

"github.com/elisescu/tty-share/proxy"
"github.com/elisescu/tty-share/server"
ttyServer "github.com/elisescu/tty-share/server"
log "github.com/sirupsen/logrus"
)

// This should be updated manually. Most of distro packagers prefer to build golang without
// complex linker flags that could set the version from the outside
var version string = "2.4.1"

func createServer(frontListenAddress string, frontendPath string, pty server.PTYHandler, sessionID string, allowTunneling bool, crossOrigin bool, baseUrlPath string) *server.TTYServer {
const exitCodex509UnknownAuthorityError = 2
const exitCodeGenericError = 1

func createServer(frontListener net.Listener, frontendPath string, pty server.PTYHandler, sessionID string, allowTunneling bool, crossOrigin bool, baseUrlPath string, greetTimeout time.Duration, seats int) *server.TTYServer {
config := ttyServer.TTYServerConfig{
FrontListenAddress: frontListenAddress,
FrontendPath: frontendPath,
PTY: pty,
SessionID: sessionID,
AllowTunneling: allowTunneling,
CrossOrigin: crossOrigin,
BaseUrlPath: baseUrlPath,
FrontListener: frontListener,
FrontendPath: frontendPath,
PTY: pty,
SessionID: sessionID,
AllowTunneling: allowTunneling,
CrossOrigin: crossOrigin,
BaseUrlPath: baseUrlPath,
GreetTimeout: greetTimeout,
Seats: seats,
}

server := ttyServer.NewTTYServer(config)
Expand Down Expand Up @@ -74,7 +84,7 @@ Flags:
}
commandArgs := flag.String("args", "", "[s] The command arguments")
logFileName := flag.String("logfile", "-", "The name of the file to log")
listenAddress := flag.String("listen", "localhost:8000", "[s] tty-server address")
listenAddress := flag.String("listen", "localhost:8000", "[s] tty-server address, \"localhost:0\" to allocate a free port")
versionFlag := flag.Bool("version", false, "Print the tty-share version")
frontendPath := flag.String("frontend-path", "", "[s] The path to the frontend resources. By default, these resources are included in the server binary, so you only need this path if you don't want to use the bundled ones.")
proxyServerAddress := flag.String("tty-proxy", "on.tty-share.com:4567", "[s] Address of the proxy for public facing connections")
Expand All @@ -85,7 +95,10 @@ Flags:
headless := flag.Bool("headless", false, "[s] Don't expect an interactive terminal at stdin")
headlessCols := flag.Int("headless-cols", 80, "[s] Number of cols for the allocated pty when running headless")
headlessRows := flag.Int("headless-rows", 25, "[s] Number of rows for the allocated pty when running headless")
greetTimeout := flag.Duration("greet-timeout", 0, "[s] Maximum duration clients are accepted. Set to 0 for no limit.")
seats := flag.Int("seats", 0, "[s] The number of allowed proxy connections. Zero means as many as possible.")
detachKeys := flag.String("detach-keys", "ctrl-o,ctrl-c", "[c] Sequence of keys to press for closing the connection. Supported: https://godoc.org/github.com/moby/term#pkg-variables.")
insecure := flag.Bool("k", false, "Accept to run with a proxy presenting a invalid certificate.")
allowTunneling := flag.Bool("A", false, "[s] Allow clients to create a TCP tunnel")
tunnelConfig := flag.String("L", "", "[c] TCP tunneling addresses: local_port:remote_host:remote_port. The client will listen on local_port for TCP connections, and will forward those to the from the server side to remote_host:remote_port")
crossOrgin := flag.Bool("cross-origin", false, "[s] Allow cross origin requests to the server")
Expand Down Expand Up @@ -126,11 +139,17 @@ Flags:
if len(args) == 1 {
connectURL := args[0]

client := newTtyShareClient(connectURL, *detachKeys, tunnelConfig)
client := newTtyShareClient(connectURL, *detachKeys, tunnelConfig, *insecure)

err := client.Run()
if err != nil {
if errors.As(err, &x509.UnknownAuthorityError{}) || errors.As(err, &x509.HostnameError{}) {
_, suffix, _ := strings.Cut(err.Error(), ": ")
fmt.Printf("%s\n", suffix)
os.Exit(exitCodex509UnknownAuthorityError)
}
fmt.Printf("Cannot connect to the remote session. Make sure the URL points to a valid tty-share session.\n")
os.Exit(exitCodeGenericError)
}
fmt.Printf("\ntty-share disconnected\n\n")
return
Expand All @@ -139,16 +158,29 @@ Flags:
// tty-share works as a server, from here on
if !isStdinTerminal() && !*headless {
fmt.Printf("Input not a tty\n")
os.Exit(1)
os.Exit(exitCodeGenericError)
}

// allocate a free port if port is zero
listener, err := net.Listen("tcp", *listenAddress)
if err != nil {
log.Errorf("Can't listen on %s: %s\n", *listenAddress, err.Error())
return
}

defer listener.Close()
*listenAddress = listener.Addr().String()

sessionID := ""
publicURL := ""
if *publicSession {
proxy, err := proxy.NewProxyConnection(*listenAddress, *proxyServerAddress, *noTLS)
proxy, err := proxy.NewProxyConnection(*listenAddress, *proxyServerAddress, *noTLS, *insecure)
if err != nil {
log.Errorf("Can't connect to the proxy: %s\n", err.Error())
return
if errors.As(err, &x509.UnknownAuthorityError{}) || errors.As(err, &x509.HostnameError{}) {
os.Exit(exitCodex509UnknownAuthorityError)
}
os.Exit(exitCodeGenericError)
}

go proxy.RunProxy()
Expand All @@ -170,7 +202,7 @@ Flags:
}

ptyMaster := ptyMasterNew(*headless, *headlessCols, *headlessRows)
err := ptyMaster.Start(*commandName, strings.Fields(*commandArgs), envVars)
err = ptyMaster.Start(*commandName, strings.Fields(*commandArgs), envVars)
if err != nil {
log.Errorf("Cannot start the %s command: %s", *commandName, err.Error())
return
Expand Down Expand Up @@ -209,7 +241,7 @@ Flags:
pty = &nilPTY{}
}

server := createServer(*listenAddress, *frontendPath, pty, sessionID, *allowTunneling, *crossOrgin, sanitizedBaseUrlPath)
server := createServer(listener, *frontendPath, pty, sessionID, *allowTunneling, *crossOrgin, sanitizedBaseUrlPath, *greetTimeout, *seats)
if cols, rows, e := ptyMaster.GetWinSize(); e == nil {
server.WindowSize(cols, rows)
}
Expand Down
8 changes: 6 additions & 2 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type proxyConnection struct {
PublicURL string
}

func NewProxyConnection(backConnAddrr, proxyAddr string, noTLS bool) (*proxyConnection, error) {
func NewProxyConnection(backConnAddrr, proxyAddr string, noTLS bool, insecure bool) (*proxyConnection, error) {
var conn net.Conn
var err error

Expand All @@ -44,7 +44,11 @@ func NewProxyConnection(backConnAddrr, proxyAddr string, noTLS bool) (*proxyConn
if err != nil {
return nil, err
}
conn, err = tls.Dial("tcp", proxyAddr, &tls.Config{RootCAs: roots})
tlsConfig := tls.Config{
RootCAs: roots,
InsecureSkipVerify: insecure,
}
conn, err = tls.Dial("tcp", proxyAddr, &tlsConfig)
if err != nil {
return nil, err
}
Expand Down
53 changes: 42 additions & 11 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"path/filepath"
"time"

"github.com/gorilla/mux"
"github.com/gorilla/websocket"
Expand All @@ -36,13 +37,15 @@ type AASessionTemplateModel struct {

// TTYServerConfig is used to configure the tty server before it is started
type TTYServerConfig struct {
FrontListenAddress string
FrontendPath string
PTY PTYHandler
SessionID string
AllowTunneling bool
CrossOrigin bool
BaseUrlPath string
FrontListener net.Listener
FrontendPath string
PTY PTYHandler
SessionID string
AllowTunneling bool
CrossOrigin bool
BaseUrlPath string
GreetTimeout time.Duration
Seats int
}

// TTYServer represents the instance of a tty server
Expand Down Expand Up @@ -90,9 +93,7 @@ func NewTTYServer(config TTYServerConfig) (server *TTYServer) {
server = &TTYServer{
config: config,
}
server.httpServer = &http.Server{
Addr: config.FrontListenAddress,
}
server.httpServer = &http.Server{}
routesHandler := mux.NewRouter()

installHandlers := func(session string) {
Expand Down Expand Up @@ -159,6 +160,20 @@ func (server *TTYServer) handleTTYWebsocket(w http.ResponseWriter, r *http.Reque
w.WriteHeader(http.StatusForbidden)
return
}

switch server.config.Seats {
case 0:
// unlimited
case -1:
log.Warnf("No longer accepting clients")
return
default:
if server.session.ttyProtoConnectionsLen() >= server.config.Seats {
log.Warnf("All seats used (%d)", server.config.Seats)
return
}
}

upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
Expand All @@ -183,6 +198,12 @@ func (server *TTYServer) handleTTYWebsocket(w http.ResponseWriter, r *http.Reque
// On a new connection, ask for a refresh/redraw of the terminal app
server.config.PTY.Refresh()
server.session.HandleWSConnection(conn)

// When a connection ends, close the server if
// there are no more connections and we can't accept new ones.
if server.config.Seats < 0 && server.session.ttyProtoConnectionsLen() == 0 {
server.httpServer.Close()
}
}

func (server *TTYServer) handleTunnelWebsocket(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -288,7 +309,17 @@ func (server *TTYServer) handleWithTemplateHtml(responseWriter http.ResponseWrit
}

func (server *TTYServer) Run() (err error) {
err = server.httpServer.ListenAndServe()
if server.config.GreetTimeout > 0 {
time.AfterFunc(server.config.GreetTimeout, func() {
if server.session.ttyProtoConnectionsLen() == 0 {
server.Stop()
} else {
// stop accepting new connections
server.config.Seats = -1
}
})
}
err = server.httpServer.Serve(server.config.FrontListener)
log.Debug("Server finished")
return
}
Expand Down
11 changes: 9 additions & 2 deletions server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type ttyShareSession struct {
ttyProtoConnections *list.List
isAlive bool
lastWindowSizeMsg MsgTTYWinSize
ptyHandler PTYHandler
ptyHandler PTYHandler
}

func copyList(l *list.List) *list.List {
Expand All @@ -28,7 +28,7 @@ func newTTYShareSession(ptyHandler PTYHandler) *ttyShareSession {

ttyShareSession := &ttyShareSession{
ttyProtoConnections: list.New(),
ptyHandler: ptyHandler,
ptyHandler: ptyHandler,
}

return ttyShareSession
Expand Down Expand Up @@ -72,6 +72,13 @@ func (session *ttyShareSession) forEachReceiverLock(cb func(rcvConn *TTYProtocol
}
}

// ttyProtoConnectionsLen is a thread safe length func for the ttyProtoConnections linked list.
func (session *ttyShareSession) ttyProtoConnectionsLen() int {
session.mainRWLock.RLock()
defer session.mainRWLock.RUnlock()
return session.ttyProtoConnections.Len()
}

// Will run on the TTYReceiver connection go routine (e.g.: on the websockets connection routine)
// When HandleWSConnection will exit, the connection to the TTYReceiver will be closed
func (session *ttyShareSession) HandleWSConnection(wsConn *websocket.Conn) {
Expand Down