mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 11:39:57 +00:00
* Migrate to profile ids * Migrate android profile manager * Clean up * Fix review * Add ID type * Fix test and runes in ShortID() * Fix profile switch on up and android comments * Revert android profile to string id * Fix feedback * Fix UI feedback * Fix id assignment * Add renaming of profiles * Fix review * Remove ui binary * Fix getProfileConfigPath not validating id * Change resolve handle order and fix server merge problems * Fix mdm test
119 lines
3.2 KiB
Go
119 lines
3.2 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
|
||
)
|
||
|
||
type ID string
|
||
|
||
// generateProfileID returns a new random hex ID for a profile file.
|
||
func generateProfileID() (ID, error) {
|
||
buf := make([]byte, profileIDByteLen)
|
||
if _, err := rand.Read(buf); err != nil {
|
||
return "", fmt.Errorf("read random bytes: %w", err)
|
||
}
|
||
return ID(hex.EncodeToString(buf)), nil
|
||
}
|
||
|
||
// IsValidProfileFilenameStem reports whether id is safe to use as the stem
|
||
// of a profile JSON filename.
|
||
func IsValidProfileFilenameStem(id ID) bool {
|
||
s := id.String()
|
||
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 (id ID) ShortID() string {
|
||
if id == DefaultProfileName {
|
||
return DefaultProfileName
|
||
}
|
||
runes := []rune(id)
|
||
if len(runes) <= shortIDLen {
|
||
return id.String()
|
||
}
|
||
return string(runes[:shortIDLen])
|
||
}
|
||
|
||
func (id ID) String() string {
|
||
return string(id)
|
||
}
|