diff --git a/client/client_test.go b/client/client_test.go index 1f0eb5a..f312f41 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2971,14 +2971,14 @@ func TestWebUi(t *testing.T) { tester.RunInteractiveShell(t, `echo foobar`) // Start the server - require.NoError(t, tester.RunInteractiveShellBackground(t, `hishtory start-web-ui`)) + require.NoError(t, tester.RunInteractiveShellBackground(t, `hishtory start-web-ui --force-creds hishtory:my_password`)) time.Sleep(time.Second) defer tester.RunInteractiveShell(t, `killall hishtory`) // And check that the server seems to be returning valid data req, err := http.NewRequest("GET", "http://localhost:8000?q=foobar", nil) require.NoError(t, err) - req.SetBasicAuth("hishtory", hctx.GetConf(hctx.MakeContext()).UserSecret) + req.SetBasicAuth("hishtory", "my_password") resp, err := http.DefaultClient.Do(req) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) diff --git a/client/cmd/webui.go b/client/cmd/webui.go index 8607c32..5469a56 100644 --- a/client/cmd/webui.go +++ b/client/cmd/webui.go @@ -1,7 +1,9 @@ package cmd import ( + "fmt" "os" + "strings" "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" @@ -9,15 +11,34 @@ import ( "github.com/spf13/cobra" ) +var disableAuth *bool +var forceCreds *string + var webUiCmd = &cobra.Command{ Use: "start-web-ui", Short: "Serve a basic web UI for interacting with your shell history", Run: func(cmd *cobra.Command, args []string) { - lib.CheckFatalError(webui.StartWebUiServer(hctx.MakeContext())) + overridenUsername := "" + overridenPassword := "" + if *forceCreds != "" { + if strings.Contains(*forceCreds, ":") { + splitCreds := strings.SplitN(*forceCreds, ":", 2) + overridenUsername = splitCreds[0] + overridenPassword = splitCreds[1] + } else { + lib.CheckFatalError(fmt.Errorf("--force-creds=%#v doesn't contain a colon to delimit username and password", *forceCreds)) + } + } + if *disableAuth && *forceCreds != "" { + lib.CheckFatalError(fmt.Errorf("cannot specify both --disable-auth and --force-creds")) + } + lib.CheckFatalError(webui.StartWebUiServer(hctx.MakeContext(), *disableAuth, overridenUsername, overridenPassword)) os.Exit(1) }, } func init() { rootCmd.AddCommand(webUiCmd) + disableAuth = webUiCmd.Flags().Bool("disable-auth", false, "Disable authentication for the Web UI (Warning: This means your entire shell history will be accessible from the local web server)") + forceCreds = webUiCmd.Flags().String("force-creds", "", "Specify the credentials to use for basic auth in the form `user:password`") } diff --git a/client/webui/webui.go b/client/webui/webui.go index d5ed468..837edf3 100644 --- a/client/webui/webui.go +++ b/client/webui/webui.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "net/url" - "os" "html/template" @@ -127,19 +126,21 @@ func secureStringEquals(s1, s2 string) bool { return subtle.ConstantTimeCompare([]byte(s1), []byte(s2)) == 1 } -func StartWebUiServer(ctx context.Context) error { +func StartWebUiServer(ctx context.Context, disableAuth bool, overridenUsername, overridenPassword string) error { username := "hishtory" // Note that uuid.NewRandom() uses crypto/rand and returns a UUID with 122 bits of security password := uuid.Must(uuid.NewRandom()).String() - if os.Getenv("HISHTORY_TEST") != "" { - // For testing, we also support having the password be the secret key. This is still mostly secure, but - // it has the risk of the secret key being exposed over HTTP. It also means that the password doesn't - // rotate with each server instance. This is why we don't prefer this normally, but as a test-only method - // this is still plenty secure. - password = hctx.GetConf(ctx).UserSecret + if overridenUsername != "" && overridenPassword != "" { + username = overridenUsername + password = overridenPassword } - http.Handle("/", withBasicAuth(username, password)(http.HandlerFunc(webuiHandler))) - http.Handle("/htmx/results-table", withBasicAuth(username, password)(http.HandlerFunc(htmx_resultsTable))) + wba := withBasicAuth(username, password) + if disableAuth { + // No-op wrapper that doesn't enforce auth + wba = func(h http.Handler) http.Handler { return h } + } + http.Handle("/", wba(http.HandlerFunc(webuiHandler))) + http.Handle("/htmx/results-table", wba(http.HandlerFunc(htmx_resultsTable))) server := http.Server{ BaseContext: func(l net.Listener) context.Context { return ctx },