diff --git a/api/client-server/v1/guest_events.yaml b/api/client-server/v1/guest_events.yaml new file mode 100644 index 00000000..bbb5799a --- /dev/null +++ b/api/client-server/v1/guest_events.yaml @@ -0,0 +1,103 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server v1 Sync Guest API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/events": + get: + summary: Listen on the event stream. + description: |- + This will listen for new events related to a particular room and return + them to the caller. This will block until an event is received, or until + the ``timeout`` is reached. + + This API is the same as the non-guest /events endpoint, but can be + called by guest users. + security: + - accessToken: [] + parameters: + - in: query + type: string + name: from + description: |- + The token to stream from. This token is either from a previous + request to this API or from the initial sync API. + required: false + x-example: "s3456_9_0" + - in: query + type: integer + name: timeout + description: The maximum time in milliseconds to wait for an event. + required: false + x-example: "35000" + - in: query + type: array + items: + type: string + name: room_id + description: |- + The room IDs for which events should be returned. + x-example: + - "!somewhere:over" + - "!the:rainbow" + responses: + 200: + description: "The events received, which may be none." + examples: + application/json: |- + { + "start": "s3456_9_0", + "end": "s3457_9_0", + "chunk": [ + { + "age": 32, + "content": { + "body": "incoming message", + "msgtype": "m.text" + }, + "event_id": "$14328055551tzaee:localhost", + "origin_server_ts": 1432804485886, + "room_id": "!TmaZBKYIFrIPVGoUYp:localhost", + "type": "m.room.message", + "user_id": "@bob:localhost" + } + ] + } + schema: + type: object + properties: + start: + type: string + description: |- + A token which correlates to the first value in ``chunk``. This + is usually the same token supplied to ``from=``. + end: + type: string + description: |- + A token which correlates to the last value in ``chunk``. This + token should be used in the next request to ``/events``. + chunk: + type: array + description: "An array of events." + items: + type: object + title: Event + allOf: + - "$ref": "core-event-schema/room_event.json" + 400: + description: "Bad pagination ``from`` parameter." diff --git a/event-schemas/examples/v1/m.room.guest_access b/event-schemas/examples/v1/m.room.guest_access new file mode 100644 index 00000000..8ad4d294 --- /dev/null +++ b/event-schemas/examples/v1/m.room.guest_access @@ -0,0 +1,12 @@ +{ + "age": 242353, + "content": { + "guest_access": "can_join" + }, + "state_key": "", + "origin_server_ts": 1431961217938, + "event_id": "$WLGTSEFSEG:localhost", + "type": "m.room.guest_access", + "room_id": "!Cuyf34gef24u:localhost", + "user_id": "@example:localhost" +} diff --git a/event-schemas/schema/v1/m.room.guest_access b/event-schemas/schema/v1/m.room.guest_access new file mode 100644 index 00000000..a0141f37 --- /dev/null +++ b/event-schemas/schema/v1/m.room.guest_access @@ -0,0 +1,30 @@ +{ + "type": "object", + "title": "Controls whether guest users are allowed to join rooms.", + "description": "This event controls whether guest users are allowed to join rooms. If this event is absent, servers should act as if it is present and has the guest_access value \"forbidden\".", + "allOf": [{ + "$ref": "core-event-schema/state_event.json" + }], + "properties": { + "content": { + "type": "object", + "properties": { + "guest_access": { + "type": "string", + "description": "Whether guests can join the room.", + "enum": ["can_join", "forbidden"] + } + }, + "required": ["guest_access"] + }, + "state_key": { + "type": "string", + "description": "A zero-length string.", + "pattern": "^$" + }, + "type": { + "type": "string", + "enum": ["m.room.guest_access"] + } + } +} diff --git a/scripts/continuserv/main.go b/scripts/continuserv/main.go index 2c6072a3..14d0c5bf 100644 --- a/scripts/continuserv/main.go +++ b/scripts/continuserv/main.go @@ -116,8 +116,14 @@ func serve(w http.ResponseWriter, req *http.Request) { wgMu.Lock() wg.Wait() wgMu.Unlock() - b := toServe.Load().([]byte) - w.Write(b) + b := toServe.Load().(bytesOrErr) + if b.err != nil { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(b.err.Error())) + } else { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(b.bytes)) + } } func populateOnce(dir string) { @@ -130,15 +136,15 @@ func populateOnce(dir string) { cmd.Stderr = &b err := cmd.Run() if err != nil { - toServe.Store([]byte(fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String()).Error())) + toServe.Store(bytesOrErr{nil, fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String())}) return } specBytes, err := ioutil.ReadFile(path.Join(dir, "scripts", "gen", "specification.html")) if err != nil { - toServe.Store([]byte(fmt.Errorf("error reading spec: %v", err).Error())) + toServe.Store(bytesOrErr{nil, fmt.Errorf("error reading spec: %v", err)}) return } - toServe.Store(specBytes) + toServe.Store(bytesOrErr{specBytes, nil}) } func doPopulate(ch chan struct{}, dir string) { @@ -165,3 +171,8 @@ func exists(path string) bool { _, err := os.Stat(path) return !os.IsNotExist(err) } + +type bytesOrErr struct { + bytes []byte + err error +} diff --git a/specification/modules/guest_access.rst b/specification/modules/guest_access.rst new file mode 100644 index 00000000..ac373af9 --- /dev/null +++ b/specification/modules/guest_access.rst @@ -0,0 +1,81 @@ +Guest access +============ + +.. _module:guest-access: + +There are times when it is desirable for clients to be able to interact with +rooms without having to fully register for an account on a homeserver or join +the room. This module specifies how these clients should interact with servers +in order to participate in rooms as guests. + +Guest users retrieve access tokens from a homeserver using the ordinary +`register endpoint <#post-matrix-client-api-v2-alpha-register>`_, specifying +the ``kind`` parameter as ``guest``. They may then interact with the +client-server API as any other user would, but will only have access to a subset +of the API as described the Client behaviour subsection below. +Homeservers may choose not to allow this access at all to their local users, but +have no information about whether users on other homeservers are guests or not. + +This module does not fully factor in federation; it relies on individual +homeservers properly adhering to the rules set out in this module, rather than +allowing all homeservers to enforce the rules on each other. + +Events +------ +{{m_room_guest_access_event}} + +Client behaviour +---------------- +The following API endpoints are allowed to be accessed by guest accounts for +retrieving events: + +* `GET /rooms/:room_id/state <#get-matrix-client-api-v1-rooms-roomid-state>`_ +* `GET /rooms/:room_id/state/:event_type/:state_key <#get-matrix-client-api-v1-rooms-roomid-state-eventtype-statekey>`_ +* `GET /rooms/:room_id/messages <#get-matrix-client-api-v1-rooms-roomid-messages>`_ + +There is also a special version of the +`GET /events <#get-matrix-client-api-v1-events>`_ endpoint: + +{{guest_events_http_api}} + +They will only return events which happened while the room state had the +``m.room.history_visibility`` state event present with ``history_visibility`` +value ``world_readable``. Guest clients do not need to join rooms in order to +receive events for them. + +The following API endpoints are allowed to be accessed by guest accounts for +sending events: + +* `POST /rooms/:room_id/join <#post-matrix-client-api-v1-rooms-roomid-join>`_ +* `PUT /rooms/:room_id/send/m.room.message/:txn_id <#put-matrix-client-api-v1-rooms-roomid-send-eventtype-txnid>`_ + +Guest clients *do* need to join rooms in order to send events to them. + +Server behaviour +---------------- +Servers are required to only return events to guest accounts for rooms where +the room state at the event had the ``m.room.history_visibility`` state event +present with ``history_visibility`` value ``world_readable``. These events may +be returned even if the anonymous user is not joined to the room. + +Servers MUST only allow guest users to join rooms if the ``m.room.guest_access`` +state event is present on the room, and has the ``guest_access`` value +``can_join``. If the ``m.room.guest_access`` event is changed to stop this from +being the case, the server MUST set those users' ``m.room.member`` state to +``leave``. + +Security considerations +----------------------- +Each homeserver manages its own guest accounts itself, and whether an account +is a guest account or not is not information passed from server to server. +Accordingly, any server participating in a room is trusted to properly enforce +the permissions outlined in this section. + +Clients may wish to display to their users that rooms which are +``world_readable`` *may* be showing messages to non-joined users. There is no +way using this module to find out whether any non-joined guest users *do* see +events in the room, or to list or count any guest users. + +Homeservers may want to enable protections such as captchas for guest +registration to prevent spam, denial of service, and similar attacks. + diff --git a/specification/targets.yaml b/specification/targets.yaml index 2482dcfd..8e6a2ce0 100644 --- a/specification/targets.yaml +++ b/specification/targets.yaml @@ -25,6 +25,7 @@ groups: # reusable blobs of files when prefixed with 'group:' - modules/push.rst - modules/third_party_invites.rst - modules/search.rst + - modules/guest_access.rst title_styles: ["=", "-", "~", "+", "^", "`"] diff --git a/templating/matrix_templates/units.py b/templating/matrix_templates/units.py index cec5b83e..adb7f427 100644 --- a/templating/matrix_templates/units.py +++ b/templating/matrix_templates/units.py @@ -369,7 +369,7 @@ class MatrixUnits(Units): ] if len(params_missing_examples) == 0: path_template = api.get("basePath", "").rstrip("/") + path - qps = {} + qps = [] body = "" for param in single_api.get("parameters", []): if param["in"] == "path": @@ -381,7 +381,12 @@ class MatrixUnits(Units): elif param["in"] == "body": body = param["schema"]["example"] elif param["in"] == "query": - qps[param["name"]] = param["x-example"] + example = param["x-example"] + if type(example) == list: + for value in example: + qps.append((param["name"], value)) + else: + qps.append((param["name"], example)) query_string = "" if len(qps) == 0 else "?"+urllib.urlencode(qps) if body: endpoint["example"]["req"] = "%s %s%s HTTP/1.1\nContent-Type: application/json\n\n%s" % (