feat: install code of 3 to 24 letters and digits with a button to make a new one; guesses are limited per address
This commit is contained in:
1 parent
47f1b2e55b
commit
29353e5e6a
4 files changed
+69
-10
No files matched your search
@@ -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)
|
||||
// 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
|
||||
}
|
||||
return installCodeDef
|
||||
}
|
||||
|
||||
// shortCode returns a random code of n English letters and digits for the
|
||||
// /i/<code> 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())
|
||||
}
|
||||
for i := range b {
|
||||
b[i] = alphabet[int(b[i])%len(alphabet)]
|
||||
// 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)
|
||||
}
|
||||
@@ -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/<code> 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/<code> 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/<code> | 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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -35,6 +39,15 @@
|
||||
<label>Короткая команда (секрет уже вшит в ссылку)</label>
|
||||
<div class="copybox"><pre id="short">{{.InstallShort}}</pre><button type="button" class="btn ghost small icon sq copy" data-copy="short" title="Скопировать" aria-label="Скопировать"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg></button></div>
|
||||
|
||||
<form method="post" action="/settings/install/code" class="codeform"
|
||||
onsubmit="return confirm('Сделать новый код? Старая короткая ссылка перестанет работать.')">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<label for="codelen">Символов в коде</label>
|
||||
<input type="number" id="codelen" name="len" min="3" max="24" value="{{.CodeLen}}">
|
||||
<button type="submit" class="btn ghost small ico"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>Новый код</button>
|
||||
<span class="muted">латиница и цифры, от 3 до 24; новый код отменяет старую ссылку</span>
|
||||
</form>
|
||||
|
||||
<details style="margin-top:10px">
|
||||
<summary class="muted" style="cursor:pointer">Полные варианты (секрет в команде)</summary>
|
||||
<label>curl</label>
|
||||
|
||||
Reference in new issue
Block a user