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()