diff --git a/README.md b/README.md index 91ec7a3..fb9507b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Configurations can be set via flags or environment variables. To view available | Configuration | Flag | Environment Variable | Default Value | Description | | --- | --- | --- | --- | --- | | Allowed hostnames | `--allowed-hostnames` | `ALLOWED_HOSTNAMES` | `"localhost"` | Comma delimited list of hostnames that are allowed to connect to the websocket | +| Authentication | `--authentication` | `AUTHENTICATION` | `""` | If set, require this *username*:*password* using HTTP Basic Authorization | | Arguments | `--arguments` | `ARGUMENTS` | `"-l"` | Comma delimited list of arguments that should be passed to the target binary | | Command | `--command` | `COMMAND` | `"/bin/bash"` | Absolute path to the binary to run | | Connection error limit | `--connection-error-limit` | `CONNECTION_ERROR_LIMIT` | `10` | Number of times a connection should be re-attempted by the server to the XTerm.js frontend before the connection is considered dead and shut down | diff --git a/cmd/cloudshell/config.go b/cmd/cloudshell/config.go index 83ffd7a..2784de1 100644 --- a/cmd/cloudshell/config.go +++ b/cmd/cloudshell/config.go @@ -14,6 +14,11 @@ var conf = config.Map{ Usage: "comma-delimited list of hostnames that are allowed to connect to the websocket", Shorthand: "H", }, + "authentication": &config.String{ + Default: "", + Usage: "require this username:password using HTTP Basic Authorization", + Shorthand: "A", + }, "arguments": &config.StringSlice{ Default: []string{}, Usage: "comma-delimited list of arguments that should be passed to the terminal command", diff --git a/cmd/cloudshell/main.go b/cmd/cloudshell/main.go index 4b11f67..59dcc3f 100644 --- a/cmd/cloudshell/main.go +++ b/cmd/cloudshell/main.go @@ -41,6 +41,7 @@ func runE(_ *cobra.Command, _ []string) error { connectionErrorLimit := conf.GetInt("connection-error-limit") arguments := conf.GetStringSlice("arguments") allowedHostnames := conf.GetStringSlice("allowed-hostnames") + authentication := conf.GetString("authentication") keepalivePingTimeout := time.Duration(conf.GetInt("keepalive-ping-timeout")) * time.Second maxBufferSizeBytes := conf.GetInt("max-buffer-size-bytes") pathLiveness := conf.GetString("path-liveness") @@ -131,11 +132,17 @@ func runE(_ *cobra.Command, _ []string) error { } }(time.NewTicker(time.Second * 30)) + var handler http.Handler = router + handler = addIncomingRequestLogging(handler) + if authentication != "" { + handler = addAuthentication(handler, authentication) + } + // listen listenOnAddress := fmt.Sprintf("%s:%v", serverAddress, serverPort) server := http.Server{ Addr: listenOnAddress, - Handler: addIncomingRequestLogging(router), + Handler: handler, } log.Infof("starting server on interface:port '%s'...", listenOnAddress) diff --git a/cmd/cloudshell/middleware.go b/cmd/cloudshell/middleware.go index 0011781..14f5bff 100644 --- a/cmd/cloudshell/middleware.go +++ b/cmd/cloudshell/middleware.go @@ -18,3 +18,17 @@ func addIncomingRequestLogging(next http.Handler) http.Handler { createRequestLog(r).Infof("request completed in %vms", float64(duration.Nanoseconds())/1000000) }) } + +func addAuthentication(next http.Handler, authentication string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + + username, password, ok := r.BasicAuth() + if !ok || username + ":" + password != authentication { + http.Error(w, "Unauthorized", 401) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/pkg/xtermjs/utils.go b/pkg/xtermjs/utils.go index 67484a4..b9e8b96 100644 --- a/pkg/xtermjs/utils.go +++ b/pkg/xtermjs/utils.go @@ -23,7 +23,7 @@ func getConnectionUpgrader( return true } } - logger.Warnf("failed to find '%s' in the list of allowed hostnames ('%s')", requesterHostname) + logger.Warnf("failed to find '%s' in the list of allowed hostnames ('%s')", requesterHostname, allowedHostnames) return false }, HandshakeTimeout: 0,