Selaa lähdekoodia

Add variable WEBMASTER_EMAIL and if present, display it on error page (#235)

* Add variable WEBMASTER_EMAIL and if present, display it on error page

    - Adresses issue https://github.com/TecharoHQ/anubis/issues/115

* web: regenerate templates

Signed-off-by: Xe Iaso <me@xeiaso.net>

* update docs

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
eerielili 2 kuukautta sitten
vanhempi
commit
acce3604a4
7 muutettua tiedostoa jossa 84 lisäystä ja 35 poistoa
  1. 2 0
      cmd/anubis/main.go
  2. 1 0
      docs/docs/CHANGELOG.md
  3. 1 0
      docs/docs/admin/installation.mdx
  4. 17 15
      lib/anubis.go
  5. 2 2
      web/index.go
  6. 8 2
      web/index.templ
  7. 53 16
      web/index_templ.go

+ 2 - 0
cmd/anubis/main.go

@@ -58,6 +58,7 @@ var (
 	ogPassthrough            = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
 	ogPassthrough            = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
 	ogTimeToLive             = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
 	ogTimeToLive             = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
 	extractResources         = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
 	extractResources         = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
+	webmasterEmail		 = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
 )
 )
 
 
 func keyFromHex(value string) (ed25519.PrivateKey, error) {
 func keyFromHex(value string) (ed25519.PrivateKey, error) {
@@ -260,6 +261,7 @@ func main() {
 		OGPassthrough:     *ogPassthrough,
 		OGPassthrough:     *ogPassthrough,
 		OGTimeToLive:      *ogTimeToLive,
 		OGTimeToLive:      *ogTimeToLive,
 		Target:            *target,
 		Target:            *target,
+		WebmasterEmail:     *webmasterEmail,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		log.Fatalf("can't construct libanubis.Server: %v", err)
 		log.Fatalf("can't construct libanubis.Server: %v", err)

+ 1 - 0
docs/docs/CHANGELOG.md

@@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added support for [OpenGraph tags](https://ogp.me/) when rendering the challenge page. This allows for social previews to be generated when sharing the challenge page on social media platforms ([#195](https://github.com/TecharoHQ/anubis/pull/195))
 - Added support for [OpenGraph tags](https://ogp.me/) when rendering the challenge page. This allows for social previews to be generated when sharing the challenge page on social media platforms ([#195](https://github.com/TecharoHQ/anubis/pull/195))
 - Added an `--extract-resources` flag to extract static resources to a local folder.
 - Added an `--extract-resources` flag to extract static resources to a local folder.
 - Add noindex flag to all Anubis pages ([#227](https://github.com/TecharoHQ/anubis/issues/227)).
 - Add noindex flag to all Anubis pages ([#227](https://github.com/TecharoHQ/anubis/issues/227)).
+- Added `WEBMASTER_EMAIL` variable, if it is present then display that email address on error pages ([#235](https://github.com/TecharoHQ/anubis/pull/235), [#115](https://github.com/TecharoHQ/anubis/issues/115))
 
 
 ## v1.15.1
 ## v1.15.1
 
 

+ 1 - 0
docs/docs/admin/installation.mdx

@@ -60,6 +60,7 @@ Anubis uses these environment variables for configuration:
 | `SOCKET_MODE`                  | `0770`                  | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets.                                                                                                                                                |
 | `SOCKET_MODE`                  | `0770`                  | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets.                                                                                                                                                |
 | `TARGET`                       | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`.                                                                                                                             |
 | `TARGET`                       | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`.                                                                                                                             |
 | `USE_REMOTE_ADDRESS`           | unset                   | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead.                                                                        |
 | `USE_REMOTE_ADDRESS`           | unset                   | If set to `true`, Anubis will take the client's IP from the network socket. For production deployments, it is expected that a reverse proxy is used in front of Anubis, which pass the IP using headers, instead.                                                                        |
+| `WEBMASTER_EMAIL`              | unset                   | If set, shows a contact email address when rendering error pages. This email address will be how users can get in contact with administrators.                                                                                                                                           |
 
 
 For more detailed information on configuring Open Graph tags, please refer to the [Open Graph Configuration](./configuration/open-graph.mdx) page.
 For more detailed information on configuring Open Graph tags, please refer to the [Open Graph Configuration](./configuration/open-graph.mdx) page.
 
 

+ 17 - 15
lib/anubis.go

@@ -76,6 +76,8 @@ type Options struct {
 	OGPassthrough bool
 	OGPassthrough bool
 	OGTimeToLive  time.Duration
 	OGTimeToLive  time.Duration
 	Target        string
 	Target        string
+
+	WebmasterEmail string
 }
 }
 
 
 func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
 func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
@@ -193,7 +195,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
 	cr, rule, err := s.check(r)
 	cr, rule, err := s.check(r)
 	if err != nil {
 	if err != nil {
 		lg.Error("check failed", "err", err)
 		lg.Error("check failed", "err", err)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -218,7 +220,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
 
 
 		if resp != dnsbl.AllGood {
 		if resp != dnsbl.AllGood {
 			lg.Info("DNSBL hit", "status", resp.String())
 			lg.Info("DNSBL hit", "status", resp.String())
-			templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip))), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
+			templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -233,17 +235,17 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
 		lg.Info("explicit deny")
 		lg.Info("explicit deny")
 		if rule == nil {
 		if rule == nil {
 			lg.Error("rule is nil, cannot calculate checksum")
 			lg.Error("rule is nil, cannot calculate checksum")
-			templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+			templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 			return
 			return
 		}
 		}
 		hash, err := rule.Hash()
 		hash, err := rule.Hash()
 		if err != nil {
 		if err != nil {
 			lg.Error("can't calculate checksum of rule", "err", err)
 			lg.Error("can't calculate checksum of rule", "err", err)
-			templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+			templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 			return
 			return
 		}
 		}
 		lg.Debug("rule hash", "hash", hash)
 		lg.Debug("rule hash", "hash", hash)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("Access Denied: error code %s", hash))), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("Access Denied: error code %s", hash), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
 		return
 		return
 	case config.RuleChallenge:
 	case config.RuleChallenge:
 		lg.Debug("challenge requested")
 		lg.Debug("challenge requested")
@@ -253,7 +255,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	default:
 	default:
 		s.ClearCookie(w)
 		s.ClearCookie(w)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -399,7 +401,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	cr, rule, err := s.check(r)
 	cr, rule, err := s.check(r)
 	if err != nil {
 	if err != nil {
 		lg.Error("check failed", "err", err)
 		lg.Error("check failed", "err", err)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 	lg = lg.With("check_result", cr)
 	lg = lg.With("check_result", cr)
@@ -408,7 +410,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if nonceStr == "" {
 	if nonceStr == "" {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("no nonce")
 		lg.Debug("no nonce")
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -416,7 +418,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if elapsedTimeStr == "" {
 	if elapsedTimeStr == "" {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("no elapsedTime")
 		lg.Debug("no elapsedTime")
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -424,7 +426,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 	if err != nil {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("elapsedTime doesn't parse", "err", err)
 		lg.Debug("elapsedTime doesn't parse", "err", err)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -440,7 +442,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 	if err != nil {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("nonce doesn't parse", "err", err)
 		lg.Debug("nonce doesn't parse", "err", err)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -450,7 +452,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
 	if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("hash does not match", "got", response, "want", calculated)
 		lg.Debug("hash does not match", "got", response, "want", calculated)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
 		failedValidations.Inc()
 		failedValidations.Inc()
 		return
 		return
 	}
 	}
@@ -459,7 +461,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
 	if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
 		s.ClearCookie(w)
 		s.ClearCookie(w)
 		lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
 		lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
 		failedValidations.Inc()
 		failedValidations.Inc()
 		return
 		return
 	}
 	}
@@ -477,7 +479,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 	if err != nil {
 		lg.Error("failed to sign JWT", "err", err)
 		lg.Error("failed to sign JWT", "err", err)
 		s.ClearCookie(w)
 		s.ClearCookie(w)
-		templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT")), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+		templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 		return
 		return
 	}
 	}
 
 
@@ -498,7 +500,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
 
 
 func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
 func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
 	err := r.FormValue("err")
 	err := r.FormValue("err")
-	templ.Handler(web.Base("Oh noes!", web.ErrorPage(err)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
+	templ.Handler(web.Base("Oh noes!", web.ErrorPage(err, s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
 }
 }
 
 
 // Check evaluates the list of rules, and returns the result
 // Check evaluates the list of rules, and returns the result

+ 2 - 2
web/index.go

@@ -16,8 +16,8 @@ func Index() templ.Component {
 	return index()
 	return index()
 }
 }
 
 
-func ErrorPage(msg string) templ.Component {
-	return errorPage(msg)
+func ErrorPage(msg string, mail string) templ.Component {
+	return errorPage(msg, mail)
 }
 }
 
 
 func Bench() templ.Component {
 func Bench() templ.Component {

+ 8 - 2
web/index.templ

@@ -129,7 +129,7 @@ templ index() {
 </div>
 </div>
 }
 }
 
 
-templ errorPage(message string) {
+templ errorPage(message string, mail string) {
 <div class="centered-div">
 <div class="centered-div">
     <img
     <img
             id="image"
             id="image"
@@ -139,7 +139,13 @@ templ errorPage(message string) {
     />
     />
     <p>{ message }.</p>
     <p>{ message }.</p>
     <button onClick="window.location.reload();">Try again</button>
     <button onClick="window.location.reload();">Try again</button>
-    <p><a href="/">Go home</a></p>
+    if mail != "" {
+	    <p><a href="/">Go home</a> or if you believe you should not be blocked, please contact the webmaster at <a href={"mailto:" + templ.SafeURL(mail) }>
+	  { mail }</a>
+	   </p>
+    } else {
+	    <p><a href="/">Go home</a></p>
+   }
 </div>
 </div>
 }
 }
 
 

+ 53 - 16
web/index_templ.go

@@ -204,7 +204,7 @@ func index() templ.Component {
 	})
 	})
 }
 }
 
 
-func errorPage(message string) templ.Component {
+func errorPage(message string, mail string) templ.Component {
 	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
 	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
 		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
 		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
 		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
 		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -251,7 +251,44 @@ func errorPage(message string) templ.Component {
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ".</p><button onClick=\"window.location.reload();\">Try again</button><p><a href=\"/\">Go home</a></p></div>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ".</p><button onClick=\"window.location.reload();\">Try again</button> ")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if mail != "" {
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p><a href=\"/\">Go home</a> or if you believe you should not be blocked, please contact the webmaster at <a href=\"")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var14 templ.SafeURL = "mailto:" + templ.SafeURL(mail)
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var15 string
+			templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(mail)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 144, Col: 9}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></p>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		} else {
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<p><a href=\"/\">Go home</a></p>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
@@ -275,40 +312,40 @@ func bench() templ.Component {
 			}()
 			}()
 		}
 		}
 		ctx = templ.InitializeContext(ctx)
 		ctx = templ.InitializeContext(ctx)
-		templ_7745c5c3_Var14 := templ.GetChildren(ctx)
-		if templ_7745c5c3_Var14 == nil {
-			templ_7745c5c3_Var14 = templ.NopComponent
+		templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var16 == nil {
+			templ_7745c5c3_Var16 = templ.NopComponent
 		}
 		}
 		ctx = templ.ClearChildren(ctx)
 		ctx = templ.ClearChildren(ctx)
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		var templ_7745c5c3_Var15 string
-		templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
+		var templ_7745c5c3_Var17 string
+		templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
 			anubis.Version)
 			anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 22}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 175, Col: 22}
 		}
 		}
-		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		var templ_7745c5c3_Var16 string
-		templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(
+		var templ_7745c5c3_Var18 string
+		templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(
 			"/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
 			"/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 173, Col: 89}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 179, Col: 89}
 		}
 		}
-		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
 		if templ_7745c5c3_Err != nil {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 			return templ_7745c5c3_Err
 		}
 		}