Compare commits

...

25 Commits

Author SHA1 Message Date
Maxime Dor
c816217b22 Send new invite notification to same user if rooms are different 2017-09-28 02:45:01 +02:00
Maxime Dor
2e7b5d2a87 Refactor packages (cosmetic) 2017-09-27 03:59:45 +02:00
Max Dor
09208d55d7 Merge pull request #33 from kamax-io/email-connector-sendgrid
Email connector sendgrid
2017-09-27 03:33:13 +02:00
Maxime Dor
05c76a657e Fix extra placeholders in smtp sender 2017-09-27 03:30:53 +02:00
Maxime Dor
f3bbc7c7c6 Add support for SendGrid as Email notification handler 2017-09-27 01:55:37 +02:00
Maxime Dor
61addd297a Use the correct formatting for MSISDN 2017-09-26 04:26:39 +02:00
Maxime Dor
1de0951733 Support 3PID listing during auth with Google Firebase 2017-09-26 03:11:15 +02:00
Maxime Dor
d348ebd813 Improved README to point to dedicated documents 2017-09-25 18:25:58 +02:00
Maxime Dor
0499c10a2c Better sample config, better README 2017-09-25 18:20:18 +02:00
Maxime Dor
13e248c71e Do not enforce Twilio config by default 2017-09-25 18:04:21 +02:00
Maxime Dor
d221b2c5de Fix groovy rollback issue 2017-09-25 17:56:06 +02:00
Maxime Dor
3a1900cbb2 Add link to 3PID session documentation 2017-09-25 17:15:37 +02:00
Max Dor
9f1867a030 Merge pull request #32 from kamax-io/phone_numbers-validation
Phone numbers validation
2017-09-25 17:12:59 +02:00
Maxime Dor
a061241291 Use relative links 2017-09-25 17:11:58 +02:00
Maxime Dor
fefa81e935 Add link to phone numbers in TOC 2017-09-25 17:06:39 +02:00
Maxime Dor
1e77bf43c6 Add documentation to validate phone numbers 2017-09-25 17:03:50 +02:00
Maxime Dor
c73bbf675e First prototype to validate phone numbers 2017-09-25 05:53:07 +02:00
Maxime Dor
6c2e65ace5 Code formatting, cosmetic 2017-09-25 02:35:16 +02:00
Maxime Dor
33263d3cff Bye bye Groovy, you won't be missed :( 2017-09-25 02:31:31 +02:00
Maxime Dor
af19fed6e7 More links to specific config docs 2017-09-25 00:15:30 +02:00
Maxime Dor
246dc4f8d1 Add web views link 2017-09-25 00:10:25 +02:00
Maxime Dor
31efa3e33f More 3PID sessions configuration documentation 2017-09-25 00:08:58 +02:00
Maxime Dor
bee2a5129b Fix inconsistencies into DNS library 2017-09-24 21:29:14 +02:00
Maxime Dor
f1e78af80b Fix empty JSON object on empty lookup results 2017-09-24 21:12:49 +02:00
Max Dor
e0022e549e Merge pull request #31 from kamax-io/binding-validation
3PID sessions support (email only)
2017-09-24 05:26:37 +02:00
175 changed files with 3725 additions and 2144 deletions

View File

@@ -23,7 +23,7 @@ mxisd only aims to support workflows that do NOT break federation or basic looku
# Features # Features
- Single lookup of 3PID (E-mail, phone number, etc.) by the Matrix Client or Homeserver. - Single lookup of 3PID (E-mail, phone number, etc.) by the Matrix Client or Homeserver.
- Bulk lookups when trying to find possible matches within contacts in Android and iOS clients. - Bulk lookups when trying to find possible matches within contacts in Android and iOS clients.
- Bind of 3PID by a Matrix user within a Matrix client. - Bind of 3PID by a Matrix user within a Matrix client - See [documentation](docs/sessions/3pid.md)
- Support of invitation to rooms by e-mail with e-mail notification to invitee. - Support of invitation to rooms by e-mail with e-mail notification to invitee.
- Authentication support in [synapse](https://github.com/matrix-org/synapse) via the [REST auth module](https://github.com/kamax-io/matrix-synapse-rest-auth). - Authentication support in [synapse](https://github.com/matrix-org/synapse) via the [REST auth module](https://github.com/kamax-io/matrix-synapse-rest-auth).
@@ -126,7 +126,7 @@ curl "http://localhost:8090/_matrix/identity/api/v1/lookup?medium=email&address=
If you plan on testing the integration with a homeserver, you will need to run an HTTPS reverse proxy in front of it If you plan on testing the integration with a homeserver, you will need to run an HTTPS reverse proxy in front of it
as the reference Home Server implementation [synapse](https://github.com/matrix-org/synapse) requires a HTTPS connection as the reference Home Server implementation [synapse](https://github.com/matrix-org/synapse) requires a HTTPS connection
to an ID server. to an ID server.
See the [Integration section](https://github.com/kamax-io/mxisd#integration) for more details. See the [Integration section](#integration) for more details.
## Install ## Install
After [building](#build) the software, run all the following commands as `root` or using `sudo` After [building](#build) the software, run all the following commands as `root` or using `sudo`
@@ -171,7 +171,8 @@ systemctl start mxisd
After following the specific instructions to create a config file from the sample: After following the specific instructions to create a config file from the sample:
1. Set the `matrix.domain` value to the domain value used in your Home Server configuration 1. Set the `matrix.domain` value to the domain value used in your Home Server configuration
2. Set an absolute location for the signing keys using `key.path` 2. Set an absolute location for the signing keys using `key.path`
3. Configure the E-mail invite sender with items starting in `invite.sender.email` 3. Configure the E-mail notification sender following [the documentation](docs/threepids/medium/email/smtp-connector.md)
4. If you would like to support Phone number validation, see the [Twilio configuration](docs/threepids/medium/msisdn/twilio-connector.md)
In case your IS public domain does not match your Matrix domain, see `server.name` and `server.publicUrl` In case your IS public domain does not match your Matrix domain, see `server.name` and `server.publicUrl`
config items. config items.

View File

@@ -301,18 +301,19 @@ key.path: '/path/to/sign.key'
############################# ###################################
# 3PID invites config items # # 3PID notifications config items #
############################# ###################################
# If you would like to change the content, see https://github.com/kamax-io/mxisd/blob/master/docs/threepids/notifications/template-generator.md
# #
#### E-mail invite sender #### E-mail invite sender
# #
# SMTP host # SMTP host
invite.sender.email.host: "smtp.example.org" threepid.medium.email.connectors.smtp.host: "smtp.example.org"
# SMTP port # SMTP port
invite.sender.email.port: 587 threepid.medium.email.connectors.smtp.port: 587
# TLS mode for the connection. # TLS mode for the connection.
@@ -322,51 +323,19 @@ invite.sender.email.port: 587
# 1 Enable TLS if supported by server # 1 Enable TLS if supported by server
# 2 Force TLS and fail if not available # 2 Force TLS and fail if not available
# #
#invite.sender.email.tls: 1 #threepid.medium.email.connectors.smtp.tls: 1
# Login for SMTP # Login for SMTP
invite.sender.email.login: "matrix-identity@example.org" threepid.medium.email.connectors.smtp.login: "matrix-identity@example.org"
# Password for the account # Password for the account
invite.sender.email.password: "ThePassword" threepid.medium.email.connectors.smtp.password: "ThePassword"
# The e-mail to send as. If empty, will be the same as login # The e-mail to send as. If empty, will be the same as login
invite.sender.email.email: "matrix-identity@example.org" threepid.medium.email.identity.from: "matrix-identity@example.org"
# The display name used in the e-mail
#
#invite.sender.email.name: "mxisd Identity Server"
# The E-mail template to use, using built-in template by default
#
# The template is expected to be a full e-mail body, including client headers, using MIME and UTF-8 encoding.
# The following headers will be set by mxisd directly and should not be present in the template:
# - From
# - To
# - Date
# - Message-Id
# - X-Mailer
#
# The following placeholders are available:
# - %DOMAIN% Domain name as per server.name config item
# - %DOMAIN_PRETTY% Word capitalize version of the domain. e.g. example.org -> Example.org
# - %FROM_EMAIL% Value of this section's email config item
# - %FROM_NAME% Value of this section's name config item
# - %SENDER_ID% Matrix ID of the invitation sender
# - %SENDER_NAME% Display name of the invitation sender, empty if not available
# - %SENDER_NAME_OR_ID% Value of %SENDER_NAME% or, if empty, value of %SENDER_ID%
# - %INVITE_MEDIUM% Medium of the invite (e.g. email, msisdn)
# - %INVITE_ADDRESS% Address used to invite
# - %ROOM_ID% ID of the room where the invitation took place
# - %ROOM_NAME% Name of the room, empty if not available
# - %ROOM_NAME_OR_ID% Value of %ROOM_NAME% or, if empty, value of %ROOM_ID%
#
#invite.sender.email.template: "/absolute/path/to/file"

View File

@@ -1,5 +1,3 @@
import java.util.regex.Pattern
/* /*
* mxisd - Matrix Identity Server Daemon * mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor * Copyright (C) 2017 Maxime Dor
@@ -20,7 +18,9 @@ import java.util.regex.Pattern
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
apply plugin: 'groovy' import java.util.regex.Pattern
apply plugin: 'java'
apply plugin: 'org.springframework.boot' apply plugin: 'org.springframework.boot'
def confFileName = "application.example.yaml" def confFileName = "application.example.yaml"
@@ -47,7 +47,7 @@ String gitVersion() {
def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?") def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?")
ByteArrayOutputStream out = new ByteArrayOutputStream() ByteArrayOutputStream out = new ByteArrayOutputStream()
exec { exec {
commandLine = [ 'git', 'describe', '--always', '--dirty' ] commandLine = ['git', 'describe', '--always', '--dirty']
standardOutput = out standardOutput = out
} }
def v = out.toString().replace(System.lineSeparator(), '') def v = out.toString().replace(System.lineSeparator(), '')
@@ -70,9 +70,6 @@ repositories {
} }
dependencies { dependencies {
// We are a groovy project
compile 'org.codehaus.groovy:groovy-all:2.4.7'
// Easy file management // Easy file management
compile 'commons-io:commons-io:2.5' compile 'commons-io:commons-io:2.5'
@@ -119,6 +116,12 @@ dependencies {
// PostgreSQL // PostgreSQL
compile 'org.postgresql:postgresql:42.1.4' compile 'org.postgresql:postgresql:42.1.4'
// Twilio SDK for SMS
compile 'com.twilio.sdk:twilio:7.14.5'
// SendGrid SDK to send emails from GCE
compile 'com.sendgrid:sendgrid-java:2.2.2'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'com.github.tomakehurst:wiremock:2.8.0' testCompile 'com.github.tomakehurst:wiremock:2.8.0'
} }

View File

@@ -0,0 +1,82 @@
# Web pages for the 3PID session processes
You can customize the various pages used during a 3PID validation using [Thymeleaf templates](http://www.thymeleaf.org/).
## Configuration
```
view:
session:
local:
onTokenSubmit:
success: '/path/to/session/local/tokenSubmitSuccess-page.html'
failure: '/path/to/session/local/tokenSubmitFailure-page.html'
localRemote:
onTokenSubmit:
success: '/path/to/session/localRemote/tokenSubmitSuccess-page.html'
failure: '/path/to/session/local/tokenSubmitFailure-page.html'
remote:
onRequest:
success: '/path/to/session/remote/requestSuccess-page.html'
failure: '/path/to/session/remote/requestFailure-page.html'
onCheck:
success: '/path/to/session/remote/checkSuccess-page.html'
failure: '/path/to/session/remote/checkFailure-page.html'
```
3PID session are divided into three config sections:
- `local` for local-only 3PID sessions
- `localRemote` for local 3PID sessions that can also be turned into remote sessions, if the user so desires
- `remote` for remote-only 3PID sessions
Each section contains a sub-key per support event. Finally, a `success` and `failure` key is available depending on the
outcome of the request.
## Local
### onTokenSubmit
This is triggered when a user submit a validation token for a 3PID session. It is typically visited when clicking the
link in a validation email.
The template should typically inform the user that the validation was successful and to go back in their Matrix client
to finish the validation process.
#### Placeholders
No object/placeholder are currently available.
## Local & Remote
### onTokenSubmit
This is triggered when a user submit a validation token for a 3PID session. It is typically visited when clicking the
link in a validation email.
The template should typically inform the user that their 3PID address will not yet be publicly/globally usable. In case
they want to make it, they should start a Remote 3PID session with a given link or that they can go back to their Matrix
client if they do not wish to proceed any further.
#### Placeholders
##### Success
`<a th:href="${remoteSessionLink}">text</a>` can be used to display the link to start a Remote 3PID session.
##### Failure
No object/placeholder are currently available.
## Remote
### onRequest
This is triggered when a user starts a Remote 3PID session, usually from a link produced in the `local.onTokenSubmit`
view or in a remote-only 3PID notification.
The template should typically inform the user that the remote creation was successful, followed the instructions sent by
the remote Identity server and, once that is done, click a link to validate the session.
#### Placeholders
##### Success
`<a th:href="${checkLink}">text</a>` can be used to display the link to validate the Remote 3PID session.
##### Failure
No object/placeholder are currently available.
### onCheck
This is triggered when a user attempts to inform the Identity server that the Remote 3PID session has been validated
with the remote Identity server.
The template should typically inform the user that the validation was successful and to go back in their Matrix client
to finish the validation process.
#### Placeholders
No object/placeholder are currently available.

View File

@@ -6,8 +6,10 @@
- [Session scope](#session-scope) - [Session scope](#session-scope)
- [Notifications](#notifications) - [Notifications](#notifications)
- [Email](#email) - [Email](#email)
- [Phone numbers](#msisdn-phone-numbers)
- [Usage](#usage) - [Usage](#usage)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Web views](#web-views)
- [Scenarios](#scenarios) - [Scenarios](#scenarios)
- [Default](#default) - [Default](#default)
- [Local sessions only](#local-sessions-only) - [Local sessions only](#local-sessions-only)
@@ -97,11 +99,17 @@ Built-in generators and connectors for supported 3PID types:
### Email ### Email
Generators: Generators:
- Template - [Template](../threepids/notifications/template-generator.md)
Connectors: Connectors:
- SMTP - [SMTP](../threepids/medium/email/smtp-connector.md)
#### MSISDN (Phone numbers)
Generators:
- [Template](../threepids/notifications/template-generator.md)
Connectors:
- [Twilio](../threepids/medium/msisdn/twilio-connector.md) with SMS
## Usage ## Usage
### Configuration ### Configuration
@@ -114,29 +122,22 @@ Please refer to the full example config file to see which keys are mandatory and
matrix: matrix:
identity: identity:
servers: servers:
root: # Not to be included in config! Already present in default config! configExample: # Not to be included in config! Already present in default config!
- 'https://matrix.org' - 'https://example.org'
threepid: threepid:
medium: medium:
email: email:
connector: 'smtp' connector: 'example1' # Not to be included in config! Already present in default config!
generator: 'template' generator: 'example2' # Not to be included in config! Already present in default config!
connectors: connectors:
smtp: example1:
host: ''
port: 587
tls: 1
login: ''
password: ''
generators: generators:
template: # Not to be included in config! Already present in default config! example1:
invite: 'classpath:email/invite-template.eml' key: "value"
session: example2:
validation: key: "value"
local: 'classpath:email/validate-local-template.eml'
remote: 'classpath:email/validate-remote-template.eml'
session: session:
policy: policy:
@@ -157,7 +158,7 @@ session:
``` ```
`matrix.identity.servers` is the namespace to configure arbitrary list of Identity servers with a label as parent key. `matrix.identity.servers` is the namespace to configure arbitrary list of Identity servers with a label as parent key.
In the above example, the list with label `configExample` contains a single server entry pointing to `https://matrix.org`. In the above example, the list with label `configExample` contains a single server entry pointing to `https://example.org`.
**NOTE:** The server list is set to `root` by default and should typically NOT be included in your config. **NOTE:** The server list is set to `root` by default and should typically NOT be included in your config.
@@ -181,14 +182,8 @@ ID for each generator.
- `connector` is given the ID of the connector to be used at runtime. - `connector` is given the ID of the connector to be used at runtime.
- `generator` is given the ID of the generator to be used at runtime. - `generator` is given the ID of the generator to be used at runtime.
In the above example, emails notifications are generated by the `template` module and sent with the `smtp` module. In the above example, emails notifications are generated by the `example2` module and sent with the `example1` module.
By default, `template` is used as generator and `smtp` as connector.
mxisd comes with the following IDs built-in:
**Connectors**
- `smtp` for a basic SMTP connector, attempting STARTLS by default.
**Generators**
- `template`, loading content from template files, using built-in mxisd templates by default.
--- ---
@@ -207,6 +202,14 @@ Each scope is divided into three parts:
If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID
locally validated. locally validated.
### Web views
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submited.
If the session or token is invalid, an error page is displayed.
Workflow pages are also available for the remote 3PID session process.
See [the dedicated document](3pid-views.md)
on how to configure/customize/brand those pages to your liking.
### Scenarios ### Scenarios
It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile. It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile.
Instead, when queried for bindings, mxisd will query Identity backends which are responsible to store this kind of information. Instead, when queried for bindings, mxisd will query Identity backends which are responsible to store this kind of information.

View File

@@ -0,0 +1,19 @@
# Email notifications - SMTP connector
Connector ID: `smtp`
Example configuration:
```
threepid:
medium:
email:
identity:
from: 'identityServerEmail@example.org'
name: 'My Identity Server'
connectors:
smtp:
host: 'smtpHostname'
port: 587
tls: 1 # 0 = no STARTLS, 1 = try, 2 = force
login: 'smtpLogin'
password: 'smtpPassword'
```

View File

@@ -0,0 +1,15 @@
# SMS notifications - Twilio connector
Connector ID: `twilio`
Example configuration:
```
threepid:
medium:
msisdn:
connectors:
twilio:
accountSid: 'myAccountSid'
authToken: 'myAuthToken'
number: '+123456789'
```

View File

@@ -0,0 +1,73 @@
# Notifications: Generate from templates
To create notification content, you can use the `template` generator if supported for the 3PID medium which will read
content from configured files.
Placeholders can be integrated into the templates to dynamically populate such content with relevant information like
the 3PID that was requested, the domain of your Identity server, etc.
Templates can be configured for each event that would send a notification to the end user. Events share a set of common
placeholders and also have their own individual set of placeholders.
## Configuration
To configure paths to the various templates:
```
threepid:
medium:
<YOUR 3PID MEDIUM HERE>:
generators:
template:
invite: '/path/to/invite-template.eml'
session:
validation:
local: '/path/to/validate-local-template.eml'
remote: 'path/to/validate-remote-template.eml'
```
The `template` generator is usually the default, so no further configuration is needed.
## Global placeholders
| Placeholder | Purpose |
|-----------------------|------------------------------------------------------------------------------|
| `%DOMAIN%` | Identity server authoritative domain, as configured in `matrix.domain` |
| `%DOMAIN_PRETTY%` | Same as `%DOMAIN%` with the first letter upper case and all other lower case |
| `%FROM_EMAIL%` | Email address configured in `threepid.medium.<3PID medium>.identity.from` |
| `%FROM_NAME%` | Name configured in `threepid.medium.<3PID medium>.identity.name` |
| `%RECIPIENT_MEDIUM%` | The 3PID medium, like `email` or `msisdn` |
| `%RECIPIENT_ADDRESS%` | The address to which the notification is sent |
## Events
### Room invitation
This template is used when someone is invited into a room using an email address which has no known bind to a Matrix ID.
#### Placeholders
| Placeholder | Purpose |
|-----------------------|------------------------------------------------------------------------------------------|
| `%SENDER_ID%` | Matrix ID of the user who made the invite |
| `%SENDER_NAME%` | Display name of the user who made the invite, if not available/set, empty |
| `%SENDER_NAME_OR_ID%` | Display name of the user who made the invite. If not available/set, its Matrix ID |
| `%INVITE_MEDIUM%` | The 3PID medium for the invite. |
| `%INVITE_ADDRESS%` | The 3PID address for the invite. |
| `%ROOM_ID%` | The Matrix ID of the Room in which the invite took place |
| `%ROOM_NAME%` | The Name of the room in which the invite took place. If not available/set, empty |
| `%ROOM_NAME_OR_ID%` | The Name of the room in which the invite took place. If not available/set, its Matrix ID |
### Local validation of 3PID Session
This template is used when to user which added their 3PID address to their profile/settings and the session policy
allows at least local sessions.
#### Placeholders
| Placeholder | Purpose |
|----------------------|--------------------------------------------------------------------------------------|
| `%VALIDATION_LINK%` | URL, including token, to validate the 3PID session. |
| `%VALIDATION_TOKEN%` | The token needed to validate the local session, in case the user cannot use the link |
### Remote validation of 3PID Session
This template is used when to user which added their 3PID address to their profile/settings and the session policy only
allows remote sessions.
**NOTE:** 3PID session always require local validation of a token, even if a remote session is enforced.
One cannot bind a MXID to the session until both local and remote sessions have been validated.
#### Placeholders
| Placeholder | Purpose |
|----------------------|--------------------------------------------------------|
| `%VALIDATION_TOKEN%` | The token needed to validate the session |
| `%NEXT_URL%` | URL to continue with remote validation of the session. |

View File

@@ -1,193 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.firebase
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.*
import com.google.firebase.internal.NonNull
import com.google.firebase.tasks.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener
import io.kamax.matrix.ThreePidMedium
import io.kamax.matrix._MatrixID
import io.kamax.mxisd.ThreePid
import io.kamax.mxisd.UserIdType
import io.kamax.mxisd.auth.provider.AuthenticatorProvider
import io.kamax.mxisd.auth.provider.BackendAuthResult
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk
private boolean isEnabled;
private String domain;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
try {
l.await(timeout, unit);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.failure();
}
}
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
this(true);
this.domain = domain;
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
} catch (IOException e) {
throw new RuntimeException("Error when initializing Firebase", e);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
} else {
return FirebaseCredentials.applicationDefault();
}
}
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
if (StringUtils.isBlank(db)) {
throw new IllegalArgumentException("Firebase database is not configured");
}
return new FirebaseOptions.Builder()
.setCredential(getCreds(credsPath))
.setDatabaseUrl(db)
.build();
}
@Override
public boolean isEnabled() {
return isEnabled;
}
private void waitOnLatch(CountDownLatch l) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
}
}
@Override
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
log.info("Trying to authenticate {}", mxid);
BackendAuthResult result = BackendAuthResult.failure();
String localpart = m.group(1);
CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
@Override
void onSuccess(FirebaseToken token) {
try {
if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
result = BackendAuthResult.failure();
return;
}
result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, token.getName());
log.info("{} was successfully authenticated", mxid);
log.info("Fetching profile for {}", mxid);
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() {
@Override
void onSuccess(UserRecord user) {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
}
} finally {
userRecordLatch.countDown();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
try {
log.warn("Unable to fetch Firebase user profile for {}", mxid);
result = BackendAuthResult.failure();
} finally {
userRecordLatch.countDown();
}
}
});
waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile");
} finally {
l.countDown()
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
try {
if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else {
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
log.info("Exception", e);
}
result = BackendAuthResult.failure();
} finally {
l.countDown()
}
}
});
waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check");
return result;
}
}

View File

@@ -1,169 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.ldap
import io.kamax.mxisd.config.MatrixConfig
import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.provider.IThreePidProvider
import org.apache.commons.lang.StringUtils
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException
import org.apache.directory.api.ldap.model.cursor.EntryCursor
import org.apache.directory.api.ldap.model.entry.Attribute
import org.apache.directory.api.ldap.model.entry.Entry
import org.apache.directory.api.ldap.model.message.SearchScope
import org.apache.directory.ldap.client.api.LdapConnection
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
@Component
class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
public static final String UID = "uid"
public static final String MATRIX_ID = "mxid"
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class)
@Autowired
private MatrixConfig mxCfg
@Override
boolean isEnabled() {
return getCfg().isEnabled()
}
private String getUidAttribute() {
return getCfg().getAttribute().getUid().getValue();
}
@Override
boolean isLocal() {
return true
}
@Override
int getPriority() {
return 20
}
Optional<String> lookup(LdapConnection conn, String medium, String value) {
String uidAttribute = getUidAttribute()
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium)
if (!queryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
return Optional.empty()
}
String searchQuery = queryOpt.get().replaceAll("%3pid", value)
EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)
try {
while (cursor.next()) {
Entry entry = cursor.get()
log.info("Found possible match, DN: {}", entry.getDn().getName())
Attribute attribute = entry.get(uidAttribute)
if (attribute == null) {
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute())
continue
}
String data = attribute.get().toString()
if (data.length() < 1) {
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute())
continue
}
StringBuilder matrixId = new StringBuilder()
// TODO Should we turn this block into a map of functions?
String uidType = getCfg().getAttribute().getUid().getType()
if (StringUtils.equals(UID, uidType)) {
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain())
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
matrixId.append(data)
} else {
log.warn("Bind was found but type {} is not supported", uidType)
continue
}
log.info("DN {} is a valid match", entry.getDn().getName())
return Optional.of(matrixId.toString())
}
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value)
} finally {
cursor.close()
}
return Optional.empty()
}
@Override
Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}")
LdapConnection conn = getConn()
try {
bind(conn)
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
if (mxid.isPresent()) {
return Optional.of(new SingleLookupReply(request, mxid.get()));
}
} finally {
conn.close()
}
log.info("No match found")
return Optional.empty()
}
@Override
List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
log.info("Looking up {} mappings", mappings.size())
List<ThreePidMapping> mappingsFound = new ArrayList<>()
LdapConnection conn = getConn()
try {
bind(conn)
for (ThreePidMapping mapping : mappings) {
try {
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue())
if (mxid.isPresent()) {
mapping.setMxid(mxid.get())
mappingsFound.add(mapping)
}
} catch (IllegalArgumentException e) {
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium())
}
}
} finally {
conn.close()
}
return mappingsFound
}
}

View File

@@ -1,83 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
class RecursiveLookupBridgeConfig implements InitializingBean {
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class)
private boolean enabled
private boolean recursiveOnly
private String server
private Map<String, String> mappings = new HashMap<>()
boolean getEnabled() {
return enabled
}
void setEnabled(boolean enabled) {
this.enabled = enabled
}
boolean getRecursiveOnly() {
return recursiveOnly
}
void setRecursiveOnly(boolean recursiveOnly) {
this.recursiveOnly = recursiveOnly
}
String getServer() {
return server
}
void setServer(String server) {
this.server = server
}
Map<String, String> getMappings() {
return mappings
}
void setMappings(Map<String, String> mappings) {
this.mappings = mappings
}
@Override
void afterPropertiesSet() throws Exception {
log.info("--- Bridge integration lookups config ---")
log.info("Enabled: {}", getEnabled())
if (getEnabled()) {
log.info("Recursive only: {}", getRecursiveOnly())
log.info("Fallback Server: {}", getServer())
log.info("Mappings: {}", mappings.size())
}
}
}

View File

@@ -1,129 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap
import groovy.json.JsonOutput
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct
@Configuration
@ConfigurationProperties(prefix = "ldap")
class LdapConfig {
private Logger log = LoggerFactory.getLogger(LdapConfig.class)
private boolean enabled
@Autowired
private LdapConnectionConfig conn
private LdapAttributeConfig attribute
private LdapAuthConfig auth
private LdapIdentityConfig identity
boolean isEnabled() {
return enabled
}
void setEnabled(boolean enabled) {
this.enabled = enabled
}
LdapConnectionConfig getConn() {
return conn
}
void setConn(LdapConnectionConfig conn) {
this.conn = conn
}
LdapAttributeConfig getAttribute() {
return attribute
}
void setAttribute(LdapAttributeConfig attribute) {
this.attribute = attribute
}
LdapAuthConfig getAuth() {
return auth
}
void setAuth(LdapAuthConfig auth) {
this.auth = auth
}
LdapIdentityConfig getIdentity() {
return identity
}
void setIdentity(LdapIdentityConfig identity) {
this.identity = identity
}
@PostConstruct
void afterPropertiesSet() {
log.info("--- LDAP Config ---")
log.info("Enabled: {}", isEnabled())
if (!isEnabled()) {
return
}
if (StringUtils.isBlank(conn.getHost())) {
throw new IllegalStateException("LDAP Host must be configured!")
}
if (1 > conn.getPort() || 65535 < conn.getPort()) {
throw new IllegalStateException("LDAP port is not valid")
}
if (StringUtils.isBlank(attribute.getUid().getType())) {
throw new IllegalStateException("Attribute UID Type cannot be empty")
}
if (StringUtils.isBlank(attribute.getUid().getValue())) {
throw new IllegalStateException("Attribute UID value cannot be empty")
}
String uidType = attribute.getUid().getType();
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType)
}
log.info("Host: {}", conn.getHost())
log.info("Port: {}", conn.getPort())
log.info("Bind DN: {}", conn.getBindDn())
log.info("Base DN: {}", conn.getBaseDn())
log.info("Attribute: {}", JsonOutput.toJson(attribute))
log.info("Auth: {}", JsonOutput.toJson(auth))
log.info("Identity: {}", JsonOutput.toJson(identity))
}
}

View File

@@ -1,121 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
import com.google.gson.Gson
import com.google.gson.JsonObject
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson
import io.kamax.mxisd.lookup.*
import io.kamax.mxisd.lookup.strategy.LookupStrategy
import io.kamax.mxisd.signature.SignatureManager
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest
import static org.springframework.web.bind.annotation.RequestMethod.GET
import static org.springframework.web.bind.annotation.RequestMethod.POST
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class MappingController {
private Logger log = LoggerFactory.getLogger(MappingController.class)
private JsonSlurper json = new JsonSlurper()
private Gson gson = new Gson()
@Autowired
private LookupStrategy strategy
@Autowired
private SignatureManager signMgr
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
lookupReq.setRequester(req.getRemoteAddr())
String xff = req.getHeader("X-FORWARDED-FOR")
lookupReq.setRecursive(StringUtils.isNotBlank(xff))
if (lookupReq.isRecursive()) {
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")))
}
lookupReq.setUserAgent(req.getHeader("USER-AGENT"))
}
@RequestMapping(value = "/lookup", method = GET)
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
SingleLookupRequest lookupRequest = new SingleLookupRequest()
setRequesterInfo(lookupRequest, request)
lookupRequest.setType(medium)
lookupRequest.setThreePid(address)
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest)
if (!lookupOpt.isPresent()) {
log.info("No mapping was found, return empty JSON object")
return JsonOutput.toJson([])
}
SingleLookupReply lookup = lookupOpt.get()
if (lookup.isSigned()) {
log.info("Lookup is already signed, sending as-is")
return lookup.getBody();
} else {
log.info("Lookup is not signed, signing")
JsonObject obj = new Gson().toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject()
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)))
return gson.toJson(obj)
}
}
@RequestMapping(value = "/bulk_lookup", method = POST)
String bulkLookup(HttpServletRequest request) {
BulkLookupRequest lookupRequest = new BulkLookupRequest()
setRequesterInfo(lookupRequest, request)
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(request.getInputStream().getText())
List<ThreePidMapping> mappings = new ArrayList<>()
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping()
mapping.setMedium(mappingRaw.get(0))
mapping.setValue(mappingRaw.get(1))
mappings.add(mapping)
}
lookupRequest.setMappings(mappings)
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer()
answer.addAll(strategy.find(lookupRequest))
return JsonOutput.toJson(answer)
}
}

View File

@@ -1,106 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.key
import io.kamax.mxisd.config.KeyConfig
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.KeyPairGenerator
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.apache.commons.io.FileUtils
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyPair
import java.security.MessageDigest
import java.security.PrivateKey
@Component
class KeyManager implements InitializingBean {
@Autowired
private KeyConfig keyCfg
private EdDSAParameterSpec keySpecs
private EdDSAEngine signEngine
private List<KeyPair> keys
@Override
void afterPropertiesSet() throws Exception {
keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
signEngine = new EdDSAEngine(MessageDigest.getInstance(keySpecs.getHashAlgorithm()))
keys = new ArrayList<>()
Path privKey = Paths.get(keyCfg.getPath())
if (!Files.exists(privKey)) {
KeyPair pair = (new KeyPairGenerator()).generateKeyPair()
String keyEncoded = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded())
FileUtils.writeStringToFile(privKey.toFile(), keyEncoded, StandardCharsets.ISO_8859_1)
keys.add(pair)
} else {
if (Files.isDirectory(privKey)) {
throw new RuntimeException("Invalid path for private key: ${privKey.toString()}")
}
if (Files.isReadable(privKey)) {
byte[] seed = Base64.getDecoder().decode(FileUtils.readFileToString(privKey.toFile(), StandardCharsets.ISO_8859_1))
EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, keySpecs)
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs)
keys.add(new KeyPair(new EdDSAPublicKey(pubKeySpec), new EdDSAPrivateKey(privKeySpec)))
}
}
}
int getCurrentIndex() {
return 0
}
KeyPair getKeys(int index) {
return keys.get(index)
}
PrivateKey getPrivateKey(int index) {
return getKeys(index).getPrivate()
}
EdDSAPublicKey getPublicKey(int index) {
return (EdDSAPublicKey) getKeys(index).getPublic()
}
EdDSAParameterSpec getSpecs() {
return keySpecs
}
String getPublicKeyBase64(int index) {
return Base64.getEncoder().encodeToString(getPublicKey(index).getAbyte())
}
}

View File

@@ -1,135 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.lookup.provider
import groovy.json.JsonException
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.kamax.mxisd.controller.v1.ClientBulkLookupRequest
import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher
import io.kamax.mxisd.matrix.IdentityServerUtils
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.ContentType
import org.apache.http.impl.client.HttpClients
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Scope
import org.springframework.stereotype.Component
@Component
@Scope("prototype")
@Lazy
public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher {
private Logger log = LoggerFactory.getLogger(RemoteIdentityServerFetcher.class)
private JsonSlurper json = new JsonSlurper()
@Override
boolean isUsable(String remote) {
return IdentityServerUtils.isUsable(remote)
}
@Override
Optional<SingleLookupReply> find(String remote, SingleLookupRequest request) {
log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote)
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
"${remote}/_matrix/identity/api/v1/lookup?medium=${request.getType()}&address=${request.getThreePid()}"
).openConnection()
try {
String outputRaw = rootSrvConn.getInputStream().getText()
def output = json.parseText(outputRaw)
if (output['address']) {
log.info("Found 3PID mapping: {}", output)
return Optional.of(SingleLookupReply.fromRecursive(request, outputRaw))
}
log.info("Empty 3PID mapping from {}", remote)
return Optional.empty()
} catch (IOException e) {
log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage())
return Optional.empty()
} catch (JsonException e) {
log.warn("Invalid JSON answer from {}", remote)
return Optional.empty()
}
}
@Override
List<ThreePidMapping> find(String remote, List<ThreePidMapping> mappings) {
List<ThreePidMapping> mappingsFound = new ArrayList<>()
ClientBulkLookupRequest mappingRequest = new ClientBulkLookupRequest()
mappingRequest.setMappings(mappings)
String url = "${remote}/_matrix/identity/api/v1/bulk_lookup"
HttpClient client = HttpClients.createDefault()
try {
HttpPost request = new HttpPost(url)
request.setEntity(
EntityBuilder.create()
.setText(JsonOutput.toJson(mappingRequest))
.setContentType(ContentType.APPLICATION_JSON)
.build()
)
HttpResponse response = client.execute(request)
try {
if (response.getStatusLine().getStatusCode() != 200) {
log.info("Could not perform lookup at {} due to HTTP return code: {}", url, response.getStatusLine().getStatusCode())
return mappingsFound
}
HttpEntity entity = response.getEntity()
if (entity != null) {
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(entity.getContent().getText())
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping()
mapping.setMedium(mappingRaw.get(0))
mapping.setValue(mappingRaw.get(1))
mapping.setMxid(mappingRaw.get(2))
mappingsFound.add(mapping)
}
} else {
log.info("HTTP response from {} was empty", remote)
}
return mappingsFound
} finally {
response.close()
}
} finally {
client.close()
}
}
}

View File

@@ -1,210 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.lookup.strategy
import edazdarevic.commons.net.CIDRUtils
import io.kamax.mxisd.config.RecursiveLookupConfig
import io.kamax.mxisd.lookup.*
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher
import io.kamax.mxisd.lookup.provider.IThreePidProvider
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.util.function.Predicate
import java.util.stream.Collectors
@Component
class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBean {
private Logger log = LoggerFactory.getLogger(RecursivePriorityLookupStrategy.class)
@Autowired
private RecursiveLookupConfig recursiveCfg
@Autowired
private List<IThreePidProvider> providers
@Autowired
private IBridgeFetcher bridge
private List<CIDRUtils> allowedCidr = new ArrayList<>()
@Override
void afterPropertiesSet() throws Exception {
log.info("Found ${providers.size()} providers")
providers.sort(new Comparator<IThreePidProvider>() {
@Override
int compare(IThreePidProvider o1, IThreePidProvider o2) {
return Integer.compare(o2.getPriority(), o1.getPriority())
}
})
log.info("Recursive lookup enabled: {}", recursiveCfg.isEnabled())
for (String cidr : recursiveCfg.getAllowedCidr()) {
log.info("{} is allowed for recursion", cidr)
allowedCidr.add(new CIDRUtils(cidr))
}
}
boolean isAllowedForRecursive(String source) {
boolean canRecurse = false
if (recursiveCfg.isEnabled()) {
log.debug("Checking {} CIDRs for recursion", allowedCidr.size())
for (CIDRUtils cidr : allowedCidr) {
if (cidr.isInRange(source)) {
log.debug("{} is in range {}, allowing recursion", source, cidr.getNetworkAddress())
canRecurse = true
break
} else {
log.debug("{} is not in range {}", source, cidr.getNetworkAddress())
}
}
}
return canRecurse
}
List<IThreePidProvider> listUsableProviders(ALookupRequest request) {
return listUsableProviders(request, false);
}
List<IThreePidProvider> listUsableProviders(ALookupRequest request, boolean forceRecursive) {
List<IThreePidProvider> usableProviders = new ArrayList<>()
boolean canRecurse = forceRecursive || isAllowedForRecursive(request.getRequester())
log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse)
for (IThreePidProvider provider : providers) {
if (provider.isEnabled() && (provider.isLocal() || canRecurse || forceRecursive)) {
usableProviders.add(provider)
}
}
return usableProviders
}
@Override
List<IThreePidProvider> getLocalProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
boolean test(IThreePidProvider iThreePidProvider) {
return iThreePidProvider.isEnabled() && iThreePidProvider.isLocal()
}
}).collect(Collectors.toList())
}
List<IThreePidProvider> getRemoteProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
boolean test(IThreePidProvider iThreePidProvider) {
return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal()
}
}).collect(Collectors.toList())
}
private static SingleLookupRequest build(String medium, String address) {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(medium)
req.setThreePid(address)
req.setRequester("Internal")
return req;
}
@Override
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
return find(build(medium, address), recursive)
}
@Override
Optional<SingleLookupReply> findLocal(String medium, String address) {
return find(build(medium, address), getLocalProviders())
}
@Override
Optional<SingleLookupReply> findRemote(String medium, String address) {
return find(build(medium, address), getRemoteProviders())
}
Optional<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
return find(request, listUsableProviders(request, forceRecursive));
}
Optional<SingleLookupReply> find(SingleLookupRequest request, List<IThreePidProvider> providers) {
for (IThreePidProvider provider : providers) {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request)
if (lookupDataOpt.isPresent()) {
return lookupDataOpt
}
}
if (
recursiveCfg.getBridge() != null &&
recursiveCfg.getBridge().getEnabled() &&
(!recursiveCfg.getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
) {
log.info("Using bridge failover for lookup")
return bridge.find(request)
}
return Optional.empty()
}
@Override
Optional<SingleLookupReply> find(SingleLookupRequest request) {
return find(request, false)
}
@Override
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request) {
return find(request, true)
}
@Override
List<ThreePidMapping> find(BulkLookupRequest request) {
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())
List<ThreePidMapping> mapFoundAll = new ArrayList<>()
for (IThreePidProvider provider : listUsableProviders(request)) {
if (mapToDo.isEmpty()) {
log.info("No more mappings to lookup")
break
} else {
log.info("{} mappings remaining overall", mapToDo.size())
}
log.info("Using provider {} for remaining mappings", provider.getClass().getSimpleName())
List<ThreePidMapping> mapFound = provider.populate(mapToDo)
log.info("Provider {} returned {} mappings", provider.getClass().getSimpleName(), mapFound.size())
mapFoundAll.addAll(mapFound)
mapToDo.removeAll(mapFound)
}
return mapFoundAll
}
}

View File

@@ -1,78 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.signature
import com.google.gson.JsonObject
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.key.KeyManager
import net.i2p.crypto.eddsa.EdDSAEngine
import org.json.JSONObject
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.security.MessageDigest
@Component
class SignatureManager implements InitializingBean {
@Autowired
private KeyManager keyMgr
@Autowired
private ServerConfig srvCfg
private EdDSAEngine signEngine
private String sign(String message) {
byte[] signRaw = signEngine.signOneShot(message.getBytes())
return Base64.getEncoder().encodeToString(signRaw)
}
JSONObject signMessageJson(String message) {
String sign = sign(message)
JSONObject keySignature = new JSONObject()
keySignature.put("ed25519:${keyMgr.getCurrentIndex()}", sign)
JSONObject signature = new JSONObject()
signature.put("${srvCfg.getName()}", keySignature)
return signature
}
JsonObject signMessageGson(String message) {
String sign = sign(message)
JsonObject keySignature = new JsonObject()
keySignature.addProperty("ed25519:${keyMgr.getCurrentIndex()}", sign)
JsonObject signature = new JsonObject()
signature.add("${srvCfg.getName()}", keySignature);
return signature
}
@Override
void afterPropertiesSet() throws Exception {
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
signEngine.initSign(keyMgr.getPrivateKey(keyMgr.getCurrentIndex()))
}
}

View File

@@ -1,154 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification.email;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
import io.kamax.mxisd.controller.v1.IdentityAPIv1;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@Component
public class EmailNotificationGenerator implements IEmailNotificationGenerator {
private Logger log = LoggerFactory.getLogger(EmailNotificationGenerator.class);
private EmailConfig cfg;
private EmailTemplateConfig templateCfg;
private MatrixConfig mxCfg;
private ServerConfig srvCfg;
@Autowired
private ApplicationContext app;
@Autowired
public EmailNotificationGenerator(EmailTemplateConfig templateCfg, EmailConfig cfg, MatrixConfig mxCfg, ServerConfig srvCfg) {
this.cfg = cfg;
this.templateCfg = templateCfg;
this.mxCfg = mxCfg;
this.srvCfg = srvCfg;
}
@Override
public String getId() {
return "template";
}
private String getTemplateContent(String location) {
try {
InputStream is = StringUtils.startsWith(location, "classpath:") ?
app.getResource(location).getInputStream() : new FileInputStream(location);
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage());
}
}
private String populateCommon(String content, ThreePid recipient) {
String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain());
content = content.replace("%DOMAIN%", mxCfg.getDomain());
content = content.replace("%DOMAIN_PRETTY%", domainPretty);
content = content.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom());
content = content.replace("%FROM_NAME%", cfg.getIdentity().getName());
content = content.replace("%RECIPIENT_MEDIUM%", recipient.getMedium());
content = content.replace("%RECIPIENT_ADDRESS%", recipient.getAddress());
return content;
}
private String getTemplateAndPopulate(String location, ThreePid recipient) {
return populateCommon(getTemplateContent(location), recipient);
}
@Override
public String getForInvite(IThreePidInviteReply invite) {
ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress());
String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid);
String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault("room_name", "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId());
templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId());
templateBody = templateBody.replace("%SENDER_NAME%", senderName);
templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId);
templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium());
templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress());
templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId());
templateBody = templateBody.replace("%ROOM_NAME%", roomName);
templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId);
return templateBody;
}
@Override
public String getForValidation(IThreePidSession session) {
log.info("Generating notification content for 3PID Session validation");
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid());
// FIXME should have a global link builder, most likely in the SDK?
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
"/validate/" + session.getThreePid().getMedium() +
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
"&token=" + session.getToken();
templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink);
templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken());
return templateBody;
}
@Override
public String getForRemoteValidation(IThreePidSession session) {
log.info("Generating notification content for remote-only 3PID session");
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid());
// FIXME should have a global link builder, most likely in the SDK?
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
"/validate/" + session.getThreePid().getMedium() +
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
"&token=" + session.getToken();
templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink);
templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken());
return templateBody;
}
}

View File

@@ -1,87 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification.email;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.notification.INotificationHandler;
import io.kamax.mxisd.threepid.connector.email.IEmailConnector;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class EmailNotificationHandler implements INotificationHandler {
private EmailConfig cfg;
private IEmailNotificationGenerator generator;
private IEmailConnector connector;
@Autowired
public EmailNotificationHandler(EmailConfig cfg, List<IEmailNotificationGenerator> generators, List<IEmailConnector> connectors) {
this.cfg = cfg;
generator = generators.stream()
.filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId()))
.findFirst()
.orElseThrow(() -> new ConfigurationException("Email notification generator [" + cfg.getGenerator() + "] could not be found"));
connector = connectors.stream()
.filter(o -> StringUtils.equals(cfg.getConnector(), o.getId()))
.findFirst()
.orElseThrow(() -> new ConfigurationException("Email sender connector [" + cfg.getConnector() + "] could not be found"));
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
private void send(String recipient, String content) {
connector.send(
cfg.getIdentity().getFrom(),
cfg.getIdentity().getName(),
recipient,
content
);
}
@Override
public void sendForInvite(IThreePidInviteReply invite) {
send(invite.getInvite().getAddress(), generator.getForInvite(invite));
}
@Override
public void sendForValidation(IThreePidSession session) {
send(session.getThreePid().getAddress(), generator.getForValidation(session));
}
@Override
public void sendForRemoteValidation(IThreePidSession session) {
send(session.getThreePid().getAddress(), generator.getForRemoteValidation(session));
}
}

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd package io.kamax.mxisd;
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
class MatrixIdentityServerApplication { public class MatrixIdentityServerApplication {
static void main(String[] args) throws Exception { public static void main(String[] args) {
SpringApplication.run(MatrixIdentityServerApplication.class, args) SpringApplication.run(MatrixIdentityServerApplication.class, args);
} }
} }

View File

@@ -48,4 +48,22 @@ public class ThreePid {
return getMedium() + ":" + getAddress(); return getMedium() + ":" + getAddress();
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThreePid threePid = (ThreePid) o;
if (!medium.equals(threePid.medium)) return false;
return address.equals(threePid.address);
}
@Override
public int hashCode() {
int result = medium.hashCode();
result = 31 * result + address.hashCode();
return result;
}
} }

View File

@@ -28,7 +28,7 @@ public enum UserIdType {
Localpart("localpart"), Localpart("localpart"),
MatrixID("mxid"), MatrixID("mxid"),
EmailLocalpart("email_localpart"), EmailLocalpart("email_localpart"),
Email("email"); Email("threepids/email");
private String id; private String id;

View File

@@ -71,14 +71,14 @@ public class AuthManager {
continue; continue;
} }
UserAuthResult authResult = new UserAuthResult().success(mxId, result.getProfile().getDisplayName()); UserAuthResult authResult = new UserAuthResult().success(result.getProfile().getDisplayName());
for (ThreePid pid : result.getProfile().getThreePids()) { for (ThreePid pid : result.getProfile().getThreePids()) {
authResult.withThreePid(pid.getMedium(), pid.getAddress()); authResult.withThreePid(pid.getMedium(), pid.getAddress());
} }
log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName());
for (ThreePid pid : authResult.getThreePids()) { for (ThreePid pid : authResult.getThreePids()) {
log.info("Processing {} for {}", pid, id); log.info("Processing {} for {}", pid, id);
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid())); invMgr.publishMappingIfInvited(new ThreePidMapping(pid, mxId));
} }
invMgr.lookupMappingsForInvites(); invMgr.lookupMappingsForInvites();

View File

@@ -20,31 +20,30 @@
package io.kamax.mxisd.auth; package io.kamax.mxisd.auth;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.ThreePid;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.HashSet;
import java.util.Set;
public class UserAuthResult { public class UserAuthResult {
private boolean success; private boolean success;
private String mxid;
private String displayName; private String displayName;
private List<ThreePid> threePids = new ArrayList<>(); private String photo;
private Set<ThreePid> threePids = new HashSet<>();
public UserAuthResult failure() { public UserAuthResult failure() {
success = false; success = false;
mxid = null;
displayName = null; displayName = null;
photo = null;
threePids.clear();
return this; return this;
} }
public UserAuthResult success(String mxid, String displayName) { public UserAuthResult success(String displayName) {
setSuccess(true); setSuccess(true);
setMxid(mxid);
setDisplayName(displayName); setDisplayName(displayName);
return this; return this;
@@ -58,14 +57,6 @@ public class UserAuthResult {
this.success = success; this.success = success;
} }
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getDisplayName() { public String getDisplayName() {
return displayName; return displayName;
} }
@@ -74,8 +65,12 @@ public class UserAuthResult {
this.displayName = displayName; this.displayName = displayName;
} }
public UserAuthResult withThreePid(ThreePidMedium medium, String address) { public String getPhoto() {
return withThreePid(medium.getId(), address); return photo;
}
public void setPhoto(String photo) {
this.photo = photo;
} }
public UserAuthResult withThreePid(String medium, String address) { public UserAuthResult withThreePid(String medium, String address) {
@@ -84,8 +79,8 @@ public class UserAuthResult {
return this; return this;
} }
public List<ThreePid> getThreePids() { public Set<ThreePid> getThreePids() {
return Collections.unmodifiableList(threePids); return Collections.unmodifiableSet(threePids);
} }
} }

View File

@@ -24,21 +24,21 @@ import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.UserID; import io.kamax.mxisd.UserID;
import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.UserIdType;
import java.util.ArrayList; import java.util.HashSet;
import java.util.List; import java.util.Set;
public class BackendAuthResult { public class BackendAuthResult {
public static class BackendAuthProfile { public static class BackendAuthProfile {
private String displayName; private String displayName;
private List<ThreePid> threePids = new ArrayList<>(); private Set<ThreePid> threePids = new HashSet<>();
public String getDisplayName() { public String getDisplayName() {
return displayName; return displayName;
} }
public List<ThreePid> getThreePids() { public Set<ThreePid> getThreePids() {
return threePids; return threePids;
} }
} }
@@ -49,20 +49,27 @@ public class BackendAuthResult {
return r; return r;
} }
public void fail() {
success = false;
}
public static BackendAuthResult success(String id, UserIdType type, String displayName) { public static BackendAuthResult success(String id, UserIdType type, String displayName) {
return success(id, type.getId(), displayName); return success(id, type.getId(), displayName);
} }
public static BackendAuthResult success(String id, String type, String displayName) { public static BackendAuthResult success(String id, String type, String displayName) {
BackendAuthResult r = new BackendAuthResult(); BackendAuthResult r = new BackendAuthResult();
r.success = true; r.succeed(id, type, displayName);
r.id = new UserID(type, id);
r.profile = new BackendAuthProfile();
r.profile.displayName = displayName;
return r; return r;
} }
public void succeed(String id, String type, String displayName) {
this.success = true;
this.id = new UserID(type, id);
this.profile = new BackendAuthProfile();
this.profile.displayName = displayName;
}
private Boolean success; private Boolean success;
private UserID id; private UserID id;
private BackendAuthProfile profile = new BackendAuthProfile(); private BackendAuthProfile profile = new BackendAuthProfile();

View File

@@ -0,0 +1,210 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.firebase;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.auth.UserInfo;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private boolean isEnabled;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db) {
this(true);
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
} catch (IOException e) {
throw new RuntimeException("Error when initializing Firebase", e);
}
}
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.fail();
}
}
private void toEmail(BackendAuthResult result, String email) {
if (StringUtils.isBlank(email)) {
return;
}
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), email));
}
private void toMsisdn(BackendAuthResult result, String phoneNumber) {
if (StringUtils.isBlank(phoneNumber)) {
return;
}
try {
String number = phoneUtil.format(
phoneUtil.parse(
phoneNumber,
null // No default region
),
PhoneNumberUtil.PhoneNumberFormat.E164
).substring(1); // We want without the leading +
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), number));
} catch (NumberParseException e) {
log.warn("Invalid phone number: {}", phoneNumber);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
} else {
return FirebaseCredentials.applicationDefault();
}
}
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
if (StringUtils.isBlank(db)) {
throw new IllegalArgumentException("Firebase database is not configured");
}
return new FirebaseOptions.Builder()
.setCredential(getCreds(credsPath))
.setDatabaseUrl(db)
.build();
}
@Override
public boolean isEnabled() {
return isEnabled;
}
private void waitOnLatch(CountDownLatch l) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
}
}
@Override
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
log.info("Trying to authenticate {}", mxid);
final BackendAuthResult result = BackendAuthResult.failure();
String localpart = mxid.getLocalPart();
CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(token -> {
try {
if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failure to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", mxid, localpart, token.getUid());
result.fail();
return;
}
result.succeed(mxid.getId(), UserIdType.MatrixID.getId(), token.getName());
log.info("{} was successfully authenticated", mxid);
log.info("Fetching profile for {}", mxid);
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> {
try {
toEmail(result, user.getEmail());
toMsisdn(result, user.getPhoneNumber());
for (UserInfo info : user.getProviderData()) {
toEmail(result, info.getEmail());
toMsisdn(result, info.getPhoneNumber());
}
log.info("Got {} 3PIDs in profile", result.getProfile().getThreePids().size());
} finally {
userRecordLatch.countDown();
}
}).addOnFailureListener(e -> {
try {
log.warn("Unable to fetch Firebase user profile for {}", mxid);
result.fail();
} finally {
userRecordLatch.countDown();
}
});
waitOnLatch(result, userRecordLatch, "Firebase user profile");
} finally {
l.countDown();
}
}).addOnFailureListener(e -> {
try {
if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else {
log.info("Failure to authenticate {}: {}", mxid, e.getMessage(), e);
log.info("Exception", e);
}
result.fail();
} finally {
l.countDown();
}
});
waitOnLatch(result, l, "Firebase auth check");
return result;
}
}

View File

@@ -18,40 +18,40 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.backend.firebase package io.kamax.mxisd.backend.firebase;
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.auth.UserRecord import com.google.firebase.auth.UserRecord;
import com.google.firebase.internal.NonNull import com.google.firebase.tasks.OnFailureListener;
import com.google.firebase.tasks.OnFailureListener import com.google.firebase.tasks.OnSuccessListener;
import com.google.firebase.tasks.OnSuccessListener import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch import java.io.FileInputStream;
import java.util.concurrent.TimeUnit import java.io.IOException;
import java.util.function.Consumer import java.util.ArrayList;
import java.util.regex.Pattern import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class GoogleFirebaseProvider implements IThreePidProvider { public class GoogleFirebaseProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class); private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)");
private boolean isEnabled; private boolean isEnabled;
private String domain; private String domain;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth; private FirebaseAuth fbAuth;
public GoogleFirebaseProvider(boolean isEnabled) { public GoogleFirebaseProvider(boolean isEnabled) {
@@ -61,8 +61,9 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
public GoogleFirebaseProvider(String credsPath, String db, String domain) { public GoogleFirebaseProvider(String credsPath, String db, String domain) {
this(true); this(true);
this.domain = domain; this.domain = domain;
try { try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider"); FirebaseApp fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
fbAuth = FirebaseAuth.getInstance(fbApp); fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready"); log.info("Google Firebase Authentication is ready");
@@ -91,7 +92,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
} }
private String getMxid(UserRecord record) { private String getMxid(UserRecord record) {
return "@${record.getUid()}:${domain}"; return new MatrixID(record.getUid(), domain).getId();
} }
@Override @Override
@@ -118,71 +119,59 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
} }
private Optional<UserRecord> findInternal(String medium, String address) { private Optional<UserRecord> findInternal(String medium, String address) {
UserRecord r; final UserRecord[] r = new UserRecord[1];
CountDownLatch l = new CountDownLatch(1); CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() { OnSuccessListener<UserRecord> success = result -> {
@Override log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid());
void onSuccess(UserRecord result) { r[0] = result;
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid()) l.countDown();
r = result;
l.countDown()
}
}; };
OnFailureListener failure = new OnFailureListener() { OnFailureListener failure = e -> {
@Override log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage());
void onFailure(@NonNull Exception e) { r[0] = null;
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage()) l.countDown();
r = null;
l.countDown()
}
}; };
if (ThreePidMedium.Email.is(medium)) { if (ThreePidMedium.Email.is(medium)) {
log.info("Performing E-mail 3PID lookup for {}", address) log.info("Performing E-mail 3PID lookup for {}", address);
fbAuth.getUserByEmail(address) fbAuth.getUserByEmail(address)
.addOnSuccessListener(success) .addOnSuccessListener(success)
.addOnFailureListener(failure); .addOnFailureListener(failure);
waitOnLatch(l); waitOnLatch(l);
} else if (ThreePidMedium.PhoneNumber.is(medium)) { } else if (ThreePidMedium.PhoneNumber.is(medium)) {
log.info("Performing msisdn 3PID lookup for {}", address) log.info("Performing msisdn 3PID lookup for {}", address);
fbAuth.getUserByPhoneNumber(address) fbAuth.getUserByPhoneNumber(address)
.addOnSuccessListener(success) .addOnSuccessListener(success)
.addOnFailureListener(failure); .addOnFailureListener(failure);
waitOnLatch(l); waitOnLatch(l);
} else { } else {
log.info("{} is not a supported 3PID medium", medium); log.info("{} is not a supported 3PID medium", medium);
r = null; r[0] = null;
} }
return Optional.ofNullable(r); return Optional.ofNullable(r[0]);
} }
@Override @Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) { public Optional<SingleLookupReply> find(SingleLookupRequest request) {
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid()) Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid());
if (urOpt.isPresent()) { return urOpt.map(userRecord -> new SingleLookupReply(request, getMxid(userRecord)));
return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get())));
}
return Optional.empty();
} }
@Override @Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) { public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>(); List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() { mappings.parallelStream().forEach(o -> {
@Override
void accept(ThreePidMapping o) {
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue()); Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
if (urOpt.isPresent()) { if (urOpt.isPresent()) {
ThreePidMapping result = new ThreePidMapping(); ThreePidMapping result = new ThreePidMapping();
result.setMedium(o.getMedium()) result.setMedium(o.getMedium());
result.setValue(o.getValue()) result.setValue(o.getValue());
result.setMxid(getMxid(urOpt.get())) result.setMxid(getMxid(urOpt.get()));
results.add(result) results.add(result);
}
} }
}); });
return results; return results;

View File

@@ -0,0 +1,174 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.ldap;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Component
public class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
public static final String UID = "uid";
public static final String MATRIX_ID = "mxid";
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class);
@Autowired
private MatrixConfig mxCfg;
@Override
public boolean isEnabled() {
return getCfg().isEnabled();
}
private String getUidAttribute() {
return getCfg().getAttribute().getUid().getValue();
}
@Override
public boolean isLocal() {
return true;
}
@Override
public int getPriority() {
return 20;
}
private Optional<String> lookup(LdapConnection conn, String medium, String value) {
String uidAttribute = getUidAttribute();
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium);
if (!queryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium);
return Optional.empty();
}
String searchQuery = queryOpt.get().replaceAll("%3pid", value);
try (EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Attribute attribute = entry.get(uidAttribute);
if (attribute == null) {
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute());
continue;
}
String data = attribute.get().toString();
if (data.length() < 1) {
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute());
continue;
}
StringBuilder matrixId = new StringBuilder();
// TODO Should we turn this block into a map of functions?
String uidType = getCfg().getAttribute().getUid().getType();
if (StringUtils.equals(UID, uidType)) {
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain());
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
matrixId.append(data);
} else {
log.warn("Bind was found but type {} is not supported", uidType);
continue;
}
log.info("DN {} is a valid match", entry.getDn().getName());
return Optional.of(matrixId.toString());
}
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
return Optional.empty();
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}");
try (LdapConnection conn = getConn()) {
bind(conn);
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid());
if (mxid.isPresent()) {
return Optional.of(new SingleLookupReply(request, mxid.get()));
}
} catch (LdapException | IOException e) {
throw new InternalServerError(e);
}
log.info("No match found");
return Optional.empty();
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
log.info("Looking up {} mappings", mappings.size());
List<ThreePidMapping> mappingsFound = new ArrayList<>();
try (LdapConnection conn = getConn()) {
bind(conn);
for (ThreePidMapping mapping : mappings) {
try {
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue());
if (mxid.isPresent()) {
mapping.setMxid(mxid.get());
mappingsFound.add(mapping);
}
} catch (IllegalArgumentException e) {
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium());
}
}
} catch (LdapException | IOException e) {
throw new InternalServerError(e);
}
return mappingsFound;
}
}

View File

@@ -85,7 +85,7 @@ public class FirebaseConfig {
if (!enabled) { if (!enabled) {
return new GoogleFirebaseAuthenticator(false); return new GoogleFirebaseAuthenticator(false);
} else { } else {
return new GoogleFirebaseAuthenticator(credentials, database, mxCfg.getDomain()); return new GoogleFirebaseAuthenticator(credentials, database);
} }
} }

View File

@@ -18,23 +18,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.config package io.kamax.mxisd.config;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration @Configuration
@ConfigurationProperties(prefix = "forward") @ConfigurationProperties(prefix = "forward")
class ForwardConfig { public class ForwardConfig {
private List<String> servers = new ArrayList<>() private List<String> servers = new ArrayList<>();
List<String> getServers() { public List<String> getServers() {
return servers return servers;
} }
void setServers(List<String> servers) { public void setServers(List<String> servers) {
this.servers = servers this.servers = servers;
} }
} }

View File

@@ -18,32 +18,33 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.config package io.kamax.mxisd.config;
import io.kamax.mxisd.exception.ConfigurationException import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct;
@Configuration @Configuration
@ConfigurationProperties(prefix = "key") @ConfigurationProperties(prefix = "key")
class KeyConfig implements InitializingBean { public class KeyConfig {
private String path private String path;
void setPath(String path) { public void setPath(String path) {
this.path = path this.path = path;
} }
String getPath() { public String getPath() {
return path return path;
} }
@Override @PostConstruct
void afterPropertiesSet() throws Exception { public void build() {
if (StringUtils.isBlank(getPath())) { if (StringUtils.isBlank(getPath())) {
throw new ConfigurationException("key.path") throw new ConfigurationException("key.path");
} }
} }

View File

@@ -0,0 +1,86 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
public class RecursiveLookupBridgeConfig {
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class);
private boolean enabled;
private boolean recursiveOnly;
private String server;
private Map<String, String> mappings = new HashMap<>();
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean getRecursiveOnly() {
return recursiveOnly;
}
public void setRecursiveOnly(boolean recursiveOnly) {
this.recursiveOnly = recursiveOnly;
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public Map<String, String> getMappings() {
return mappings;
}
public void setMappings(Map<String, String> mappings) {
this.mappings = mappings;
}
@PostConstruct
public void build() {
log.info("--- Bridge integration lookups config ---");
log.info("Enabled: {}", getEnabled());
if (getEnabled()) {
log.info("Recursive only: {}", getRecursiveOnly());
log.info("Fallback Server: {}", getServer());
log.info("Mappings: {}", mappings.size());
}
}
}

View File

@@ -18,41 +18,43 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.config package io.kamax.mxisd.config;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration @Configuration
@ConfigurationProperties(prefix = "lookup.recursive") @ConfigurationProperties(prefix = "lookup.recursive")
class RecursiveLookupConfig { public class RecursiveLookupConfig {
private boolean enabled private boolean enabled;
private List<String> allowedCidr private List<String> allowedCidr;
private RecursiveLookupBridgeConfig bridge private RecursiveLookupBridgeConfig bridge;
boolean isEnabled() { public boolean isEnabled() {
return enabled return enabled;
} }
void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled this.enabled = enabled;
} }
List<String> getAllowedCidr() { public List<String> getAllowedCidr() {
return allowedCidr return allowedCidr;
} }
void setAllowedCidr(List<String> allowedCidr) { public void setAllowedCidr(List<String> allowedCidr) {
this.allowedCidr = allowedCidr this.allowedCidr = allowedCidr;
} }
RecursiveLookupBridgeConfig getBridge() { public RecursiveLookupBridgeConfig getBridge() {
return bridge return bridge;
} }
void setBridge(RecursiveLookupBridgeConfig bridge) { public void setBridge(RecursiveLookupBridgeConfig bridge) {
this.bridge = bridge this.bridge = bridge;
} }
} }

View File

@@ -18,56 +18,59 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.config package io.kamax.mxisd.config;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct;
import java.net.MalformedURLException;
import java.net.URL;
@Configuration @Configuration
@ConfigurationProperties(prefix = "server") @ConfigurationProperties(prefix = "server")
class ServerConfig implements InitializingBean { public class ServerConfig {
private Logger log = LoggerFactory.getLogger(ServerConfig.class); private Logger log = LoggerFactory.getLogger(ServerConfig.class);
@Autowired @Autowired
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
private String name private String name;
private int port private int port;
private String publicUrl private String publicUrl;
String getName() { public String getName() {
return name return name;
} }
void setName(String name) { public void setName(String name) {
this.name = name this.name = name;
} }
int getPort() { public int getPort() {
return port return port;
} }
void setPort(int port) { public void setPort(int port) {
this.port = port this.port = port;
} }
String getPublicUrl() { public String getPublicUrl() {
return publicUrl return publicUrl;
} }
void setPublicUrl(String publicUrl) { public void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl this.publicUrl = publicUrl;
} }
@Override @PostConstruct
void afterPropertiesSet() throws Exception { public void build() {
log.info("--- Server config ---") log.info("--- Server config ---");
if (StringUtils.isBlank(getName())) { if (StringUtils.isBlank(getName())) {
setName(mxCfg.getDomain()); setName(mxCfg.getDomain());
@@ -75,21 +78,21 @@ class ServerConfig implements InitializingBean {
} }
if (StringUtils.isBlank(getPublicUrl())) { if (StringUtils.isBlank(getPublicUrl())) {
setPublicUrl("https://${getName()}"); setPublicUrl("https://" + getName());
log.debug("Public URL is empty, generating from name"); log.debug("Public URL is empty, generating from name");
} else { } else {
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName())); setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
} }
try { try {
new URL(getPublicUrl()) new URL(getPublicUrl());
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>")) log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"));
} }
log.info("Name: {}", getName()) log.info("Name: {}", getName());
log.info("Port: {}", getPort()) log.info("Port: {}", getPort());
log.info("Public URL: {}", getPublicUrl()) log.info("Public URL: {}", getPublicUrl());
} }
} }

View File

@@ -0,0 +1,131 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import com.google.gson.Gson;
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties(prefix = "ldap")
public class LdapConfig {
private static Gson gson = new Gson();
private Logger log = LoggerFactory.getLogger(LdapConfig.class);
private boolean enabled;
@Autowired
private LdapConnectionConfig conn;
private LdapAttributeConfig attribute;
private LdapAuthConfig auth;
private LdapIdentityConfig identity;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public LdapConnectionConfig getConn() {
return conn;
}
public void setConn(LdapConnectionConfig conn) {
this.conn = conn;
}
public LdapAttributeConfig getAttribute() {
return attribute;
}
public void setAttribute(LdapAttributeConfig attribute) {
this.attribute = attribute;
}
public LdapAuthConfig getAuth() {
return auth;
}
public void setAuth(LdapAuthConfig auth) {
this.auth = auth;
}
public LdapIdentityConfig getIdentity() {
return identity;
}
public void setIdentity(LdapIdentityConfig identity) {
this.identity = identity;
}
@PostConstruct
public void build() {
log.info("--- LDAP Config ---");
log.info("Enabled: {}", isEnabled());
if (!isEnabled()) {
return;
}
if (StringUtils.isBlank(conn.getHost())) {
throw new IllegalStateException("LDAP Host must be configured!");
}
if (1 > conn.getPort() || 65535 < conn.getPort()) {
throw new IllegalStateException("LDAP port is not valid");
}
if (StringUtils.isBlank(attribute.getUid().getType())) {
throw new IllegalStateException("Attribute UID Type cannot be empty");
}
if (StringUtils.isBlank(attribute.getUid().getValue())) {
throw new IllegalStateException("Attribute UID value cannot be empty");
}
String uidType = attribute.getUid().getType();
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType);
}
log.info("Host: {}", conn.getHost());
log.info("Port: {}", conn.getPort());
log.info("Bind DN: {}", conn.getBindDn());
log.info("Base DN: {}", conn.getBaseDn());
log.info("Attribute: {}", gson.toJson(attribute));
log.info("Auth: {}", gson.toJson(auth));
log.info("Identity: {}", gson.toJson(identity));
}
}

View File

@@ -0,0 +1,202 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.connector;
import io.kamax.mxisd.util.GsonUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("notification.handlers.sendgrid")
public class EmailSendGridConfig {
public static class EmailTemplate {
public static class EmailBody {
private String text;
private String html;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
}
private String subject;
private EmailBody body = new EmailBody();
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public EmailBody getBody() {
return body;
}
public void setBody(EmailBody body) {
this.body = body;
}
}
public static class Api {
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static class Identity {
private String from;
private String name;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Templates {
public static class TemplateSession {
private EmailTemplate local = new EmailTemplate();
private EmailTemplate remote = new EmailTemplate();
public EmailTemplate getLocal() {
return local;
}
public void setLocal(EmailTemplate local) {
this.local = local;
}
public EmailTemplate getRemote() {
return remote;
}
public void setRemote(EmailTemplate remote) {
this.remote = remote;
}
}
private EmailTemplate invite = new EmailTemplate();
private TemplateSession session = new TemplateSession();
public EmailTemplate getInvite() {
return invite;
}
public void setInvite(EmailTemplate invite) {
this.invite = invite;
}
public TemplateSession getSession() {
return session;
}
public void setSession(TemplateSession session) {
this.session = session;
}
}
private Logger log = LoggerFactory.getLogger(EmailSendGridConfig.class);
private Api api = new Api();
private Identity identity = new Identity();
private Templates templates = new Templates();
public Api getApi() {
return api;
}
public void setApi(Api api) {
this.api = api;
}
public Identity getIdentity() {
return identity;
}
public void setIdentity(Identity identity) {
this.identity = identity;
}
public Templates getTemplates() {
return templates;
}
public void setTemplates(Templates templates) {
this.templates = templates;
}
@PostConstruct
public void build() {
log.info("--- Email SendGrid connector config ---");
log.info("API key configured?: {}", StringUtils.isNotBlank(api.getKey()));
log.info("Identity: {}", GsonUtil.build().toJson(identity));
log.info("Templates: {}", GsonUtil.build().toJson(templates));
}
}

View File

@@ -0,0 +1,73 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties(prefix = PhoneTwilioConfig.NAMESPACE)
public class PhoneTwilioConfig {
static final String NAMESPACE = "threepid.medium.msisdn.connectors.twilio";
private Logger log = LoggerFactory.getLogger(PhoneTwilioConfig.class);
private String accountSid;
private String authToken;
private String number;
public String getAccountSid() {
return accountSid;
}
public void setAccountSid(String accountSid) {
this.accountSid = accountSid;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@PostConstruct
public void build() {
log.info("--- Phone SMS Twilio connector config ---");
log.info("Account SID: {}", getAccountSid());
log.info("Sender number: {}", getNumber());
}
}

View File

@@ -36,6 +36,8 @@ import javax.annotation.PostConstruct;
@ConfigurationProperties("threepid.medium.email") @ConfigurationProperties("threepid.medium.email")
public class EmailConfig { public class EmailConfig {
private Logger log = LoggerFactory.getLogger(EmailConfig.class);
public static class Identity { public static class Identity {
private String from; private String from;
private String name; private String name;
@@ -61,8 +63,6 @@ public class EmailConfig {
private String generator; private String generator;
private String connector; private String connector;
private Logger log = LoggerFactory.getLogger(EmailConfig.class);
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
private Identity identity = new Identity(); private Identity identity = new Identity();

View File

@@ -0,0 +1,45 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.medium;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("threepid.medium.email.generators.template")
public class EmailTemplateConfig extends GenericTemplateConfig {
private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class);
@PostConstruct
public void build() {
log.info("--- E-mail Generator templates config ---");
log.info("Invite: {}", getName(getInvite()));
log.info("Session validation:");
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
}
}

View File

@@ -21,21 +21,12 @@
package io.kamax.mxisd.config.threepid.medium; package io.kamax.mxisd.config.threepid.medium;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct; public class GenericTemplateConfig {
@Configuration
@ConfigurationProperties("threepid.medium.email.generators.template")
public class EmailTemplateConfig {
private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class);
private static final String classpathPrefix = "classpath:"; private static final String classpathPrefix = "classpath:";
private static String getName(String path) { protected static String getName(String path) {
if (StringUtils.startsWith(path, classpathPrefix)) { if (StringUtils.startsWith(path, classpathPrefix)) {
return "Built-in (" + path.substring(classpathPrefix.length()) + ")"; return "Built-in (" + path.substring(classpathPrefix.length()) + ")";
} }
@@ -95,13 +86,4 @@ public class EmailTemplateConfig {
return session; return session;
} }
@PostConstruct
public void build() {
log.info("--- E-mail Generator templates config ---");
log.info("Invite: {}", getName(getInvite()));
log.info("Session validation:");
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
}
} }

View File

@@ -0,0 +1,73 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.medium;
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("threepid.medium.msisdn")
public class PhoneConfig {
private Logger log = LoggerFactory.getLogger(PhoneConfig.class);
private String generator;
private String connector;
public String getGenerator() {
return generator;
}
public void setGenerator(String generator) {
this.generator = generator;
}
public String getConnector() {
return connector;
}
public void setConnector(String connector) {
this.connector = connector;
}
@PostConstruct
public void build() {
log.info("--- Phone config ---");
if (StringUtils.isBlank(getGenerator())) {
throw new ConfigurationException("generator");
}
if (StringUtils.isBlank(getConnector())) {
throw new ConfigurationException("connector");
}
log.info("Generator: {}", getGenerator());
log.info("Connector: {}", getConnector());
}
}

View File

@@ -0,0 +1,45 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.medium;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("threepid.medium.msisdn.generators.template")
public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class);
@PostConstruct
public void build() {
log.info("--- SMS Generator templates config ---");
log.info("Invite: {}", getName(getInvite()));
log.info("Session validation:");
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
}
}

View File

@@ -0,0 +1,57 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.notification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties("notification")
public class NotificationConfig {
private Logger log = LoggerFactory.getLogger(NotificationConfig.class);
private Map<String, String> handler = new HashMap<>();
public Map<String, String> getHandler() {
return handler;
}
public void setHandler(Map<String, String> handler) {
this.handler = handler;
}
@PostConstruct
public void build() {
log.info("--- Notification config ---");
log.info("Handlers:");
handler.forEach((k, v) -> {
log.info("\t{}: {}", k, v);
});
}
}

View File

@@ -18,15 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.auth.v1;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.UserAuthResult; import io.kamax.mxisd.auth.UserAuthResult;
import org.apache.commons.io.IOUtils; import io.kamax.mxisd.controller.auth.v1.io.CredentialsValidationResponse;
import io.kamax.mxisd.exception.JsonMemberNotFoundException;
import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.GsonUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -38,7 +40,6 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
@RestController @RestController
@CrossOrigin @CrossOrigin
@@ -47,7 +48,8 @@ public class AuthController {
private Logger log = LoggerFactory.getLogger(AuthController.class); private Logger log = LoggerFactory.getLogger(AuthController.class);
private Gson gson = new Gson(); private Gson gson = GsonUtil.build();
private GsonParser parser = new GsonParser(gson);
@Autowired @Autowired
private AuthManager mgr; private AuthManager mgr;
@@ -55,14 +57,9 @@ public class AuthController {
@RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST) @RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST)
public String checkCredentials(HttpServletRequest req) { public String checkCredentials(HttpServletRequest req) {
try { try {
JsonElement el = new JsonParser().parse(IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8)); JsonObject authData = parser.parse(req.getInputStream(), "user");
if (!el.isJsonObject() || !el.getAsJsonObject().has("user")) {
throw new IllegalArgumentException("Missing user key");
}
JsonObject authData = el.getAsJsonObject().get("user").getAsJsonObject();
if (!authData.has("id") || !authData.has("password")) { if (!authData.has("id") || !authData.has("password")) {
throw new IllegalArgumentException("Missing id or password keys"); throw new JsonMemberNotFoundException("Missing id or password keys");
} }
String id = authData.get("id").getAsString(); String id = authData.get("id").getAsString();
@@ -70,16 +67,17 @@ public class AuthController {
String password = authData.get("password").getAsString(); String password = authData.get("password").getAsString();
UserAuthResult result = mgr.authenticate(id, password); UserAuthResult result = mgr.authenticate(id, password);
CredentialsValidationResponse response = new CredentialsValidationResponse(result.isSuccess());
JsonObject authObj = new JsonObject();
authObj.addProperty("success", result.isSuccess());
if (result.isSuccess()) { if (result.isSuccess()) {
authObj.addProperty("mxid", result.getMxid()); response.setDisplayName(result.getDisplayName());
authObj.addProperty("display_name", result.getDisplayName()); response.getProfile().setThreePids(result.getThreePids());
} }
JsonObject obj = new JsonObject(); JsonElement authObj = gson.toJsonTree(response);
obj.add("authentication", authObj); JsonObject obj = new JsonObject();
obj.add("auth", authObj);
obj.add("authentication", authObj); // TODO remove later, legacy support
return gson.toJson(obj); return gson.toJson(obj);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View File

@@ -0,0 +1,74 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.auth.v1.io;
import io.kamax.mxisd.ThreePid;
import java.util.HashSet;
import java.util.Set;
public class CredentialsValidationResponse {
public static class Profile {
private String displayName;
private Set<ThreePid> threePids = new HashSet<>();
public String getDisplayName() {
return displayName;
}
public Set<ThreePid> getThreePids() {
return threePids;
}
public void setThreePids(Set<ThreePid> threePids) {
this.threePids = new HashSet<>(threePids);
}
}
private boolean success;
private String displayName; // TODO remove later, legacy support
private Profile profile = new Profile();
public CredentialsValidationResponse(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
this.profile.displayName = displayName;
}
public Profile getProfile() {
return profile;
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.identity.v1;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;

View File

@@ -18,28 +18,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1 package io.kamax.mxisd.controller.identity.v1;
import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.ThreePidMapping;
class ClientBulkLookupRequest { import java.util.ArrayList;
import java.util.List;
private List<List<String>> threepids = new ArrayList<>() public class ClientBulkLookupRequest {
List<List<String>> getThreepids() { private List<List<String>> threepids = new ArrayList<>();
return threepids
public List<List<String>> getThreepids() {
return threepids;
} }
void setThreepids(List<List<String>> threepids) { public void setThreepids(List<List<String>> threepids) {
this.threepids = threepids this.threepids = threepids;
} }
void setMappings(List<ThreePidMapping> mappings) { public void setMappings(List<ThreePidMapping> mappings) {
for (ThreePidMapping mapping : mappings) { for (ThreePidMapping mapping : mappings) {
List<String> threepid = new ArrayList<>() List<String> threepid = new ArrayList<>();
threepid.add(mapping.getMedium()) threepid.add(mapping.getMedium());
threepid.add(mapping.getValue()) threepid.add(mapping.getValue());
threepids.add(threepid) threepids.add(threepid);
} }
} }

View File

@@ -18,14 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.*;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.MatrixException;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -51,6 +48,7 @@ public class DefaultExceptionHandler {
JsonObject obj = new JsonObject(); JsonObject obj = new JsonObject();
obj.addProperty("errcode", erroCode); obj.addProperty("errcode", erroCode);
obj.addProperty("error", error); obj.addProperty("error", error);
obj.addProperty("success", false);
return gson.toJson(obj); return gson.toJson(obj);
} }
@@ -77,6 +75,18 @@ public class DefaultExceptionHandler {
return handle("M_INVALID_BODY", e.getMessage()); return handle("M_INVALID_BODY", e.getMessage());
} }
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidResponseJsonException.class)
public String handle(InvalidResponseJsonException e) {
return handle("M_INVALID_JSON", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(JsonMemberNotFoundException.class)
public String handle(JsonMemberNotFoundException e) {
return handle("M_JSON_MISSING_KEYS", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MappingAlreadyExistsException.class) @ExceptionHandler(MappingAlreadyExistsException.class)
public String handle(MappingAlreadyExistsException e) { public String handle(MappingAlreadyExistsException e) {

View File

@@ -0,0 +1,32 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.identity.v1;
public class IdentityAPIv1 {
public static final String BASE = "/_matrix/identity/api/v1";
public static String getValidate(String medium, String sid, String secret, String token) {
// FIXME use some kind of URLBuilder
return BASE + "/validate/" + medium + "/submitToken?sid=" + sid + "&client_secret=" + secret + "&token=" + token;
}
}

View File

@@ -18,47 +18,49 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1 package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson import com.google.gson.Gson;
import io.kamax.matrix.MatrixID import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO import io.kamax.mxisd.controller.identity.v1.io.ThreePidInviteReplyIO;
import io.kamax.mxisd.invitation.IThreePidInvite import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.invitation.ThreePidInvite import io.kamax.mxisd.invitation.ThreePidInvite;
import io.kamax.mxisd.key.KeyManager import io.kamax.mxisd.key.KeyManager;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.web.bind.annotation.RequestMethod.POST import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class InvitationController { class InvitationController {
private Logger log = LoggerFactory.getLogger(InvitationController.class) private Logger log = LoggerFactory.getLogger(InvitationController.class);
@Autowired @Autowired
private InvitationManager mgr private InvitationManager mgr;
@Autowired @Autowired
private KeyManager keyMgr private KeyManager keyMgr;
@Autowired @Autowired
private ServerConfig srvCfg private ServerConfig srvCfg;
private Gson gson = new Gson() private Gson gson = new Gson();
@RequestMapping(value = "/store-invite", method = POST) @RequestMapping(value = "/store-invite", method = POST)
String store( String store(
@@ -67,14 +69,14 @@ class InvitationController {
@RequestParam String medium, @RequestParam String medium,
@RequestParam String address, @RequestParam String address,
@RequestParam("room_id") String roomId) { @RequestParam("room_id") String roomId) {
Map<String, String> parameters = new HashMap<>() Map<String, String> parameters = new HashMap<>();
for (String key : request.getParameterMap().keySet()) { for (String key : request.getParameterMap().keySet()) {
parameters.put(key, request.getParameter(key)); parameters.put(key, request.getParameter(key));
} }
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters) IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters);
IThreePidInviteReply reply = mgr.storeInvite(invite) IThreePidInviteReply reply = mgr.storeInvite(invite);
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl())) return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()));
} }
} }

View File

@@ -18,64 +18,64 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1 package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson import com.google.gson.Gson;
import groovy.json.JsonOutput import com.google.gson.JsonObject;
import io.kamax.mxisd.controller.v1.io.KeyValidityJson import io.kamax.mxisd.controller.identity.v1.io.KeyValidityJson;
import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.key.KeyManager import io.kamax.mxisd.key.KeyManager;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest;
import static org.springframework.web.bind.annotation.RequestMethod.GET import static org.springframework.web.bind.annotation.RequestMethod.GET;
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class KeyController { public class KeyController {
private Logger log = LoggerFactory.getLogger(KeyController.class) private Logger log = LoggerFactory.getLogger(KeyController.class);
@Autowired @Autowired
private KeyManager keyMgr private KeyManager keyMgr;
private Gson gson = new Gson(); private Gson gson = new Gson();
private String validKey = gson.toJson(new KeyValidityJson(true)); private String validKey = gson.toJson(new KeyValidityJson(true));
private String invalidKey = gson.toJson(new KeyValidityJson(false)); private String invalidKey = gson.toJson(new KeyValidityJson(false));
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET) @RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET)
String getKey(@PathVariable String keyType, @PathVariable int keyId) { public String getKey(@PathVariable String keyType, @PathVariable int keyId) {
if (!"ed25519".contentEquals(keyType)) { if (!"ed25519".contentEquals(keyType)) {
throw new BadRequestException("Invalid algorithm: " + keyType) throw new BadRequestException("Invalid algorithm: " + keyType);
} }
log.info("Key {}:{} was requested", keyType, keyId) log.info("Key {}:{} was requested", keyType, keyId);
return JsonOutput.toJson([ JsonObject obj = new JsonObject();
public_key: keyMgr.getPublicKeyBase64(keyId) obj.addProperty("public_key", keyMgr.getPublicKeyBase64(keyId));
]) return gson.toJson(obj);
} }
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET) @RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
String checkEphemeralKeyValidity(HttpServletRequest request) { public String checkEphemeralKeyValidity(HttpServletRequest request) {
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid") log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid");
return invalidKey return invalidKey;
} }
@RequestMapping(value = "/pubkey/isvalid", method = GET) @RequestMapping(value = "/pubkey/isvalid", method = GET)
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) { public String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
log.info("Validating public key {}", pubKey) log.info("Validating public key {}", pubKey);
// TODO do in manager // TODO do in manager
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())) boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()));
return valid ? validKey : invalidKey return valid ? validKey : invalidKey;
} }
} }

View File

@@ -0,0 +1,130 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.controller.identity.v1.io.SingeLookupReplyJson;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.lookup.*;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.signature.SignatureManager;
import io.kamax.mxisd.util.GsonParser;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class MappingController {
private Logger log = LoggerFactory.getLogger(MappingController.class);
private Gson gson = new Gson();
private GsonParser parser = new GsonParser(gson);
@Autowired
private LookupStrategy strategy;
@Autowired
private SignatureManager signMgr;
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
lookupReq.setRequester(req.getRemoteAddr());
String xff = req.getHeader("X-FORWARDED-FOR");
lookupReq.setRecursive(StringUtils.isNotBlank(xff));
if (lookupReq.isRecursive()) {
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")));
}
lookupReq.setUserAgent(req.getHeader("USER-AGENT"));
}
@RequestMapping(value = "/lookup", method = GET)
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
SingleLookupRequest lookupRequest = new SingleLookupRequest();
setRequesterInfo(lookupRequest, request);
lookupRequest.setType(medium);
lookupRequest.setThreePid(address);
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest);
if (!lookupOpt.isPresent()) {
log.info("No mapping was found, return empty JSON object");
return "{}";
}
SingleLookupReply lookup = lookupOpt.get();
if (lookup.isSigned()) {
log.info("Lookup is already signed, sending as-is");
return lookup.getBody();
} else {
log.info("Lookup is not signed, signing");
JsonObject obj = gson.toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject();
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)));
return gson.toJson(obj);
}
}
@RequestMapping(value = "/bulk_lookup", method = POST)
String bulkLookup(HttpServletRequest request) {
BulkLookupRequest lookupRequest = new BulkLookupRequest();
setRequesterInfo(lookupRequest, request);
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
try {
ClientBulkLookupRequest input = parser.parse(request, ClientBulkLookupRequest.class);
List<ThreePidMapping> mappings = new ArrayList<>();
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(mappingRaw.get(0));
mapping.setValue(mappingRaw.get(1));
mappings.add(mapping);
}
lookupRequest.setMappings(mappings);
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer();
answer.addAll(strategy.find(lookupRequest));
return gson.toJson(answer);
} catch (IOException e) {
throw new InternalServerError(e);
}
}
}

View File

@@ -18,41 +18,45 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1 package io.kamax.mxisd.controller.identity.v1;
import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1 import io.kamax.mxisd.controller.identity.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.session.SessionMananger import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.session.ValidationResult import io.kamax.mxisd.session.SessionMananger;
import org.slf4j.Logger import io.kamax.mxisd.session.ValidationResult;
import org.slf4j.LoggerFactory import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
@Controller @Controller
@RequestMapping(path = IdentityAPIv1.BASE) @RequestMapping(path = IdentityAPIv1.BASE)
class SessionController { class SessionController {
private Logger log = LoggerFactory.getLogger(SessionController.class) private Logger log = LoggerFactory.getLogger(SessionController.class);
@Autowired @Autowired
private ServerConfig srvCfg; private ServerConfig srvCfg;
@Autowired @Autowired
private SessionMananger mgr private SessionMananger mgr;
@Autowired @Autowired
private ViewConfig viewCfg; private ViewConfig viewCfg;
@RequestMapping(value = "/validate/{medium}/submitToken") @RequestMapping(value = "/validate/{medium}/submitToken", method = GET)
String validate( public String validate(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
@RequestParam String sid, @RequestParam String sid,
@@ -60,21 +64,27 @@ class SessionController {
@RequestParam String token, @RequestParam String token,
Model model Model model
) { ) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString());
ValidationResult r = mgr.validate(sid, secret, token) ValidationResult r = mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid) log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) { if (r.getNextUrl().isPresent()) {
String url = srvCfg.getPublicUrl() + r.getNextUrl().get() String url = srvCfg.getPublicUrl() + r.getNextUrl().get();
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url) log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
response.sendRedirect(url) try {
response.sendRedirect(url);
return "";
} catch (IOException e) {
log.warn("Unable to redirect user to {}", url);
throw new InternalServerError(e);
}
} else { } else {
if (r.isCanRemote()) { if (r.isCanRemote()) {
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret()); String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
model.addAttribute("remoteSessionLink", url) model.addAttribute("remoteSessionLink", url);
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess() return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess();
} else { } else {
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess() return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess();
} }
} }
} }

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@@ -26,25 +26,30 @@ import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig; import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson; import io.kamax.mxisd.controller.identity.v1.io.SessionEmailTokenRequestJson;
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson; import io.kamax.mxisd.controller.identity.v1.io.SessionPhoneTokenRequestJson;
import io.kamax.mxisd.controller.identity.v1.io.SuccessStatusJson;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.session.SessionMananger; import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.session.ValidationResult;
import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.GsonParser;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@@ -114,6 +119,23 @@ public class SessionRestController {
return gson.toJson(obj); return gson.toJson(obj);
} }
@RequestMapping(value = "/validate/{medium}/submitToken", method = POST)
public String validate(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam String sid,
@RequestParam("client_secret") String secret,
@RequestParam String token,
Model model
) {
log.info("Requested: {}", request.getRequestURL());
ValidationResult r = mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid);
return gson.toJson(new SuccessStatusJson(true));
}
@RequestMapping(value = "/3pid/getValidated3pid") @RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response, String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) { @RequestParam String sid, @RequestParam("client_secret") String secret) {

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.identity.v1;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
public abstract class GenericTokenRequestJson { public abstract class GenericTokenRequestJson {

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
public class KeyValidityJson { public class KeyValidityJson {

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
public class RequestTokenResponse { public class RequestTokenResponse {

View File

@@ -18,14 +18,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
public class SessionEmailTokenRequestJson extends GenericTokenRequestJson { public class SessionEmailTokenRequestJson extends GenericTokenRequestJson {
private String email; private String email;
public String getMedium() { public String getMedium() {
return "email"; return "threepids/email";
} }
public String getValue() { public String getValue() {

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;

View File

@@ -0,0 +1,35 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.identity.v1.io;
public class SuccessStatusJson {
private boolean success;
public SuccessStatusJson(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.io; package io.kamax.mxisd.controller.identity.v1.io;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.controller.v1.remote; package io.kamax.mxisd.controller.identity.v1.remote;
public class RemoteIdentityAPIv1 { public class RemoteIdentityAPIv1 {

View File

@@ -1,4 +1,4 @@
package io.kamax.mxisd.controller.v1.remote; package io.kamax.mxisd.controller.identity.v1.remote;
import io.kamax.mxisd.config.ViewConfig; import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.exception.SessionNotValidatedException;
@@ -14,8 +14,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_CHECK; import static io.kamax.mxisd.controller.identity.v1.remote.RemoteIdentityAPIv1.SESSION_CHECK;
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN; import static io.kamax.mxisd.controller.identity.v1.remote.RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN;
@Controller @Controller
public class RemoteSessionController { public class RemoteSessionController {

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.exception package io.kamax.mxisd.exception;
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseStatus(value = HttpStatus.BAD_REQUEST)
class BadRequestException extends RuntimeException { public class BadRequestException extends RuntimeException {
BadRequestException(String s) { public BadRequestException(String s) {
super(s) super(s);
} }
} }

View File

@@ -43,6 +43,10 @@ public class InternalServerError extends MatrixException {
this.internalReason = internalReason; this.internalReason = internalReason;
} }
public InternalServerError(Throwable t) {
this(t.getMessage());
}
public String getReference() { public String getReference() {
return reference; return reference;
} }

Some files were not shown because too many files have changed in this diff Show More