diff --git a/src/github.com/matrix-org/go-neb/api/api.go b/src/github.com/matrix-org/go-neb/api/api.go index e003a6a..cbb8687 100644 --- a/src/github.com/matrix-org/go-neb/api/api.go +++ b/src/github.com/matrix-org/go-neb/api/api.go @@ -1,3 +1,11 @@ +// Package api contains the fundamental data types used by Go-NEB. +// +// Most HTTP API calls and/or config file sections are just ways of representing these +// data types. +// +// See also +// +// Package "api.handlers" for information on the HTTP API calls. package api import ( @@ -8,32 +16,69 @@ import ( // ConfigureAuthRealmRequest is a request to /configureAuthRealm type ConfigureAuthRealmRequest struct { - ID string - Type string + // An arbitrary unique identifier for this auth realm. This can be anything. + // Using an existing ID will REPLACE the entire existing AuthRealm with the new information. + ID string + // The type of auth realm. This determines which code is loaded to execute the + // auth realm. It must be a known type. E.g. "github". + Type string + // AuthRealm specific config information. See the docs for the auth realm you're interested in. + Config json.RawMessage +} + +// RequestAuthSessionRequest is a request to /requestAuthSession +type RequestAuthSessionRequest struct { + // The realm ID to request a new auth session on. The realm MUST already exist. + RealmID string + // The Matrix user ID requesting the auth session. If the auth is successful, + // this user ID will be associated with the third-party credentials obtained. + UserID string + // AuthRealm specific config information. See the docs for the auth realm you're interested in. Config json.RawMessage } // ConfigureServiceRequest is a request to /configureService type ConfigureServiceRequest struct { - ID string - Type string + // An arbitrary unique identifier for this service. This can be anything. + // Using an existing ID will REPLACE the entire Service with the new information. + ID string + // The type of service. This determines which code is loaded to execute the + // service. It must be a known type, e.g. "github". + Type string + // The user ID of the configured client that this service will use to communicate with Matrix. + // The user MUST already be configured. UserID string + // Service-specific config information. See the docs for the service you're interested in. Config json.RawMessage } -// A ClientConfig is the configuration for a matrix client for a bot to use. It is -// a request to /configureClient +// A ClientConfig contains the configuration information for a matrix client so that +// Go-NEB can drive it. It forms the HTTP body to /configureClient requests. type ClientConfig struct { - UserID string // The matrix UserId to connect with. - HomeserverURL string // A URL with the host and port of the matrix server. E.g. https://matrix.org:8448 - AccessToken string // The matrix access token to authenticate the requests with. - Sync bool // True to start a sync stream for this user - AutoJoinRooms bool // True to automatically join all rooms for this user - DisplayName string // The display name to set for the matrix client + // The matrix User ID to connect with. E.g. @alice:matrix.org + UserID string + // A URL with the host and port of the matrix server. E.g. https://matrix.org:8448 + HomeserverURL string + // The matrix access token to authenticate the requests with. + AccessToken string + // True to start a sync stream for this user. If false, no /sync goroutine will be + // created and this client won't listen for new events from Matrix. For services + // which only SEND events into Matrix, it may be desirable to set Sync to false to reduce the + // number of goroutines Go-NEB has to maintain. For services which respond to !commands, + // Sync MUST be set to true in order to receive those commands. + Sync bool + // True to automatically join every room this client is invited to. + // This is desirable for services which have !commands as that means anyone can pull the bot + // into the room. It is up to the service to decide which, if any, users to respond to however. + AutoJoinRooms bool + // The desired display name for this client. + // This does not automatically set the display name for this client. See /configureClient. + DisplayName string } -// SessionRequest are usually multi-stage things so this type only exists for the config form -type SessionRequest struct { +// Session contains the complete auth session information for a given user on a given realm. +// They are created for use with ConfigFile. +type Session struct { SessionID string RealmID string UserID string @@ -45,7 +90,7 @@ type ConfigFile struct { Clients []ClientConfig Realms []ConfigureAuthRealmRequest Services []ConfigureServiceRequest - Sessions []SessionRequest + Sessions []Session } // Check validates the /configureService request @@ -65,14 +110,14 @@ func (c *ConfigureAuthRealmRequest) Check() error { } // Check validates the session config request -func (c *SessionRequest) Check() error { +func (c *Session) Check() error { if c.SessionID == "" || c.UserID == "" || c.RealmID == "" || c.Config == nil { return errors.New(`Must supply a "SessionID", a "RealmID", a "UserID" and a "Config"`) } return nil } -// Check that the client has the correct fields. +// Check that the client has supplied the correct fields. func (c *ClientConfig) Check() error { if c.UserID == "" || c.HomeserverURL == "" || c.AccessToken == "" { return errors.New(`Must supply a "UserID", a "HomeserverURL", and an "AccessToken"`) @@ -82,3 +127,11 @@ func (c *ClientConfig) Check() error { } return nil } + +// Check that the request is valid. +func (r *RequestAuthSessionRequest) Check() error { + if r.UserID == "" || r.RealmID == "" || r.Config == nil { + return errors.New(`Must supply a "UserID", a "RealmID" and a "Config"`) + } + return nil +} diff --git a/src/github.com/matrix-org/go-neb/api/handlers/auth.go b/src/github.com/matrix-org/go-neb/api/handlers/auth.go index 7c8a7cf..4179397 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/auth.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/auth.go @@ -15,19 +15,36 @@ import ( "github.com/matrix-org/go-neb/types" ) +// RequestAuthSession represents an HTTP handler capable of processing /admin/requestAuthSession requests. type RequestAuthSession struct { Db *database.ServiceDB } +// OnIncomingRequest handles POST requests to /admin/requestAuthSession. The HTTP body MUST be +// a JSON object representing type "api.RequestAuthSessionRequest". +// +// This will return HTTP 400 if there are missing fields or the Realm ID is unknown. +// For the format of the response, see the specific AuthRealm that the Realm ID corresponds to. +// +// Request: +// POST /admin/requestAuthSession +// { +// "RealmID": "github_realm_id", +// "UserID": "@my_user:localhost", +// "Config": { +// // AuthRealm specific config info +// } +// } +// Response: +// HTTP/1.1 200 OK +// { +// // AuthRealm-specific information +// } func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} } - var body struct { - RealmID string - UserID string - Config json.RawMessage - } + var body api.RequestAuthSessionRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} } @@ -36,8 +53,8 @@ func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, "user_id": body.UserID, }).Print("Incoming auth session request") - if body.UserID == "" || body.RealmID == "" || body.Config == nil { - return nil, &errors.HTTPError{nil, `Must supply a "UserID", a "RealmID" and a "Config"`, 400} + if err := body.Check(); err != nil { + return nil, &errors.HTTPError{err, err.Error(), 400} } realm, err := h.Db.LoadAuthRealm(body.RealmID) @@ -55,10 +72,24 @@ func (h *RequestAuthSession) OnIncomingRequest(req *http.Request) (interface{}, return response, nil } +// RemoveAuthSession represents an HTTP handler capable of processing /admin/removeAuthSession requests. type RemoveAuthSession struct { Db *database.ServiceDB } +// OnIncomingRequest handles POST requests to /admin/removeAuthSession. +// +// The JSON object MUST contain the keys "RealmID" and "UserID" to identify the session to remove. +// +// Request +// POST /admin/removeAuthSession +// { +// "RealmID": "github-realm", +// "UserID": "@my_user:localhost" +// } +// Response: +// HTTP/1.1 200 OK +// {} func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} @@ -91,10 +122,15 @@ func (h *RemoveAuthSession) OnIncomingRequest(req *http.Request) (interface{}, * return []byte(`{}`), nil } +// RealmRedirect represents an HTTP handler which can process incoming redirects for auth realms. type RealmRedirect struct { Db *database.ServiceDB } +// Handle requests for an auth realm. +// +// The last path segment of the URL MUST be the base64 form of the Realm ID. What response +// this returns depends on the specific AuthRealm implementation. func (rh *RealmRedirect) Handle(w http.ResponseWriter, req *http.Request) { segments := strings.Split(req.URL.Path, "/") // last path segment is the base64d realm ID which we will pass the incoming request to @@ -121,10 +157,35 @@ func (rh *RealmRedirect) Handle(w http.ResponseWriter, req *http.Request) { realm.OnReceiveRedirect(w, req) } +// ConfigureAuthRealm represents an HTTP handler capable of processing /admin/configureAuthRealm requests. type ConfigureAuthRealm struct { Db *database.ServiceDB } +// OnIncomingRequest handles POST requests to /admin/configureAuthRealm. The JSON object +// provided is of type "api.ConfigureAuthRealmRequest". +// +// Request: +// POST /admin/configureAuthRealm +// { +// "ID": "my-realm-id", +// "Type": "github", +// "Config": { +// // Realm-specific configuration information +// } +// } +// Response: +// HTTP/1.1 200 OK +// { +// "ID": "my-realm-id", +// "Type": "github", +// "OldConfig": { +// // Old auth realm config information +// }, +// "NewConfig": { +// // New auth realm config information +// }, +// } func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} @@ -160,10 +221,37 @@ func (h *ConfigureAuthRealm) OnIncomingRequest(req *http.Request) (interface{}, }{body.ID, body.Type, oldRealm, realm}, nil } +// GetSession represents an HTTP handler capable of processing /admin/getSession requests. type GetSession struct { Db *database.ServiceDB } +// OnIncomingRequest handles POST requests to /admin/getSession. +// +// The JSON object provided MUST have a "RealmID" and "UserID" in order to fetch the +// correct AuthSession. If there is no session for this tuple of realm and user ID, +// a 200 OK is still returned with "Authenticated" set to false. +// +// Request: +// POST /admin/getSession +// { +// "RealmID": "my-realm", +// "UserID": "@my_user:localhost" +// } +// Response: +// HTTP/1.1 200 OK +// { +// "ID": "session_id", +// "Authenticated": true, +// "Info": { +// // Session-specific config info +// } +// } +// Response if session not found: +// HTTP/1.1 200 OK +// { +// "Authenticated": false +// } func (h *GetSession) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} diff --git a/src/github.com/matrix-org/go-neb/api/handlers/client.go b/src/github.com/matrix-org/go-neb/api/handlers/client.go index 036fe2f..a814dd3 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/client.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/client.go @@ -9,11 +9,36 @@ import ( "github.com/matrix-org/go-neb/errors" ) -// ConfigureClient represents an HTTP handler capable of processing /configureClient requests +// ConfigureClient represents an HTTP handler capable of processing /admin/configureClient requests. type ConfigureClient struct { Clients *clients.Clients } +// OnIncomingRequest handles POST requests to /admin/configureClient. The JSON object provided +// is of type "api.ClientConfig". +// +// If a DisplayName is supplied, this request will set this client's display name +// if the old ClientConfig DisplayName differs from the new ClientConfig DisplayName. +// +// Request: +// POST /admin/configureClient +// { +// "UserID": "@my_bot:localhost", +// "HomeserverURL": "http://localhost:8008", +// "Sync": true, +// "DisplayName": "My Bot" +// } +// +// Response: +// HTTP/1.1 200 OK +// { +// "OldClient": { +// // The old api.ClientConfig +// }, +// "NewClient": { +// // The new api.ClientConfig +// } +// } func (s *ConfigureClient) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} diff --git a/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go b/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go index 817311a..1917971 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/heartbeat.go @@ -1,8 +1,17 @@ +// Package handlers contains the HTTP handlers for Go-NEB. +// +// This includes detail on the API paths and top-level JSON keys. For specific service JSON, +// see the service you're interested in. +// +// See also +// +// Package "api" for the format of the JSON request bodies. package handlers import ( - "github.com/matrix-org/go-neb/errors" "net/http" + + "github.com/matrix-org/go-neb/errors" ) // Heartbeat implements the heartbeat API @@ -11,16 +20,12 @@ type Heartbeat struct{} // OnIncomingRequest returns an empty JSON object which can be used to detect liveness of Go-NEB. // // Request: -// ``` -// GET /test -// ``` +// GET /test +// // // Response: -// ``` -// HTTP/1.1 200 OK -// Content-Type: applicatoin/json -// {} -// ``` +// HTTP/1.1 200 OK +// {} func (*Heartbeat) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { return &struct{}{}, nil } diff --git a/src/github.com/matrix-org/go-neb/api/handlers/service.go b/src/github.com/matrix-org/go-neb/api/handlers/service.go index d6478bd..4b32605 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/service.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/service.go @@ -16,6 +16,7 @@ import ( "github.com/matrix-org/go-neb/types" ) +// ConfigureService represents an HTTP handler which can process /admin/configureService requests. type ConfigureService struct { db *database.ServiceDB clients *clients.Clients @@ -23,6 +24,7 @@ type ConfigureService struct { mutexByServiceID map[string]*sync.Mutex } +// NewConfigureService creates a new ConfigureService handler func NewConfigureService(db *database.ServiceDB, clients *clients.Clients) *ConfigureService { return &ConfigureService{ db: db, @@ -45,6 +47,32 @@ func (s *ConfigureService) getMutexForServiceID(serviceID string) *sync.Mutex { return m } +// OnIncomingRequest handles POST requests to /admin/configureService. +// +// The request body MUST be of type "api.ConfigureServiceRequest". +// +// Request: +// POST /admin/configureService +// { +// "ID": "my_service_id", +// "Type": "service-type", +// "UserID": "@my_bot:localhost", +// "Config": { +// // service-specific config information +// } +// } +// Response: +// HTTP/1.1 200 OK +// { +// "ID": "my_service_id", +// "Type": "service-type", +// "OldConfig": { +// // old service-specific config information +// }, +// "NewConfig": { +// // new service-specific config information +// }, +// } func (s *ConfigureService) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} @@ -123,10 +151,30 @@ func (s *ConfigureService) createService(req *http.Request) (types.Service, *err return service, nil } +// GetService represents an HTTP handler which can process /admin/getService requests. type GetService struct { Db *database.ServiceDB } +// OnIncomingRequest handles POST requests to /admin/getService. +// +// The request body MUST be a JSON body which has an "ID" key which represents +// the service ID to get. +// +// Request: +// POST /admin/getService +// { +// "ID": "my_service_id" +// } +// Response: +// HTTP/1.1 200 OK +// { +// "ID": "my_service_id", +// "Type": "github", +// "Config": { +// // service-specific config information +// } +// } func (h *GetService) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { if req.Method != "POST" { return nil, &errors.HTTPError{nil, "Unsupported Method", 405} diff --git a/src/github.com/matrix-org/go-neb/api/handlers/webhook.go b/src/github.com/matrix-org/go-neb/api/handlers/webhook.go index 392a6ff..7229bc9 100644 --- a/src/github.com/matrix-org/go-neb/api/handlers/webhook.go +++ b/src/github.com/matrix-org/go-neb/api/handlers/webhook.go @@ -2,12 +2,13 @@ package handlers import ( "encoding/base64" + "net/http" + "strings" + log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/clients" "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/metrics" - "net/http" - "strings" ) // Webhook represents an HTTP handler capable of accepting webhook requests on behalf of services. @@ -24,7 +25,9 @@ func NewWebhook(db *database.ServiceDB, cli *clients.Clients) *Webhook { // Handle an incoming webhook HTTP request. // // The webhook MUST have a known base64 encoded service ID as the last path segment -// in order for this request to be passed to the correct service. +// in order for this request to be passed to the correct service, or else this will return +// HTTP 400. If the base64 encoded service ID is unknown, this will return HTTP 404. +// Beyond this, the exact response is determined by the specific Service implementation. func (wh *Webhook) Handle(w http.ResponseWriter, req *http.Request) { log.WithField("path", req.URL.Path).Print("Incoming webhook request") segments := strings.Split(req.URL.Path, "/")