// SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2024-2026 Quad4.io package controlapi import ( "encoding/json" "errors" "io" "net/http" ) // errorResponse is the JSON body returned for any non-2xx HTTP response. type errorResponse struct { Error string `json:"error"` } // healthResponse is the body of GET /v1/health. type healthResponse struct { Status string `json:"status"` TransportID string `json:"transport_id,omitempty"` TransportUptime float64 `json:"transport_uptime_seconds"` } // interfaceStatJSON mirrors the subset of transport.InterfaceStat exposed // over the control API. type interfaceStatJSON struct { Name string `json:"name"` Type string `json:"type"` Status bool `json:"status"` RXBytes uint64 `json:"rx_bytes"` TXBytes uint64 `json:"tx_bytes"` Bitrate int64 `json:"bitrate"` Clients *int `json:"clients,omitempty"` BlockedIPs *int `json:"blocked_ips,omitempty"` BlockedIPList []string `json:"blocked_ip_list,omitempty"` I2PConnectable *bool `json:"i2p_connectable,omitempty"` I2PB32 *string `json:"i2p_b32,omitempty"` TunnelState *string `json:"tunnelstate,omitempty"` I2PLastError *string `json:"i2p_last_error,omitempty"` IFACFail uint64 `json:"ifac_fail"` HMACFail uint64 `json:"hmac_fail"` AnnounceSigFail uint64 `json:"announce_sig_fail"` UnpackFail uint64 `json:"unpack_fail"` AnnounceDup uint64 `json:"announce_dup"` PathRespSuppressed uint64 `json:"path_resp_suppressed"` PathReqDup uint64 `json:"path_req_dup"` PathReqNoCache uint64 `json:"path_req_no_cache"` PathRespQueuedSkip uint64 `json:"path_resp_queued_skip"` LinkRelayUnknownIface uint64 `json:"link_relay_unknown_iface"` IntegrityFailRate float64 `json:"integrity_fail_rate"` StaleCloses uint64 `json:"stale_closes"` LinkStaleClose uint64 `json:"link_stale_close"` KeepaliveTimeout uint64 `json:"keepalive_timeout"` } // statusResponse is the body of GET /v1/status. type statusResponse struct { TransportID string `json:"transport_id"` Interfaces []interfaceStatJSON `json:"interfaces"` Protect protectSnapshotJSON `json:"protect"` } type protectSnapshotJSON struct { Mode string `json:"mode"` Phase string `json:"phase"` Enforcement string `json:"enforcement"` Fingerprint string `json:"fingerprint"` SheddingMemory bool `json:"shedding_memory"` TripCounts struct { PPS uint64 `json:"pps"` BPS uint64 `json:"bps"` Handler uint64 `json:"handler"` Conn uint64 `json:"conn"` Resource uint64 `json:"resource"` Memory uint64 `json:"memory"` Crypto uint64 `json:"crypto"` Handshake uint64 `json:"handshake"` CoolDown uint64 `json:"cooldown"` } `json:"trip_counts"` } // pathTableEntryJSON mirrors transport.PathTableEntry with hex-encoded hashes. type pathTableEntryJSON struct { Hash string `json:"hash"` Via string `json:"via"` Hops uint8 `json:"hops"` Expires float64 `json:"expires"` Interface string `json:"interface"` } // createSessionRequest is the body of POST /v1/sessions. IdentityPath, when // set, is a server-local filesystem path used to load or create a // persistent identity. When empty a new in-memory identity is generated and // discarded on session close. type createSessionRequest struct { IdentityPath string `json:"identity_path,omitempty"` } // createSessionResponse is the body returned by POST /v1/sessions. type createSessionResponse struct { SessionID string `json:"session_id"` IdentityHash string `json:"identity_hash"` } // registerDestinationRequest is the body of // POST /v1/sessions/{id}/destinations. When AcceptsLinks is true, remote // peers may open links to this destination. Establishment and teardown are // reported as link.established/link.failed/link.closed events on every // WebSocket connection attached to the session. type registerDestinationRequest struct { AppName string `json:"app_name"` Aspects []string `json:"aspects,omitempty"` AcceptsLinks bool `json:"accepts_links,omitempty"` } // registerDestinationResponse is returned by destination registration. type registerDestinationResponse struct { DestinationHash string `json:"destination_hash"` } // registerRequestHandlerRequest is the body of // POST /v1/sessions/{id}/destinations/{hash}/requests. It registers a path // that, when a peer sends a link request to it, is bridged to the session's // WebSocket connections as a request.incoming event. The application // answers with a request.respond command. Allow is one of "all" (default), // "none", or "list". AllowedIdentities is required hex identity hashes when // Allow is "list". type registerRequestHandlerRequest struct { Path string `json:"path"` Allow string `json:"allow,omitempty"` AllowedIdentities []string `json:"allowed_identities,omitempty"` } // announceRequest is the body of // POST /v1/sessions/{id}/destinations/{hash}/announce. AppData, when set, is // base64-encoded and becomes the destination's default app data before // announcing. type announceRequest struct { AppData string `json:"app_data,omitempty"` } // pathRequestRequest is the body of POST /v1/sessions/{id}/path/request. type pathRequestRequest struct { DestinationHash string `json:"destination_hash"` } // pathRequestResponse is returned after a path request is emitted. // WaitS is the bitrate-sized window the caller should wait for HasPath. type pathRequestResponse struct { WaitS float64 `json:"wait_s"` Error string `json:"error,omitempty"` } // wsCommandEnvelope is decoded first to dispatch an inbound WebSocket // message on its type field before decoding the full command. type wsCommandEnvelope struct { Type string `json:"type"` } // subscribeAnnouncesCommand subscribes the connection to announceEvent // pushes. Empty Filter delivers every announce. A non-empty Filter must be // an exact 16-byte destination hash as hex and only matching announces are // forwarded. type subscribeAnnouncesCommand struct { Type string `json:"type"` Filter string `json:"filter,omitempty"` } // announceEvent is pushed to subscribed WebSocket clients for every // announce the node's transport receives. type announceEvent struct { Type string `json:"type"` DestinationHash string `json:"destination_hash"` IdentityHash string `json:"identity_hash,omitempty"` AppData string `json:"app_data,omitempty"` Hops uint8 `json:"hops"` } // linkOpenCommand requests an outbound link to a destination the node has // already learned about via an announce (see identity.Recall). The result // arrives as a linkEstablishedEvent or linkFailedEvent. type linkOpenCommand struct { Type string `json:"type"` DestinationHash string `json:"destination_hash"` } // linkSendCommand sends data over an already-established link. type linkSendCommand struct { Type string `json:"type"` LinkID string `json:"link_id"` Data string `json:"data"` } // linkCloseCommand tears down an established link. type linkCloseCommand struct { Type string `json:"type"` LinkID string `json:"link_id"` } // requestRespondCommand answers a pending requestIncomingEvent. // When Filename is set the response is NomadNet-style [filename, bytes]. type requestRespondCommand struct { Type string `json:"type"` RequestID string `json:"request_id"` Data string `json:"data,omitempty"` Filename string `json:"filename,omitempty"` } // linkRequestCommand sends an outbound request on an established link. type linkRequestCommand struct { Type string `json:"type"` LinkID string `json:"link_id"` Path string `json:"path"` Data string `json:"data,omitempty"` TimeoutMs int `json:"timeout_ms,omitempty"` } // linkSendResourceCommand transfers a payload as a link resource. type linkSendResourceCommand struct { Type string `json:"type"` LinkID string `json:"link_id"` Data string `json:"data"` Name string `json:"name,omitempty"` } // linkIdentifyCommand identifies the session identity on an established link. type linkIdentifyCommand struct { Type string `json:"type"` LinkID string `json:"link_id"` } // commandErrorEvent reports a WebSocket command that could not be applied. type commandErrorEvent struct { Type string `json:"type"` Command string `json:"command,omitempty"` Error string `json:"error"` } // requestResponseEvent reports a successful outbound link.request. type requestResponseEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` RequestID string `json:"request_id"` Path string `json:"path,omitempty"` Data string `json:"data,omitempty"` } // requestFailedEvent reports an outbound link.request that failed or timed out. type requestFailedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` RequestID string `json:"request_id,omitempty"` Path string `json:"path,omitempty"` Error string `json:"error,omitempty"` } // resourceStartedEvent reports a resource transfer beginning on a link. type resourceStartedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` } // resourceConcludedEvent reports a finished resource transfer. type resourceConcludedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` Name string `json:"name,omitempty"` Hash string `json:"hash,omitempty"` Data string `json:"data,omitempty"` Success bool `json:"success"` Error string `json:"error,omitempty"` } // linkRemoteIdentifiedEvent reports a peer identity learned via link identify. type linkRemoteIdentifiedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` IdentityHash string `json:"identity_hash"` } // linkEstablishedEvent reports a link (outbound or inbound) becoming // active and ready for link.send. type linkEstablishedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` RemoteHash string `json:"remote_hash,omitempty"` } // linkFailedEvent reports an outbound link.open that never became active. type linkFailedEvent struct { Type string `json:"type"` LinkID string `json:"link_id,omitempty"` DestinationHash string `json:"destination_hash,omitempty"` Error string `json:"error,omitempty"` } // linkDataEvent carries data received over an established link. type linkDataEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` Data string `json:"data"` } // linkClosedEvent reports a previously-active link tearing down. type linkClosedEvent struct { Type string `json:"type"` LinkID string `json:"link_id"` } // requestIncomingEvent reports a peer request arriving on a path // registered via POST .../destinations/{hash}/requests. The application // must reply with a requestRespondCommand carrying the same RequestID. type requestIncomingEvent struct { Type string `json:"type"` DestinationHash string `json:"destination_hash"` LinkID string `json:"link_id"` RequestID string `json:"request_id"` Path string `json:"path"` Data string `json:"data,omitempty"` RemoteIdentityHash string `json:"remote_identity_hash,omitempty"` } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, errorResponse{Error: message}) } // maxHTTPBodyBytes caps JSON request bodies on the control API HTTP surface. // Matches the WebSocket inbound frame cap so HTTP cannot be used as a larger // memory bomb than the event channel. const maxHTTPBodyBytes = 1 << 20 // decodeJSONBody reads at most maxHTTPBodyBytes from r into dst. // Oversized bodies yield StatusRequestEntityTooLarge. func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst any) error { if r.Body == nil { return io.EOF } body := http.MaxBytesReader(w, r.Body, maxHTTPBodyBytes) defer body.Close() dec := json.NewDecoder(body) if err := dec.Decode(dst); err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { writeError(w, http.StatusRequestEntityTooLarge, "request body too large") return err } return err } return nil } func isBodyTooLarge(err error) bool { var maxErr *http.MaxBytesError return errors.As(err, &maxErr) }