From 398675aa3cb1256f6dd696e69c2d91182700c9ec Mon Sep 17 00:00:00 2001 From: WeebDataHoarder Date: Fri, 25 Apr 2025 17:27:23 +0200 Subject: [PATCH] config: Add string replacement for templates, add example config.yml (close #10) --- README.md | 4 + embed/assets/static/anubis/style.css | 14 ++ embed/templates/challenge-anubis.gohtml | 175 +++-------------------- embed/templates/challenge-forgejo.gohtml | 39 +++-- examples/config.yml | 101 +++++++++++++ lib/challenge/script.go | 1 + lib/challenge/script.mjs | 25 ++-- lib/http.go | 36 ----- lib/interface.go | 73 ---------- lib/settings/settings.go | 8 +- lib/settings/strings.go | 43 +++++- lib/template.go | 114 +++++++++++++++ 12 files changed, 323 insertions(+), 310 deletions(-) create mode 100644 examples/config.yml create mode 100644 lib/template.go diff --git a/README.md b/README.md index 8a72b1d..bdb957f 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,10 @@ go-away can take plaintext HTTP/1 and _HTTP/2_ / _h2c_ connections if desired ov We also support the `autocert` parameter to configure HTTP(s). This will also allow TLS Fingerprinting to be done on incoming clients. This doesn't require any upstream proxies, and we recommend it's exposed directly or via SNI / Layer 4 proxying. +### Config + +While most basic configuration can be passed via the command line, we support passing a [config.yml](examples/config.yml) with more advanced setup, including string replacement or custom backends configuration. + ### Binary / Go Requires Go 1.24+. Builds statically without CGo usage. diff --git a/embed/assets/static/anubis/style.css b/embed/assets/static/anubis/style.css index 30d6cd8..1ae6ed3 100644 --- a/embed/assets/static/anubis/style.css +++ b/embed/assets/static/anubis/style.css @@ -103,3 +103,17 @@ footer { padding: 0.5em 10px; } } + +body, +html { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + margin-left: auto; + margin-right: auto; +} + +.centered-div { + text-align: center; +} \ No newline at end of file diff --git a/embed/templates/challenge-anubis.gohtml b/embed/templates/challenge-anubis.gohtml index cb82718..c086e2a 100644 --- a/embed/templates/challenge-anubis.gohtml +++ b/embed/templates/challenge-anubis.gohtml @@ -4,6 +4,7 @@ {{ .Title }} + {{ range $key, $value := .Meta }} {{ if eq $key "refresh"}} @@ -14,132 +15,6 @@ {{ range .HeaderTags }} {{ . }} {{ end }} -
@@ -154,43 +29,29 @@ src="{{ .Path }}/assets/static/logo.png?cacheBust={{ .Random }}" /> {{if .Challenge }} -

Loading challenge {{ .Challenge }}...

+

{{ .Strings.Get "status_loading_challenge" }} {{ .Challenge }}...

{{else if .Error}} -

Error: {{ .Error }}

+

{{ .Strings.Get "status_error" }} {{ .Error }}

{{else}} -

Loading...

+

{{ .Strings.Get "status_loading" }}

{{end}} - {{if not .HideSpinner }} -
-
-
-
-
-
-
-
-
-
- {{end}} -
- Why am I seeing this? -

You are seeing this because the administrator of this website has set up go-away to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.

-

Please note that some challenges requires the use of modern JavaScript features and some plugins may will disable. Please disable such plugins for this domain (for example, JShelter).

-

If you have any issues contact the administrator and provide this Request Id: {{ .Id }}

+
+ {{ .Strings.Get "details_title" }} + + {{.Strings.Get "details_text"}}
- - {{if .Redirect }} - Refresh page + {{ .Strings.Get "button_refresh_page" }} {{end}} -
+ {{if .EndTags }} + + {{end}} + +

{{ .Strings.Get "details_contact_admin_with_request_id" }}: {{ .Id }}

@@ -198,6 +59,10 @@

Protected by go-away :: Request Id {{ .Id }} + + {{ range .Links }} + :: {{ .Name }} + {{ end }}

diff --git a/embed/templates/challenge-forgejo.gohtml b/embed/templates/challenge-forgejo.gohtml index 4b587f3..0dafdbf 100644 --- a/embed/templates/challenge-forgejo.gohtml +++ b/embed/templates/challenge-forgejo.gohtml @@ -8,7 +8,7 @@ {{ .Title }} - + {{ range $key, $value := .Meta }} {{ if eq $key "refresh"}} @@ -61,36 +61,30 @@ {{if .Challenge }} -

Loading challenge {{ .Challenge }}...

+

{{ .Strings.Get "status_loading_challenge" }} {{ .Challenge }}...

{{else if .Error}} -

Error: {{ .Error }}

+

{{ .Strings.Get "status_error" }} {{ .Error }}

{{else}} -

Loading...

+

{{ .Strings.Get "status_loading" }}

{{end}} -
-
- Why am I seeing this? -

You are seeing this because the administrator of this website has set up go-away to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.

-

Please note that some challenges requires the use of modern JavaScript features and some plugins may will disable. Please disable such plugins for this domain (for example, JShelter).

-

If you have any issues contact the administrator and provide the Request Id: {{ .Id }}

+
+ {{ .Strings.Get "details_title" }} + + {{.Strings.Get "details_text"}}
- - {{if .Redirect }} - + {{end}} + -
+

{{ .Strings.Get "details_contact_admin_with_request_id" }}: {{ .Id }}

@@ -106,6 +100,9 @@
diff --git a/examples/config.yml b/examples/config.yml new file mode 100644 index 0000000..b6132af --- /dev/null +++ b/examples/config.yml @@ -0,0 +1,101 @@ +# Configuration file +# Parameters that exist both on config and cmdline will have cmdline as preference + +bind: + #address: ":8080" + #network: "tcp" + #socket-mode": "0770" + + # Enable PROXY mode on this listener, to allow passing origin info. Default false + #proxy: true + + # Enable passthrough mode, which will allow traffic onto the backends while rules load. Default false + #passthrough: true + + # Enable TLS on this listener and obtain certificates via an ACME directory URL, or letsencrypt + #tls-acme-autocert: "letsencrypt" + + # Enable TLS on this listener and obtain certificates via a certificate and key file on disk + # Only set one of tls-acme-autocert or tls-certificate+tls-key + #tls-certificate: "" + #tls-key: "" + +# Bind the Go debug port +#bind-debug: ":6060" + +# Bind the Prometheus metrics onto /metrics path on this port +#bind-metrics ":9090" + +# These links will be shown on the presented challenge or error pages +links: + #- name: Privacy + # url: "/privacy.html" + + #- name: Contact + # url: "mailto:admin@example.com" + + #- name: Donations + # url: "https://donations.example.com/abcd" + +# HTML Template to use for challenge or error pages +# External templates can be included by providing a disk path +# Bundled templates: +# anubis: An Anubis-like template with no configuration parameters +# forgejo: Looks like native Forgejo. Includes logos and resources from your instance. Supports Theme. +# +#challenge-template: "anubis" + +# Allows overriding specific settings set on templates. Key-Values will be passed to templates as-is +challenge-template-overrides: + # Set template theme if supported + #Theme: "forgejo-auto" + +# Advanced backend configuration +# Backends setup via cmdline will be added here +backends: + # Example HTTP backend and setting client ip header + #"git.example.com": + # url: "http://forgejo:3000" + # ip-header: "X-Client-Ip" + + + # Example HTTPS backend with host/SNI override, HTTP/2 and no certificate verification + #"ssl.example.com": + # url: "https://127.0.0.1:8443" + # host: ssl.example.com + # http2-enabled: true + # tls-skip-verify: true + +# List of strings you can replace to alter the presentation on challenge/error templates +# Can use other languages. +# Note raw HTML is allowed, be careful with it. +# Default strings exist in code, uncomment any to set it +strings: + #title_challenge: "Checking you are not a bot" + #title_error: "Oh no!" + #noscript_warning: "

Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works.

" + #details_title: "Why am I seeing this?" + #details_text: > + #

+ # You are seeing this because the administrator of this website has set up go-away + # to protect the server against the scourge of AI companies aggressively scraping websites. + #

+ #

+ # Mass scraping can and does cause downtime for the websites, which makes their resources inaccessible for everyone. + #

+ #

+ # Please note that some challenges requires the use of modern JavaScript features and some plugins may disable these. + # Disable such plugins for this domain (for example, JShelter) if you encounter any issues. + #

+ + #details_contact_admin_with_request_id: "If you have any issues contact the site administrator and provide the following Request Id" + + #button_refresh_page: "Refresh page" + + #status_loading_challenge: "Loading challenge" + #status_starting_challenge: "Starting challenge" + #status_loading: "Loading..." + #status_calculating: "Calculating..." + #status_challenge_success: "Challenge success!" + #status_challenge_done_took: "Done! Took" + #status_error: "Error:" \ No newline at end of file diff --git a/lib/challenge/script.go b/lib/challenge/script.go index 14849c5..6699dde 100644 --- a/lib/challenge/script.go +++ b/lib/challenge/script.go @@ -33,6 +33,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat "Random": utils.CacheBust(), "Challenge": reg.Name, "ChallengeScript": script, + "Strings": data.State.Options().Strings, }) if err != nil { //TODO: log diff --git a/lib/challenge/script.mjs b/lib/challenge/script.mjs index 3c74ff2..ec85659 100644 --- a/lib/challenge/script.mjs +++ b/lib/challenge/script.mjs @@ -14,9 +14,8 @@ const u = (url = "", params = {}) => { (async () => { const status = document.getElementById('status'); const title = document.getElementById('title'); - const spinner = document.getElementById('spinner'); - status.innerText = 'Starting challenge {{ .Challenge }}...'; + status.innerText = '{{ .Strings.Get "status_starting_challenge" }} {{ .Challenge }}...'; try { const info = await setup({ @@ -25,15 +24,13 @@ const u = (url = "", params = {}) => { }); if (info != "") { - status.innerText = 'Calculating... ' + info + status.innerText = '{{ .Strings.Get "status_calculating" }} ' + info } else { - status.innerText = 'Calculating...'; + status.innerText = '{{ .Strings.Get "status_calculating" }}'; } } catch (err) { - title.innerHTML = "Oh no!"; - status.innerHTML = `Failed to initialize: ${err.message}`; - spinner.innerHTML = ""; - spinner.style.display = "none"; + title.innerHTML = '{{ .Strings.Get "title_error" }}'; + status.innerHTML = `{{ .Strings.Get "status_error" }} ${err.message}`; return } @@ -44,11 +41,11 @@ const u = (url = "", params = {}) => { const t1 = Date.now(); console.log({ result, info }); - title.innerHTML = "Challenge success!"; + title.innerHTML = '{{ .Strings.Get "status_challenge_success" }}'; if (info != "") { - status.innerHTML = `Done! Took ${t1 - t0}ms, ${info}`; + status.innerHTML = `{{ .Strings.Get "status_challenge_done_took" }} ${t1 - t0}ms, ${info}`; } else { - status.innerHTML = `Done! Took ${t1 - t0}ms`; + status.innerHTML = `{{ .Strings.Get "status_challenge_done_took" }} ${t1 - t0}ms`; } setTimeout(() => { @@ -62,9 +59,7 @@ const u = (url = "", params = {}) => { }); }, 500); } catch (err) { - title.innerHTML = "Oh no!"; - status.innerHTML = `Failed to challenge: ${err.message}`; - spinner.innerHTML = ""; - spinner.style.display = "none"; + title.innerHTML = '{{ .Strings.Get "title_error" }}'; + status.innerHTML = `{{ .Strings.Get "status_error" }} ${err.message}`; } })(); \ No newline at end of file diff --git a/lib/http.go b/lib/http.go index be231c2..eace837 100644 --- a/lib/http.go +++ b/lib/http.go @@ -8,47 +8,11 @@ import ( "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/utils" - "html/template" "log/slog" "net/http" "strings" ) -var templates map[string]*template.Template - -func init() { - - templates = make(map[string]*template.Template) - - dir, err := embed.TemplatesFs.ReadDir(".") - if err != nil { - panic(err) - } - for _, e := range dir { - if e.IsDir() { - continue - } - data, err := embed.TemplatesFs.ReadFile(e.Name()) - if err != nil { - panic(err) - } - err = initTemplate(e.Name(), string(data)) - if err != nil { - panic(err) - } - } -} - -func initTemplate(name, data string) error { - tpl := template.New(name) - _, err := tpl.Parse(data) - if err != nil { - return err - } - templates[name] = tpl - return nil -} - func GetLoggerForRequest(r *http.Request) *slog.Logger { data := challenge.RequestDataFromContext(r.Context()) args := []any{ diff --git a/lib/interface.go b/lib/interface.go index bfb749c..9061ab0 100644 --- a/lib/interface.go +++ b/lib/interface.go @@ -1,7 +1,6 @@ package lib import ( - "bytes" "crypto/ed25519" "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/policy" @@ -9,7 +8,6 @@ import ( "git.gammaspectra.live/git/go-away/utils" "github.com/google/cel-go/cel" "log/slog" - "maps" "net/http" ) @@ -84,77 +82,6 @@ func (state *State) Logger(r *http.Request) *slog.Logger { return GetLoggerForRequest(r) } -func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status int, reg *challenge.Registration, params map[string]any) { - data := challenge.RequestDataFromContext(r.Context()) - input := make(map[string]any) - input["Id"] = data.Id.String() - input["Random"] = utils.CacheBust() - - input["Path"] = state.UrlPath() - for k, v := range state.Options().ChallengeTemplateOverrides { - input[k] = v - } - for k, v := range state.Options().Strings { - input["str_"+k] = v - } - - if reg != nil { - input["Challenge"] = reg.Name - } - - maps.Copy(input, params) - - if _, ok := input["Title"]; !ok { - input["Title"] = state.Options().Strings.Get("challenge_are_you_bot") - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - - buf := bytes.NewBuffer(make([]byte, 0, 8192)) - - err := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) - if err != nil { - state.ErrorPage(w, r, http.StatusInternalServerError, err, "") - } else { - w.WriteHeader(status) - _, _ = w.Write(buf.Bytes()) - } -} - -func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) { - data := challenge.RequestDataFromContext(r.Context()) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - - buf := bytes.NewBuffer(make([]byte, 0, 8192)) - - input := map[string]any{ - "Id": data.Id.String(), - "Random": utils.CacheBust(), - "Error": err.Error(), - "Path": state.UrlPath(), - "Theme": "", - "Title": state.Options().Strings.Get("error") + " " + http.StatusText(status), - "HideSpinner": true, - "Challenge": "", - "Redirect": redirect, - } - for k, v := range state.Options().ChallengeTemplateOverrides { - input[k] = v - } - for k, v := range state.Options().Strings { - input["str_"+k] = v - } - - err2 := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) - if err2 != nil { - // nested errors! - panic(err2) - } else { - w.WriteHeader(status) - _, _ = w.Write(buf.Bytes()) - } -} - func (state *State) GetChallenge(id challenge.Id) (*challenge.Registration, bool) { reg, ok := state.challenges.Get(id) return reg, ok diff --git a/lib/settings/settings.go b/lib/settings/settings.go index 6f56616..10771d9 100644 --- a/lib/settings/settings.go +++ b/lib/settings/settings.go @@ -3,12 +3,12 @@ package settings import "maps" type Settings struct { - Bind Bind `json:"bind"` + Bind Bind `yaml:"bind"` - Backends map[string]Backend `json:"backends"` + Backends map[string]Backend `yaml:"backends"` - BindDebug string `json:"bind-debug"` - BindMetrics string `json:"bind-metrics"` + BindDebug string `yaml:"bind-debug"` + BindMetrics string `yaml:"bind-metrics"` Strings Strings `yaml:"strings"` diff --git a/lib/settings/strings.go b/lib/settings/strings.go index 1fc4533..9a4648a 100644 --- a/lib/settings/strings.go +++ b/lib/settings/strings.go @@ -1,12 +1,43 @@ package settings -import "maps" +import ( + "html/template" + "maps" +) type Strings map[string]string var DefaultStrings = make(Strings).set(map[string]string{ - "challenge_are_you_bot": "Checking you are not a bot", - "error": "Oh no!", + "title_challenge": "Checking you are not a bot", + "title_error": "Oh no!", + + "noscript_warning": "

Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works.

", + + "details_title": "Why am I seeing this?", + "details_text": ` +

+ You are seeing this because the administrator of this website has set up go-away + to protect the server against the scourge of AI companies aggressively scraping websites. +

+

+ Mass scraping can and does cause downtime for the websites, which makes their resources inaccessible for everyone. +

+

+ Please note that some challenges requires the use of modern JavaScript features and some plugins may disable these. + Disable such plugins for this domain (for example, JShelter) if you encounter any issues. +

+`, + "details_contact_admin_with_request_id": "If you have any issues contact the site administrator and provide the following Request Id", + + "button_refresh_page": "Refresh page", + + "status_loading_challenge": "Loading challenge", + "status_starting_challenge": "Starting challenge", + "status_loading": "Loading...", + "status_calculating": "Calculating...", + "status_challenge_success": "Challenge success!", + "status_challenge_done_took": "Done! Took", + "status_error": "Error:", }) func (s Strings) set(v map[string]string) Strings { @@ -14,11 +45,11 @@ func (s Strings) set(v map[string]string) Strings { return s } -func (s Strings) Get(value string) string { +func (s Strings) Get(value string) template.HTML { v, ok := (s)[value] if !ok { // fallback - return "string:" + value + return template.HTML("string:" + value) } - return v + return template.HTML(v) } diff --git a/lib/template.go b/lib/template.go new file mode 100644 index 0000000..c21d708 --- /dev/null +++ b/lib/template.go @@ -0,0 +1,114 @@ +package lib + +import ( + "bytes" + "git.gammaspectra.live/git/go-away/embed" + "git.gammaspectra.live/git/go-away/lib/challenge" + "git.gammaspectra.live/git/go-away/utils" + "html/template" + "maps" + "net/http" +) + +var templates map[string]*template.Template + +func init() { + + templates = make(map[string]*template.Template) + + dir, err := embed.TemplatesFs.ReadDir(".") + if err != nil { + panic(err) + } + for _, e := range dir { + if e.IsDir() { + continue + } + data, err := embed.TemplatesFs.ReadFile(e.Name()) + if err != nil { + panic(err) + } + err = initTemplate(e.Name(), string(data)) + if err != nil { + panic(err) + } + } +} + +func initTemplate(name, data string) error { + tpl := template.New(name) + _, err := tpl.Parse(data) + if err != nil { + return err + } + templates[name] = tpl + return nil +} + +func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status int, reg *challenge.Registration, params map[string]any) { + data := challenge.RequestDataFromContext(r.Context()) + input := make(map[string]any) + input["Id"] = data.Id.String() + input["Random"] = utils.CacheBust() + + input["Path"] = state.UrlPath() + input["Links"] = state.Options().Links + input["Strings"] = state.Options().Strings + for k, v := range state.Options().ChallengeTemplateOverrides { + input[k] = v + } + + if reg != nil { + input["Challenge"] = reg.Name + } + + maps.Copy(input, params) + + if _, ok := input["Title"]; !ok { + input["Title"] = state.Options().Strings.Get("title_challenge") + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + buf := bytes.NewBuffer(make([]byte, 0, 8192)) + + err := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) + if err != nil { + state.ErrorPage(w, r, http.StatusInternalServerError, err, "") + } else { + w.WriteHeader(status) + _, _ = w.Write(buf.Bytes()) + } +} + +func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) { + data := challenge.RequestDataFromContext(r.Context()) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + buf := bytes.NewBuffer(make([]byte, 0, 8192)) + + input := map[string]any{ + "Id": data.Id.String(), + "Random": utils.CacheBust(), + "Error": err.Error(), + "Path": state.UrlPath(), + "Theme": "", + "Title": template.HTML(string(state.Options().Strings.Get("title_error")) + " " + http.StatusText(status)), + "Challenge": "", + "Redirect": redirect, + "Links": state.Options().Links, + "Strings": state.Options().Strings, + } + for k, v := range state.Options().ChallengeTemplateOverrides { + input[k] = v + } + + err2 := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) + if err2 != nil { + // nested errors! + panic(err2) + } else { + w.WriteHeader(status) + _, _ = w.Write(buf.Bytes()) + } +}