2015-08-16 09:47:23 +00:00
package app
import (
2015-08-19 11:35:04 +00:00
"crypto/rand"
2015-09-28 15:22:39 +00:00
"crypto/tls"
"crypto/x509"
2015-08-20 06:40:38 +00:00
"encoding/base64"
2015-10-05 07:50:13 +00:00
"encoding/json"
2015-08-23 11:40:18 +00:00
"errors"
2015-08-23 13:55:06 +00:00
"io/ioutil"
2015-08-16 09:47:23 +00:00
"log"
2015-08-19 11:35:04 +00:00
"math/big"
2015-08-23 05:04:31 +00:00
"net"
2015-08-16 09:47:23 +00:00
"net/http"
2015-08-23 20:34:56 +00:00
"net/url"
2015-08-19 11:35:04 +00:00
"strconv"
2015-08-16 09:47:23 +00:00
"strings"
2015-09-30 14:48:34 +00:00
"sync"
2017-01-09 03:01:30 +00:00
"sync/atomic"
"time"
2015-08-16 09:47:23 +00:00
2017-01-09 16:13:36 +00:00
"github.com/yudai/gotty/backends"
2017-01-09 14:06:46 +00:00
"github.com/yudai/gotty/utils"
2015-08-24 10:22:25 +00:00
"github.com/braintree/manners"
2015-08-16 09:47:23 +00:00
"github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/websocket"
2015-10-08 05:32:49 +00:00
"github.com/yudai/umutex"
2015-08-16 09:47:23 +00:00
)
2015-10-05 07:50:13 +00:00
type InitMessage struct {
Arguments string ` json:"Arguments,omitempty" `
AuthToken string ` json:"AuthToken,omitempty" `
}
2015-08-16 09:47:23 +00:00
type App struct {
2017-01-09 16:13:36 +00:00
manager backends . ClientContextManager
2015-08-27 06:23:54 +00:00
options * Options
2015-08-21 09:22:08 +00:00
2015-09-01 06:04:14 +00:00
upgrader * websocket . Upgrader
server * manners . GracefulServer
2015-08-23 13:55:06 +00:00
2015-10-08 05:32:49 +00:00
onceMutex * umutex . UnblockingMutex
2017-01-09 03:01:30 +00:00
timer * time . Timer
2016-08-13 07:29:21 +00:00
2017-01-09 03:01:30 +00:00
// clientContext writes concurrently
// Use atomic operations.
connections * int64
2015-08-21 03:48:07 +00:00
}
type Options struct {
2017-01-09 14:06:46 +00:00
Address string ` hcl:"address" flagName:"address" flagSName:"a" flagDescribe:"IP address to listen" default:"" `
Port string ` hcl:"port" flagName:"port" flagSName:"p" flagDescribe:"Port number to liten" default:"8080" `
PermitWrite bool ` hcl:"permit_write" flagName:"permit-write" flagSName:"w" flagDescribe:"Permit clients to write to the TTY (BE CAREFUL)" default:"false" `
EnableBasicAuth bool ` hcl:"enable_basic_auth" default:"false" `
Credential string ` hcl:"credential" flagName:"credential" flagSName:"c" flagDescribe:"Credential for Basic Authentication (ex: user:pass, default disabled)" default:"" `
EnableRandomUrl bool ` hcl:"enable_random_url flagName:"random-url" flagSName:"r" flagDescribe:"Add a random string to the URL"" default:"false" `
RandomUrlLength int ` hcl:"random_url_length" flagName:"random-url-length" flagDescribe:"Random URL length" default:"8" `
IndexFile string ` hcl:"index_file" flagName:"index" flagDescribe:"Custom index.html file" default:"" `
EnableTLS bool ` hcl:"enable_tls" flagName:"tls" flagSName:"t" flagDescribe:"Enable TLS/SSL" default:"false" `
TLSCrtFile string ` hcl:"tls_crt_file" flagName:"tls-crt" flagDescribe:"TLS/SSL certificate file path" default:"~/.gotty.crt" `
TLSKeyFile string ` hcl:"tls_key_file" flagName:"tls-key" flagDescribe:"TLS/SSL key file path" default:"~/.gotty.key" `
EnableTLSClientAuth bool ` hcl:"enable_tls_client_auth" default:"false" `
TLSCACrtFile string ` hcl:"tls_ca_crt_file" flagName:"tls-ca-crt" flagDescribe:"TLS/SSL CA certificate file for client certifications" default:"~/.gotty.ca.crt" `
EnableReconnect bool ` hcl:"enable_reconnect" flagName:"reconnect" flagDescribe:"Enable reconnection" default:"false" `
ReconnectTime int ` hcl:"reconnect_time" flagName:"reconnect-time" flagDescribe:"Time to reconnect" default:"10" `
MaxConnection int ` hcl:"max_connection" flagName:"max-connection" flagDescribe:"Maximum connection to gotty" default:"0" `
Once bool ` hcl:"once" flagName:"once" flagDescribe:"Accept only one client and exit on disconnection" default:"false" `
Timeout int ` hcl:"timeout" flagName:"timeout" flagDescribe:"Timeout seconds for waiting a client(0 to disable)" default:"0" `
PermitArguments bool ` hcl:"permit_arguments" flagName:"permit-arguments" flagDescribe:"Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)" default:"true" `
2015-10-12 01:24:46 +00:00
Preferences HtermPrefernces ` hcl:"preferences" `
RawPreferences map [ string ] interface { } ` hcl:"preferences" `
2017-01-09 14:06:46 +00:00
Width int ` hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0" `
Height int ` hcl:"height" flagName:"height" flagDescribe:"Static height of the screen, 0(default) means dynamically resize" default:"0" `
2015-08-16 09:47:23 +00:00
}
2017-05-21 05:38:03 +00:00
var Version = "1.0.0"
2015-10-02 08:01:37 +00:00
2017-01-09 16:13:36 +00:00
func New ( manager backends . ClientContextManager , options * Options ) ( * App , error ) {
2017-01-09 03:01:30 +00:00
connections := int64 ( 0 )
2015-08-24 07:13:22 +00:00
return & App {
2017-01-09 16:13:36 +00:00
manager : manager ,
2015-08-24 07:13:22 +00:00
options : options ,
upgrader : & websocket . Upgrader {
ReadBufferSize : 1024 ,
WriteBufferSize : 1024 ,
Subprotocols : [ ] string { "gotty" } ,
} ,
2017-01-09 03:01:30 +00:00
onceMutex : umutex . New ( ) ,
connections : & connections ,
2015-08-24 07:13:22 +00:00
} , nil
}
2015-10-05 07:08:25 +00:00
func CheckConfig ( options * Options ) error {
if options . EnableTLSClientAuth && ! options . EnableTLS {
return errors . New ( "TLS client authentication is enabled, but TLS is not enabled" )
}
return nil
}
2015-08-16 09:47:23 +00:00
func ( app * App ) Run ( ) error {
2015-08-23 12:00:52 +00:00
if app . options . PermitWrite {
log . Printf ( "Permitting clients to write input to the PTY." )
}
2015-08-31 06:54:34 +00:00
if app . options . Once {
log . Printf ( "Once option is provided, accepting only one client" )
}
2015-08-23 05:04:31 +00:00
path := ""
2015-08-27 06:23:54 +00:00
if app . options . EnableRandomUrl {
path += "/" + generateRandomString ( app . options . RandomUrlLength )
2015-08-19 11:35:04 +00:00
}
2015-08-23 20:22:47 +00:00
endpoint := net . JoinHostPort ( app . options . Address , app . options . Port )
2015-08-22 03:51:37 +00:00
2015-08-23 05:04:31 +00:00
wsHandler := http . HandlerFunc ( app . handleWS )
2015-08-31 06:54:34 +00:00
customIndexHandler := http . HandlerFunc ( app . handleCustomIndex )
authTokenHandler := http . HandlerFunc ( app . handleAuthToken )
2015-08-22 03:51:37 +00:00
staticHandler := http . FileServer (
2015-08-23 05:04:31 +00:00
& assetfs . AssetFS { Asset : Asset , AssetDir : AssetDir , Prefix : "static" } ,
2015-08-22 03:51:37 +00:00
)
var siteMux = http . NewServeMux ( )
2015-08-29 04:12:04 +00:00
if app . options . IndexFile != "" {
log . Printf ( "Using index file at " + app . options . IndexFile )
2015-08-31 06:54:34 +00:00
siteMux . Handle ( path + "/" , customIndexHandler )
2015-08-29 04:12:04 +00:00
} else {
siteMux . Handle ( path + "/" , http . StripPrefix ( path + "/" , staticHandler ) )
}
2015-08-31 06:54:34 +00:00
siteMux . Handle ( path + "/auth_token.js" , authTokenHandler )
2015-08-29 04:12:04 +00:00
siteMux . Handle ( path + "/js/" , http . StripPrefix ( path + "/" , staticHandler ) )
2015-08-30 07:27:40 +00:00
siteMux . Handle ( path + "/favicon.png" , http . StripPrefix ( path + "/" , staticHandler ) )
2015-08-22 03:51:37 +00:00
siteHandler := http . Handler ( siteMux )
2015-08-27 06:23:54 +00:00
if app . options . EnableBasicAuth {
2015-08-22 03:51:37 +00:00
log . Printf ( "Using Basic Authentication" )
siteHandler = wrapBasicAuth ( siteHandler , app . options . Credential )
2015-08-20 06:40:38 +00:00
}
2015-08-22 03:51:37 +00:00
2015-10-02 08:01:37 +00:00
siteHandler = wrapHeaders ( siteHandler )
2015-08-31 06:54:34 +00:00
wsMux := http . NewServeMux ( )
wsMux . Handle ( "/" , siteHandler )
wsMux . Handle ( path + "/ws" , wsHandler )
siteHandler = ( http . Handler ( wsMux ) )
2015-08-22 03:51:37 +00:00
siteHandler = wrapLogger ( siteHandler )
2015-08-24 07:43:03 +00:00
scheme := "http"
if app . options . EnableTLS {
scheme = "https"
}
2015-08-22 04:10:08 +00:00
if app . options . Address != "" {
2015-08-23 20:34:56 +00:00
log . Printf (
2015-08-24 07:43:03 +00:00
"URL: %s" ,
( & url . URL { Scheme : scheme , Host : endpoint , Path : path + "/" } ) . String ( ) ,
2015-08-23 20:34:56 +00:00
)
2015-08-22 04:10:08 +00:00
} else {
for _ , address := range listAddresses ( ) {
2015-08-23 20:34:56 +00:00
log . Printf (
"URL: %s" ,
( & url . URL {
2015-08-24 07:43:03 +00:00
Scheme : scheme ,
2015-08-23 20:34:56 +00:00
Host : net . JoinHostPort ( address , app . options . Port ) ,
Path : path + "/" ,
} ) . String ( ) ,
)
2015-08-22 04:10:08 +00:00
}
}
2015-08-24 07:43:03 +00:00
2015-10-05 07:08:25 +00:00
server , err := app . makeServer ( endpoint , & siteHandler )
if err != nil {
return errors . New ( "Failed to build server: " + err . Error ( ) )
2015-09-28 15:22:39 +00:00
}
2015-08-24 10:22:25 +00:00
app . server = manners . NewWithServer (
2015-09-28 15:22:39 +00:00
server ,
2015-08-24 10:22:25 +00:00
)
2015-10-05 07:08:25 +00:00
2017-01-09 03:01:30 +00:00
if app . options . Timeout > 0 {
app . timer = time . NewTimer ( time . Duration ( app . options . Timeout ) * time . Second )
go func ( ) {
<- app . timer . C
app . Exit ( )
} ( )
}
2015-08-24 07:43:03 +00:00
if app . options . EnableTLS {
2017-01-09 14:06:46 +00:00
crtFile := utils . ExpandHomeDir ( app . options . TLSCrtFile )
keyFile := utils . ExpandHomeDir ( app . options . TLSKeyFile )
2015-08-30 22:16:34 +00:00
log . Printf ( "TLS crt file: " + crtFile )
log . Printf ( "TLS key file: " + keyFile )
2015-10-05 07:08:25 +00:00
2015-08-30 22:16:34 +00:00
err = app . server . ListenAndServeTLS ( crtFile , keyFile )
2015-08-24 07:43:03 +00:00
} else {
2015-08-24 10:22:25 +00:00
err = app . server . ListenAndServe ( )
2015-08-24 07:43:03 +00:00
}
if err != nil {
2015-08-16 09:47:23 +00:00
return err
}
2015-08-24 10:22:25 +00:00
log . Printf ( "Exiting..." )
2015-08-16 09:47:23 +00:00
return nil
}
2015-10-05 07:08:25 +00:00
func ( app * App ) makeServer ( addr string , handler * http . Handler ) ( * http . Server , error ) {
server := & http . Server {
Addr : addr ,
Handler : * handler ,
}
if app . options . EnableTLSClientAuth {
2017-01-09 14:06:46 +00:00
caFile := utils . ExpandHomeDir ( app . options . TLSCACrtFile )
2015-10-05 07:08:25 +00:00
log . Printf ( "CA file: " + caFile )
caCert , err := ioutil . ReadFile ( caFile )
if err != nil {
return nil , errors . New ( "Could not open CA crt file " + caFile )
}
caCertPool := x509 . NewCertPool ( )
if ! caCertPool . AppendCertsFromPEM ( caCert ) {
return nil , errors . New ( "Could not parse CA crt file data in " + caFile )
}
tlsConfig := & tls . Config {
ClientCAs : caCertPool ,
ClientAuth : tls . RequireAndVerifyClientCert ,
}
server . TLSConfig = tlsConfig
}
return server , nil
}
2017-01-09 03:01:30 +00:00
func ( app * App ) stopTimer ( ) {
if app . options . Timeout > 0 {
app . timer . Stop ( )
}
}
func ( app * App ) restartTimer ( ) {
if app . options . Timeout > 0 {
app . timer . Reset ( time . Duration ( app . options . Timeout ) * time . Second )
}
}
2015-08-22 03:51:37 +00:00
func ( app * App ) handleWS ( w http . ResponseWriter , r * http . Request ) {
2017-01-09 03:01:30 +00:00
app . stopTimer ( )
connections := atomic . AddInt64 ( app . connections , 1 )
2017-01-09 16:13:36 +00:00
defer func ( ) {
connections := atomic . AddInt64 ( app . connections , - 1 )
if app . options . MaxConnection != 0 {
log . Printf ( "Connection closed: %s, connections: %d/%d" ,
r . RemoteAddr , connections , app . options . MaxConnection )
} else {
log . Printf ( "Connection closed: %s, connections: %d" ,
r . RemoteAddr , connections )
}
if connections == 0 {
app . restartTimer ( )
}
} ( )
2017-01-09 03:01:30 +00:00
if int64 ( app . options . MaxConnection ) != 0 {
2017-08-08 07:44:26 +00:00
if connections > int64 ( app . options . MaxConnection ) {
2016-08-13 07:29:21 +00:00
log . Printf ( "Reached max connection: %d" , app . options . MaxConnection )
return
}
}
2015-08-21 09:22:08 +00:00
log . Printf ( "New client connected: %s" , r . RemoteAddr )
2015-08-16 09:47:23 +00:00
2015-08-21 09:22:08 +00:00
if r . Method != "GET" {
http . Error ( w , "Method not allowed" , 405 )
return
}
2015-08-16 09:47:23 +00:00
2015-08-21 09:22:08 +00:00
conn , err := app . upgrader . Upgrade ( w , r , nil )
if err != nil {
2015-09-20 04:41:24 +00:00
log . Print ( "Failed to upgrade connection: " + err . Error ( ) )
2015-08-21 09:22:08 +00:00
return
}
2017-01-09 16:13:36 +00:00
defer conn . Close ( )
2015-08-21 09:22:08 +00:00
2015-10-05 07:50:13 +00:00
_ , stream , err := conn . ReadMessage ( )
if err != nil {
2015-08-31 06:54:34 +00:00
log . Print ( "Failed to authenticate websocket connection" )
return
}
2015-10-05 07:50:13 +00:00
var init InitMessage
2015-08-31 06:54:34 +00:00
2015-10-05 07:50:13 +00:00
err = json . Unmarshal ( stream , & init )
if err != nil {
log . Printf ( "Failed to parse init message %v" , err )
return
}
if init . AuthToken != app . options . Credential {
log . Print ( "Failed to authenticate websocket connection" )
return
}
2017-01-09 16:13:36 +00:00
var queryPath string
if app . options . PermitArguments && init . Arguments != "" {
queryPath = init . Arguments
} else {
queryPath = "?"
}
query , err := url . Parse ( queryPath )
if err != nil {
log . Print ( "Failed to parse arguments" )
return
}
params := query . Query ( )
ctx , err := app . manager . New ( params )
if err != nil {
log . Printf ( "Failed to new client context %v" , err )
return
2015-10-05 07:50:13 +00:00
}
2015-10-08 05:32:49 +00:00
app . server . StartRoutine ( )
2017-01-09 16:13:36 +00:00
defer app . server . FinishRoutine ( )
2015-10-08 05:32:49 +00:00
if app . options . Once {
if app . onceMutex . TryLock ( ) { // no unlock required, it will die soon
log . Printf ( "Last client accepted, closing the listener." )
app . server . Close ( )
} else {
log . Printf ( "Server is already closing." )
conn . Close ( )
return
}
}
2017-01-09 16:13:36 +00:00
context := & clientContext { app : app , connection : conn , writeMutex : & sync . Mutex { } , ClientContext : ctx }
2015-08-21 09:51:43 +00:00
context . goHandleClient ( )
2015-08-21 09:22:08 +00:00
}
2015-08-31 06:54:34 +00:00
func ( app * App ) handleCustomIndex ( w http . ResponseWriter , r * http . Request ) {
2017-01-09 14:06:46 +00:00
http . ServeFile ( w , r , utils . ExpandHomeDir ( app . options . IndexFile ) )
2015-08-31 06:54:34 +00:00
}
func ( app * App ) handleAuthToken ( w http . ResponseWriter , r * http . Request ) {
2016-06-24 19:36:04 +00:00
w . Header ( ) . Set ( "Content-Type" , "application/javascript" )
2015-09-01 06:04:14 +00:00
w . Write ( [ ] byte ( "var gotty_auth_token = '" + app . options . Credential + "';" ) )
2015-08-31 06:54:34 +00:00
}
2015-08-24 10:22:25 +00:00
func ( app * App ) Exit ( ) ( firstCall bool ) {
if app . server != nil {
2015-09-01 06:07:04 +00:00
firstCall = app . server . Close ( )
if firstCall {
log . Printf ( "Received Exit command, waiting for all clients to close sessions..." )
}
return firstCall
2015-08-24 10:22:25 +00:00
}
return true
}
2015-08-22 03:51:37 +00:00
func wrapLogger ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2015-09-20 04:41:24 +00:00
rw := & responseWrapper { w , 200 }
handler . ServeHTTP ( rw , r )
log . Printf ( "%s %d %s %s" , r . RemoteAddr , rw . status , r . Method , r . URL . Path )
2015-08-22 03:51:37 +00:00
} )
}
2015-10-02 08:01:37 +00:00
func wrapHeaders ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Server" , "GoTTY/" + Version )
handler . ServeHTTP ( w , r )
} )
}
2015-08-22 03:51:37 +00:00
func wrapBasicAuth ( handler http . Handler , credential string ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
token := strings . SplitN ( r . Header . Get ( "Authorization" ) , " " , 2 )
if len ( token ) != 2 || strings . ToLower ( token [ 0 ] ) != "basic" {
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="GoTTY" ` )
http . Error ( w , "Bad Request" , http . StatusUnauthorized )
return
}
payload , err := base64 . StdEncoding . DecodeString ( token [ 1 ] )
if err != nil {
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
return
}
if credential != string ( payload ) {
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="GoTTY" ` )
http . Error ( w , "authorization failed" , http . StatusUnauthorized )
return
}
log . Printf ( "Basic Authentication Succeeded: %s" , r . RemoteAddr )
handler . ServeHTTP ( w , r )
} )
}
2015-08-21 09:22:08 +00:00
func generateRandomString ( length int ) string {
const base = 36
size := big . NewInt ( base )
n := make ( [ ] byte , length )
for i , _ := range n {
c , _ := rand . Int ( rand . Reader , size )
n [ i ] = strconv . FormatInt ( c . Int64 ( ) , base ) [ 0 ]
2015-08-16 09:47:23 +00:00
}
2015-08-21 09:22:08 +00:00
return string ( n )
2015-08-16 09:47:23 +00:00
}
2015-08-22 04:10:08 +00:00
func listAddresses ( ) ( addresses [ ] string ) {
ifaces , _ := net . Interfaces ( )
addresses = make ( [ ] string , 0 , len ( ifaces ) )
for _ , iface := range ifaces {
ifAddrs , _ := iface . Addrs ( )
for _ , ifAddr := range ifAddrs {
switch v := ifAddr . ( type ) {
case * net . IPNet :
2015-08-23 20:22:47 +00:00
addresses = append ( addresses , v . IP . String ( ) )
2015-08-22 04:10:08 +00:00
case * net . IPAddr :
2015-08-23 20:05:58 +00:00
addresses = append ( addresses , v . IP . String ( ) )
2015-08-22 04:10:08 +00:00
}
}
}
return
}