CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Implements PLAN.md Phase 9. internal/appservice reads registration files (appservice.config_files), matches namespaces the way Synapse does (anchored at the start), and delivers events to each service as transactions read from the roomserver's event stream at a cursor kept in appservice_state. A transaction in flight is recorded, so after a restart it is resent under the same ID; failures back off from 2s to 5m, and a successful ping cuts the backoff short. In the Client-Server API an as_token acts as the service's sender or, with ?user_id=, as any existing user in its namespace. Services register and log in their users with m.login.application_service; exclusive user and alias namespaces are refused to everybody else with M_EXCLUSIVE. Unknown aliases and users in a namespace are asked of the service first. The ping endpoint reports M_URL_NOT_SET, M_BAD_STATUS, M_CONNECTION_FAILED and M_CONNECTION_TIMEOUT. Services' own requests, and their users' when rate_limited is false, skip the rate limiter. Users can change their password (POST /account/password) and deactivate their account (POST /account/deactivate), both behind the password. Deactivation revokes tokens, deletes devices, optionally erases the profile and leaves every room through a new userapi.AccountObserver. The admin API is complete: user detail, the admin flag, deactivation and reactivation, password resets, devices, media quotas (media.max_user_storage_bytes plus per-user overrides), registration tokens with use limits and expiry (registration.require_token), server notices (internal/servernotices, server_notices.*), an editor for the federation allow/deny lists persisted in server_settings, a delivery view of the application services, and an audit log of every change. Migration 0011 adds the registration token, server notice room, media quota, audit log and appservice cursor tables. Policy lists are now returned sorted. The Complement entrypoint loads the registrations Complement mounts in /complement/appservice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
195 lines
7.0 KiB
Go
195 lines
7.0 KiB
Go
// Package httputil holds the HTTP plumbing shared by every Warp API: the
|
|
// Matrix error envelope, JSON helpers, CORS and middleware.
|
|
package httputil
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// Matrix error codes used so far. The full list lives in the Client-Server
|
|
// API spec, section "Standard error response"; codes are added as the
|
|
// endpoints that return them are implemented.
|
|
const (
|
|
ErrCodeForbidden = "M_FORBIDDEN"
|
|
ErrCodeUnknownToken = "M_UNKNOWN_TOKEN"
|
|
ErrCodeMissingToken = "M_MISSING_TOKEN"
|
|
ErrCodeBadJSON = "M_BAD_JSON"
|
|
ErrCodeNotJSON = "M_NOT_JSON"
|
|
ErrCodeNotFound = "M_NOT_FOUND"
|
|
ErrCodeLimitExceeded = "M_LIMIT_EXCEEDED"
|
|
ErrCodeUnknown = "M_UNKNOWN"
|
|
ErrCodeUnrecognized = "M_UNRECOGNIZED"
|
|
ErrCodeUnauthorized = "M_UNAUTHORIZED"
|
|
ErrCodeMissingParam = "M_MISSING_PARAM"
|
|
ErrCodeInvalidParam = "M_INVALID_PARAM"
|
|
ErrCodeTooLarge = "M_TOO_LARGE"
|
|
ErrCodeUnsupportedVer = "M_UNSUPPORTED_ROOM_VERSION"
|
|
|
|
// Room codes, added with the roomserver in Phase 3.
|
|
ErrCodeRoomInUse = "M_ROOM_IN_USE"
|
|
|
|
// Account and session codes, added with the userapi in Phase 1.
|
|
ErrCodeUserInUse = "M_USER_IN_USE"
|
|
ErrCodeInvalidUsername = "M_INVALID_USERNAME"
|
|
ErrCodeUserDeactivated = "M_USER_DEACTIVATED"
|
|
ErrCodeExclusive = "M_EXCLUSIVE"
|
|
ErrCodeWeakPassword = "M_WEAK_PASSWORD"
|
|
|
|
// Media codes, added with the mediaapi in Phase 6.
|
|
ErrCodeNotYetUploaded = "M_NOT_YET_UPLOADED"
|
|
ErrCodeCannotOverwrite = "M_CANNOT_OVERWRITE_MEDIA"
|
|
|
|
// End-to-end encryption codes, added with the keyserver in Phase 7.
|
|
ErrCodeInvalidSignature = "M_INVALID_SIGNATURE"
|
|
ErrCodeWrongRoomKeysVersion = "M_WRONG_ROOM_KEYS_VERSION"
|
|
|
|
// Application service and admin codes, added in Phase 9.
|
|
ErrCodeURLNotSet = "M_URL_NOT_SET"
|
|
ErrCodeConnectionFailed = "M_CONNECTION_FAILED"
|
|
ErrCodeConnectionTimeout = "M_CONNECTION_TIMEOUT"
|
|
ErrCodeBadStatus = "M_BAD_STATUS"
|
|
)
|
|
|
|
// MatrixError is the standard Matrix error envelope. Every non-2xx response
|
|
// from a /_matrix endpoint is one of these, serialised as
|
|
//
|
|
// {"errcode": "M_FORBIDDEN", "error": "..."}
|
|
//
|
|
// It implements error so handlers can return it up the stack.
|
|
type MatrixError struct {
|
|
Code string `json:"errcode"`
|
|
Message string `json:"error"`
|
|
|
|
// RetryAfterMS accompanies M_LIMIT_EXCEEDED.
|
|
RetryAfterMS int64 `json:"retry_after_ms,omitempty"`
|
|
|
|
// SoftLogout accompanies M_UNKNOWN_TOKEN when the client may keep its
|
|
// encryption state and re-login.
|
|
SoftLogout bool `json:"soft_logout,omitempty"`
|
|
|
|
// CurrentVersion accompanies M_WRONG_ROOM_KEYS_VERSION: the backup
|
|
// version the client should be writing to instead.
|
|
CurrentVersion string `json:"current_version,omitempty"`
|
|
|
|
// RemoteStatus and RemoteBody accompany M_BAD_STATUS: what an
|
|
// application service answered a ping with.
|
|
RemoteStatus int `json:"status,omitempty"`
|
|
RemoteBody string `json:"body,omitempty"`
|
|
|
|
// status is the HTTP status to send; not part of the JSON body.
|
|
status int
|
|
}
|
|
|
|
func (e *MatrixError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
|
}
|
|
|
|
// StatusCode is the HTTP status this error is sent with.
|
|
func (e *MatrixError) StatusCode() int {
|
|
if e.status == 0 {
|
|
return http.StatusInternalServerError
|
|
}
|
|
return e.status
|
|
}
|
|
|
|
// NewError builds an arbitrary Matrix error.
|
|
func NewError(status int, code, format string, args ...any) *MatrixError {
|
|
return &MatrixError{Code: code, Message: fmt.Sprintf(format, args...), status: status}
|
|
}
|
|
|
|
// Forbidden is M_FORBIDDEN (403). Federation-disabled rejections use it.
|
|
func Forbidden(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusForbidden, ErrCodeForbidden, format, args...)
|
|
}
|
|
|
|
// NotFound is M_NOT_FOUND (404) for a resource that could exist but does not.
|
|
func NotFound(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusNotFound, ErrCodeNotFound, format, args...)
|
|
}
|
|
|
|
// Unrecognized is M_UNRECOGNIZED (404), the spec's answer for an endpoint
|
|
// this server does not implement at all.
|
|
func Unrecognized() *MatrixError {
|
|
return NewError(http.StatusNotFound, ErrCodeUnrecognized, "Unrecognized request")
|
|
}
|
|
|
|
// MethodNotAllowed is M_UNRECOGNIZED (405) for a known path, wrong verb.
|
|
func MethodNotAllowed() *MatrixError {
|
|
return NewError(http.StatusMethodNotAllowed, ErrCodeUnrecognized, "Unrecognized request")
|
|
}
|
|
|
|
// NotJSON is M_NOT_JSON (400): the body was not valid JSON at all.
|
|
func NotJSON() *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeNotJSON, "Content not JSON")
|
|
}
|
|
|
|
// BadJSON is M_BAD_JSON (400): valid JSON, wrong shape.
|
|
func BadJSON(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeBadJSON, format, args...)
|
|
}
|
|
|
|
// MissingParam is M_MISSING_PARAM (400).
|
|
func MissingParam(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeMissingParam, format, args...)
|
|
}
|
|
|
|
// InvalidParam is M_INVALID_PARAM (400).
|
|
func InvalidParam(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeInvalidParam, format, args...)
|
|
}
|
|
|
|
// MissingToken is M_MISSING_TOKEN (401).
|
|
func MissingToken() *MatrixError {
|
|
return NewError(http.StatusUnauthorized, ErrCodeMissingToken, "Missing access token")
|
|
}
|
|
|
|
// UnknownToken is M_UNKNOWN_TOKEN (401).
|
|
func UnknownToken(softLogout bool) *MatrixError {
|
|
err := NewError(http.StatusUnauthorized, ErrCodeUnknownToken, "Unrecognised access token")
|
|
err.SoftLogout = softLogout
|
|
return err
|
|
}
|
|
|
|
// LimitExceeded is M_LIMIT_EXCEEDED (429).
|
|
func LimitExceeded(retryAfterMS int64) *MatrixError {
|
|
err := NewError(http.StatusTooManyRequests, ErrCodeLimitExceeded, "Too many requests")
|
|
err.RetryAfterMS = retryAfterMS
|
|
return err
|
|
}
|
|
|
|
// InternalError is M_UNKNOWN (500). The message is deliberately vague: the
|
|
// detail belongs in the log, not in the response.
|
|
func InternalError() *MatrixError {
|
|
return NewError(http.StatusInternalServerError, ErrCodeUnknown, "Internal server error")
|
|
}
|
|
|
|
// UserInUse is M_USER_IN_USE (400): the requested user name is taken.
|
|
func UserInUse() *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeUserInUse, "Desired user ID is already taken")
|
|
}
|
|
|
|
// InvalidUsername is M_INVALID_USERNAME (400): the user name is not usable,
|
|
// either because of its characters or its length.
|
|
func InvalidUsername(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeInvalidUsername, format, args...)
|
|
}
|
|
|
|
// Exclusive is M_EXCLUSIVE (400): a user ID or alias in a namespace an
|
|
// application service has claimed for itself.
|
|
func Exclusive(format string, args ...any) *MatrixError {
|
|
return NewError(http.StatusBadRequest, ErrCodeExclusive, format, args...)
|
|
}
|
|
|
|
// UserDeactivated is M_USER_DEACTIVATED (403).
|
|
func UserDeactivated() *MatrixError {
|
|
return NewError(http.StatusForbidden, ErrCodeUserDeactivated, "This account has been deactivated")
|
|
}
|
|
|
|
// Unknown is M_UNKNOWN with an explicit status, for the handful of cases the
|
|
// spec answers with that code rather than a specific one — an unsupported
|
|
// login type, for instance.
|
|
func Unknown(status int, format string, args ...any) *MatrixError {
|
|
return NewError(status, ErrCodeUnknown, format, args...)
|
|
}
|