package service import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "errors" "io" "net" "net/http" "net/netip" "net/url" "strings" "time" "github.com/gorilla/websocket" "github.com/klarkxy/nekonest-cloud/relay/internal/controlplane" "github.com/klarkxy/nekonest-cloud/relay/internal/forwarder" "github.com/klarkxy/nekonest-cloud/relay/internal/registry" "github.com/klarkxy/nekonest/relaycore/protocol" ) const ( maxRegistrationBody = 64 << 10 maxHandoffBody = 64 << 10 maxRoutingBody = 1 << 20 maxFirstFrame = 128 << 10 ) type ControlPlane interface { ResolveDeviceRoute(context.Context, string, string) (controlplane.RouteResolution, error) ResolvePhoneRoute(context.Context, string, string) (controlplane.RouteResolution, error) ResolveTenantRoute(context.Context, string, int64) (controlplane.RouteResolution, error) ResolveHandoffRoute(context.Context, string, string) (controlplane.RouteResolution, error) AuthorizeDevice(context.Context, string, string) (controlplane.SnapshotResponse, error) AuthorizationSnapshot(context.Context, string, int64) (controlplane.SnapshotResponse, error) AuthorizePhone(context.Context, string, string) (controlplane.PhoneAuthorization, error) ConsumePhoneHandoff(context.Context, any) (controlplane.ConsumedHandoff, error) CompletePhoneHandoff(context.Context, string, string, string, string) error RevokePhone(context.Context, string, string, string) error RegisterDevice(context.Context, string, string, json.RawMessage) (protocol.DeviceRegistrationResponse, error) } type Config struct { ControlPlane ControlPlane Registry *registry.Registry AllowedPWAOrigins []string SourceHashSecret []byte TrustedProxyRanges []netip.Prefix AppVersion string NodeID string ForwardSecret []byte HandoffSecret []byte Forwarder *forwarder.Forwarder Now func() time.Time } type Service struct { controlPlane ControlPlane registry *registry.Registry origins map[string]struct{} sourceSecret []byte proxies []netip.Prefix appVersion string nodeID string forwardSecret []byte handoffSecret []byte forwarder *forwarder.Forwarder now func() time.Time upgrader websocket.Upgrader } func New(config Config) (*Service, error) { if config.ControlPlane == nil || config.Registry == nil || len(config.SourceHashSecret) < 32 || strings.TrimSpace(config.NodeID) == "" || len(config.ForwardSecret) < 32 || len(config.HandoffSecret) < 32 { return nil, errors.New("service requires control plane, registry, and independent 32-byte secrets") } origins := make(map[string]struct{}, len(config.AllowedPWAOrigins)) for _, origin := range config.AllowedPWAOrigins { parsed, err := url.Parse(origin) if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" { return nil, errors.New("service PWA origins must be exact origins") } origins[origin] = struct{}{} } if len(origins) == 0 { return nil, errors.New("service requires an exact PWA origin allowlist") } if config.Now == nil { config.Now = time.Now } service := &Service{ controlPlane: config.ControlPlane, registry: config.Registry, origins: origins, sourceSecret: append([]byte(nil), config.SourceHashSecret...), proxies: append([]netip.Prefix(nil), config.TrustedProxyRanges...), appVersion: strings.TrimSpace(config.AppVersion), nodeID: strings.TrimSpace(config.NodeID), forwardSecret: append([]byte(nil), config.ForwardSecret...), handoffSecret: append([]byte(nil), config.HandoffSecret...), forwarder: config.Forwarder, now: config.Now, } service.upgrader = websocket.Upgrader{ ReadBufferSize: 4096, WriteBufferSize: 4096, EnableCompression: false, CheckOrigin: func(r *http.Request) bool { origin := strings.TrimSpace(r.Header.Get("Origin")) _, allowed := service.origins[origin] return allowed }, } return service, nil } func (s *Service) Handler() http.Handler { return s.cors(http.HandlerFunc(s.serveHTTP)) } func (s *Service) serveHTTP(w http.ResponseWriter, r *http.Request) { if err := forwarder.VerifyIncoming(r, s.nodeID, s.forwardSecret, s.now()); err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "internal Relay forwarding authentication failed", false, http.StatusUnauthorized)) return } switch r.URL.Path { case "/health": s.handleHealth(w, r) case "/ws/daemon": s.handleDaemonWebSocket(w, r) case "/ws/phone": s.handlePhoneWebSocket(w, r) case "/api/devices/register": s.handleDeviceRegistration(w, r) case "/api/pwa/handoff/exchange": s.handlePhoneHandoff(w, r) default: s.handleDataPlane(w, r) } } func (s *Service) cors(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := strings.TrimSpace(r.Header.Get("Origin")) if origin != "" { if _, allowed := s.origins[origin]; !allowed { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "origin is not allowed", false, http.StatusForbidden)) return } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Add("Vary", "Origin") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Neko-Phone-Token, X-Neko-Route-Handle") w.Header().Set("Access-Control-Max-Age", "600") } if r.Method == http.MethodOptions { if origin == "" { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "origin is required", false, http.StatusForbidden)) return } w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Service) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "status": "nyan~", "service": "nekonest-cloud-relay", "server_version": s.appVersion, "protocol_version": protocol.CurrentProtocolVersion, "transport_mode": protocol.TransportSealed, }) } func readBounded(body io.ReadCloser, limit int64) ([]byte, error) { defer body.Close() data, err := io.ReadAll(io.LimitReader(body, limit+1)) if err != nil { return nil, err } if int64(len(data)) > limit { return nil, errors.New("request body exceeds limit") } return data, nil } func (s *Service) handleDeviceRegistration(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } body, err := readBounded(r.Body, maxRegistrationBody) if err != nil || !json.Valid(body) { writeHTTPError(w, apiError(protocol.ServiceErrorRegistrationDisabled, "invalid registration request", false, http.StatusBadRequest)) return } bootstrap := strings.TrimSpace(r.Header.Get("X-Neko-Bootstrap")) if bootstrap == "" { writeHTTPError(w, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "bootstrap credential is required", false, http.StatusUnauthorized)) return } result, err := s.controlPlane.RegisterDevice(r.Context(), bootstrap, s.sourceHash(r), json.RawMessage(body)) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "registration is temporarily unavailable")) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) } func (s *Service) handlePhoneHandoff(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } origin := strings.TrimSpace(r.Header.Get("Origin")) if _, allowed := s.origins[origin]; !allowed { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "PWA origin is not allowed", false, http.StatusForbidden)) return } body, err := readBounded(r.Body, maxHandoffBody) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "invalid phone handoff", false, http.StatusBadRequest)) return } var request struct { Ticket string `json:"ticket"` PWAOrigin string `json:"pwa_origin"` Name string `json:"name"` PhoneEd25519Public string `json:"phone_ed25519_public"` PhoneX25519Public string `json:"phone_x25519_public"` IdentityFingerprint string `json:"identity_fingerprint"` } decoder := json.NewDecoder(bytes.NewReader(body)) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil || decoder.Decode(&struct{}{}) != io.EOF || request.PWAOrigin != origin { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "invalid phone handoff", false, http.StatusBadRequest)) return } route, err := s.controlPlane.ResolveHandoffRoute(r.Context(), request.Ticket, request.PWAOrigin) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "phone handoff route is unavailable")) return } if !route.Local { r.Body = io.NopCloser(bytes.NewReader(body)) s.forwardHTTP(w, r, route) return } consumed, err := s.controlPlane.ConsumePhoneHandoff(r.Context(), request) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorPhoneCredentialInvalid, "phone handoff was rejected")) return } snapshot, err := s.controlPlane.AuthorizationSnapshot(r.Context(), consumed.TenantID, consumed.PlacementGeneration) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "tenant route is unavailable")) return } tenant, err := s.registry.Accept(r.Context(), snapshot) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "tenant authorization could not be verified", true, http.StatusServiceUnavailable)) return } phoneID, phoneToken, routeHandle := deriveHandoffCredentials( s.handoffSecret, consumed.HandoffID, consumed.IdentityFingerprint, ) if err := tenant.SyncPhone( phoneID, consumed.Name, controlplane.SHA256Hex(phoneToken), consumed.PhoneEd25519Public, consumed.PhoneX25519Public, ); err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "phone identity could not be created", true, http.StatusServiceUnavailable)) return } if err := s.completePhoneHandoff( r.Context(), consumed.HandoffID, phoneID, controlplane.SHA256Hex(phoneToken), controlplane.SHA256Hex(routeHandle), ); err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "phone handoff completion is indeterminate")) return } // Completion records only a pending principal. Do not cache the route yet: // the first request must prove possession to the control plane, which then // activates the principal and advances the authorization revision. w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{ "phone_id": phoneID, "phone_token": phoneToken, "route_handle": routeHandle, }) } func (s *Service) handleDaemonWebSocket(w http.ResponseWriter, r *http.Request) { // Daemons do not send browser Origin. If a caller does, require the same // exact allowlist as the PWA rather than accepting an arbitrary website. if origin := strings.TrimSpace(r.Header.Get("Origin")); origin != "" { if _, allowed := s.origins[origin]; !allowed { writeHTTPError(w, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "origin is not allowed", false, http.StatusForbidden)) return } } upgrader := s.upgrader upgrader.CheckOrigin = func(r *http.Request) bool { origin := strings.TrimSpace(r.Header.Get("Origin")) if origin == "" { return true } _, allowed := s.origins[origin] return allowed } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() first, firstType, firstFrame, err := readFirstFrame(conn, 10*time.Second) if err != nil || first.Type != protocol.MsgRegisterDevice { writeWebSocketError(conn, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "expected device authentication", false, http.StatusUnauthorized)) return } deviceID, _ := first.Payload["device_id"].(string) token, _ := first.Payload["token"].(string) if strings.TrimSpace(deviceID) == "" || strings.TrimSpace(token) == "" { writeWebSocketError(conn, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "device credential is required", false, http.StatusUnauthorized)) return } route, err := s.controlPlane.ResolveDeviceRoute(r.Context(), deviceID, controlplane.SHA256Hex(token)) if err != nil { writeWebSocketError(conn, publicError(err, protocol.ServiceErrorRouteUnavailable, "device route is unavailable")) return } if !route.Local { s.forwardWebSocket(conn, r, route, firstType, firstFrame) return } tenant, err := s.authorizeDevice(r.Context(), deviceID, token, false) if err != nil { writeWebSocketError(conn, publicError(err, protocol.ServiceErrorRouteUnavailable, "device route is unavailable")) return } tenant.Engine().ServeDaemonConn(conn, first) } func (s *Service) handlePhoneWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() first, firstType, firstFrame, err := readFirstFrame(conn, 15*time.Second) if err != nil || first.Type != protocol.MsgSubscribe { writeWebSocketError(conn, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "expected phone subscription", false, http.StatusUnauthorized)) return } routeHandle := requestRouteHandle(r, first) phoneToken := requestPhoneToken(r, first) if routeHandle == "" || phoneToken == "" { writeWebSocketError(conn, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "phone route and credential are required", false, http.StatusUnauthorized)) return } route, err := s.controlPlane.ResolvePhoneRoute(r.Context(), routeHandle, controlplane.SHA256Hex(phoneToken)) if err != nil { writeWebSocketError(conn, publicError(err, protocol.ServiceErrorRouteUnavailable, "phone route is unavailable")) return } if !route.Local { s.forwardWebSocket(conn, r, route, firstType, firstFrame) return } tenant, err := s.authorizePhone(r.Context(), routeHandle, phoneToken, false) if err != nil { writeWebSocketError(conn, publicError(err, protocol.ServiceErrorRouteUnavailable, "phone route is unavailable")) return } cloned := r.Clone(r.Context()) cloned.Header = r.Header.Clone() cloned.Header.Set("X-Neko-Phone-Token", phoneToken) tenant.Engine().ServePhoneConn(conn, cloned, first) } func readFirstFrame(conn *websocket.Conn, timeout time.Duration) (*protocol.NekoMessage, int, []byte, error) { conn.SetReadLimit(maxFirstFrame) _ = conn.SetReadDeadline(time.Now().Add(timeout)) messageType, data, err := conn.ReadMessage() if err != nil { return nil, 0, nil, err } var message protocol.NekoMessage if err := json.Unmarshal(data, &message); err != nil { return nil, 0, nil, err } return &message, messageType, data, nil } func (s *Service) forwardHTTP(w http.ResponseWriter, r *http.Request, route controlplane.RouteResolution) { if s.forwarder == nil || route.Local || route.EndpointRef == "" || route.RelayNodeID == "" { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "internal Relay route is unavailable", true, http.StatusServiceUnavailable)) return } if err := s.forwarder.ForwardHTTP(w, r, route.EndpointRef, route.RelayNodeID); err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "internal Relay forwarding failed", true, http.StatusServiceUnavailable)) } } func (s *Service) forwardWebSocket( client *websocket.Conn, r *http.Request, route controlplane.RouteResolution, firstType int, firstFrame []byte, ) { if s.forwarder == nil || route.Local || route.EndpointRef == "" || route.RelayNodeID == "" { writeWebSocketError(client, apiError(protocol.ServiceErrorRouteUnavailable, "internal Relay route is unavailable", true, http.StatusServiceUnavailable)) return } target, response, err := s.forwarder.DialWebSocket(r.Context(), r, route.EndpointRef, route.RelayNodeID) if response != nil && response.Body != nil { _ = response.Body.Close() } if err != nil { writeWebSocketError(client, apiError(protocol.ServiceErrorRouteUnavailable, "internal Relay forwarding failed", true, http.StatusServiceUnavailable)) return } defer target.Close() _ = forwarder.Tunnel(r.Context(), client, target, firstType, firstFrame) } func (s *Service) authorizeDevice(ctx context.Context, deviceID, token string, allowCached bool) (*registry.Tenant, error) { digest := controlplane.SHA256Hex(token) response, err := s.controlPlane.AuthorizeDevice(ctx, strings.TrimSpace(deviceID), digest) if err != nil { if allowCached && controlUnavailable(err) { if tenant, ok := s.registry.ByDeviceID(deviceID); ok { return tenant, nil } } return nil, err } tenant, err := s.registry.Accept(ctx, response) if err != nil { return nil, err } found := false for _, device := range tenant.Payload().Devices { if device.DeviceID == deviceID && constantTimeEqual(device.CredentialHash, digest) { found = true break } } if !found { return nil, errors.New("authorized device missing from signed snapshot") } return tenant, nil } func (s *Service) authorizePhone(ctx context.Context, routeHandle, phoneToken string, allowCached bool) (*registry.Tenant, error) { tokenHash := controlplane.SHA256Hex(phoneToken) authorization, err := s.controlPlane.AuthorizePhone(ctx, routeHandle, tokenHash) if err != nil { if allowCached && controlUnavailable(err) { if tenant, ok := s.registry.ByPhoneRoute(routeHandle); ok { return tenant, nil } } return nil, err } snapshot, err := s.controlPlane.AuthorizationSnapshot(ctx, authorization.TenantID, authorization.PlacementGeneration) if err != nil { if allowCached && controlUnavailable(err) { if tenant, ok := s.registry.ByPhoneRoute(routeHandle); ok { return tenant, nil } } return nil, err } tenant, err := s.registry.Accept(ctx, snapshot) if err != nil { return nil, err } payload := tenant.Payload() if payload.TenantID != authorization.TenantID || payload.PlacementGeneration != authorization.PlacementGeneration { return nil, errors.New("phone authorization and signed placement disagree") } if err := tenant.SyncPhone( authorization.Phone.PhoneID, authorization.Phone.Name, tokenHash, authorization.Phone.Ed25519Public, authorization.Phone.X25519Public, ); err != nil { return nil, err } if err := s.registry.BindPhoneRoute(routeHandle, tenant); err != nil { return nil, err } return tenant, nil } func (s *Service) handleDataPlane(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/attachments/") && r.Method == http.MethodGet { routeHint := strings.TrimSpace(r.URL.Query().Get("route")) if tenant, ok := s.registry.ByRouteHint(routeHint); ok { tenant.Handler().ServeHTTP(w, r) return } tenantID, generation, err := s.registry.DecodeRouteHint(routeHint) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "attachment route is unavailable", false, http.StatusNotFound)) return } route, err := s.controlPlane.ResolveTenantRoute(r.Context(), tenantID, generation) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "attachment route is unavailable")) return } if !route.Local { s.forwardHTTP(w, r, route) return } snapshot, err := s.controlPlane.AuthorizationSnapshot(r.Context(), tenantID, generation) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "attachment route is unavailable")) return } tenant, err := s.registry.Accept(r.Context(), snapshot) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "attachment route could not be restored", true, http.StatusServiceUnavailable)) return } tenant.Handler().ServeHTTP(w, r) return } if routeHandle := strings.TrimSpace(r.Header.Get("X-Neko-Route-Handle")); routeHandle != "" { phoneToken := requestPhoneToken(r, nil) if phoneToken == "" { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "phone credential is required", false, http.StatusUnauthorized)) return } if _, cached := s.registry.ByPhoneRoute(routeHandle); !cached { route, err := s.controlPlane.ResolvePhoneRoute(r.Context(), routeHandle, controlplane.SHA256Hex(phoneToken)) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorPhoneCredentialInvalid, "phone credential was rejected")) return } if !route.Local { s.forwardHTTP(w, r, route) return } } tenant, err := s.authorizePhone(r.Context(), routeHandle, phoneToken, true) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorPhoneCredentialInvalid, "phone credential was rejected")) return } if r.URL.Path == "/api/phones/revoke" { s.handlePhoneRevoke(w, r, tenant, phoneToken) return } tenant.Handler().ServeHTTP(w, r) return } body, err := readBounded(r.Body, maxRoutingBody) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "invalid device request", false, http.StatusBadRequest)) return } r.Body = io.NopCloser(bytes.NewReader(body)) deviceID, token := deviceCredential(r, body) if deviceID == "" || token == "" { writeHTTPError(w, apiError(protocol.ServiceErrorDeviceCredentialInvalid, "device credential is required", false, http.StatusUnauthorized)) return } if _, cached := s.registry.ByDeviceID(deviceID); !cached { route, err := s.controlPlane.ResolveDeviceRoute(r.Context(), deviceID, controlplane.SHA256Hex(token)) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorDeviceCredentialInvalid, "device credential was rejected")) return } if !route.Local { r.Body = io.NopCloser(bytes.NewReader(body)) s.forwardHTTP(w, r, route) return } } tenant, err := s.authorizeDevice(r.Context(), deviceID, token, true) if err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorDeviceCredentialInvalid, "device credential was rejected")) return } tenant.Handler().ServeHTTP(w, r) } func (s *Service) handlePhoneRevoke(w http.ResponseWriter, r *http.Request, tenant *registry.Tenant, _ string) { body, err := readBounded(r.Body, 16<<10) if err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "invalid phone revoke request", false, http.StatusBadRequest)) return } var request struct { PhoneID string `json:"phone_id"` } if err := json.Unmarshal(body, &request); err != nil || strings.TrimSpace(request.PhoneID) == "" { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "phone_id is required", false, http.StatusBadRequest)) return } payload := tenant.Payload() // The Cloud data plane has no admin bypass. A route/token pair resolves one // phone principal, and the control plane enforces ownership before mutation. routeHandle := strings.TrimSpace(r.Header.Get("X-Neko-Route-Handle")) token := requestPhoneToken(r, nil) auth, err := s.controlPlane.AuthorizePhone(r.Context(), routeHandle, controlplane.SHA256Hex(token)) if err != nil || auth.Phone.PhoneID != request.PhoneID { writeHTTPError(w, apiError(protocol.ServiceErrorPhoneCredentialInvalid, "phone may only revoke itself", false, http.StatusForbidden)) return } if err := s.controlPlane.RevokePhone(r.Context(), payload.TenantID, request.PhoneID, "phone self-revocation"); err != nil { writeHTTPError(w, publicError(err, protocol.ServiceErrorRouteUnavailable, "phone revocation is indeterminate")) return } if err := tenant.RevokePhone(request.PhoneID); err != nil { writeHTTPError(w, apiError(protocol.ServiceErrorRouteUnavailable, "phone revocation is indeterminate", false, http.StatusConflict)) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"status": "revoked", "phone_id": request.PhoneID}) } func deviceCredential(r *http.Request, body []byte) (string, string) { deviceID := strings.TrimSpace(r.URL.Query().Get("device_id")) token := strings.TrimSpace(r.Header.Get("X-Neko-Device-Token")) if token == "" { token = bearerToken(r.Header.Get("Authorization")) } if len(body) > 0 && json.Valid(body) { var payload struct { DeviceID string `json:"device_id"` Token string `json:"token"` } if json.Unmarshal(body, &payload) == nil { if deviceID == "" { deviceID = strings.TrimSpace(payload.DeviceID) } if token == "" { token = strings.TrimSpace(payload.Token) } } } return deviceID, token } func requestPhoneToken(r *http.Request, first *protocol.NekoMessage) string { for _, value := range []string{ strings.TrimSpace(r.Header.Get("X-Neko-Phone-Token")), bearerToken(r.Header.Get("Authorization")), } { if value != "" { return value } } if first != nil && first.Payload != nil { if value, ok := first.Payload["phone_token"].(string); ok { return strings.TrimSpace(value) } } return "" } func requestRouteHandle(r *http.Request, first *protocol.NekoMessage) string { if value := strings.TrimSpace(r.Header.Get("X-Neko-Route-Handle")); value != "" { return value } if first != nil && first.Payload != nil { if value, ok := first.Payload["route_handle"].(string); ok { return strings.TrimSpace(value) } } return "" } func bearerToken(value string) string { if len(value) > len("Bearer ") && strings.EqualFold(value[:len("Bearer ")], "Bearer ") { return strings.TrimSpace(value[len("Bearer "):]) } return "" } func (s *Service) sourceHash(r *http.Request) string { address := sourceAddress(r, s.proxies) mac := hmac.New(sha256.New, s.sourceSecret) _, _ = mac.Write([]byte("nekonest-cloud/registration-source/v1\x00" + address)) return hex.EncodeToString(mac.Sum(nil)) } func sourceAddress(r *http.Request, trusted []netip.Prefix) string { host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) if err != nil { host = strings.TrimSpace(r.RemoteAddr) } remote, err := netip.ParseAddr(host) if err != nil { return "invalid" } remote = remote.Unmap() trustedProxy := false for _, prefix := range trusted { if prefix.Contains(remote) { trustedProxy = true break } } if trustedProxy { candidate := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")) if candidate == "" { candidate = strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0]) } if forwarded, err := netip.ParseAddr(candidate); err == nil { return forwarded.Unmap().String() } } return remote.String() } func deriveHandoffCredentials(secret []byte, handoffID, identityFingerprint string) (string, string, string) { derive := func(label string) []byte { mac := hmac.New(sha256.New, secret) _, _ = mac.Write([]byte(label)) _, _ = mac.Write([]byte{0}) _, _ = mac.Write([]byte(handoffID)) _, _ = mac.Write([]byte{0}) _, _ = mac.Write([]byte(identityFingerprint)) return mac.Sum(nil) } phoneDigest := derive("phone-id") return "phone_" + hex.EncodeToString(phoneDigest[:16]), hex.EncodeToString(derive("phone-token")), hex.EncodeToString(derive("route-handle")) } func (s *Service) completePhoneHandoff( ctx context.Context, handoffID, phoneID, phoneTokenHash, routeHandleHash string, ) error { var lastErr error for attempt, delay := range []time.Duration{0, 100 * time.Millisecond, 300 * time.Millisecond} { if attempt > 0 { timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return errors.Join(lastErr, ctx.Err()) case <-timer.C: } } lastErr = s.controlPlane.CompletePhoneHandoff( ctx, handoffID, phoneID, phoneTokenHash, routeHandleHash, ) if lastErr == nil { return nil } var remote *controlplane.RemoteError if errors.As(lastErr, &remote) && remote.Status < http.StatusInternalServerError && !remote.Body.Retryable { return lastErr } } return lastErr } func constantTimeEqual(left, right string) bool { if len(left) != len(right) { return false } return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1 } func controlUnavailable(err error) bool { var remote *controlplane.RemoteError return !errors.As(err, &remote) } type serviceError struct { protocol.ServiceErrorPayload status int } func apiError(code protocol.ServiceErrorCode, message string, retryable bool, status int) serviceError { return serviceError{ServiceErrorPayload: protocol.ServiceErrorPayload{ ErrorCode: code, Message: message, Retryable: retryable, }, status: status} } func publicError(err error, fallback protocol.ServiceErrorCode, message string) serviceError { var remote *controlplane.RemoteError if errors.As(err, &remote) && remote.Body.ErrorCode != "" { body := remote.Body if body.ActionURL != "" { parsed, parseErr := url.Parse(body.ActionURL) if parseErr != nil || parsed.Scheme != "https" || parsed.Host == "" { body.ActionURL = "" } } return serviceError{ServiceErrorPayload: body, status: remote.Status} } return serviceError{ServiceErrorPayload: protocol.ServiceErrorPayload{ ErrorCode: fallback, Message: message, Retryable: true, RetryAfterSeconds: 5, }, status: http.StatusServiceUnavailable} } func writeHTTPError(w http.ResponseWriter, err serviceError) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") status := err.status if status < 400 || status > 599 { status = http.StatusInternalServerError } w.WriteHeader(status) _ = json.NewEncoder(w).Encode(err.ServiceErrorPayload) } func writeWebSocketError(conn *websocket.Conn, err serviceError) { _ = conn.WriteJSON(protocol.NekoMessage{ ProtocolVersion: protocol.CurrentProtocolVersion, TransportMode: protocol.TransportSealed, Type: protocol.MsgError, Timestamp: time.Now().Unix(), Payload: map[string]any{ "error_code": err.ErrorCode, "message": err.Message, "retryable": err.Retryable, "retry_after_seconds": err.RetryAfterSeconds, "action_url": err.ActionURL, }, }) _ = conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, string(err.ErrorCode)), time.Now().Add(time.Second)) }