package clientapi import ( "context" "errors" "net/http" "github.com/dariuszjaniak/warp/server/internal/appservice" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // The Client-Server half of the Application Service API: a service's // as_token as an access token, acting as one of its users with // ?user_id=, and the namespaces other users are kept out of. // authenticateAppService turns a request carrying a service's as_token // into an identity: the service's sender, or the user ?user_id= names. // // The user has to be the sender or in one of the service's user // namespaces — exclusive or not, the spec does not distinguish — and has // to exist: a service registers its users first, and acting as a user // nobody created would hand out an identity with no account behind it. func (c *ClientAPI) authenticateAppService(r *http.Request, svc *appservice.Service) (userapi.Authenticated, error) { userID := r.URL.Query().Get("user_id") if userID == "" { userID = svc.SenderUserID() } localpart, err := c.users.ParseLocalUserID(userID) switch { case errors.Is(err, userapi.ErrForeignServer): return userapi.Authenticated{}, httputil.Forbidden("An application service cannot act as a user of another server") case err != nil: return userapi.Authenticated{}, httputil.InvalidParam("%s is not a valid user ID", userID) } canonical := c.users.UserID(localpart) if !svc.OwnsUser(canonical) { return userapi.Authenticated{}, httputil.Forbidden("%s is not in the namespace of application service %s", canonical, svc.ID) } account, err := c.users.Account(r.Context(), localpart) switch { case errors.Is(err, userapi.ErrNoSuchUser): return userapi.Authenticated{}, httputil.Forbidden("Application service %s has not registered %s", svc.ID, canonical) case err != nil: return userapi.Authenticated{}, err case account.Deactivated: return userapi.Authenticated{}, httputil.UserDeactivated() } return userapi.Authenticated{ UserID: canonical, Localpart: localpart, Admin: account.Admin, AppService: svc.ID, }, nil } // rateLimitExempt lets an application service's own requests past the // rate limiter, and the requests of the users it acts as when its // registration says rate_limited: false. // // A bridge puppeting a thousand users from one address would otherwise // share one bucket among all of them. The check is a map lookup on the // token, so it is cheap enough to run ahead of the limiter. func (c *ClientAPI) rateLimitExempt(r *http.Request) bool { svc := c.appservices.Registry().ByToken(accessTokenFrom(r)) if svc == nil { return false } userID := r.URL.Query().Get("user_id") return userID == "" || userID == svc.SenderUserID() || !svc.RateLimitedUsers() } // checkExclusiveUser refuses a user ID in a namespace an application // service has claimed, unless the request comes from that service. func (c *ClientAPI) checkExclusiveUser(userID, asking string) error { if owner := c.appservices.Registry().ExclusiveUserOwner(userID); owner != nil && owner.ID != asking { return httputil.Exclusive("%s is reserved by an application service", userID) } return nil } // checkExclusiveAlias is checkExclusiveUser for a room alias. func (c *ClientAPI) checkExclusiveAlias(alias string, auth userapi.Authenticated) error { if owner := c.appservices.Registry().ExclusiveAliasOwner(alias); owner != nil && owner.ID != auth.AppService { return httputil.Exclusive("%s is reserved by an application service", alias) } return nil } // resolveAlias resolves an alias, asking the application services whose // namespace it is in when this server does not know it — which is how a // bridge creates a portal room the first time somebody looks for it. func (c *ClientAPI) resolveAlias(ctx context.Context, alias string) (roomserver.ResolvedAlias, error) { resolved, err := c.rooms.ResolveAlias(ctx, alias) if !errors.Is(err, roomserver.ErrNoSuchAlias) { return resolved, err } if !c.appservices.QueryAlias(ctx, alias) { return resolved, err } return c.rooms.ResolveAlias(ctx, alias) } // ensureAppServiceUser gives the application services a chance to create // a local user that does not exist, before something that needs the user // gives up on it. It reports whether the user exists afterwards. func (c *ClientAPI) ensureAppServiceUser(ctx context.Context, localpart string) (bool, error) { exists, err := c.users.AccountExists(ctx, localpart) if err != nil || exists { return exists, err } if !c.appservices.QueryUser(ctx, c.users.UserID(localpart)) { return false, nil } return c.users.AccountExists(ctx, localpart) } // pingRequest is the body of the ping endpoint. type pingRequest struct { TransactionID string `json:"transaction_id"` } // handleAppServicePing implements // POST /_matrix/client/v1/appservice/{appserviceId}/ping: a service asking // this server to ping it back, to check the two can reach each other. func (c *ClientAPI) handleAppServicePing(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { svc := c.appservices.Registry().ByID(r.PathValue("appserviceID")) if svc == nil || auth.AppService != svc.ID { return httputil.Forbidden("Only the application service itself may ask for its ping") } var req pingRequest if _, err := httputil.DecodeOptionalJSON(w, r, &req); err != nil { return err } took, err := c.appservices.Ping(r.Context(), svc, req.TransactionID) var status *appservice.BadStatusError switch { case err == nil: case errors.Is(err, appservice.ErrNoURL): return httputil.NewError(http.StatusBadRequest, httputil.ErrCodeURLNotSet, "Application service %s has no URL", svc.ID) case errors.As(err, &status): e := httputil.NewError(http.StatusBadGateway, httputil.ErrCodeBadStatus, "The application service answered the ping with %d", status.Status) e.RemoteStatus, e.RemoteBody = status.Status, status.Body return e case errors.Is(err, appservice.ErrConnectionTimeout): return httputil.NewError(http.StatusGatewayTimeout, httputil.ErrCodeConnectionTimeout, "The application service did not answer the ping in time") case errors.Is(err, appservice.ErrConnectionFailed): return httputil.NewError(http.StatusBadGateway, httputil.ErrCodeConnectionFailed, "Could not connect to the application service") default: return err } httputil.WriteJSON(w, http.StatusOK, map[string]int64{"duration_ms": took.Milliseconds()}) return nil }