- логотип сведён в web/static/logo.svg (буквы геометрией, оптическая центровка); прежний favicon с «play» убран - <head> шести страниц свёрнут в партиал head с og-мета; адреса абсолютные, база берётся из запроса, хост из недоверенного заголовка проверяется - растровые ассеты og-cover.jpg и apple-touch-icon.png собирает `task brand` из исходников web/*.html
78 lines
2.9 KiB
Go
78 lines
2.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// pageMeta — мета-шапка страницы: заголовок вкладки и адреса для превью
|
|
// ссылки (og:*). Заполняется только на страничных путях; htmx-фрагменты
|
|
// рендерят те же view с пустым pageMeta и <head> не отдают.
|
|
type pageMeta struct {
|
|
Title string // <title> и og:title
|
|
URL string // адрес страницы (og:url)
|
|
Image string // адрес карточки превью (og:image)
|
|
}
|
|
|
|
// pageMetaFor собирает мету страницы. Адреса делаются абсолютными: краулер
|
|
// мессенджера не обязан разрешать относительный og:image, а без картинки
|
|
// превью ссылки вырождается в голый заголовок.
|
|
func (s *server) pageMetaFor(r *http.Request, title string) pageMeta {
|
|
base := absBase(r)
|
|
return pageMeta{
|
|
Title: title,
|
|
URL: base + r.URL.EscapedPath(),
|
|
Image: base + "/static/og-cover.jpg?v=" + s.assetVer,
|
|
}
|
|
}
|
|
|
|
// absBase — база адреса («https://host»), какой её видит клиент: за обратным
|
|
// прокси схему и хост несут X-Forwarded-Proto/X-Forwarded-Host, иначе берём
|
|
// соединение и Host запроса. Значение уходит только в мета-теги превью,
|
|
// доменных решений на нём не строится. Хост непохожей формы отбрасываем
|
|
// целиком: пустая база оставляет в мете корректный относительный адрес.
|
|
func absBase(r *http.Request) string {
|
|
scheme := "http"
|
|
if r.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
switch v := firstForwarded(r.Header.Get("X-Forwarded-Proto")); v {
|
|
case "http", "https":
|
|
scheme = v
|
|
}
|
|
host := r.Host
|
|
if v := firstForwarded(r.Header.Get("X-Forwarded-Host")); v != "" {
|
|
host = v
|
|
}
|
|
if !validHost(host) {
|
|
return ""
|
|
}
|
|
return scheme + "://" + host
|
|
}
|
|
|
|
// firstForwarded берёт первое значение списка «a, b, c»: цепочка прокси
|
|
// дописывает свои значения справа, клиенту ближе левое.
|
|
func firstForwarded(v string) string {
|
|
if i := strings.IndexByte(v, ','); i >= 0 {
|
|
v = v[:i]
|
|
}
|
|
return strings.TrimSpace(v)
|
|
}
|
|
|
|
// validHost — проверка хоста из недоверенного заголовка: допустимы только
|
|
// знаки имени, порта и скобок IPv6.
|
|
func validHost(host string) bool {
|
|
if host == "" || len(host) > 255 {
|
|
return false
|
|
}
|
|
for _, c := range host {
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
|
|
case c == '.', c == '-', c == '_', c == ':', c == '[', c == ']':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|