main
go 128 lines 3.87 KB
Raw
1 package utils
2
3 import (
4 "net"
5 "net/url"
6 "strings"
7 )
8
9 var exposeNameOpeners = []string{
10 "arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
11 "fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
12 "hyper", "jazzy", "jolly", "lively", "lucky", "magic", "mellow", "minty",
13 "misty", "moonlit", "mystic", "neon", "nova", "peppy", "pixel", "playful",
14 "poppy", "rapid", "rocket", "rowdy", "snappy", "snazzy", "sparkly", "spicy",
15 "sprightly", "starry", "sunny", "swift", "tangy", "tidy", "toasty", "turbo",
16 "velvet", "vivid", "wavy", "whimsy", "wild", "wonky", "zany", "zesty",
17 }
18
19 var exposeNameCenters = []string{
20 "alpaca", "badger", "banjo", "beacon", "biscuit", "capybara", "comet", "cricket",
21 "dragon", "falcon", "feather", "fjord", "fox", "gadget", "gecko", "gizmo",
22 "harbor", "heron", "iguana", "jelly", "koala", "lemur", "mango", "narwhal",
23 "nebula", "noodle", "octopus", "otter", "panda", "pepper", "phoenix", "pickle",
24 "puffin", "quokka", "radar", "ranger", "rocket", "scooter", "seahorse", "skylark",
25 "sprocket", "starling", "sunbeam", "taco", "thimble", "tiger", "toucan", "triton",
26 "walrus", "widget", "willow", "wombat", "yeti", "zeppelin", "zigzag", "zinnia",
27 }
28
29 var exposeNameClosers = []string{
30 "arcade", "beacon", "boogie", "bounce", "burst", "cascade", "chorus", "dash",
31 "disco", "drift", "echo", "fiesta", "flare", "flash", "flight", "flip",
32 "glow", "groove", "jam", "jive", "launch", "loop", "march", "orbit",
33 "parade", "party", "pulse", "quest", "rally", "riot", "ripple", "rodeo",
34 "roll", "rush", "serenade", "shuffle", "signal", "sketch", "spark", "sprint",
35 "starlight", "stride", "sway", "swoop", "twirl", "uplift", "vibe", "voyage",
36 "whirl", "wink", "zap", "zenith", "zip", "zoom", "zest", "zone",
37 }
38
39 const (
40 defaultExposeTargetPort = "3000"
41 defaultExposeTargetHost = "127.0.0.1"
42 )
43
44 // DefaultExposeName generates a deterministic 3-word DNS label from a target
45 // address and seed using FNV-1a hashing. The algorithm matches the frontend
46 // implementation in frontend/src/lib/exposeName.ts:buildDefaultExposeName.
47 func DefaultExposeName(target, rawSeed string) (string, error) {
48 seed := strings.TrimSpace(rawSeed)
49 if cut, ok := strings.CutPrefix(seed, "cli_"); ok {
50 seed = cut
51 }
52 if seed == "" {
53 seed = "portal"
54 }
55
56 input := []byte(seed + "|" + normalizeExposeTarget(target))
57 first := fnv1a32(input, 0x811c9dc5)
58 second := fnv1a32(input, 0x9e3779b9)
59 third := fnv1a32(input, 0x85ebca6b)
60
61 label := strings.Join([]string{
62 exposeNameOpeners[int(first&0xff)%len(exposeNameOpeners)],
63 exposeNameCenters[int(second&0xff)%len(exposeNameCenters)],
64 exposeNameClosers[int(third&0xff)%len(exposeNameClosers)],
65 }, "-")
66
67 return NormalizeDNSLabel(label)
68 }
69
70 // normalizeExposeTarget normalizes a target address for deterministic name
71 // generation. Must match frontend/src/lib/exposeName.ts:normalizeExposeTarget.
72 func normalizeExposeTarget(raw string) string {
73 trimmed := strings.TrimSpace(raw)
74 candidate := trimmed
75 if candidate == "" {
76 candidate = defaultExposeTargetPort
77 }
78
79 if isAllDigits(candidate) {
80 return defaultExposeTargetHost + ":" + candidate
81 }
82
83 if strings.Contains(candidate, "://") {
84 u, err := url.Parse(candidate)
85 if err != nil {
86 return candidate
87 }
88 if (u.Scheme == "http" || u.Scheme == "https") &&
89 u.Host != "" &&
90 (u.Path == "" || u.Path == "/") &&
91 u.RawQuery == "" &&
92 u.Fragment == "" {
93 return u.Host
94 }
95 return candidate
96 }
97
98 u, err := url.Parse("tcp://" + candidate)
99 if err != nil || u.Hostname() == "" {
100 return candidate
101 }
102 port := u.Port()
103 if port == "" {
104 port = "80"
105 }
106 return net.JoinHostPort(u.Hostname(), port)
107 }
108
109 func isAllDigits(s string) bool {
110 if s == "" {
111 return false
112 }
113 for _, c := range s {
114 if c < '0' || c > '9' {
115 return false
116 }
117 }
118 return true
119 }
120
121 func fnv1a32(data []byte, seed uint32) uint32 {
122 h := seed
123 for _, b := range data {
124 h ^= uint32(b)
125 h *= 0x01000193
126 }
127 return h
128 }