refactor: remove contracts and netutil re-export wrappers, use types directly
Delete portal/contracts/ and portal/netutil/ thin wrapper packages. Rewrite all consumer imports to use gosuda.org/portal/types directly. Consumers updated: cmd/portal-tunnel/main.go, cmd/relay-server/main.go, cmd/relay-server/serve.go, cmd/relay-server/utils.go, sdk/client.go, sdk/listener.go, portal/keyless/client.go.
cognitive committed
Mar 4, 2026 at 21:48 UTC
fb02e56c8cfaf72af164626f7eea275985d62722
9 files changed
+49
-172
cmd/portal-tunnel/main.go
+9
-10
@@ -17,9 +17,8 @@ import (
17
"github.com/rs/zerolog"
18
"github.com/rs/zerolog/log"
19
20
- "gosuda.org/portal/portal/contracts"
21
- "gosuda.org/portal/portal/netutil"
20
"gosuda.org/portal/sdk"
21
+ "gosuda.org/portal/types"
22
)
23
24
var (
@@ -64,7 +63,7 @@ func runTunnel() error {
63
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
64
defer stop()
65
67
- relayURLs := netutil.ParseURLs(flagRelayURLs)
66
+ relayURLs := types.ParseURLs(flagRelayURLs)
67
if len(relayURLs) == 0 {
68
return errors.New("no relay URLs provided")
69
}
@@ -86,11 +85,11 @@ func runTunnel() error {
85
86
listener, err := sdkClient.Listen(
87
flagName,
89
- contracts.WithDescription(flagDesc),
90
- contracts.WithTags(netutil.ParseURLs(flagTags)),
91
- contracts.WithOwner(flagOwner),
92
- contracts.WithThumbnail(flagThumbnail),
93
- contracts.WithHide(flagHide),
88
+ types.WithDescription(flagDesc),
89
+ types.WithTags(types.ParseURLs(flagTags)),
90
+ types.WithOwner(flagOwner),
91
+ types.WithThumbnail(flagThumbnail),
92
+ types.WithHide(flagHide),
93
)
94
if err != nil {
95
return fmt.Errorf("service %s: failed to register service: %w", flagName, err)
@@ -169,7 +168,7 @@ loop:
168
func normalizeRelayURLsForReverseConnect(relayURLs []string) ([]string, error) {
169
normalized := make([]string, 0, len(relayURLs))
170
for _, relayURL := range relayURLs {
172
- normalizedURL, err := netutil.NormalizeRelayAPIURL(relayURL)
171
+ normalizedURL, err := types.NormalizeRelayAPIURL(relayURL)
172
if err != nil {
173
return nil, fmt.Errorf("invalid relay URL %q: %w", relayURL, err)
174
}
@@ -188,7 +187,7 @@ var bufferPool = sync.Pool{
187
func proxyConnection(ctx context.Context, localAddr string, relayConn net.Conn) error {
188
defer relayConn.Close()
189
191
- targetAddr, err := netutil.NormalizeTargetAddr(localAddr)
190
+ targetAddr, err := types.NormalizeTargetAddr(localAddr)
191
if err != nil {
192
return fmt.Errorf("invalid --host value %q: %w", localAddr, err)
193
}
cmd/relay-server/main.go
+6
-6
@@ -15,9 +15,9 @@ import (
15
"github.com/rs/zerolog/log"
16
17
"gosuda.org/portal/portal"
18
- "gosuda.org/portal/portal/netutil"
18
"gosuda.org/portal/portal/policy"
19
"gosuda.org/portal/portal/sni"
20
+ "gosuda.org/portal/types"
21
)
22
23
const (
@@ -51,9 +51,9 @@ func main() {
51
}
52
bootstrapsCSV := trimmedEnv("BOOTSTRAP_URIS")
53
if bootstrapsCSV == "" {
54
- bootstrapsCSV = netutil.DefaultBootstrapFrom(portalURL)
54
+ bootstrapsCSV = types.DefaultBootstrapFrom(portalURL)
55
}
56
- sniPort := netutil.ParsePortNumber(os.Getenv("SNI_PORT"), defaultSNIPort)
56
+ sniPort := types.ParsePortNumber(os.Getenv("SNI_PORT"), defaultSNIPort)
57
keylessDir := trimmedEnv("KEYLESS_DIR")
58
if keylessDir == "" {
59
keylessDir = defaultKeylessDir
@@ -75,7 +75,7 @@ func main() {
75
flag.StringVar(&cfg.CloudflareToken, "cloudflare-token", cloudflareToken, "Cloudflare DNS API token (Zone:Read + DNS:Edit) (env: CLOUDFLARE_TOKEN)")
76
flag.Parse()
77
78
- cfg.Bootstraps = netutil.ParseURLs(bootstrapsCSV)
78
+ cfg.Bootstraps = types.ParseURLs(bootstrapsCSV)
79
parsedTrustedProxyCIDRs, err := parseTrustedProxyCIDRs(cfg.TrustedProxyCIDRs)
80
if err != nil {
81
log.Fatal().Err(err).Msg("parse trusted proxy CIDRs")
@@ -98,8 +98,8 @@ func runServer(cfg relayServerConfig) error {
98
Strs("bootstrap_uris", cfg.Bootstraps).
99
Msg("[server] frontend configuration")
100
101
- rootHost := netutil.PortalRootHost(cfg.PortalURL)
102
- apiUpstreamAddr := netutil.LoopbackForwardAddr(fmt.Sprintf(":%d", cfg.AdminPort))
101
+ rootHost := types.PortalRootHost(cfg.PortalURL)
102
+ apiUpstreamAddr := types.LoopbackForwardAddr(fmt.Sprintf(":%d", cfg.AdminPort))
103
serv, err := portal.NewRelayServer(ctx, cfg.Bootstraps, sniListenAddr, rootHost, cfg.KeylessDir, cfg.CloudflareToken)
104
if err != nil {
105
return fmt.Errorf("create relay server: %w", err)
cmd/relay-server/serve.go
+12
-13
@@ -16,10 +16,9 @@ import (
16
"github.com/rs/zerolog/log"
17
18
"gosuda.org/portal/portal"
19
- "gosuda.org/portal/portal/contracts"
19
"gosuda.org/portal/portal/keyless"
21
- "gosuda.org/portal/portal/netutil"
20
"gosuda.org/portal/portal/policy"
21
+ "gosuda.org/portal/types"
22
)
23
24
const defaultHTTPSPort = "443"
@@ -42,21 +41,21 @@ func serveAPI(addr string, serv *portal.RelayServer, admin *Admin, frontend *Fro
41
frontend.ServeAsset(appMux, "/favicon.svg", "favicon.svg", "image/svg+xml")
42
43
// Portal app assets (JS, CSS, etc.) - served from /app/
45
- appMux.HandleFunc(contracts.PathAppPrefix, func(w http.ResponseWriter, r *http.Request) {
44
+ appMux.HandleFunc(types.PathAppPrefix, func(w http.ResponseWriter, r *http.Request) {
45
setCORSHeaders(w)
46
if r.Method == http.MethodOptions {
47
w.WriteHeader(http.StatusOK)
48
return
49
}
51
- p := strings.TrimPrefix(r.URL.Path, contracts.PathAppPrefix)
50
+ p := strings.TrimPrefix(r.URL.Path, types.PathAppPrefix)
51
frontend.ServeAppStatic(w, r, p, serv)
52
})
53
54
// Tunnel installer script and binaries
56
- appMux.HandleFunc(contracts.PathTunnelScript, func(w http.ResponseWriter, r *http.Request) {
55
+ appMux.HandleFunc(types.PathTunnelScript, func(w http.ResponseWriter, r *http.Request) {
56
serveTunnelScript(w, r, cfg.PortalURL)
57
})
59
- appMux.HandleFunc(contracts.PathTunnelBinary, func(w http.ResponseWriter, r *http.Request) {
58
+ appMux.HandleFunc(types.PathTunnelBinary, func(w http.ResponseWriter, r *http.Request) {
59
serveTunnelBinary(w, r)
60
})
61
@@ -70,12 +69,12 @@ func serveAPI(addr string, serv *portal.RelayServer, admin *Admin, frontend *Fro
69
portalURL: cfg.PortalURL,
70
trustProxyHeaders: cfg.TrustProxyHeaders,
71
}
73
- appMux.HandleFunc(contracts.PathSDKPrefix, func(w http.ResponseWriter, r *http.Request) {
72
+ appMux.HandleFunc(types.PathSDKPrefix, func(w http.ResponseWriter, r *http.Request) {
73
registry.HandleSDKRequest(w, r, serv)
74
})
75
76
// Keyless signer endpoint.
78
- appMux.HandleFunc(contracts.PathKeylessSign, func(w http.ResponseWriter, r *http.Request) {
77
+ appMux.HandleFunc(types.PathKeylessSign, func(w http.ResponseWriter, r *http.Request) {
78
handleKeylessSign(w, r, serv.GetKeylessSigner())
79
})
80
@@ -86,7 +85,7 @@ func serveAPI(addr string, serv *portal.RelayServer, admin *Admin, frontend *Fro
85
frontend.ServeAppStatic(w, r, p, serv)
86
})
87
89
- appMux.HandleFunc(contracts.PathHealthz, func(w http.ResponseWriter, _ *http.Request) {
88
+ appMux.HandleFunc(types.PathHealthz, func(w http.ResponseWriter, _ *http.Request) {
89
w.WriteHeader(http.StatusOK)
90
if _, err := w.Write([]byte("{\"status\":\"ok\"}")); err != nil {
91
log.Debug().Err(err).Msg("[healthz] failed to write response")
@@ -94,15 +93,15 @@ func serveAPI(addr string, serv *portal.RelayServer, admin *Admin, frontend *Fro
93
})
94
95
// Admin API
97
- appMux.HandleFunc(contracts.PathAdminPrefix+"/", func(w http.ResponseWriter, r *http.Request) {
96
+ appMux.HandleFunc(types.PathAdminPrefix+"/", func(w http.ResponseWriter, r *http.Request) {
97
admin.HandleAdminRequest(w, r, serv)
98
})
99
100
// Create the main handler
102
- appDomain := netutil.DefaultAppPattern(cfg.PortalURL)
101
+ appDomain := types.DefaultAppPattern(cfg.PortalURL)
102
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
103
// Handle subdomain requests
105
- if netutil.IsSubdomain(appDomain, r.Host) {
104
+ if types.IsSubdomain(appDomain, r.Host) {
105
log.Debug().
106
Str("host", r.Host).
107
Str("url", r.URL.String()).
@@ -129,7 +128,7 @@ func serveAPI(addr string, serv *portal.RelayServer, admin *Admin, frontend *Fro
128
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
129
}
130
acmeManager := serv.GetACMEManager()
132
- rootHost := netutil.PortalRootHost(cfg.PortalURL)
131
+ rootHost := types.PortalRootHost(cfg.PortalURL)
132
srv.TLSConfig = &tls.Config{
133
ClientAuth: tls.RequestClientCert,
134
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cmd/relay-server/utils.go
+9
-10
@@ -11,10 +11,9 @@ import (
11
"github.com/rs/zerolog/log"
12
13
"gosuda.org/portal/portal"
14
- "gosuda.org/portal/portal/contracts"
14
"gosuda.org/portal/portal/keyless"
16
- "gosuda.org/portal/portal/netutil"
15
"gosuda.org/portal/portal/policy"
16
+ "gosuda.org/portal/types"
17
)
18
19
const (
@@ -195,15 +194,15 @@ func (r *leaseRow) fromLeaseEntry(entry *portal.LeaseEntry, admin *Admin, portal
194
r.FirstSeenISO = entry.FirstSeen.UTC().Format(time.RFC3339)
195
r.TTL = r.formatDuration(time.Until(entry.Expires))
196
linkLabel := identityID
198
- if normalized, ok := netutil.NormalizeServiceName(lease.Name); ok {
197
+ if normalized, ok := types.NormalizeServiceName(lease.Name); ok {
198
linkLabel = normalized
200
- } else if normalized, ok := netutil.NormalizeServiceName(identityID); ok {
199
+ } else if normalized, ok := types.NormalizeServiceName(identityID); ok {
200
linkLabel = normalized
201
}
202
204
- publicHost := netutil.PortalRootHost(portalURL)
203
+ publicHost := types.PortalRootHost(portalURL)
204
if publicHost == "" {
206
- publicHost = netutil.PortalHostPort(portalURL)
205
+ publicHost = types.PortalHostPort(portalURL)
206
}
207
if linkLabel != "" && publicHost != "" {
208
r.Link = fmt.Sprintf("//%s.%s/", linkLabel, publicHost)
@@ -281,7 +280,7 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer, admin *Admin, forAdmin
280
func writeAPIData(w http.ResponseWriter, status int, data any) {
281
w.Header().Set("Content-Type", "application/json")
282
w.WriteHeader(status)
284
- if err := json.NewEncoder(w).Encode(contracts.APIEnvelope{
283
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{
284
OK: true,
285
Data: data,
286
}); err != nil {
@@ -292,7 +291,7 @@ func writeAPIData(w http.ResponseWriter, status int, data any) {
291
func writeAPIOK(w http.ResponseWriter, status int) {
292
w.Header().Set("Content-Type", "application/json")
293
w.WriteHeader(status)
295
- if err := json.NewEncoder(w).Encode(contracts.APIEnvelope{OK: true}); err != nil {
294
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{OK: true}); err != nil {
295
log.Error().Err(err).Msg("[HTTP] Failed to encode API success response")
296
}
297
}
@@ -304,10 +303,10 @@ func writeAPIError(w http.ResponseWriter, status int, code, message string) {
303
func writeAPIErrorWithData(w http.ResponseWriter, status int, code, message string, data any) {
304
w.Header().Set("Content-Type", "application/json")
305
w.WriteHeader(status)
307
- if err := json.NewEncoder(w).Encode(contracts.APIEnvelope{
306
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{
307
OK: false,
308
Data: data,
310
- Error: &contracts.APIError{
309
+ Error: &types.APIError{
310
Code: code,
311
Message: message,
312
},
portal/contracts/contracts.go
deleted
-58
@@ -1,58 +0,0 @@
1
-package contracts
2
-
3
-import "gosuda.org/portal/types"
4
-
5
-// API path constants for Portal relay server.
6
-const (
7
- PathSDKPrefix = types.PathSDKPrefix
8
- PathSDKRegister = types.PathSDKRegister
9
- PathSDKUnregister = types.PathSDKUnregister
10
- PathSDKRenew = types.PathSDKRenew
11
- PathSDKDomain = types.PathSDKDomain
12
- PathSDKConnect = types.PathSDKConnect
13
-
14
- PathAdminPrefix = types.PathAdminPrefix
15
- PathAdminLogin = types.PathAdminLogin
16
- PathAdminLogout = types.PathAdminLogout
17
- PathAdminAuthStatus = types.PathAdminAuthStatus
18
- PathAdminLeases = types.PathAdminLeases
19
- PathAdminLeasesBanned = types.PathAdminLeasesBanned
20
- PathAdminStats = types.PathAdminStats
21
- PathAdminSettings = types.PathAdminSettings
22
- PathAdminApprovalMode = types.PathAdminApprovalMode
23
-
24
- PathKeylessSign = types.PathKeylessSign
25
- PathHealthz = types.PathHealthz
26
-
27
- PathTunnelScript = types.PathTunnelScript
28
- PathTunnelBinary = types.PathTunnelBinary
29
-
30
- PathAppPrefix = types.PathAppPrefix
31
-)
32
-
33
-type (
34
- APIError = types.APIError
35
- APIEnvelope = types.APIEnvelope
36
- Metadata = types.Metadata
37
- MetadataOption = types.MetadataOption
38
-)
39
-
40
-func WithDescription(description string) MetadataOption {
41
- return types.WithDescription(description)
42
-}
43
-
44
-func WithTags(tags []string) MetadataOption {
45
- return types.WithTags(tags)
46
-}
47
-
48
-func WithThumbnail(thumbnail string) MetadataOption {
49
- return types.WithThumbnail(thumbnail)
50
-}
51
-
52
-func WithOwner(owner string) MetadataOption {
53
- return types.WithOwner(owner)
54
-}
55
-
56
-func WithHide(hide bool) MetadataOption {
57
- return types.WithHide(hide)
58
-}
portal/keyless/client.go
+2
-2
@@ -16,7 +16,7 @@ import (
16
17
keylesstls "github.com/gosuda/keyless_tls/keyless"
18
19
- "gosuda.org/portal/portal/netutil"
19
+ "gosuda.org/portal/types"
20
)
21
22
// BuildClientTLSConfig builds a keyless TLS server config for tunnel-side TLS termination.
@@ -185,7 +185,7 @@ func FetchEndpointCertificateChain(ctx context.Context, endpoint string, serverN
185
tlsConn := tls.Client(rawConn, &tls.Config{
186
MinVersion: tls.VersionTLS12,
187
ServerName: serverName,
188
- InsecureSkipVerify: netutil.IsLocalhost(host),
188
+ InsecureSkipVerify: types.IsLocalhost(host),
189
})
190
defer tlsConn.Close()
191
if err := tlsConn.HandshakeContext(ctx); err != nil {
portal/netutil/netutil.go
deleted
-59
@@ -1,59 +0,0 @@
1
-package netutil
2
-
3
-import "gosuda.org/portal/types"
4
-
5
-func NormalizeServiceName(name string) (string, bool) {
6
- return types.NormalizeServiceName(name)
7
-}
8
-
9
-func IsSubdomain(domain, host string) bool {
10
- return types.IsSubdomain(domain, host)
11
-}
12
-
13
-func DefaultAppPattern(base string) string {
14
- return types.DefaultAppPattern(base)
15
-}
16
-
17
-func PortalHostPort(portalURL string) string {
18
- return types.PortalHostPort(portalURL)
19
-}
20
-
21
-func PortalRootHost(portalURL string) string {
22
- return types.PortalRootHost(portalURL)
23
-}
24
-
25
-func IsLocalhost(host string) bool {
26
- return types.IsLocalhost(host)
27
-}
28
-
29
-func DefaultBootstrapFrom(base string) string {
30
- return types.DefaultBootstrapFrom(base)
31
-}
32
-
33
-func ParseURLs(raw string) []string {
34
- return types.ParseURLs(raw)
35
-}
36
-
37
-func ParsePortNumber(raw string, fallback int) int {
38
- return types.ParsePortNumber(raw, fallback)
39
-}
40
-
41
-func LoopbackForwardAddr(listenAddr string) string {
42
- return types.LoopbackForwardAddr(listenAddr)
43
-}
44
-
45
-func IsValidLeaseName(name string) bool {
46
- return types.IsValidLeaseName(name)
47
-}
48
-
49
-func NormalizeRelayAPIURLs(bootstrapServers []string) ([]string, error) {
50
- return types.NormalizeRelayAPIURLs(bootstrapServers)
51
-}
52
-
53
-func NormalizeRelayAPIURL(relayURL string) (string, error) {
54
- return types.NormalizeRelayAPIURL(relayURL)
55
-}
56
-
57
-func NormalizeTargetAddr(targetAddr string) (string, error) {
58
- return types.NormalizeTargetAddr(targetAddr)
59
-}
sdk/client.go
+3
-4
@@ -24,7 +24,6 @@ import (
24
25
"gosuda.org/portal/portal"
26
"gosuda.org/portal/portal/keyless"
27
- "gosuda.org/portal/portal/netutil"
27
"gosuda.org/portal/types"
28
)
29
@@ -95,11 +94,11 @@ func (c *Client) Listen(name string, options ...types.MetadataOption) (net.Liste
94
if name == "" {
95
return nil, errors.New("name is required")
96
}
98
- if !netutil.IsValidLeaseName(name) {
97
+ if !types.IsValidLeaseName(name) {
98
return nil, ErrInvalidName
99
}
100
102
- relayAddrs, err := netutil.NormalizeRelayAPIURLs(c.config.BootstrapServers)
101
+ relayAddrs, err := types.NormalizeRelayAPIURLs(c.config.BootstrapServers)
102
if err != nil {
103
return nil, ErrNoAvailableRelay
104
}
@@ -372,7 +371,7 @@ func (c *Client) buildTLSConfig(relayAddr, leaseName string) (*tls.Config, []fun
371
if keylessServerName == "" {
372
return nil, nil, fmt.Errorf("relay hostname is required: %s", relayAddr)
373
}
375
- baseHost := netutil.PortalRootHost(relayAddr)
374
+ baseHost := types.PortalRootHost(relayAddr)
375
if baseHost == "" {
376
return nil, nil, fmt.Errorf("keyless base host is required for relay %s", relayAddr)
377
}
sdk/listener.go
+8
-10
@@ -19,8 +19,6 @@ import (
19
"github.com/rs/zerolog/log"
20
21
"gosuda.org/portal/portal"
22
- "gosuda.org/portal/portal/contracts"
23
- "gosuda.org/portal/portal/netutil"
22
"gosuda.org/portal/types"
23
)
24
@@ -122,16 +120,16 @@ func NewListener(relayAddr string, lease *portal.Lease, tlsConfig *tls.Config, c
120
if tlsConfig == nil {
121
return nil, errors.New("tls config is required")
122
}
125
- apiURL, err := netutil.NormalizeRelayAPIURL(relayAddr)
123
+ apiURL, err := types.NormalizeRelayAPIURL(relayAddr)
124
if err != nil {
125
return nil, err
126
}
129
- host := netutil.PortalRootHost(apiURL)
127
+ host := types.PortalRootHost(apiURL)
128
clientTransport := http.DefaultTransport.(*http.Transport).Clone()
129
transportTLSConfig := &tls.Config{
130
MinVersion: tls.VersionTLS12,
131
ServerName: host,
134
- InsecureSkipVerify: netutil.IsLocalhost(host),
132
+ InsecureSkipVerify: types.IsLocalhost(host),
133
}
134
if len(controlPlaneCert.Certificate) > 0 {
135
transportTLSConfig.Certificates = []tls.Certificate{controlPlaneCert}
@@ -409,7 +407,7 @@ func (l *Listener) openReverseConnection() (net.Conn, error) {
407
reverseTLSConfig := &tls.Config{
408
MinVersion: tls.VersionTLS12,
409
ServerName: serverName,
412
- InsecureSkipVerify: netutil.IsLocalhost(serverName),
410
+ InsecureSkipVerify: types.IsLocalhost(serverName),
411
}
412
if len(l.controlPlaneCert.Certificate) > 0 {
413
reverseTLSConfig.Certificates = []tls.Certificate{l.controlPlaneCert}
@@ -666,7 +664,7 @@ func (l *Listener) registerWithRelay() error {
664
ReverseToken: l.lease.ReverseToken,
665
}
666
669
- return l.postJSON(contracts.PathSDKRegister, reqBody)
667
+ return l.postJSON(types.PathSDKRegister, reqBody)
668
}
669
670
func (l *Listener) unregisterFromRelay() error {
@@ -674,7 +672,7 @@ func (l *Listener) unregisterFromRelay() error {
672
LeaseID: l.lease.ID,
673
ReverseToken: l.lease.ReverseToken,
674
}
677
- return l.postJSON(contracts.PathSDKUnregister, reqBody)
675
+ return l.postJSON(types.PathSDKUnregister, reqBody)
676
}
677
678
func (l *Listener) sendKeepalive() error {
@@ -682,7 +680,7 @@ func (l *Listener) sendKeepalive() error {
680
LeaseID: l.lease.ID,
681
ReverseToken: l.lease.ReverseToken,
682
}
685
- return l.postJSON(contracts.PathSDKRenew, reqBody)
683
+ return l.postJSON(types.PathSDKRenew, reqBody)
684
}
685
686
func (l *Listener) postJSON(path string, body any) error {
@@ -761,7 +759,7 @@ func relayConnectURL(relayAddr, leaseID, token string) (string, error) {
759
if u.Scheme != "https" {
760
return "", fmt.Errorf("unsupported relay URL scheme: %q (use https)", u.Scheme)
761
}
764
- u.Path = contracts.PathSDKConnect
762
+ u.Path = types.PathSDKConnect
763
q := u.Query()
764
q.Set("lease_id", leaseID)
765
u.RawQuery = q.Encode()