diff --git a/docs/index.adoc b/docs/index.adoc index cb7c288b..e48edf22 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -180,6 +180,18 @@ image:img/logs.png[logs] You can view recorded videos, accessible on the selenoid, in case there are any on a separate tab. +=== Restricting Access + +You can restrict access to Selenoid UI with a https://en.wikipedia.org/wiki/.htpasswd[htpasswd] file as follows: + +. Create a file with user `test` and password `test-password`: ++ + $ htpasswd -bc /path/to/users.htpasswd test test-password + +. Run Selenoid UI with `-users` flag pointing to this file: ++ + $ ./selenoid-ui -users /path/to/users.htpasswd + === Using With Ggr or Moon By default Selenoid UI considers that the same daemon (Selenoid) is handles Selenium session creation and returns a list of already running sessions at `/status` API. However in more complicated cases like connecting Selenoid UI to a https://github.com/aerokube/ggr[Ggr] or https://github.com/aerokube/moon[Moon] cluster two separate daemons exist: @@ -210,6 +222,8 @@ The following flags are supported by `selenoid-ui` command: status uri to fetch data from -timeout duration response timeout, e.g. 5s or 1m (default 3s) + -users string + users file path -version Show version and exit -webdriver-uri string diff --git a/go.mod b/go.mod index b22b7ba3..b670f3bc 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,13 @@ go 1.15 require ( github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e + github.com/abbot/go-http-auth v0.4.0 github.com/aerokube/util v0.0.0-20181001032247-3a9b3f70da09 github.com/gorilla/websocket v1.4.0 // indirect + github.com/hashicorp/go-version v1.2.1 // indirect github.com/koding/websocketproxy v0.0.0-20180716164433-0fa3f994f6e7 + github.com/mitchellh/gox v1.0.1 // indirect github.com/rakyll/statik v0.1.7 + golang.org/x/crypto v0.0.0-20201002094018-c90954cbb977 // indirect + golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 // indirect ) diff --git a/go.sum b/go.sum index 07f689e1..9dace0fb 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,31 @@ github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e h1:ogUKYFNcdYUIBSLibE4+EjbTJazoHr5JsWWx21Lpn8c= github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e/go.mod h1:cbmYNkm9xeQlNoWEPtOUcvNok2gSD7ErMnYkRW+eHi8= +github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= +github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= github.com/aerokube/util v0.0.0-20181001032247-3a9b3f70da09 h1:T3Y7aciF+tYzT5wXHiwhLQNEozQHiyNDYXYDbi6EN2g= github.com/aerokube/util v0.0.0-20181001032247-3a9b3f70da09/go.mod h1:a1duJwYg3lfxakFPqpJsD41PYdkBmeUFPA4S+ISkHhQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/koding/websocketproxy v0.0.0-20180716164433-0fa3f994f6e7 h1:UPc4az2SLy5Usu+JKfOV4KtfzuRQXXUxY6QOWf9QBJU= github.com/koding/websocketproxy v0.0.0-20180716164433-0fa3f994f6e7/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA= +github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= +github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002094018-c90954cbb977 h1:yH6opeNE+0SY+7pXT4gclZUoKHogXeC2EvOSHGOMGPU= +golang.org/x/crypto v0.0.0-20201002094018-c90954cbb977/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 h1:YfxMZzv3PjGonQYNUaeU2+DhAdqOxerQ30JFB6WgAXo= +golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 905dfe79..07911fb7 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "fmt" + auth "github.com/abbot/go-http-auth" "github.com/aerokube/selenoid-ui/selenoid" "github.com/aerokube/util/sse" "github.com/koding/websocketproxy" @@ -30,6 +31,7 @@ var ( webdriverUriString string statusUriString string allowedOrigin string + users string timeout time.Duration period time.Duration @@ -146,11 +148,17 @@ func init() { flag.StringVar(&webdriverUriString, "webdriver-uri", "", "webdriver uri used to create new sessions") flag.StringVar(&statusUriString, "status-uri", "", "status uri to fetch data from") flag.StringVar(&allowedOrigin, "allowed-origin", "", "comma separated list of allowed Origin headers (use * to allow all)") + flag.StringVar(&users, "users", "", "htpasswd file path containing users information") flag.DurationVar(&timeout, "timeout", 3*time.Second, "response timeout, e.g. 5s or 1m") flag.DurationVar(&period, "period", 5*time.Second, "data refresh period, e.g. 5s or 1m") flag.BoolVar(&version, "version", false, "Show version and exit") flag.Parse() + if version { + showVersion() + os.Exit(0) + } + if webdriverUriString == "" { webdriverUriString = selenoidUri } @@ -176,9 +184,8 @@ func init() { } webdriverURI = wu - if version { - showVersion() - os.Exit(0) + if _, err := os.Stat(users); users != "" && err != nil { + log.Fatalf("[INIT] [Invalid users file: %v]", err) } } @@ -199,6 +206,16 @@ func main() { br.Notify(status) }, period, stop) + mux := mux(broker) + if users != "" { + log.Printf("[INIT] [Reading users from %s]", users) + authenticator := auth.NewBasicAuthenticator( + "Selenoid UI", + auth.HtpasswdFileProvider(users), + ) + mux = auth.JustCheck(authenticator, mux.ServeHTTP) + } + log.Printf("[INIT] [Listening on %s]", listen) - log.Fatal(http.ListenAndServe(listen, mux(broker))) + log.Fatal(http.ListenAndServe(listen, mux)) }