Files
forgetting/internal/ui/prefix.go
T
Claude 13da9a7200 Secret URL path for the panel
The panel port is open to the internet, and its login page and JS bundle
gave it away to any scanner in one GET. The installer now generates a
random UI_PREFIX: from outside the panel answers only under
http://IP:PORT/<prefix>/ and gives Go's plain "404 page not found" to
everything else. Loopback needs no prefix (CLI, local Prometheus, nginx on
the same host); while a move code is out, POST /migrate/v1/import is let
through without it, since the code carries no path.

A reinstall with kept data keeps the prefix; UI_PREFIX=off or a custom value
can be set before running the installer. A moved panel brings its prefix in
UI_PREFIX_MOVED, so old bookmarks and API clients keep working through the
relay. awg-ctrl status and credentials show the full panel address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:41:09 +00:00

82 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
// Секретный префикс панели (UI_PREFIX): снаружи панель отвечает только на /<префикс>/…, на всё
// остальное — как пустой Go-сервер (`404 page not found`). Порт панели открыт в интернет, и без
// префикса сканер узнавал её по странице логина и JS-бандлу — одним GET.
//
// UI_PREFIX_MOVED — префикс панели, переехавшей сюда (`migrate`): её закладки и внешний API
// приходят через ретранслятор старого сервера со своим путём. Принимаются оба.
//
// С loopback префикс не нужен: CLI, Prometheus этой машины (127.0.0.1:PORT/metrics) и nginx
// на этой же машине ходят по-старому (nginx в подпапке и есть свой секретный путь). Адрес —
// соединения, не X-Forwarded-For: заголовок подделывается.
import (
"net"
"net/http"
"os"
"strings"
"github.com/maeneko/forgetting/internal/migrate"
)
// uiPrefixes — UI_PREFIX и UI_PREFIX_MOVED из окружения; пустые и кривые пропускаются.
func uiPrefixes() []string {
var out []string
for _, k := range []string{"UI_PREFIX", "UI_PREFIX_MOVED"} {
p := strings.Trim(strings.TrimSpace(os.Getenv(k)), "/")
switch {
case p == "":
case !migrate.ValidUIPrefix(p):
warnf("ui: %s «%s» не подходит (8–64 символа A–Z a–z 0–9 _ -) — пропущен", k, p)
default:
out = append(out, p)
}
}
return out
}
func isLoopback(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
host = remoteAddr
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
// prefixGate пропускает в панель только запросы под префиксом и срезает его: дальше роутер
// видит те же пути, что без префикса. allow — исключения вне префикса (приём переноса).
func prefixGate(prefixes []string, next http.Handler, allow func(*http.Request) bool) http.Handler {
if len(prefixes) == 0 {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
for _, px := range prefixes {
root := "/" + px
if p == root {
// Относительный адрес, как у static: за прокси в подпапке абсолютный увёл бы из неё.
w.Header().Set("Location", px+"/")
w.WriteHeader(http.StatusMovedPermanently)
return
}
if strings.HasPrefix(p, root+"/") {
r2 := r.Clone(r.Context())
r2.URL.Path = p[len(root):]
if r.URL.RawPath != "" {
r2.URL.RawPath = strings.TrimPrefix(r.URL.RawPath, root)
}
r2.RequestURI = r2.URL.RequestURI()
next.ServeHTTP(w, r2)
return
}
}
if isLoopback(r.RemoteAddr) || allow != nil && allow(r) {
next.ServeHTTP(w, r)
return
}
http.NotFound(w, r)
})
}