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 }} -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"}}{{ .Strings.Get "details_contact_admin_with_request_id" }}: {{ .Id }}
@@ -198,6 +59,10 @@Protected by go-away :: Request Id {{ .Id }} + + {{ range .Links }} + :: {{ .Name }} + {{ end }}
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"}}{{ .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()) + } +}