mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 17:01:29 +02:00
111 lines
3.0 KiB
Go
111 lines
3.0 KiB
Go
package profilemanager
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"path/filepath"
|
||
"strings"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
const (
|
||
// profileIDByteLen is the number of random bytes generated for a new
|
||
// profile ID. The resulting hex string is twice this length.
|
||
profileIDByteLen = 16
|
||
|
||
// shortIDLen is the number of leading characters of an ID we render in
|
||
// list output. Profiles per device are few, so 8 chars is collision-safe
|
||
// in practice and easy to type as a prefix.
|
||
shortIDLen = 8
|
||
|
||
// maxProfileNameLen caps the human-readable profile name to keep table
|
||
// output legible and prevent denial-of-service via huge JSON fields.
|
||
maxProfileNameLen = 128
|
||
|
||
// maxProfileIDLen bounds the on-disk filename we'll accept. New
|
||
// IDs are 32 hex chars, legacy stems are sanitized profile names. The
|
||
// cap is generous enough to cover both without permitting absurdly
|
||
// long filenames.
|
||
maxProfileIDLen = 64
|
||
)
|
||
|
||
// generateProfileID returns a new random hex ID for a profile file.
|
||
func generateProfileID() (string, error) {
|
||
buf := make([]byte, profileIDByteLen)
|
||
if _, err := rand.Read(buf); err != nil {
|
||
return "", fmt.Errorf("read random bytes: %w", err)
|
||
}
|
||
return hex.EncodeToString(buf), nil
|
||
}
|
||
|
||
// isValidProfileFilenameStem reports whether s is safe to use as the stem
|
||
// of a profile JSON filename.
|
||
func isValidProfileFilenameStem(s string) bool {
|
||
if s == "" || len(s) > maxProfileIDLen {
|
||
return false
|
||
}
|
||
if s == defaultProfileName {
|
||
return true
|
||
}
|
||
if strings.ContainsAny(s, `/\`) || strings.Contains(s, "..") {
|
||
return false
|
||
}
|
||
// filepath.Base catches any leftover separators on platforms with
|
||
// exotic path conventions.
|
||
if filepath.Base(s) != s {
|
||
return false
|
||
}
|
||
for _, r := range s {
|
||
if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-') {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// sanitizeDisplayName normalizes a user-supplied profile display name for
|
||
// storage. It strips ASCII control characters, rejects invalid UTF-8, and
|
||
// caps the length. Emojis, spaces, punctuation, and non-ASCII letters are
|
||
// preserved. Returns an error if nothing usable remains.
|
||
func sanitizeDisplayName(name string) (string, error) {
|
||
if !utf8.ValidString(name) {
|
||
return "", fmt.Errorf("name is not valid UTF-8")
|
||
}
|
||
name = StripCtrlChars(name)
|
||
name = strings.TrimSpace(name)
|
||
if name == "" {
|
||
return "", fmt.Errorf("name is empty after sanitization")
|
||
}
|
||
if utf8.RuneCountInString(name) > maxProfileNameLen {
|
||
return "", fmt.Errorf("name exceeds %d characters", maxProfileNameLen)
|
||
}
|
||
return name, nil
|
||
}
|
||
|
||
// StripCtrlChars control characters from a name before printing it.
|
||
func StripCtrlChars(name string) string {
|
||
var b strings.Builder
|
||
b.Grow(len(name))
|
||
for _, r := range name {
|
||
// Skip C0 controls and DEL, plus C1 controls (0x80–0x9F).
|
||
if r < 0x20 || r == 0x7F || (r >= 0x80 && r <= 0x9F) {
|
||
continue
|
||
}
|
||
b.WriteRune(r)
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// ShortID truncates an ID for display.
|
||
func ShortID(id string) string {
|
||
if id == DefaultProfileName {
|
||
return id
|
||
}
|
||
if len(id) <= shortIDLen {
|
||
return id
|
||
}
|
||
return id[:shortIDLen]
|
||
}
|