Merge branch develop into server2server_signing

Conflicts:
	synapse/app/homeserver.py
This commit is contained in:
Mark Haines 2014-10-13 10:58:36 +01:00
commit 984e207b59
22 changed files with 911 additions and 527 deletions

View File

@ -4,11 +4,11 @@ Introduction
Matrix is an ambitious new ecosystem for open federated Instant Messaging and Matrix is an ambitious new ecosystem for open federated Instant Messaging and
VoIP. The basics you need to know to get up and running are: VoIP. The basics you need to know to get up and running are:
- Chatrooms are distributed and do not exist on any single server. Rooms - Chatrooms are distributed and do not exist on any single server. Rooms
can be found using aliases like ``#matrix:matrix.org`` or can be found using aliases like ``#matrix:matrix.org`` or
``#test:localhost:8008`` or they can be ephemeral. ``#test:localhost:8008`` or they can be ephemeral.
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future - Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
you will normally refer to yourself and others using a 3PID: email you will normally refer to yourself and others using a 3PID: email
address, phone number, etc rather than manipulating Matrix user IDs) address, phone number, etc rather than manipulating Matrix user IDs)
@ -20,43 +20,50 @@ The overall architecture is::
WARNING WARNING
======= =======
**Synapse is currently in a state of rapid development, and not all features are yet functional. **Synapse is currently in a state of rapid development, and not all features
Critically, some security features are still in development, which means Synapse can *not* are yet functional. Critically, some security features are still in
be considered secure or reliable at this point.** For instance: development, which means Synapse can *not* be considered secure or reliable at
this point.** For instance:
- **SSL Certificates used by server-server federation are not yet validated.** - **SSL Certificates used by server-server federation are not yet validated.**
- **Room permissions are not yet enforced on traffic received via federation.** - **Room permissions are not yet enforced on traffic received via federation.**
- **Homeservers do not yet cryptographically sign their events to avoid tampering** - **Homeservers do not yet cryptographically sign their events to avoid
tampering**
- Default configuration provides open signup to the service from the internet - Default configuration provides open signup to the service from the internet
Despite this, we believe Synapse is more than useful as a way for experimenting and Despite this, we believe Synapse is more than useful as a way for experimenting
exploring Synapse, and the missing features will land shortly. **Until then, please do *NOT* and exploring Synapse, and the missing features will land shortly. **Until
use Synapse for any remotely important or secure communication.** then, please do *NOT* use Synapse for any remotely important or secure
communication.**
Quick Start Quick Start
=========== ===========
System requirements: System requirements:
- POSIX-compliant system (tested on Linux & OSX) - POSIX-compliant system (tested on Linux & OSX)
- Python 2.7 - Python 2.7
To get up and running: To get up and running:
- To simply play with an **existing** homeserver you can - To simply play with an **existing** homeserver you can
just go straight to http://matrix.org/alpha. just go straight to http://matrix.org/alpha.
- To run your own **private** homeserver on localhost:8008, install synapse with - To run your own **private** homeserver on localhost:8008, generate a basic
``python setup.py develop --user`` and then run ``./synctl start`` twice (once to config file: ``./synctl start`` will give you instructions on how to do this.
generate a config; once to actually run) - you will find a webclient running at For this purpose, you can use 'localhost' or your hostname as a server name.
http://localhost:8008. Please use a recent Chrome, Safari or Firefox for now... Once you've done so, running ``./synctl start`` again will start your private
home sserver. You will find a webclient running at http://localhost:8008.
Please use a recent Chrome or Firefox for now (or Safari if you don't need
VoIP support).
- To run a **public** homeserver and let it exchange messages with other homeservers - To run a **public** homeserver and let it exchange messages with other
and participate in the global Matrix federation, you must expose port 8448 to the homeservers and participate in the global Matrix federation, you must expose
internet and edit homeserver.yaml to specify server_name (the public DNS entry for port 8448 to the internet and edit homeserver.yaml to specify server_name
this server) and then run ``synctl start``. If you changed the server_name, you may (the public DNS entry for this server) and then run ``synctl start``. If you
need to move the old database (homeserver.db) out of the way first. Then come join changed the server_name, you may need to move the old database
``#matrix:matrix.org`` and say hi! :) (homeserver.db) out of the way first. Then come join ``#matrix:matrix.org``
and say hi! :)
For more detailed setup instructions, please see further down this document. For more detailed setup instructions, please see further down this document.
@ -67,21 +74,21 @@ About Matrix
Matrix specifies a set of pragmatic RESTful HTTP JSON APIs as an open standard, Matrix specifies a set of pragmatic RESTful HTTP JSON APIs as an open standard,
which handle: which handle:
- Creating and managing fully distributed chat rooms with no - Creating and managing fully distributed chat rooms with no
single points of control or failure single points of control or failure
- Eventually-consistent cryptographically secure[1] synchronisation of room - Eventually-consistent cryptographically secure[1] synchronisation of room
state across a global open network of federated servers and services state across a global open network of federated servers and services
- Sending and receiving extensible messages in a room with (optional) - Sending and receiving extensible messages in a room with (optional)
end-to-end encryption[2] end-to-end encryption[2]
- Inviting, joining, leaving, kicking, banning room members - Inviting, joining, leaving, kicking, banning room members
- Managing user accounts (registration, login, logout) - Managing user accounts (registration, login, logout)
- Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers, - Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
Facebook accounts to authenticate, identify and discover users on Matrix. Facebook accounts to authenticate, identify and discover users on Matrix.
- Placing 1:1 VoIP and Video calls - Placing 1:1 VoIP and Video calls
These APIs are intended to be implemented on a wide range of servers, services These APIs are intended to be implemented on a wide range of servers, services
and clients, letting developers build messaging and VoIP functionality on top of and clients, letting developers build messaging and VoIP functionality on top
the entirely open Matrix ecosystem rather than using closed or proprietary of the entirely open Matrix ecosystem rather than using closed or proprietary
solutions. The hope is for Matrix to act as the building blocks for a new solutions. The hope is for Matrix to act as the building blocks for a new
generation of fully open and interoperable messaging and VoIP apps for the generation of fully open and interoperable messaging and VoIP apps for the
internet. internet.
@ -96,17 +103,17 @@ In Matrix, every user runs one or more Matrix clients, which connect through to
a Matrix homeserver which stores all their personal chat history and user a Matrix homeserver which stores all their personal chat history and user
account information - much as a mail client connects through to an IMAP/SMTP account information - much as a mail client connects through to an IMAP/SMTP
server. Just like email, you can either run your own Matrix homeserver and server. Just like email, you can either run your own Matrix homeserver and
control and own your own communications and history or use one hosted by someone control and own your own communications and history or use one hosted by
else (e.g. matrix.org) - there is no single point of control or mandatory someone else (e.g. matrix.org) - there is no single point of control or
service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc. mandatory service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
Synapse ships with two basic demo Matrix clients: webclient (a basic group chat Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
web client demo implemented in AngularJS) and cmdclient (a basic Python web client demo implemented in AngularJS) and cmdclient (a basic Python
command line utility which lets you easily see what the JSON APIs are up to). command line utility which lets you easily see what the JSON APIs are up to).
We'd like to invite you to take a look at the Matrix spec, try to run a We'd like to invite you to take a look at the Matrix spec, try to run a
homeserver, and join the existing Matrix chatrooms already out there, experiment homeserver, and join the existing Matrix chatrooms already out there,
with the APIs and the demo clients, and let us know your thoughts at experiment with the APIs and the demo clients, and let us know your thoughts at
https://github.com/matrix-org/synapse/issues or at matrix@matrix.org. https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
Thanks for trying Matrix! Thanks for trying Matrix!
@ -122,11 +129,11 @@ Homeserver Installation
First, the dependencies need to be installed. Start by installing First, the dependencies need to be installed. Start by installing
'python2.7-dev' and the various tools of the compiler toolchain. 'python2.7-dev' and the various tools of the compiler toolchain.
Installing prerequisites on Ubuntu:: Installing prerequisites on Ubuntu::
$ sudo apt-get install build-essential python2.7-dev libffi-dev $ sudo apt-get install build-essential python2.7-dev libffi-dev
Installing prerequisites on Mac OS X:: Installing prerequisites on Mac OS X::
$ xcode-select --install $ xcode-select --install
@ -136,20 +143,20 @@ to install by making setup.py do so, in --user mode::
$ python setup.py develop --user $ python setup.py develop --user
You'll need a version of setuptools new enough to know about git, so you You'll need a version of setuptools new enough to know about git, so you
may need to also run: may need to also run::
$ sudo apt-get install python-pip $ sudo apt-get install python-pip
$ sudo pip install --upgrade setuptools $ sudo pip install --upgrade setuptools
If you don't have access to github, then you may need to install ``syutil`` If you don't have access to github, then you may need to install ``syutil``
manually by checking it out and running ``python setup.py develop --user`` on it manually by checking it out and running ``python setup.py develop --user`` on
too. it too.
If you get errors about ``sodium.h`` being missing, you may also need to If you get errors about ``sodium.h`` being missing, you may also need to
manually install a newer PyNaCl via pip as setuptools installs an old one. Or manually install a newer PyNaCl via pip as setuptools installs an old one. Or
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
installing it. Installing PyNaCl using pip may also work (remember to remove any installing it. Installing PyNaCl using pip may also work (remember to remove
other versions installed by setuputils in, for example, ~/.local/lib). any other versions installed by setuputils in, for example, ~/.local/lib).
On OSX, if you encounter ``clang: error: unknown argument: '-mno-fused-madd'`` On OSX, if you encounter ``clang: error: unknown argument: '-mno-fused-madd'``
you will need to ``export CFLAGS=-Qunused-arguments``. you will need to ``export CFLAGS=-Qunused-arguments``.
@ -185,11 +192,11 @@ be publicly visible on the internet, and they will need to know its host name.
You have two choices here, which will influence the form of your Matrix user You have two choices here, which will influence the form of your Matrix user
IDs: IDs:
1) Use the machine's own hostname as available on public DNS in the form of its 1) Use the machine's own hostname as available on public DNS in the form of
A or AAAA records. This is easier to set up initially, perhaps for testing, its A or AAAA records. This is easier to set up initially, perhaps for
but lacks the flexibility of SRV. testing, but lacks the flexibility of SRV.
2) Set up a SRV record for your domain name. This requires you create a SRV 2) Set up a SRV record for your domain name. This requires you create a SRV
record in DNS, but gives the flexibility to run the server on your own record in DNS, but gives the flexibility to run the server on your own
choice of TCP port, on a machine that might not be the same name as the choice of TCP port, on a machine that might not be the same name as the
domain name. domain name.
@ -247,7 +254,7 @@ http://localhost:8080. Simply run::
Running The Demo Web Client Running The Demo Web Client
=========================== ===========================
The homeserver runs a web client by default at http://localhost:8080. The homeserver runs a web client by default at https://localhost:8448/.
If this is the first time you have used the client from that browser (it uses If this is the first time you have used the client from that browser (it uses
HTML5 local storage to remember its config), you will need to log in to your HTML5 local storage to remember its config), you will need to log in to your
@ -267,8 +274,8 @@ account. Your name will take the form of::
Specify your desired localpart in the topmost box of the "Register for an Specify your desired localpart in the topmost box of the "Register for an
account" form, and click the "Register" button. Hostnames can contain ports if account" form, and click the "Register" button. Hostnames can contain ports if
required due to lack of SRV records (e.g. @matthew:localhost:8080 on an internal required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
synapse sandbox running on localhost) internal synapse sandbox running on localhost)
Logging In To An Existing Account Logging In To An Existing Account
@ -283,9 +290,9 @@ Identity Servers
The job of authenticating 3PIDs and tracking which 3PIDs are associated with a The job of authenticating 3PIDs and tracking which 3PIDs are associated with a
given Matrix user is very security-sensitive, as there is obvious risk of spam given Matrix user is very security-sensitive, as there is obvious risk of spam
if it is too easy to sign up for Matrix accounts or harvest 3PID data. Meanwhile if it is too easy to sign up for Matrix accounts or harvest 3PID data.
the job of publishing the end-to-end encryption public keys for Matrix users is Meanwhile the job of publishing the end-to-end encryption public keys for
also very security-sensitive for similar reasons. Matrix users is also very security-sensitive for similar reasons.
Therefore the role of managing trusted identity in the Matrix ecosystem is Therefore the role of managing trusted identity in the Matrix ecosystem is
farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix

View File

@ -1,9 +0,0 @@
Broad-sweeping stuff which would be nice to have
================================================
- Additional SQL backends beyond sqlite
- homeserver implementation in go
- homeserver implementation in node.js
- client SDKs
- libpurple library
- irssi plugin?

View File

@ -31,7 +31,7 @@ Registration
The aim of registration is to get a user ID and access token which you will need The aim of registration is to get a user ID and access token which you will need
when accessing other APIs:: when accessing other APIs::
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8008/_matrix/client/api/v1/register" curl -XPOST -d '{"user":"example", "password":"wordpass", "type":"m.login.password"}' "http://localhost:8008/_matrix/client/api/v1/register"
{ {
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc", "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
@ -39,14 +39,15 @@ when accessing other APIs::
"user_id": "@example:localhost" "user_id": "@example:localhost"
} }
NB: If a ``user_id`` is not specified, one will be randomly generated for you. NB: If a ``user`` is not specified, one will be randomly generated for you.
If you do not specify a ``password``, you will be unable to login to the account If you do not specify a ``password``, you will be unable to login to the account
if you forget the ``access_token``. if you forget the ``access_token``.
Implementation note: The matrix specification does not enforce how users Implementation note: The matrix specification does not enforce how users
register with a server. It just specifies the URL path and absolute minimum register with a server. It just specifies the URL path and absolute minimum
keys. The reference home server uses a username/password to authenticate user, keys. The reference home server uses a username/password to authenticate user,
but other home servers may use different methods. but other home servers may use different methods. This is why you need to
specify the ``type`` of method.
Login Login
----- -----

View File

@ -1,103 +1,3 @@
========
Presence
========
A description of presence information and visibility between users.
Overview
========
Each user has the concept of Presence information. This encodes a sense of the
"availability" of that user, suitable for display on other user's clients.
Presence Information
====================
The basic piece of presence information is an enumeration of a small set of
state; such as "free to chat", "online", "busy", or "offline". The default state
unless the user changes it is "online". Lower states suggest some amount of
decreased availability from normal, which might have some client-side effect
like muting notification sounds and suggests to other users not to bother them
unless it is urgent. Equally, the "free to chat" state exists to let the user
announce their general willingness to receive messages moreso than default.
Home servers should also allow a user to set their state as "hidden" - a state
which behaves as offline, but allows the user to see the client state anyway and
generally interact with client features such as reading message history or
accessing contacts in the address book.
This basic state field applies to the user as a whole, regardless of how many
client devices they have connected. The home server should synchronise this
status choice among multiple devices to ensure the user gets a consistent
experience.
Idle Time
---------
As well as the basic state field, the presence information can also show a sense
of an "idle timer". This should be maintained individually by the user's
clients, and the homeserver can take the highest reported time as that to
report. Likely this should be presented in fairly coarse granularity; possibly
being limited to letting the home server automatically switch from a "free to
chat" or "online" mode into "idle".
When a user is offline, the Home Server can still report when the user was last
seen online, again perhaps in a somewhat coarse manner.
Device Type
-----------
Client devices that may limit the user experience somewhat (such as "mobile"
devices with limited ability to type on a real keyboard or read large amounts of
text) should report this to the home server, as this is also useful information
to report as "presence" if the user cannot be expected to provide a good typed
response to messages.
Presence List
=============
Each user's home server stores a "presence list" for that user. This stores a
list of other user IDs the user has chosen to add to it (remembering any ACL
Pointer if appropriate).
To be added to a contact list, the user being added must grant permission. Once
granted, both user's HS(es) store this information, as it allows the user who
has added the contact some more abilities; see below. Since such subscriptions
are likely to be bidirectional, HSes may wish to automatically accept requests
when a reverse subscription already exists.
As a convenience, presence lists should support the ability to collect users
into groups, which could allow things like inviting the entire group to a new
("ad-hoc") chat room, or easy interaction with the profile information ACL
implementation of the HS.
Presence and Permissions
========================
For a viewing user to be allowed to see the presence information of a target
user, either
* The target user has allowed the viewing user to add them to their presence
list, or
* The two users share at least one room in common
In the latter case, this allows for clients to display some minimal sense of
presence information in a user list for a room.
Home servers can also use the user's choice of presence state as a signal for
how to handle new private one-to-one chat message requests. For example, it
might decide:
"free to chat": accept anything
"online": accept from anyone in my addres book list
"busy": accept from anyone in this "important people" group in my address
book list
API Efficiency API Efficiency
============== ==============

View File

@ -48,6 +48,22 @@
"paramType": "body" "paramType": "body"
} }
] ]
},
{
"method": "DELETE",
"summary": "Removes a mapping of room alias to room ID.",
"notes": "Only privileged users can perform this action.",
"type": "void",
"nickname": "remove_room_alias",
"parameters": [
{
"name": "roomAlias",
"description": "The room alias to remove.",
"required": true,
"type": "string",
"paramType": "path"
}
]
} }
] ]
} }

53
docs/definitions.rst Normal file
View File

@ -0,0 +1,53 @@
Definitions
===========
# *Event* -- A JSON object that represents a piece of information to be
distributed to the the room. The object includes a payload and metadata,
including a `type` used to indicate what the payload is for and how to process
them. It also includes one or more references to previous events.
# *Event graph* -- Events and their references to previous events form a
directed acyclic graph. All events must be a descendant of the first event in a
room, except for a few special circumstances.
# *State event* -- A state event is an event that has a non-null string valued
`state_key` field. It may also include a `prev_state` key referencing exactly
one state event with the same type and state key, in the same event graph.
# *State tree* -- A state tree is a tree formed by a collection of state events
that have the same type and state key (all in the same event graph.
# *State resolution algorithm* -- An algorithm that takes a state tree as input
and selects a single leaf node.
# *Current state event* -- The leaf node of a given state tree that has been
selected by the state resolution algorithm.
# *Room state* / *state dictionary* / *current state* -- A mapping of the pair
(event type, state key) to the current state event for that pair.
# *Room* -- An event graph and its associated state dictionary. An event is in
the room if it is part of the event graph.
# *Topological ordering* -- The partial ordering that can be extracted from the
event graph due to it being a DAG.
(The state definitions are purposely slightly ill-defined, since if we allow
deleting events we might end up with multiple state trees for a given event
type and state key pair.)
Federation specific
-------------------
# *(Persistent data unit) PDU* -- An encoding of an event for distribution of
the server to server protocol.
# *(Ephemeral data unit) EDU* -- A piece of information that is sent between
servers and doesn't encode an event.
Client specific
---------------
# *Child events* -- Events that reference a single event in the same room
independently of the event graph.
# *Collapsed events* -- Events that have all child events that reference it
included in the JSON object.

View File

@ -0,0 +1,30 @@
Matrix Specification NOTHAVEs
=============================
This document contains sections of the main specification that have been
temporarily removed, because they specify intentions or aspirations that have
in no way yet been implemented. Rather than outright-deleting them, they have
been moved here so as to stand as an initial version for such time as they
become extant.
Presence
========
Idle Time
---------
As well as the basic ``presence`` field, the presence information can also show
a sense of an "idle timer". This should be maintained individually by the
user's clients, and the home server can take the highest reported time as that
to report. When a user is offline, the home server can still report when the
user was last seen online.
Device Type
-----------
Client devices that may limit the user experience somewhat (such as "mobile"
devices with limited ability to type on a real keyboard or read large amounts of
text) should report this to the home server, as this is also useful information
to report as "presence" if the user cannot be expected to provide a good typed
response to messages.

File diff suppressed because it is too large Load Diff

51
docs/state_resolution.rst Normal file
View File

@ -0,0 +1,51 @@
State Resolution
================
This section describes why we need state resolution and how it works.
Motivation
-----------
We want to be able to associate some shared state with rooms, e.g. a room name
or members list. This is done by having a current state dictionary that maps
from the pair event type and state key to an event.
However, since the servers involved in the room are distributed we need to be
able to handle the case when two (or more) servers try and update the state at
the same time. This is done via the state resolution algorithm.
State Tree
------------
State events contain a reference to the state it is trying to replace. These
relations form a tree where the current state is one of the leaf nodes.
Note that state events are events, and so are part of the PDU graph. Thus we
can be sure that (modulo the internet being particularly broken) we will see
all state events eventually.
Algorithm requirements
----------------------
We want the algorithm to have the following properties:
- Since we aren't guaranteed what order we receive state events in, except that
we see parents before children, the state resolution algorithm must not depend
on the order and must always come to the same result.
- If we receive a state event whose parent is the current state, then the
algorithm will select it.
- The algorithm does not depend on internal state, ensuring all servers should
come to the same decision.
These three properties mean it is enough to keep track of the current state and
compare it with any new proposed state, rather than having to keep track of all
the leafs of the tree and recomputing across the entire state tree.
Current Implementation
----------------------
The current implementation works as follows: Upon receipt of a newly proposed
state change we first find the common ancestor. Then we take the maximum
across each branch of the users' power levels, if one is higher then it is
selected as the current state. Otherwise, we check if one chain is longer than
the other, if so we choose that one. If that also fails, then we concatenate
all the pdu ids and take a SHA1 hash and compare them to select a common
ancestor.

View File

@ -110,7 +110,7 @@ $('.register').live('click', function() {
url: "http://localhost:8008/_matrix/client/api/v1/register", url: "http://localhost:8008/_matrix/client/api/v1/register",
type: "POST", type: "POST",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }), data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json", dataType: "json",
success: function(data) { success: function(data) {
onLoggedIn(data); onLoggedIn(data);

View File

@ -14,7 +14,7 @@ $('.register').live('click', function() {
url: "http://localhost:8008/_matrix/client/api/v1/register", url: "http://localhost:8008/_matrix/client/api/v1/register",
type: "POST", type: "POST",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }), data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json", dataType: "json",
success: function(data) { success: function(data) {
showLoggedIn(data); showLoggedIn(data);

View File

@ -25,8 +25,8 @@ from twisted.web.static import File
from twisted.web.server import Site from twisted.web.server import Site
from synapse.http.server import JsonResource, RootRedirect from synapse.http.server import JsonResource, RootRedirect
from synapse.http.content_repository import ContentRepoResource from synapse.http.content_repository import ContentRepoResource
from synapse.http.client import TwistedHttpClient
from synapse.http.server_key_resource import LocalKey from synapse.http.server_key_resource import LocalKey
from synapse.http.client import MatrixHttpClient
from synapse.api.urls import ( from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX, CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
SERVER_KEY_PREFIX, SERVER_KEY_PREFIX,
@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
class SynapseHomeServer(HomeServer): class SynapseHomeServer(HomeServer):
def build_http_client(self): def build_http_client(self):
return TwistedHttpClient(self) return MatrixHttpClient(self)
def build_resource_for_client(self): def build_resource_for_client(self):
return JsonResource() return JsonResource()

View File

@ -123,6 +123,8 @@ class Config(object):
# style mode markers into the file, to hint to people that # style mode markers into the file, to hint to people that
# this is a YAML file. # this is a YAML file.
yaml.dump(config, config_file, default_flow_style=False) yaml.dump(config, config_file, default_flow_style=False)
print "A config file has been generated in %s for server name '%s') with corresponding SSL keys and self-signed certificates. Please review this file and customise it to your needs." % (config_args.config_path, config['server_name'])
print "If this server name is incorrect, you will need to regenerate the SSL certificates"
sys.exit(0) sys.exit(0)
return cls(args) return cls(args)

View File

@ -163,7 +163,8 @@ class ReplicationLayer(object):
return defer.succeed(None) return defer.succeed(None)
@log_function @log_function
def make_query(self, destination, query_type, args): def make_query(self, destination, query_type, args,
retry_on_dns_fail=True):
"""Sends a federation Query to a remote homeserver of the given type """Sends a federation Query to a remote homeserver of the given type
and arguments. and arguments.
@ -178,7 +179,9 @@ class ReplicationLayer(object):
a Deferred which will eventually yield a JSON object from the a Deferred which will eventually yield a JSON object from the
response response
""" """
return self.transport_layer.make_query(destination, query_type, args) return self.transport_layer.make_query(
destination, query_type, args, retry_on_dns_fail=retry_on_dns_fail
)
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function

View File

@ -195,13 +195,14 @@ class TransportLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def make_query(self, destination, query_type, args): def make_query(self, destination, query_type, args, retry_on_dns_fail):
path = PREFIX + "/query/%s" % query_type path = PREFIX + "/query/%s" % query_type
response = yield self.client.get_json( response = yield self.client.get_json(
destination=destination, destination=destination,
path=path, path=path,
args=args args=args,
retry_on_dns_fail=retry_on_dns_fail,
) )
defer.returnValue(response) defer.returnValue(response)

View File

@ -18,7 +18,6 @@ from twisted.internet import defer
from ._base import BaseHandler from ._base import BaseHandler
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.http.client import HttpClient
from synapse.api.events.room import RoomAliasesEvent from synapse.api.events.room import RoomAliasesEvent
import logging import logging
@ -98,8 +97,8 @@ class DirectoryHandler(BaseHandler):
query_type="directory", query_type="directory",
args={ args={
"room_alias": room_alias.to_string(), "room_alias": room_alias.to_string(),
HttpClient.RETRY_DNS_LOOKUP_FAILURES: False },
} retry_on_dns_fail=False,
) )
if result and "room_id" in result and "servers" in result: if result and "room_id" in result and "servers" in result:

View File

@ -17,7 +17,7 @@ from twisted.internet import defer
from ._base import BaseHandler from ._base import BaseHandler
from synapse.api.errors import LoginError, Codes from synapse.api.errors import LoginError, Codes
from synapse.http.client import PlainHttpClient from synapse.http.client import IdentityServerHttpClient
from synapse.util.emailutils import EmailException from synapse.util.emailutils import EmailException
import synapse.util.emailutils as emailutils import synapse.util.emailutils as emailutils
@ -97,7 +97,7 @@ class LoginHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def _query_email(self, email): def _query_email(self, email):
httpCli = PlainHttpClient(self.hs) httpCli = IdentityServerHttpClient(self.hs)
data = yield httpCli.get_json( data = yield httpCli.get_json(
'matrix.org:8090', # TODO FIXME This should be configurable. 'matrix.org:8090', # TODO FIXME This should be configurable.
"/_matrix/identity/api/v1/lookup?medium=email&address=" + "/_matrix/identity/api/v1/lookup?medium=email&address=" +

View File

@ -22,7 +22,8 @@ from synapse.api.errors import (
) )
from ._base import BaseHandler from ._base import BaseHandler
import synapse.util.stringutils as stringutils import synapse.util.stringutils as stringutils
from synapse.http.client import PlainHttpClient from synapse.http.client import IdentityServerHttpClient
from synapse.http.client import CaptchaServerHttpClient
import base64 import base64
import bcrypt import bcrypt
@ -154,7 +155,9 @@ class RegistrationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def _threepid_from_creds(self, creds): def _threepid_from_creds(self, creds):
httpCli = PlainHttpClient(self.hs) # TODO: get this from the homeserver rather than creating a new one for
# each request
httpCli = IdentityServerHttpClient(self.hs)
# XXX: make this configurable! # XXX: make this configurable!
trustedIdServers = ['matrix.org:8090'] trustedIdServers = ['matrix.org:8090']
if not creds['idServer'] in trustedIdServers: if not creds['idServer'] in trustedIdServers:
@ -173,7 +176,7 @@ class RegistrationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def _bind_threepid(self, creds, mxid): def _bind_threepid(self, creds, mxid):
httpCli = PlainHttpClient(self.hs) httpCli = IdentityServerHttpClient(self.hs)
data = yield httpCli.post_urlencoded_get_json( data = yield httpCli.post_urlencoded_get_json(
creds['idServer'], creds['idServer'],
"/_matrix/identity/api/v1/3pid/bind", "/_matrix/identity/api/v1/3pid/bind",
@ -203,7 +206,9 @@ class RegistrationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def _submit_captcha(self, ip_addr, private_key, challenge, response): def _submit_captcha(self, ip_addr, private_key, challenge, response):
client = PlainHttpClient(self.hs) # TODO: get this from the homeserver rather than creating a new one for
# each request
client = CaptchaServerHttpClient(self.hs)
data = yield client.post_urlencoded_get_raw( data = yield client.post_urlencoded_get_raw(
"www.google.com:80", "www.google.com:80",
"/recaptcha/api/verify", "/recaptcha/api/verify",

View File

@ -35,56 +35,6 @@ import urllib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# FIXME: SURELY these should be killed?!
_destination_mappings = {
"red": "localhost:8080",
"blue": "localhost:8081",
"green": "localhost:8082",
}
class HttpClient(object):
""" Interface for talking json over http
"""
RETRY_DNS_LOOKUP_FAILURES = "__retry_dns"
def put_json(self, destination, path, data):
""" Sends the specifed json data using PUT
Args:
destination (str): The remote server to send the HTTP request
to.
path (str): The HTTP path.
data (dict): A dict containing the data that will be used as
the request body. This will be encoded as JSON.
Returns:
Deferred: Succeeds when we get a 2xx HTTP response. The result
will be the decoded JSON body. On a 4xx or 5xx error response a
CodeMessageException is raised.
"""
pass
def get_json(self, destination, path, args=None):
""" Get's some json from the given host homeserver and path
Args:
destination (str): The remote server to send the HTTP request
to.
path (str): The HTTP path.
args (dict): A dictionary used to create query strings, defaults to
None.
**Note**: The value of each key is assumed to be an iterable
and *not* a string.
Returns:
Deferred: Succeeds when we get *any* HTTP response.
The result of the deferred is a tuple of `(code, response)`,
where `response` is a dict representing the decoded JSON body.
"""
pass
class MatrixHttpAgent(_AgentBase): class MatrixHttpAgent(_AgentBase):
@ -109,113 +59,14 @@ class MatrixHttpAgent(_AgentBase):
parsed_URI.originForm) parsed_URI.originForm)
class TwistedHttpClient(HttpClient): class BaseHttpClient(object):
""" Wrapper around the twisted HTTP client api. """Base class for HTTP clients using twisted.
Attributes:
agent (twisted.web.client.Agent): The twisted Agent used to send the
requests.
""" """
def __init__(self, hs): def __init__(self, hs):
self.agent = MatrixHttpAgent(reactor) self.agent = MatrixHttpAgent(reactor)
self.hs = hs self.hs = hs
@defer.inlineCallbacks
def put_json(self, destination, path, data, on_send_callback=None):
if destination in _destination_mappings:
destination = _destination_mappings[destination]
response = yield self._create_request(
destination.encode("ascii"),
"PUT",
path.encode("ascii"),
producer=_JsonProducer(data),
headers_dict={"Content-Type": ["application/json"]},
on_send_callback=on_send_callback,
)
logger.debug("Getting resp body")
body = yield readBody(response)
logger.debug("Got resp body")
defer.returnValue((response.code, body))
@defer.inlineCallbacks
def get_json(self, destination, path, args={}):
if destination in _destination_mappings:
destination = _destination_mappings[destination]
logger.debug("get_json args: %s", args)
retry_on_dns_fail = True
if HttpClient.RETRY_DNS_LOOKUP_FAILURES in args:
# FIXME: This isn't ideal, but the interface exposed in get_json
# isn't comprehensive enough to give caller's any control over
# their connection mechanics.
retry_on_dns_fail = args.pop(HttpClient.RETRY_DNS_LOOKUP_FAILURES)
query_bytes = urllib.urlencode(args, True)
logger.debug("Query bytes: %s Retry DNS: %s", args, retry_on_dns_fail)
response = yield self._create_request(
destination.encode("ascii"),
"GET",
path.encode("ascii"),
query_bytes=query_bytes,
retry_on_dns_fail=retry_on_dns_fail
)
body = yield readBody(response)
defer.returnValue(json.loads(body))
@defer.inlineCallbacks
def post_urlencoded_get_json(self, destination, path, args={}):
if destination in _destination_mappings:
destination = _destination_mappings[destination]
logger.debug("post_urlencoded_get_json args: %s", args)
query_bytes = urllib.urlencode(args, True)
response = yield self._create_request(
destination.encode("ascii"),
"POST",
path.encode("ascii"),
producer=FileBodyProducer(StringIO(urllib.urlencode(args))),
headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]}
)
body = yield readBody(response)
defer.returnValue(json.loads(body))
# XXX FIXME : I'm so sorry.
@defer.inlineCallbacks
def post_urlencoded_get_raw(self, destination, path, accept_partial=False, args={}):
if destination in _destination_mappings:
destination = _destination_mappings[destination]
query_bytes = urllib.urlencode(args, True)
response = yield self._create_request(
destination.encode("ascii"),
"POST",
path.encode("ascii"),
producer=FileBodyProducer(StringIO(urllib.urlencode(args))),
headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]}
)
try:
body = yield readBody(response)
defer.returnValue(body)
except PartialDownloadError as e:
if accept_partial:
defer.returnValue(e.response)
else:
raise e
@defer.inlineCallbacks @defer.inlineCallbacks
def _create_request(self, destination, method, path_bytes, param_bytes=b"", def _create_request(self, destination, method, path_bytes, param_bytes=b"",
query_bytes=b"", producer=None, headers_dict={}, query_bytes=b"", producer=None, headers_dict={},
@ -239,7 +90,6 @@ class TwistedHttpClient(HttpClient):
retries_left = 5 retries_left = 5
# TODO: setup and pass in an ssl_context to enable TLS
endpoint = self._getEndpoint(reactor, destination); endpoint = self._getEndpoint(reactor, destination);
while True: while True:
@ -290,6 +140,85 @@ class TwistedHttpClient(HttpClient):
defer.returnValue(response) defer.returnValue(response)
class MatrixHttpClient(BaseHttpClient):
""" Wrapper around the twisted HTTP client api. Implements
Attributes:
agent (twisted.web.client.Agent): The twisted Agent used to send the
requests.
"""
RETRY_DNS_LOOKUP_FAILURES = "__retry_dns"
@defer.inlineCallbacks
def put_json(self, destination, path, data, on_send_callback=None):
""" Sends the specifed json data using PUT
Args:
destination (str): The remote server to send the HTTP request
to.
path (str): The HTTP path.
data (dict): A dict containing the data that will be used as
the request body. This will be encoded as JSON.
Returns:
Deferred: Succeeds when we get a 2xx HTTP response. The result
will be the decoded JSON body. On a 4xx or 5xx error response a
CodeMessageException is raised.
"""
response = yield self._create_request(
destination.encode("ascii"),
"PUT",
path.encode("ascii"),
producer=_JsonProducer(data),
headers_dict={"Content-Type": ["application/json"]},
on_send_callback=on_send_callback,
)
logger.debug("Getting resp body")
body = yield readBody(response)
logger.debug("Got resp body")
defer.returnValue((response.code, body))
@defer.inlineCallbacks
def get_json(self, destination, path, args={}, retry_on_dns_fail=True):
""" Get's some json from the given host homeserver and path
Args:
destination (str): The remote server to send the HTTP request
to.
path (str): The HTTP path.
args (dict): A dictionary used to create query strings, defaults to
None.
**Note**: The value of each key is assumed to be an iterable
and *not* a string.
Returns:
Deferred: Succeeds when we get *any* HTTP response.
The result of the deferred is a tuple of `(code, response)`,
where `response` is a dict representing the decoded JSON body.
"""
logger.debug("get_json args: %s", args)
query_bytes = urllib.urlencode(args, True)
logger.debug("Query bytes: %s Retry DNS: %s", args, retry_on_dns_fail)
response = yield self._create_request(
destination.encode("ascii"),
"GET",
path.encode("ascii"),
query_bytes=query_bytes,
retry_on_dns_fail=retry_on_dns_fail
)
body = yield readBody(response)
defer.returnValue(json.loads(body))
def _getEndpoint(self, reactor, destination): def _getEndpoint(self, reactor, destination):
return matrix_endpoint( return matrix_endpoint(
reactor, destination, timeout=10, reactor, destination, timeout=10,
@ -297,10 +226,63 @@ class TwistedHttpClient(HttpClient):
) )
class PlainHttpClient(TwistedHttpClient): class IdentityServerHttpClient(BaseHttpClient):
"""Separate HTTP client for talking to the Identity servers since they
don't use SRV records and talk x-www-form-urlencoded rather than JSON.
"""
def _getEndpoint(self, reactor, destination):
#TODO: This should be talking TLS
return matrix_endpoint(reactor, destination, timeout=10)
@defer.inlineCallbacks
def post_urlencoded_get_json(self, destination, path, args={}):
logger.debug("post_urlencoded_get_json args: %s", args)
query_bytes = urllib.urlencode(args, True)
response = yield self._create_request(
destination.encode("ascii"),
"POST",
path.encode("ascii"),
producer=FileBodyProducer(StringIO(query_bytes)),
headers_dict={
"Content-Type": ["application/x-www-form-urlencoded"]
}
)
body = yield readBody(response)
defer.returnValue(json.loads(body))
class CaptchaServerHttpClient(MatrixHttpClient):
"""Separate HTTP client for talking to google's captcha servers"""
def _getEndpoint(self, reactor, destination): def _getEndpoint(self, reactor, destination):
return matrix_endpoint(reactor, destination, timeout=10) return matrix_endpoint(reactor, destination, timeout=10)
@defer.inlineCallbacks
def post_urlencoded_get_raw(self, destination, path, accept_partial=False,
args={}):
query_bytes = urllib.urlencode(args, True)
response = yield self._create_request(
destination.encode("ascii"),
"POST",
path.encode("ascii"),
producer=FileBodyProducer(StringIO(query_bytes)),
headers_dict={
"Content-Type": ["application/x-www-form-urlencoded"]
}
)
try:
body = yield readBody(response)
defer.returnValue(body)
except PartialDownloadError as e:
if accept_partial:
defer.returnValue(e.response)
else:
raise e
def _print_ex(e): def _print_ex(e):
if hasattr(e, "reasons") and e.reasons: if hasattr(e, "reasons") and e.reasons:

4
synctl
View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
SYNAPSE="synapse/app/homeserver.py" SYNAPSE="python -m synapse.app.homeserver"
CONFIGFILE="homeserver.yaml" CONFIGFILE="homeserver.yaml"
PIDFILE="homeserver.pid" PIDFILE="homeserver.pid"
@ -14,7 +14,7 @@ case "$1" in
start) start)
if [ ! -f "$CONFIGFILE" ]; then if [ ! -f "$CONFIGFILE" ]; then
echo "No config file found" echo "No config file found"
echo "To generate a config file, run 'python --generate-config'" echo "To generate a config file, run '$SYNAPSE -c $CONFIGFILE --generate-config --server-name=<server name>'"
exit 1 exit 1
fi fi

View File

@ -257,7 +257,7 @@ class FederationTestCase(unittest.TestCase):
response = yield self.federation.make_query( response = yield self.federation.make_query(
destination="remote", destination="remote",
query_type="a-question", query_type="a-question",
args={"one": "1", "two": "2"} args={"one": "1", "two": "2"},
) )
self.assertEquals({"your": "response"}, response) self.assertEquals({"your": "response"}, response)
@ -265,7 +265,8 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.get_json.assert_called_with( self.mock_http_client.get_json.assert_called_with(
destination="remote", destination="remote",
path="/_matrix/federation/v1/query/a-question", path="/_matrix/federation/v1/query/a-question",
args={"one": "1", "two": "2"} args={"one": "1", "two": "2"},
retry_on_dns_fail=True,
) )
@defer.inlineCallbacks @defer.inlineCallbacks

View File

@ -20,7 +20,6 @@ from twisted.internet import defer
from mock import Mock from mock import Mock
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.http.client import HttpClient
from synapse.handlers.directory import DirectoryHandler from synapse.handlers.directory import DirectoryHandler
from synapse.storage.directory import RoomAliasMapping from synapse.storage.directory import RoomAliasMapping
@ -95,8 +94,8 @@ class DirectoryTestCase(unittest.TestCase):
query_type="directory", query_type="directory",
args={ args={
"room_alias": "#another:remote", "room_alias": "#another:remote",
HttpClient.RETRY_DNS_LOOKUP_FAILURES: False },
} retry_on_dns_fail=False,
) )
@defer.inlineCallbacks @defer.inlineCallbacks