package http import "fmt" import "log" import "time" import "net/url" import "net/http" import "html/template" import "git.tebibyte.media/sashakoshka/step" import shttp "git.tebibyte.media/sashakoshka/step/http" const defaultLifetime = 24 * 7 * time.Hour var _ step.FuncProvider = new(Provider) // Provider provides HTTP and URL functions. type Provider struct { InsecureCookie bool CookieDomain string } // Package fulfills the step.Provider interface. func (this *Provider) Package () string { return "http" } func (this *Provider) Configure (config step.Meta) error { if insecureCookieStr := config.Get("http.insecure-cookie"); insecureCookieStr != "" { if insecureCookieStr != "true" && insecureCookieStr != "false" { return step.ErrTypeMismatch } this.InsecureCookie = insecureCookieStr == "true" if this.InsecureCookie { log.Println("!!! http.insecure-cookie is active, this is not recommended") } } if cookieDomainStr := config.Get("http.cookie-domain"); cookieDomainStr != "" { this.CookieDomain = cookieDomainStr } return nil } // FuncMap fulfills the step.FuncProvider interface. func (this *Provider) FuncMap () template.FuncMap { return template.FuncMap { "statusText": http.StatusText, "parseQuery": funcParseQuery, "parseForm": funcParseForm, "error": funcError, "redirect": funcRedirect, "queryEscape": url.QueryEscape, "queryUnescape": url.QueryUnescape, "unsafe": funcUnsafe, "getCookie": this.funcGetCookie, "setCookie": this.funcSetCookie, "setCookieExpires": this.funcSetCookieExpires, } } func funcParseQuery (query string) url.Values { // wrapped here because the query might contain all sorts of nonsense, // and returning an error in a template function causes everything to // stop rather ungracefully values, err := url.ParseQuery(query) if err != nil { return nil } return values } func funcError (status int, message any) (string, error) { return "", shttp.Error { Status: status, Message: message, } } func funcRedirect (status int, pat string) (string, error) { if status < 300 || status > 399 { return "", fmt.Errorf( "provided status code %d is not in the 3XX range", status) } return "", shttp.Redirect { Status: status, Location: pat, } } func funcParseForm (req *http.Request) url.Values { err := req.ParseForm() if err != nil { return nil } return req.Form } func funcUnsafe(text string) template.HTML { return template.HTML(text) } func (this *Provider) funcGetCookie( req *http.Request, name string, ) string { cookie, err := req.Cookie(name) if err != nil { return "" } return cookie.Value } func (this *Provider) funcSetCookie( res shttp.WrappedResponseWriter, name string, value string, ) error { return this.funcSetCookieExpires(res, name, value, 0) } func (this *Provider) funcSetCookieExpires( res shttp.WrappedResponseWriter, name string, value string, lifetime time.Duration, ) error { if !this.InsecureCookie { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie // __Host- prefix: Cookies with names starting with __Host- are // sent only to the host subdomain or domain that set them, and // not to any other host. They must be set with the secure flag // must be from a secure page (HTTPS), must not have a domain // specified, and the path must be / name = "__Host-" + name } var expiration time.Time if lifetime == 0 { expiration = time.Now().Add(defaultLifetime) } else { expiration = time.Now().Add(lifetime) } cookie := &http.Cookie { Name: name, Value: value, Expires: expiration, SameSite: http.SameSiteStrictMode, Path: "/", Domain: this.CookieDomain, } if !this.InsecureCookie { cookie.Secure = true cookie.HttpOnly = true } underlyingRes := shttp.UnderlyingResponseWriter(res) http.SetCookie(underlyingRes, cookie) return nil }