From 29353e5e6a61ab0e55882dfba5a43be1fa89c2ed Mon Sep 17 00:00:00 2001 From: iRedTea Date: Wed, 14 Oct 2026 21:07:36 +0300 Subject: [PATCH] feat: install code of 3 to 24 letters and digits with a button to make a new one; guesses are limited per address --- internal/server/config.go | 43 +++++++++++++++------ internal/server/handlers_panel.go | 26 ++++++++++++- internal/server/server.go | 1 + internal/server/web/templates/settings.html | 13 +++++++ 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/internal/server/config.go b/internal/server/config.go index 1b2e1ef..5813502 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -14,6 +14,7 @@ const ( cfgEnrollSecret = "enroll_secret" cfgSetupDone = "setup_done" cfgInstallCode = "install_code" + cfgInstallLen = "install_code_len" cfgUfwEnabled = "ufw_enabled" cfgTunnelHost = "tunnel_host" cfgTunnelPort = "tunnel_port" @@ -52,7 +53,7 @@ func (db *DB) ensureDefaults() error { return err } if code == "" { - if err := db.SetConfig(cfgInstallCode, shortCode()); err != nil { + if err := db.SetConfig(cfgInstallCode, shortCode(db.InstallCodeLen())); err != nil { return err } } @@ -125,16 +126,36 @@ func randToken() string { return hex.EncodeToString(b) } -// shortCode returns a short, URL-safe, typo-resistant code for install links. -// It avoids ambiguous characters (0/O, 1/l/I) so it is easy to read and type. -func shortCode() string { - const alphabet = "abcdefghjkmnpqrstuvwxyz23456789" - b := make([]byte, 7) - if _, err := rand.Read(b); err != nil { - panic("crypto/rand failed: " + err.Error()) +// Install code length: 3 by default, up to 24. +const ( + installCodeMin = 3 + installCodeMax = 24 + installCodeDef = 3 +) + +// InstallCodeLen is how many characters a new install code has. +func (db *DB) InstallCodeLen() int { + v, _ := db.GetConfig(cfgInstallLen) + if n, err := strconv.Atoi(v); err == nil && n >= installCodeMin && n <= installCodeMax { + return n } - for i := range b { - b[i] = alphabet[int(b[i])%len(alphabet)] + return installCodeDef +} + +// shortCode returns a random code of n English letters and digits for the +// /i/ install link. Every character is equally likely. +func shortCode(n int) string { + const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + out := make([]byte, 0, n) + b := make([]byte, 1) + for len(out) < n { + if _, err := rand.Read(b); err != nil { + panic("crypto/rand failed: " + err.Error()) + } + // 248 = 4 * 62: drop the rest, so no character comes up more often. + if b[0] < 248 { + out = append(out, alphabet[int(b[0])%len(alphabet)]) + } } - return string(b) + return string(out) } diff --git a/internal/server/handlers_panel.go b/internal/server/handlers_panel.go index 3e17591..d81f3e9 100644 --- a/internal/server/handlers_panel.go +++ b/internal/server/handlers_panel.go @@ -410,6 +410,7 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) { "Admin": s.currentAdmin(r), "Host": host, "Secret": secret, + "CodeLen": s.db.InstallCodeLen(), "Admins": admins, "KeyCounts": keyCounts, "PassOff": passOff, @@ -516,7 +517,22 @@ func (s *Server) handleRotateSecret(w http.ResponseWriter, r *http.Request) { } _ = s.db.SetConfig(cfgEnrollSecret, randToken()) // Rotate the short install code too so old /i/ links stop working. - _ = s.db.SetConfig(cfgInstallCode, shortCode()) + _ = s.db.SetConfig(cfgInstallCode, shortCode(s.db.InstallCodeLen())) + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + +// POST /settings/install/code - a new install code, of the length given: +// the old /i/ link stops working. +func (s *Server) handleInstallCode(w http.ResponseWriter, r *http.Request) { + if !checkCSRF(r) { + http.Error(w, "bad csrf", http.StatusForbidden) + return + } + if n, err := strconv.Atoi(r.FormValue("len")); err == nil { + n = min(installCodeMax, max(installCodeMin, n)) + _ = s.db.SetConfig(cfgInstallLen, strconv.Itoa(n)) + } + _ = s.db.SetConfig(cfgInstallCode, shortCode(s.db.InstallCodeLen())) http.Redirect(w, r, "/settings", http.StatusSeeOther) } @@ -603,9 +619,17 @@ func (s *Server) handleInstallScript(w http.ResponseWriter, r *http.Request) { // // wget -qO- https://host/i/ | sudo sh func (s *Server) handleShortInstall(w http.ResponseWriter, r *http.Request) { + // A short code can be guessed: wrong ones count against the address + // like wrong passwords do. + ip := clientIP(r) + if s.limiter.blocked(ip) { + http.Error(w, "too many attempts", http.StatusTooManyRequests) + return + } code := r.PathValue("code") want := s.db.InstallCode() if want == "" || !constantTimeEqual(code, want) { + s.limiter.fin(ip, false) http.NotFound(w, r) return } diff --git a/internal/server/server.go b/internal/server/server.go index 8aa6984..7c49c4e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -232,6 +232,7 @@ func (s *Server) Handler() http.Handler { mux.Handle("POST /settings/host", s.requireAdmin(s.handleSetHost)) mux.Handle("POST /settings/tunnel", s.requireAdmin(s.handleSetTunnel)) mux.Handle("POST /settings/secret/rotate", s.requireAdmin(s.handleRotateSecret)) + mux.Handle("POST /settings/install/code", s.requireAdmin(s.handleInstallCode)) mux.Handle("POST /settings/admins/add", s.requireAdmin(s.handleAddAdmin)) mux.Handle("POST /settings/admins/delete", s.requireAdmin(s.handleDeleteAdmin)) mux.Handle("POST /settings/admins/keys/reset", s.requireAdmin(s.handleResetAdminKeys)) diff --git a/internal/server/web/templates/settings.html b/internal/server/web/templates/settings.html index 57986d7..cb2fc50 100644 --- a/internal/server/web/templates/settings.html +++ b/internal/server/web/templates/settings.html @@ -12,6 +12,10 @@ .copybox .copy { position:absolute; top:7px; right:7px; width:30px; height:30px; padding:0; display:inline-flex; align-items:center; justify-content:center; background:var(--panel); } .copybox .copy.done { color:var(--ok); border-color:var(--ok); } + .codeform { display:flex; flex-wrap:wrap; align-items:center; gap:8px 10px; margin-top:10px; font-size:13px; } + .codeform label { margin:0; } + .codeform input[type=number] { width:72px; height:30px; padding:4px 8px; } + .codeform .ico { display:inline-flex; align-items:center; gap:6px; height:30px; } @@ -35,6 +39,15 @@
{{.InstallShort}}
+
+ + + + + латиница и цифры, от 3 до 24; новый код отменяет старую ссылку +
+
Полные варианты (секрет в команде)