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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ By default, GoTTY starts a web server at port 8080. Open the URL on your web bro
--port value, -p value Port number to liten (default: "8080") [$GOTTY_PORT]
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
--authenticator value Script for Basic Authentication. The request information, in JSON format, can be obtained from reading the STDIN. (ex: ./auth.sh) [$GOTTY_AUTHETICATOR]
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
--tls, -t Enable TLS/SSL [$GOTTY_TLS]
Expand All @@ -71,6 +72,7 @@ By default, GoTTY starts a web server at port 8080. Open the URL on your web bro
--term value Terminal name to use on the browser, one of xterm or hterm. (default: "xterm") [$GOTTY_TERM]
--close-signal value Signal sent to the command process when gotty close it (default: SIGHUP) (default: 1) [$GOTTY_CLOSE_SIGNAL]
--close-timeout value Time in seconds to force kill process after client is disconnected (default: -1) (default: -1) [$GOTTY_CLOSE_TIMEOUT]
--sys-auth-tpl Print Linux system authenticator script example [$GOTTY_SYS_AUTH_TPL]
--config value Config file path (default: "~/.gotty") [$GOTTY_CONFIG]
--version, -v print the version
```
Expand Down
2 changes: 2 additions & 0 deletions backend/localcommand/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
type Options struct {
CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagSName:"" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"`
CloseTimeout int `hcl:"close_timeout" flagName:"close-timeout" flagSName:"" flagDescribe:"Time in seconds to force kill process after client is disconnected (default: -1)" default:"-1"`

PrintLinuxSystemAuthenticatorTemplate bool `flagName:"sys-auth-tpl" flagSName:"" flagDescribe:"Print Linux system authenticator script example" default:"false"`
}

type Factory struct {
Expand Down
34 changes: 34 additions & 0 deletions backend/localcommand/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package localcommand

func LinuxAuthenticatorTemplate() string {
return `#!/bin/bash
################ IMPORTANT NOTE: ###############
# #
# This script requires root access (or sudo) #
# #
# Usage example: #
# sudo gotty --authenticator ./auth.sh top #
# #
################################################

# #### REQUEST INFO ####
# echo REQUEST INFO:
# json_pp

username="$1"
password="$2"

salt=$(getent shadow $username | cut -d$ -f3)
epassword=$(getent shadow $username | cut -d: -f2)

match=$(python -c 'import crypt; print crypt.crypt("'"$password"'", "$6$'$salt'")')

if [ "$match" == "$epassword" ]; then
# success
exit 0
fi

# failed
exit 1
`
}
8 changes: 7 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func main() {
)

app.Action = func(c *cli.Context) {
if c.Bool("sys-auth-tpl") {
os.Stdout.Write([]byte(localcommand.LinuxAuthenticatorTemplate()))
os.Exit(0)
}
if len(c.Args()) == 0 {
msg := "Error: No command given."
cli.ShowAppHelp(c)
Expand All @@ -66,7 +70,9 @@ func main() {

utils.ApplyFlags(cliFlags, flagMappings, c, appOptions, backendOptions)

appOptions.EnableBasicAuth = c.IsSet("credential")
if !appOptions.EnableBasicAuth && (c.IsSet("credential") || c.IsSet("authenticator")) {
appOptions.EnableBasicAuth = true
}
appOptions.EnableTLSClientAuth = c.IsSet("tls-ca-crt")

err = appOptions.Validate()
Expand Down
38 changes: 38 additions & 0 deletions server/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package server

import (
"bytes"
"encoding/json"
"net/http"
)

// requestInfoReader reader of request info with JSON format
type requestInfoReader struct {
Request *http.Request
buf *bytes.Buffer
}

func (this *requestInfoReader) Read(p []byte) (n int, err error) {
if this.buf == nil {
r := this.Request
// request info to pass in STDIN
info := map[string]interface{}{
"Headers": map[string][]string(r.Header),
"RequestURI": r.RequestURI,
"Referer": r.Referer(),
"Url": map[string]interface{}{
"Scheme": r.URL.Scheme,
"Host": r.URL.Host,
"Path": r.URL.Path,
"Fragment": r.URL.Fragment,
"Query": r.URL.Query(),
},
}

// Write data to stdin
var buf bytes.Buffer
_ = json.NewEncoder(&buf).Encode(info)
this.buf = &buf
}
return this.buf.Read(p)
}
89 changes: 89 additions & 0 deletions server/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import (
"encoding/base64"
"log"
"net/http"
"os"
"os/exec"
"strings"
"syscall"
"time"
)

func (server *Server) wrapLogger(handler http.Handler) http.Handler {
Expand Down Expand Up @@ -49,3 +53,88 @@ func (server *Server) wrapBasicAuth(handler http.Handler, credential string) htt
handler.ServeHTTP(w, r)
})
}

func (server *Server) wrapBasicAuthenticator(handler http.Handler, script string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// skip verification for static files
if r.URL.Path != "/" {
handler.ServeHTTP(w, r)
return
}

// authentication failed
failed := func() {
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
http.Error(w, "authorization failed", http.StatusUnauthorized)
}

token := strings.SplitN(r.Header.Get("Authorization"), " ", 2)

if len(token) != 2 || strings.ToLower(token[0]) != "basic" {
failed()
return
}

payload, err := base64.StdEncoding.DecodeString(token[1])
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

// slplit username and password
parts := strings.SplitN(string(payload), ":", 2)

// username or password is empty
if parts[0] == "" || parts[1] == "" {
failed()
return
}

cmd := exec.Command(script, parts...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

cmd.Stdin = &requestInfoReader{Request: r}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Start()
if err != nil {
log.Printf("ERROR: Authenticator start failed: %v", err)
http.Error(w, "Internal Server Error: Authenticator start failed", http.StatusInternalServerError)
return
}

// Use a channel to signal completion so we can use a select statement
done := make(chan error)
go func() { done <- cmd.Wait() }()

// Start a timer
timeout := time.After(3 * time.Second)

// The select statement allows us to execute based on which channel
// we get a message from first.
select {
case <-timeout:
// Timeout happened first, kill the process and print a message.
cmd.Process.Kill()
log.Printf("ERROR: Authenticator timed out")
http.Error(w, "Internal Server Error: Authenticator start failed", http.StatusInternalServerError)
return
case err := <-done:
if err != nil {
status := err.(*exec.ExitError).Sys().(syscall.WaitStatus)
if status.Exited() {
if status.ExitStatus() == 1 {
failed()
return
}
}
log.Printf("ERROR: Authenticator failed: %v", err)
return
}
}

log.Printf("Basic Authentication Succeeded: %s user=%q", r.RemoteAddr, parts[0])
handler.ServeHTTP(w, r)
})
}
1 change: 1 addition & 0 deletions server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Options struct {
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:""`
Authenticator string `hcl:"authenticator" flagName:"authenticator" flagDescribe:"Script for Basic Authentication. The request information, in JSON format, can be obtained from reading the STDIN. (ex: ./auth.sh)" 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"`
EnableTLS bool `hcl:"enable_tls" flagName:"tls" flagSName:"t" flagDescribe:"Enable TLS/SSL" default:"false"`
Expand Down
9 changes: 7 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,13 @@ func (server *Server) setupHandlers(ctx context.Context, cancel context.CancelFu
siteHandler := http.Handler(siteMux)

if server.options.EnableBasicAuth {
log.Printf("Using Basic Authentication")
siteHandler = server.wrapBasicAuth(siteHandler, server.options.Credential)
if server.options.Authenticator == "" {
log.Printf("Using Basic Authentication")
siteHandler = server.wrapBasicAuth(siteHandler, server.options.Credential)
} else {
log.Printf("Using Basic Authentication with %q authenticator script", server.options.Authenticator)
siteHandler = server.wrapBasicAuthenticator(siteHandler, server.options.Authenticator)
}
}

withGz := gziphandler.GzipHandler(server.wrapHeaders(siteHandler))
Expand Down