package webui import ( "context" "crypto/subtle" "embed" "fmt" "net" "net/http" "net/url" "html/template" "github.com/ddworken/hishtory/client/data" "github.com/ddworken/hishtory/client/hctx" "github.com/ddworken/hishtory/client/lib" "github.com/google/uuid" ) //go:embed templates var templateFiles embed.FS type webUiData struct { SearchQuery string SearchResults [][]string ColumnNames []string } func getTableRowsForDisplay(ctx context.Context, searchQuery string) ([][]string, error) { results, err := lib.Search(ctx, hctx.GetDb(ctx), searchQuery, 100) if err != nil { return nil, err } return buildTableRows(ctx, results) } func htmx_resultsTable(w http.ResponseWriter, r *http.Request) { searchQuery := r.URL.Query().Get("q") tableRows, err := getTableRowsForDisplay(r.Context(), searchQuery) if err != nil { w.WriteHeader(http.StatusInternalServerError) panic(err) } w.Header().Add("Content-Type", "text/html") w.Header().Add("HX-Replace-Url", getNewUrl(r, searchQuery)) err = getTemplates().ExecuteTemplate(w, "resultsTable.html", webUiData{ SearchQuery: searchQuery, SearchResults: tableRows, ColumnNames: hctx.GetConf(r.Context()).DisplayedColumns, }) if err != nil { w.WriteHeader(http.StatusInternalServerError) panic(err) } } func getNewUrl(r *http.Request, searchQuery string) string { urlStr := r.Header.Get("Hx-Current-Url") if urlStr == "" { // In this function we purposefully want to silence any errors since updating the URL is non-critical, so // we always return an empty string rather than handling the error. return "" } url, err := url.Parse(urlStr) if err != nil { return "" } q := url.Query() q.Set("q", searchQuery) url.RawQuery = q.Encode() return url.String() } func webuiHandler(w http.ResponseWriter, r *http.Request) { searchQuery := r.URL.Query().Get("q") tableRows, err := getTableRowsForDisplay(r.Context(), searchQuery) if err != nil { w.WriteHeader(http.StatusInternalServerError) panic(err) } w.Header().Add("Content-Type", "text/html") err = getTemplates().ExecuteTemplate(w, "webui.html", webUiData{ SearchQuery: searchQuery, SearchResults: tableRows, ColumnNames: hctx.GetConf(r.Context()).DisplayedColumns, }) if err != nil { w.WriteHeader(http.StatusInternalServerError) panic(err) } } func getTemplates() *template.Template { return template.Must(template.ParseFS(templateFiles, "templates/*")) } func buildTableRows(ctx context.Context, entries []*data.HistoryEntry) ([][]string, error) { columnNames := hctx.GetConf(ctx).DisplayedColumns ret := make([][]string, 0) for _, entry := range entries { row, err := lib.BuildTableRow(ctx, columnNames, *entry, func(s string) string { return s }) if err != nil { return nil, err } ret = append(ret, row) } return ret, nil } func withBasicAuth(expectedUsername, expectedPassword string) func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, hasCreds := r.BasicAuth() if !hasCreds || !secureStringEquals(username, expectedUsername) || !secureStringEquals(password, expectedPassword) { w.Header().Add("WWW-Authenticate", "Basic realm=\"User Visible Realm\"") w.WriteHeader(401) return } h.ServeHTTP(w, r) }) } } func secureStringEquals(s1, s2 string) bool { return subtle.ConstantTimeCompare([]byte(s1), []byte(s2)) == 1 } func StartWebUiServer(ctx context.Context, port int, 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 overridenUsername != "" && overridenPassword != "" { username = overridenUsername password = overridenPassword } 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 }, Addr: fmt.Sprintf(":%d", port), } fmt.Printf("Starting web server on %s...\n", server.Addr) fmt.Printf("Username: %s\nPassword: %s\n", username, password) return server.ListenAndServe() }