diff --git a/embed/templates/challenge-anubis.gohtml b/embed/templates/challenge-anubis.gohtml
index ca03d94..d2791da 100644
--- a/embed/templates/challenge-anubis.gohtml
+++ b/embed/templates/challenge-anubis.gohtml
@@ -5,9 +5,12 @@
- {{ range .Meta }}
+ {{ range .MetaTags }}
{{ end }}
+ {{ range .LinkTags }}
+
+ {{ end }}
{{ range .HeaderTags }}
{{ . }}
{{ end }}
diff --git a/embed/templates/challenge-forgejo.gohtml b/embed/templates/challenge-forgejo.gohtml
index ffc6739..824157d 100644
--- a/embed/templates/challenge-forgejo.gohtml
+++ b/embed/templates/challenge-forgejo.gohtml
@@ -6,9 +6,12 @@
{{ .Title }}
- {{ range .Meta }}
+ {{ range .MetaTags }}
{{ end }}
+ {{ range .LinkTags }}
+
+ {{ end }}
{{ range .HeaderTags }}
{{ . }}
{{ end }}
diff --git a/examples/forgejo.yml b/examples/forgejo.yml
index d2de0c1..0e66798 100644
--- a/examples/forgejo.yml
+++ b/examples/forgejo.yml
@@ -293,6 +293,7 @@ rules:
context-set:
# Map OpenGraph or similar tags back to the reply, even if denied/challenged
proxy-meta-tags: "true"
+ # proxy-safe-link-tags: "true"
# Set additional response headers
#response-headers:
diff --git a/lib/challenge/data.go b/lib/challenge/data.go
index 0b49fe3..cc52627 100644
--- a/lib/challenge/data.go
+++ b/lib/challenge/data.go
@@ -152,8 +152,9 @@ func (d *RequestData) NetworkPrefix() netip.Addr {
}
const (
- RequestOptBackendHost = "backend-host"
- RequestOptCacheMetaTags = "proxy-meta-tags"
+ RequestOptBackendHost = "backend-host"
+ RequestOptProxyMetaTags = "proxy-meta-tags"
+ RequestOptProxySafeLinkTags = "proxy-safe-link-tags"
)
func (d *RequestData) SetOpt(n, v string) {
diff --git a/lib/challenge/refresh/refresh.go b/lib/challenge/refresh/refresh.go
index 50be69b..dfb1426 100644
--- a/lib/challenge/refresh/refresh.go
+++ b/lib/challenge/refresh/refresh.go
@@ -47,7 +47,7 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
if params.Mode == "meta" {
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
- "Meta": []map[string]string{
+ "MetaTags": []map[string]string{
{
"http-equiv": "refresh",
"content": "0; url=" + uri.String(),
diff --git a/lib/challenge/resource-load/resource-load.go b/lib/challenge/resource-load/resource-load.go
index 0fb632a..4c3e232 100644
--- a/lib/challenge/resource-load/resource-load.go
+++ b/lib/challenge/resource-load/resource-load.go
@@ -1,10 +1,8 @@
package resource_load
import (
- "fmt"
"git.gammaspectra.live/git/go-away/lib/challenge"
"github.com/goccy/go-yaml/ast"
- "html/template"
"net/http"
"time"
)
@@ -30,8 +28,12 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
w.Header().Set("Refresh", "2; url="+r.URL.String())
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
- "HeaderTags": []template.HTML{
- template.HTML(fmt.Sprintf("", uri.String())),
+ "LinkTags": []map[string]string{
+ {
+ "href": uri.String(),
+ "rel": "stylesheet",
+ "crossorigin": "use-credentials",
+ },
},
})
return challenge.VerifyResultNone
diff --git a/lib/http.go b/lib/http.go
index 8ae60f3..634e055 100644
--- a/lib/http.go
+++ b/lib/http.go
@@ -38,7 +38,7 @@ func GetLoggerForRequest(r *http.Request) *slog.Logger {
return slog.With(args...)
}
-func (state *State) fetchMetaTags(host string, backend http.Handler, r *http.Request) []html.Node {
+func (state *State) fetchTags(host string, backend http.Handler, r *http.Request, meta, link bool) []html.Node {
uri := *r.URL
q := uri.Query()
for k := range q {
@@ -54,76 +54,142 @@ func (state *State) fetchMetaTags(host string, backend http.Handler, r *http.Req
return v
}
- result := utils.FetchTags(backend, &uri, "meta")
+ result := utils.FetchTags(backend, &uri, func() (r []string) {
+ if meta {
+ r = append(r, "meta")
+ } else if link {
+ r = append(r, "link")
+ }
+ return r
+ }()...)
if result == nil {
return nil
}
entries := make([]html.Node, 0, len(result))
-
- safeAttributes := []string{"name", "property", "content"}
for _, n := range result {
if n.Namespace != "" {
continue
}
- var name string
- for _, attr := range n.Attr {
- if attr.Namespace != "" {
- continue
- }
- if attr.Key == "name" {
- name = attr.Val
- break
- }
- if attr.Key == "property" && name == "" {
- name = attr.Val
- }
- }
+ switch n.Data {
+ case "link":
+ safeAttributes := []string{"rel", "href", "hreflang", "media", "title", "type"}
- // prevent unwanted keys like CSRF and other internal entries to pass through as much as possible
-
- var keep bool
- if strings.HasPrefix("og:", name) || strings.HasPrefix("fb:", name) || strings.HasPrefix("twitter:", name) || strings.HasPrefix("profile:", name) {
- // social / OpenGraph tags
- keep = true
- } else if name == "vcs" || strings.HasPrefix("vcs:", name) {
- // source tags
- keep = true
- } else if name == "forge" || strings.HasPrefix("forge:", name) {
- // forge tags
- keep = true
- } else {
- switch name {
- // standard content tags
- case "application-name", "author", "description", "keywords", "robots", "thumbnail":
- keep = true
- case "go-import", "go-source":
- // golang tags
- keep = true
- case "apple-itunes-app":
- }
- }
-
- // prevent other arbitrary arguments
- if keep {
- newNode := html.Node{
- Type: html.ElementNode,
- Data: n.Data,
- }
+ var name string
for _, attr := range n.Attr {
if attr.Namespace != "" {
continue
}
- if slices.Contains(safeAttributes, attr.Key) {
- newNode.Attr = append(newNode.Attr, attr)
+ if attr.Key == "rel" {
+ name = attr.Val
+ break
}
}
- if len(newNode.Attr) == 0 {
+
+ if name == "" {
continue
}
- entries = append(entries, newNode)
+
+ var keep bool
+ if name == "icon" || name == "alternate icon" {
+ keep = true
+ } else if name == "alternate" || name == "canonical" || name == "search" {
+ // urls to versions of document
+ keep = true
+ } else if name == "author" || name == "privacy-policy" || name == "license" || name == "copyright" || name == "terms-of-service" {
+ keep = true
+ } else if name == "manifest" {
+ // web app manifest
+ keep = true
+ }
+
+ // prevent other arbitrary arguments
+ if keep {
+ newNode := html.Node{
+ Type: html.ElementNode,
+ Data: n.Data,
+ }
+ for _, attr := range n.Attr {
+ if attr.Namespace != "" {
+ continue
+ }
+ if slices.Contains(safeAttributes, attr.Key) {
+ newNode.Attr = append(newNode.Attr, attr)
+ }
+ }
+ if len(newNode.Attr) == 0 {
+ continue
+ }
+ entries = append(entries, newNode)
+ }
+
+ case "meta":
+
+ safeAttributes := []string{"name", "property", "content"}
+ var name string
+ for _, attr := range n.Attr {
+ if attr.Namespace != "" {
+ continue
+ }
+ if attr.Key == "name" {
+ name = attr.Val
+ break
+ }
+ if attr.Key == "property" && name == "" {
+ name = attr.Val
+ }
+ }
+
+ if name == "" {
+ continue
+ }
+
+ // prevent unwanted keys like CSRF and other internal entries to pass through as much as possible
+
+ var keep bool
+ if strings.HasPrefix("og:", name) || strings.HasPrefix("fb:", name) || strings.HasPrefix("twitter:", name) || strings.HasPrefix("profile:", name) {
+ // social / OpenGraph tags
+ keep = true
+ } else if name == "vcs" || strings.HasPrefix("vcs:", name) {
+ // source tags
+ keep = true
+ } else if name == "forge" || strings.HasPrefix("forge:", name) {
+ // forge tags
+ keep = true
+ } else {
+ switch name {
+ // standard content tags
+ case "application-name", "author", "description", "keywords", "robots", "thumbnail":
+ keep = true
+ case "go-import", "go-source":
+ // golang tags
+ keep = true
+ case "apple-itunes-app":
+ }
+ }
+
+ // prevent other arbitrary arguments
+ if keep {
+ newNode := html.Node{
+ Type: html.ElementNode,
+ Data: n.Data,
+ }
+ for _, attr := range n.Attr {
+ if attr.Namespace != "" {
+ continue
+ }
+ if slices.Contains(safeAttributes, attr.Key) {
+ newNode.Attr = append(newNode.Attr, attr)
+ }
+ }
+ if len(newNode.Attr) == 0 {
+ continue
+ }
+ entries = append(entries, newNode)
+ }
}
+
}
state.tagCache.Set(key, entries, time.Hour*6)
diff --git a/lib/template.go b/lib/template.go
index c582b5a..dc73c2c 100644
--- a/lib/template.go
+++ b/lib/template.go
@@ -52,6 +52,28 @@ func initTemplate(name, data string) error {
return nil
}
+func (state *State) addCachedTags(data *challenge.RequestData, r *http.Request, input map[string]any) {
+ proxyMetaTags := data.GetOptBool(challenge.RequestOptProxyMetaTags, false)
+ proxySafeLinkTags := data.GetOptBool(challenge.RequestOptProxySafeLinkTags, false)
+ if proxyMetaTags || proxySafeLinkTags {
+ backend, host := data.BackendHost()
+ if tags := state.fetchTags(host, backend, r, proxyMetaTags, proxySafeLinkTags); len(tags) > 0 {
+ metaTagMap, _ := input["MetaTags"].([]map[string]string)
+ linkTagMap, _ := input["LinkTags"].([]map[string]string)
+
+ for _, tag := range tags {
+ tagAttrs := make(map[string]string, len(tag.Attr))
+ for _, v := range tag.Attr {
+ tagAttrs[v.Key] = v.Val
+ }
+ metaTagMap = append(metaTagMap, tagAttrs)
+ }
+ input["MetaTags"] = metaTagMap
+ input["LinkTags"] = linkTagMap
+ }
+ }
+}
+
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)
@@ -75,21 +97,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
input["Title"] = state.opt.Strings.Get("title_challenge")
}
- if data.GetOptBool(challenge.RequestOptCacheMetaTags, false) {
- backend, host := data.BackendHost()
- if tags := state.fetchMetaTags(host, backend, r); len(tags) > 0 {
- tagMap, _ := input["Meta"].([]map[string]string)
-
- for _, tag := range tags {
- tagAttrs := make(map[string]string, len(tag.Attr))
- for _, v := range tag.Attr {
- tagAttrs[v.Key] = v.Val
- }
- tagMap = append(tagMap, tagAttrs)
- }
- input["Meta"] = tagMap
- }
- }
+ state.addCachedTags(data, r, input)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -126,21 +134,7 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int
input[k] = v
}
- if data.GetOptBool(challenge.RequestOptCacheMetaTags, false) {
- backend, host := data.BackendHost()
- if tags := state.fetchMetaTags(host, backend, r); len(tags) > 0 {
- tagMap, _ := input["Meta"].([]map[string]string)
-
- for _, tag := range tags {
- tagAttrs := make(map[string]string, len(tag.Attr))
- for _, v := range tag.Attr {
- tagAttrs[v.Key] = v.Val
- }
- tagMap = append(tagMap, tagAttrs)
- }
- input["Meta"] = tagMap
- }
- }
+ state.addCachedTags(data, r, input)
err2 := templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"].Execute(buf, input)
if err2 != nil {
diff --git a/utils/tagfetcher.go b/utils/tagfetcher.go
index 6ee90bd..00c59a0 100644
--- a/utils/tagfetcher.go
+++ b/utils/tagfetcher.go
@@ -6,9 +6,10 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "slices"
)
-func FetchTags(backend http.Handler, uri *url.URL, kind string) (result []html.Node) {
+func FetchTags(backend http.Handler, uri *url.URL, kinds ...string) (result []html.Node) {
writer := httptest.NewRecorder()
backend.ServeHTTP(writer, &http.Request{
Method: http.MethodGet,
@@ -39,7 +40,7 @@ func FetchTags(backend http.Handler, uri *url.URL, kind string) (result []html.N
}
for n := range node.Descendants() {
- if n.Type == html.ElementNode && n.Data == kind {
+ if n.Type == html.ElementNode && slices.Contains(kinds, n.Data) {
result = append(result, html.Node{
Type: n.Type,
DataAtom: n.DataAtom,