Compare commits

...

25 Commits

Author SHA1 Message Date
Max Dor
cd890d114a Add warning about possibly unresolvable 3PID invites 2019-05-14 00:49:07 +02:00
Max Dor
321ba1e325 Code formatting (cosmetic, no-op) 2019-05-14 00:39:12 +02:00
Max Dor
c3ce0a17f6 Avoid conflict between 3PID expired user and Matrix ID users event 2019-05-13 16:08:35 +02:00
Max Dor
0fcc0d9bb2 Properly inform about bad configuration for 3PID builtin configs 2019-05-13 14:04:11 +02:00
Max Dor
ce7f900543 Make various optimisations/clarifications
- Change some log levels to be less verbose
- Add privacy link
- Remove unused code
2019-05-06 23:28:38 +02:00
Max Dor
c7c009f9af Fix indentation in builtin 3PID templates (cosmetic) 2019-05-06 19:16:20 +02:00
Max Dor
3b01663245 Switch to Gradle 5 build 2019-05-05 15:56:51 +02:00
Max Dor
9cc601d582 Fix custom config for custom notification handlers 2019-05-05 13:54:12 +02:00
Max Dor
e6272b1827 Improve detection and fast-fail on empty Sendgrid template paths 2019-05-05 13:48:14 +02:00
Max Dor
8243354f39 Remove unused but bug-triggering code block (Fix #172) 2019-05-04 11:17:36 +02:00
Max Dor
25968e0737 Log denied requests due to invalid credentials in AS 2019-05-04 11:16:19 +02:00
Max Dor
44a80461a0 Ensure lookup signatures are produced in a consistent way 2019-04-28 08:55:23 +02:00
Max Dor
85d9f9e704 Fix missing return in Homeserver endpoint discovery, skipping DNS SRV 2019-04-27 20:54:02 +02:00
Max Dor
6278301672 Document mxisd does not require any maintenance task for day-to-day operations 2019-04-27 17:34:56 +02:00
Max Dor
5ed0c66cfd Improve logging
- Give means to increase logging verbosity
- Explain how to do in the troubleshooting guide
2019-04-27 17:26:38 +02:00
Max Dor
ea58b6985a Fix LDAP default attributes dead link (Fix #136) 2019-04-27 16:39:36 +02:00
Max Dor
a44f781495 Properly encode Email notification headers (Fix #137) 2019-04-27 16:36:55 +02:00
Max Dor
0d42ee695a Support new Homeserver federation discovery with well-known (Fix #127) 2019-04-27 11:11:06 +02:00
Max Dor
f331af0941 Add various Notification template generator improvements
- Add ability to set arbitrary value for some placeholders (Fix #133)
- More Unit tests
- Improve doc
2019-04-27 01:07:44 +02:00
Max Dor
e2c8a56135 Allow for full TLS/SSL in SMTP connector (Fix #125) 2019-04-26 09:58:46 +02:00
Max Dor
a67c5d7ae1 Improve documentation about the SQL Identity store (Fix #107) 2019-04-26 09:40:38 +02:00
Max Dor
80352070f1 Add documentation for installation hardening and operations guide (Fix #140) 2019-04-26 09:14:16 +02:00
Max Dor
39447b8b8b Fix handling various GET and POST content types/logic for submitToken
- Properly support Form-encoded POST
- Fix #167
2019-04-26 08:41:06 +02:00
Max Dor
9d4680f55a Fix Twilio config docs to match parsed keys (v1.3.x regression) 2019-04-23 07:00:43 +02:00
Max Dor
d1ea0fbf0f Reflect default AppSvc feature enable status in config 2019-04-15 02:29:42 +02:00
51 changed files with 1185 additions and 474 deletions

View File

@@ -48,6 +48,8 @@ def dockerImageTag = "${dockerImageName}:${mxisdVersion()}"
group = 'io.kamax' group = 'io.kamax'
mainClassName = 'io.kamax.mxisd.MxisdStandaloneExec' mainClassName = 'io.kamax.mxisd.MxisdStandaloneExec'
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
String mxisdVersion() { String mxisdVersion() {
def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?") def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?")
@@ -87,10 +89,10 @@ repositories {
dependencies { dependencies {
// Logging // Logging
compile 'org.slf4j:slf4j-simple:1.7.25' compile 'org.slf4j:slf4j-simple:1.7.25'
// Easy file management // Easy file management
compile 'commons-io:commons-io:2.5' compile 'commons-io:commons-io:2.5'
// Config management // Config management
compile 'org.yaml:snakeyaml:1.23' compile 'org.yaml:snakeyaml:1.23'
@@ -145,7 +147,7 @@ dependencies {
// HTTP server // HTTP server
compile 'io.undertow:undertow-core:2.0.16.Final' compile 'io.undertow:undertow-core:2.0.16.Final'
// Command parser for AS interface // Command parser for AS interface
implementation 'commons-cli:commons-cli:1.4' implementation 'commons-cli:commons-cli:1.4'
@@ -158,7 +160,7 @@ dependencies {
jar { jar {
manifest { manifest {
attributes( attributes(
'Implementation-Version': mxisdVersion() 'Implementation-Version': mxisdVersion()
) )
} }
} }

View File

@@ -21,7 +21,7 @@ Under the `appsvc` namespace:
| Key | Type | Required | Default | Purpose | | Key | Type | Required | Default | Purpose |
|-----------------------|---------|----------|---------|----------------------------------------------------------------| |-----------------------|---------|----------|---------|----------------------------------------------------------------|
| `enabled` | boolean | No | `true` | Globally enable/disable the feature | | `enabled` | boolean | No | `false` | Globally enable/disable the feature |
| `user.main` | string | No | `mxisd` | Localpart for the main appservice user | | `user.main` | string | No | `mxisd` | Localpart for the main appservice user |
| `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver | | `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver |
| `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver | | `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver |
@@ -31,6 +31,7 @@ Under the `appsvc` namespace:
#### Example #### Example
```yaml ```yaml
appsvc: appsvc:
enabled: true
endpoint: endpoint:
toHS: toHS:
url: 'http://localhost:8008' url: 'http://localhost:8008'

View File

@@ -153,3 +153,5 @@ infrastructure:
- [Enable extra features](features/) - [Enable extra features](features/)
- [Use your own Identity stores](stores/README.md) - [Use your own Identity stores](stores/README.md)
- [Hardening your mxisd installation](install/security.md)
- [Learn about day-to-day operations](operations.md)

View File

@@ -20,3 +20,6 @@ docker run --rm -e MATRIX_DOMAIN=example.org -v /data/mxisd/etc:/etc/mxisd -v /d
``` ```
For more info, including the list of possible tags, see [the public repository](https://hub.docker.com/r/kamax/mxisd/) For more info, including the list of possible tags, see [the public repository](https://hub.docker.com/r/kamax/mxisd/)
## Troubleshoot
Please read the [Troubleshooting guide](../troubleshooting.md).

30
docs/install/security.md Normal file
View File

@@ -0,0 +1,30 @@
# Security hardening
## Overview
This document outlines the various operations you may want to perform to increase the security of your installation and
avoid leak of credentials/key pairs
## Configuration
Your config file should have the following ownership:
- Dedicated user for mxisd, used to run the software
- Dedicated group for mxisd, used by other applications to access and read configuration files
Your config file should have the following access:
- Read and write for the mxisd user
- Read for the mxisd group
- Nothing for others
This translates into `640` and be applied with `chmod 640 /path/to/config/file.yaml`.
## Data
The only sensible place is the key store where mxisd's signing keys are stored. You should therefore limit access to only
the mxisd user, and deny access to anything else.
Your key store should have the following access:
- Read and write for the mxisd user
- Nothing for the mxisd group
- Nothing for others
The identity store can either be a file or a directory, depending on your version. v1.4 and higher are using a directory,
everything before is using a file.
- If your version is directory-based, you will want to apply chmod `700` on it.
- If your version is file-based, you will want to apply chmod `600` on it.

21
docs/operations.md Normal file
View File

@@ -0,0 +1,21 @@
# Operations Guide
- [Overview](#overview)
- [Maintenance](#maintenance)
- [Backuo](#backup)
## Overview
This document gives various information for the day-to-day management and operations of mxisd.
## Maintenance
mxisd does not require any maintenance task to run at optimal performance.
## Backup
### Run
mxisd requires all file in its configuration and data directory to be backed up.
They are usually located at:
- `/etc/mxisd`
- `/var/lib/mxisd`
### Restore
Reinstall mxisd, restore the two folders above in the appropriate location (depending on your install method) and you
will be good to go. Simply start mxisd to restore functionality.

View File

@@ -89,7 +89,7 @@ ldap:
#### 3PIDs #### 3PIDs
You can also change the attribute lists for 3PID, like email or phone numbers. You can also change the attribute lists for 3PID, like email or phone numbers.
The following example would overwrite the [default list of attributes](../../src/main/resources/application.yaml#L67) The following example would overwrite the [default list of attributes](../../src/main/java/io/kamax/mxisd/config/ldap/LdapConfig.java#L64)
for emails and phone number: for emails and phone number:
```yaml ```yaml
ldap: ldap:

View File

@@ -102,9 +102,41 @@ sql:
``` ```
### Identity ### Identity
**NOTE**: Only single lookup is supported. Bulk lookup always returns no mapping. This is a restriction as the Matrix API
does not allow paging or otherwise limit of results of the API, potentially leading to thousands and thousands 3PIDs at once.
```yaml ```yaml
sql: sql:
identity: identity:
enabled: <boolean>
type: <string> type: <string>
query: <string> query: <string>
medium:
mediumTypeExample: <dedicated query>
``` ```
`type` is used to tell mxisd how to process the returned `uid` column containing the User ID:
- `localpart` will build a full Matrix ID using the `matrix.domain` value.
- `mxid` will use the ID as-is. If it is not a valid Matrix ID, lookup(s) will fail.
A specific query can also given per 3PID medium type.
### Profile
```yaml
sql:
profile:
enabled: <boolean>
displayName:
query: <string>
threepid:
query: <string>
role:
type: <string>
query: <string>
```
For the `role` query, `type` can be used to tell mxisd how to inject the User ID in the query:
- `localpart` will extract and set only the localpart.
- `mxid` will use the ID as-is.
On each query, the first parameter `?` is set as a string with the corresponding ID format.

View File

@@ -12,8 +12,8 @@ threepid:
connectors: connectors:
smtp: smtp:
host: 'smtpHostname' host: 'smtpHostname'
port: 587 tls: 1 # 0 = no STARTLS, 1 = try, 2 = force, 3 = TLS/SSL
tls: 1 # 0 = no STARTLS, 1 = try, 2 = force port: 587 # Set appropriate value depending on your TLS setting
login: 'smtpLogin' login: 'smtpLogin'
password: 'smtpPassword' password: 'smtpPassword'
``` ```

View File

@@ -8,7 +8,7 @@ threepid:
msisdn: msisdn:
connectors: connectors:
twilio: twilio:
accountSid: 'myAccountSid' account_sid: 'myAccountSid'
authToken: 'myAuthToken' auth_token: 'myAuthToken'
number: '+123456789' number: '+123456789'
``` ```

View File

@@ -1,19 +1,99 @@
# Notifications: Generate from templates # Notifications: Template generator
To create notification content, you can use the `template` generator if supported for the 3PID medium which will read Most of the Identity actions will trigger a notification of some kind, informing the user of some confirmation, next step
content from configured files. or just informing them about the current state of things.
Placeholders can be integrated into the templates to dynamically populate such content with relevant information like Those notifications are by default generated from templates and by replacing placeholder tokens in them with the relevant
the 3PID that was requested, the domain of your Identity server, etc. values of the notification. It is possible to customize the value of some placeholders, making easy to set values in the builtin templates, and/or
provide your own custom templates.
Templates can be configured for each event that would send a notification to the end user. Events share a set of common Templates for the following events/actions are available:
placeholders and also have their own individual set of placeholders. - [3PID invite](../../features/identity.md)
- [3PID session: validation](../session/session.md)
- [3PID session: fraudulent unbind](https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#improving-your-privacy-one-commit-at-the-time)
- [Matrix ID invite](../../features/experimental/application-service.md#email-notification-about-room-invites-by-matrix-ids)
## Placeholders
All placeholders **MUST** be surrounded with `%` in the template. Per example, the `DOMAIN` placeholder would become
`%DOMAIN%` within the template. This ensures replacement doesn't happen on non-placeholder strings.
### Global
The following placeholders are available in every template:
| 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 |
### Room invitation
Specific 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 |
| `REGISTER_URL` | The URL to provide to the user allowing them to register their account, if needed |
### Validation of 3PID Session
Specific placeholders:
| Placeholder | Purpose |
|--------------------|--------------------------------------------------------------------------------------|
| `VALIDATION_LINK` | URL, including token, to validate the 3PID session. |
| `VALIDATION_TOKEN` | The token needed to validate the session, in case the user cannot use the link. |
| `NEXT_URL` | URL to redirect to after the sessions has been validated. |
## Templates
mxisd comes with a set of builtin templates to easily get started. Those templates can be found
[in the repository](https://github.com/kamax-matrix/mxisd/tree/master/src/main/resources/threepids). If you want to use
customized templates, we recommend using the builtin templates as a starting point.
> **NOTE**: The link above point to the latest version of the built-in templates. Those might be different from your
version. Be sure to view the repo at the current tag.
## Configuration ## Configuration
All configuration is specific to [3PID mediums](https://matrix.org/docs/spec/appendices.html#pid-types) and happen
under the namespace `threepid.medium.<medium>.generators.template`.
Under such namespace, the following keys are available:
- `invite`: Path to the 3PID invite notification template
- `session.validation`: Path to the 3PID session validation notification template
- `session.unbind.fraudulent`: Path to the 3PID session fraudulent unbind notification template
- `generic.matrixId`: Path to the Matrix ID invite notification template
- `placeholder`: Map of key/values to set static values for some placeholders.
The `placeholder` map supports the following keys, mapped to their respective template placeholders:
- `REGISTER_URL`
### Example
#### Simple
```yaml
threepid:
medium:
email:
generators:
template:
placeholder:
REGISTER_URL: 'https://matrix-client.example.org'
```
In this configuration, the builtin templates are used and a static value for the `REGISTER_URL` is set, allowing to point
a newly invited user to a webapp allowing the creation of its account on the server.
#### Advanced
To configure paths to the various templates: To configure paths to the various templates:
```yaml ```yaml
threepid: threepid:
medium: medium:
<YOUR 3PID MEDIUM HERE>: email:
generators: generators:
template: template:
invite: '/path/to/invite-template.eml' invite: '/path/to/invite-template.eml'
@@ -23,41 +103,7 @@ threepid:
fraudulent: '/path/to/unbind-fraudulent-template.eml' fraudulent: '/path/to/unbind-fraudulent-template.eml'
generic: generic:
matrixId: '/path/to/mxid-invite-template.eml' matrixId: '/path/to/mxid-invite-template.eml'
placeholder:
REGISTER_URL: 'https://matrix-client.example.org'
``` ```
The `template` generator is usually the default, so no further configuration is needed. In this configuration, a custom template is used for each event and a static value for the `REGISTER_URL` is set.
## 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 |
### 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 session, in case the user cannot use the link. |
| `%NEXT_URL%` | URL to redirect to after the sessions has been validated. |

View File

@@ -19,6 +19,11 @@ If you use the [Docker image](install/docker.md), this goes to the container log
For any other platform, please refer to your package maintainer. For any other platform, please refer to your package maintainer.
### Increase verbosity
To increase log verbosity and better track issues, the following means are available:
- Add the `-v` command line parameter
- Use the environment variable and value `MXISD_LOG_LEVEL=debug`
### Reading them ### Reading them
Before reporting an issue, it is important to produce clean and complete logs so they can be understood. Before reporting an issue, it is important to produce clean and complete logs so they can be understood.

Binary file not shown.

View File

@@ -1,6 +1,5 @@
#Fri Aug 11 17:19:02 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.2-bin.zip

18
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh #!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################################## ##############################################################################
## ##
## Gradle start up script for UN*X ## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"

184
gradlew.bat vendored
View File

@@ -1,84 +1,100 @@
@if "%DEBUG%" == "" @echo off @rem
@rem ########################################################################## @rem Copyright 2015 the original author or authors.
@rem @rem
@rem Gradle startup script for Windows @rem Licensed under the Apache License, Version 2.0 (the "License");
@rem @rem you may not use this file except in compliance with the License.
@rem ########################################################################## @rem You may obtain a copy of the License at
@rem
@rem Set local scope for the variables with windows NT shell @rem http://www.apache.org/licenses/LICENSE-2.0
if "%OS%"=="Windows_NT" setlocal @rem
@rem Unless required by applicable law or agreed to in writing, software
set DIRNAME=%~dp0 @rem distributed under the License is distributed on an "AS IS" BASIS,
if "%DIRNAME%" == "" set DIRNAME=. @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
set APP_BASE_NAME=%~n0 @rem See the License for the specific language governing permissions and
set APP_HOME=%DIRNAME% @rem limitations under the License.
@rem
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= @if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem Find java.exe @rem
if defined JAVA_HOME goto findJavaFromJavaHome @rem Gradle startup script for Windows
@rem
set JAVA_EXE=java.exe @rem ##########################################################################
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. set DIRNAME=%~dp0
echo. if "%DIRNAME%" == "" set DIRNAME=.
echo Please set the JAVA_HOME variable in your environment to match the set APP_BASE_NAME=%~n0
echo location of your Java installation. set APP_HOME=%DIRNAME%
goto fail @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% @rem Find java.exe
set JAVA_EXE=%JAVA_HOME%/bin/java.exe if defined JAVA_HOME goto findJavaFromJavaHome
if exist "%JAVA_EXE%" goto init set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
echo. if "%ERRORLEVEL%" == "0" goto init
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo location of your Java installation. echo.
echo Please set the JAVA_HOME variable in your environment to match the
goto fail echo location of your Java installation.
:init goto fail
@rem Get command-line arguments, handling Windows variants
:findJavaFromJavaHome
if not "%OS%" == "Windows_NT" goto win9xME_args set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
:win9xME_args
@rem Slurp the command line arguments. if exist "%JAVA_EXE%" goto init
set CMD_LINE_ARGS=
set _SKIP=2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
:win9xME_args_slurp echo.
if "x%~1" == "x" goto execute echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
set CMD_LINE_ARGS=%*
goto fail
:execute
@rem Setup the command line :init
@rem Get command-line arguments, handling Windows variants
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
if not "%OS%" == "Windows_NT" goto win9xME_args
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :win9xME_args
@rem Slurp the command line arguments.
:end set CMD_LINE_ARGS=
@rem End local scope for the variables with windows NT shell set _SKIP=2
if "%ERRORLEVEL%"=="0" goto mainEnd
:win9xME_args_slurp
:fail if "x%~1" == "x" goto execute
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! set CMD_LINE_ARGS=%*
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1 :execute
@rem Setup the command line
:mainEnd
if "%OS%"=="Windows_NT" endlocal set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:omega @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -91,19 +91,19 @@ threepid:
# SMTP host # SMTP host
host: "smtp.example.org" host: "smtp.example.org"
# SMTP port # TLS mode for the connection
port: 587
# STARTLS mode for the connection.
# SSL/TLS is currently not supported. See https://github.com/kamax-matrix/mxisd/issues/125
#
# Possible values: # Possible values:
# 0 Disable any kind of TLS entirely # 0 Disable any kind of TLS entirely
# 1 Enable STARTLS if supported by server (default) # 1 Enable STARTLS if supported by server (default)
# 2 Force STARTLS and fail if not available # 2 Force STARTLS and fail if not available
# 3 Use full TLS/SSL instead of STARTLS
# #
tls: 1 tls: 1
# SMTP port
# Be sure to adapt depending on your TLS choice, if changed from default
port: 587
# Login for SMTP # Login for SMTP
login: "matrix-identity@example.org" login: "matrix-identity@example.org"

View File

@@ -73,7 +73,6 @@ public class HttpMxisd {
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager())); HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()));
HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView()));
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing()
@@ -103,8 +102,8 @@ public class HttpMxisd {
.post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) .post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity())))
.post(StoreInviteHandler.Path, storeInvHandler) .post(StoreInviteHandler.Path, storeInvHandler)
.post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession())))
.get(SessionValidateHandler.Path, sessValidateHandler) .get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig())))
.post(SessionValidateHandler.Path, sessValidateHandler) .post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(m.getSession())))
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite()))) .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite())))
.post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) .post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession())))

View File

@@ -41,6 +41,7 @@ import io.kamax.mxisd.lookup.provider.BridgeFetcher;
import io.kamax.mxisd.lookup.provider.RemoteIdentityServerFetcher; import io.kamax.mxisd.lookup.provider.RemoteIdentityServerFetcher;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.lookup.strategy.RecursivePriorityLookupStrategy; import io.kamax.mxisd.lookup.strategy.RecursivePriorityLookupStrategy;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.matrix.IdentityServerUtils; import io.kamax.mxisd.matrix.IdentityServerUtils;
import io.kamax.mxisd.notification.NotificationHandlerSupplier; import io.kamax.mxisd.notification.NotificationHandlerSupplier;
import io.kamax.mxisd.notification.NotificationHandlers; import io.kamax.mxisd.notification.NotificationHandlers;
@@ -99,6 +100,8 @@ public class Mxisd {
.setMaxConnTotal(Integer.MAX_VALUE) .setMaxConnTotal(Integer.MAX_VALUE)
.build(); .build();
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite());
HomeserverFederationResolver resolver = new HomeserverFederationResolver(fedDns, httpClient);
IdentityServerUtils.setHttpClient(httpClient); IdentityServerUtils.setHttpClient(httpClient);
srvFetcher = new RemoteIdentityServerFetcher(httpClient); srvFetcher = new RemoteIdentityServerFetcher(httpClient);
@@ -106,18 +109,17 @@ public class Mxisd {
keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); keyMgr = CryptoFactory.getKeyManager(cfg.getKey());
signMgr = CryptoFactory.getSignatureManager(keyMgr); signMgr = CryptoFactory.getSignatureManager(keyMgr);
clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite());
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite());
synapse = new Synapse(cfg.getSynapseSql()); synapse = new Synapse(cfg.getSynapseSql());
BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher);
ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher); idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy);
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr); invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr); regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);

View File

@@ -36,6 +36,11 @@ public class MxisdStandaloneExec {
private static final Logger log = LoggerFactory.getLogger("App"); private static final Logger log = LoggerFactory.getLogger("App");
public static void main(String[] args) { public static void main(String[] args) {
String logLevel = System.getenv("MXISD_LOG_LEVEL");
if (StringUtils.isNotBlank(logLevel)) {
System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", logLevel);
}
try { try {
MxisdConfig cfg = null; MxisdConfig cfg = null;
Iterator<String> argsIt = Arrays.asList(args).iterator(); Iterator<String> argsIt = Arrays.asList(args).iterator();
@@ -46,8 +51,14 @@ public class MxisdStandaloneExec {
System.out.println(" -h, --help Show this help message"); System.out.println(" -h, --help Show this help message");
System.out.println(" --version Print the version then exit"); System.out.println(" --version Print the version then exit");
System.out.println(" -c, --config Set the configuration file location"); System.out.println(" -c, --config Set the configuration file location");
System.out.println(" -v Increase log level (log more info)");
System.out.println(" -vv Further increase log level");
System.out.println(" "); System.out.println(" ");
System.exit(0); System.exit(0);
} else if (StringUtils.equals(arg, "-v")) {
System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", "debug");
} else if (StringUtils.equals(arg, "-vv")) {
System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", "trace");
} else if (StringUtils.equalsAny(arg, "-c", "--config")) { } else if (StringUtils.equalsAny(arg, "-c", "--config")) {
String cfgFile = argsIt.next(); String cfgFile = argsIt.next();
cfg = YamlConfigLoader.loadFromFile(cfgFile); cfg = YamlConfigLoader.loadFromFile(cfgFile);
@@ -61,13 +72,13 @@ public class MxisdStandaloneExec {
} }
} }
log.info("mxisd starting");
log.info("Version: {}", Mxisd.Version);
if (Objects.isNull(cfg)) { if (Objects.isNull(cfg)) {
cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new);
} }
log.info("mxisd starting");
log.info("Version: {}", Mxisd.Version);
HttpMxisd mxisd = new HttpMxisd(cfg); HttpMxisd mxisd = new HttpMxisd(cfg);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
mxisd.stop(); mxisd.stop();

View File

@@ -176,10 +176,12 @@ public class AppSvcManager {
ensureEnabled(); ensureEnabled();
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
log.info("Denying request without a HS token");
throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token"); throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token");
} }
if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) { if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) {
log.info("Denying request with an invalid HS token");
throw new NotAllowedException("Invalid HS token"); throw new NotAllowedException("Invalid HS token");
} }

View File

@@ -27,7 +27,6 @@ import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid; import io.kamax.matrix._ThreePid;
import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
import io.kamax.matrix.event.EventKey; import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.backend.sql.synapse.Synapse;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
@@ -81,7 +80,7 @@ public class MembershipEventProcessor implements EventTypeProcessor {
_MatrixID target = MatrixID.asAcceptable(targetId); _MatrixID target = MatrixID.asAcceptable(targetId);
if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) { if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) {
log.debug("Ignoring invite for {}: not a local user"); log.debug("Ignoring invite for {}: not a local user", targetId);
return; return;
} }
@@ -89,10 +88,9 @@ public class MembershipEventProcessor implements EventTypeProcessor {
boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain()); boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain());
boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired()); boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired());
boolean isUs = isForMainUser || isForExpInvUser;
if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) { if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) {
if (!isForMainUser) { if (isForExpInvUser) {
log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId); log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId);
client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> { client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> {
@@ -108,10 +106,7 @@ public class MembershipEventProcessor implements EventTypeProcessor {
processForUserIdInvite(roomId, sender, target); processForUserIdInvite(roomId, sender, target);
} }
} else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) { } else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) {
_MatrixRoom room = client.getRoom(roomId); // TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it
if (!isUs && room.getJoinedUsers().size() == 1) {
// TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it
}
} else { } else {
log.debug("This is not an supported type of membership event, skipping"); log.debug("This is not an supported type of membership event, skipping");
} }

View File

@@ -69,16 +69,16 @@ public class RegisterConfig {
} }
private List<String> buildPatterns(List<String> domains) { private List<String> buildPatterns(List<String> domains) {
log.info("Building email policy"); log.debug("Building email policy");
return domains.stream().map(d -> { return domains.stream().map(d -> {
if (StringUtils.startsWith(d, "*")) { if (StringUtils.startsWith(d, "*")) {
log.info("Found domain and subdomain policy"); log.debug("Found domain and subdomain policy");
d = "(.*)" + d.substring(1); d = "(.*)" + d.substring(1);
} else if (StringUtils.startsWith(d, ".")) { } else if (StringUtils.startsWith(d, ".")) {
log.info("Found subdomain-only policy"); log.debug("Found subdomain-only policy");
d = "(.*)" + d; d = "(.*)" + d;
} else { } else {
log.info("Found domain-only policy"); log.debug("Found domain-only policy");
} }
return "([^@]+)@" + d.replace(".", "\\."); return "([^@]+)@" + d.replace(".", "\\.");
@@ -175,10 +175,10 @@ public class RegisterConfig {
} }
public void build() { public void build() {
log.info("--- Registration config ---"); log.debug("--- Registration config ---");
log.info("Before Build"); log.debug("Before Build");
log.info(GsonUtil.getPrettyForLog(this)); log.debug(GsonUtil.getPrettyForLog(this));
new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> { new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> {
if (ThreePidMedium.Email.is(medium)) { if (ThreePidMedium.Email.is(medium)) {
@@ -194,8 +194,8 @@ public class RegisterConfig {
getPolicy().getThreepid().put(medium, policy); getPolicy().getThreepid().put(medium, policy);
}); });
log.info("After Build"); log.debug("After Build");
log.info(GsonUtil.getPrettyForLog(this)); log.debug(GsonUtil.getPrettyForLog(this));
} }
} }

View File

@@ -77,6 +77,7 @@ public class GenericTemplateConfig {
private String invite; private String invite;
private Session session = new Session(); private Session session = new Session();
private Map<String, String> generic = new HashMap<>(); private Map<String, String> generic = new HashMap<>();
private Map<String, String> placeholder = new HashMap<>();
public String getInvite() { public String getInvite() {
return invite; return invite;
@@ -98,4 +99,12 @@ public class GenericTemplateConfig {
this.generic = generic; this.generic = generic;
} }
public Map<String, String> getPlaceholder() {
return placeholder;
}
public void setPlaceholder(Map<String, String> placeholder) {
this.placeholder = placeholder;
}
} }

View File

@@ -20,7 +20,6 @@
package io.kamax.mxisd.config.threepid.notification; package io.kamax.mxisd.config.threepid.notification;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.threepid.notification.email.EmailRawNotificationHandler; import io.kamax.mxisd.threepid.notification.email.EmailRawNotificationHandler;
import io.kamax.mxisd.threepid.notification.phone.PhoneNotificationHandler; import io.kamax.mxisd.threepid.notification.phone.PhoneNotificationHandler;
@@ -35,7 +34,7 @@ public class NotificationConfig {
private transient final Logger log = LoggerFactory.getLogger(NotificationConfig.class); private transient final Logger log = LoggerFactory.getLogger(NotificationConfig.class);
private Map<String, String> handler = new HashMap<>(); private Map<String, String> handler = new HashMap<>();
private Map<String, JsonObject> handlers = new HashMap<>(); private Map<String, Object> handlers = new HashMap<>();
public NotificationConfig() { public NotificationConfig() {
handler.put(ThreePidMedium.Email.getId(), EmailRawNotificationHandler.ID); handler.put(ThreePidMedium.Email.getId(), EmailRawNotificationHandler.ID);
@@ -50,11 +49,11 @@ public class NotificationConfig {
this.handler = handler; this.handler = handler;
} }
public Map<String, JsonObject> getHandlers() { public Map<String, Object> getHandlers() {
return handlers; return handlers;
} }
public void setHandlers(Map<String, JsonObject> handlers) { public void setHandlers(Map<String, Object> handlers) {
this.handlers = handlers; this.handlers = handlers;
} }

View File

@@ -20,12 +20,45 @@
package io.kamax.mxisd.crypto; package io.kamax.mxisd.crypto;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.MatrixJson;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects;
public interface SignatureManager { public interface SignatureManager {
/**
* Sign the message and add the signature to the <code>signatures</code> key.
* <p>
* If the key does not exist yet, it is created. If the key exist, the produced signature will be merged with any
* existing ones.
*
* @param domain The domain under which the signature should be added
* @param message The message to sign and add the produced signature to
* @return The provided message with the new signature
* @throws IllegalArgumentException If the <code>signatures</code> value is not a JSON object
*/
default JsonObject signMessageGson(String domain, JsonObject message) throws IllegalArgumentException {
JsonElement signEl = message.remove(EventKey.Signatures.get());
JsonObject oldSigns = new JsonObject();
if (!Objects.isNull(signEl)) {
if (!signEl.isJsonObject()) {
throw new IllegalArgumentException("Message contains a signatures key that is not a JSON object value");
}
oldSigns = signEl.getAsJsonObject();
}
JsonObject newSigns = signMessageGson(domain, MatrixJson.encodeCanonical(message));
oldSigns.entrySet().forEach(entry -> newSigns.add(entry.getKey(), entry.getValue()));
message.add(EventKey.Signatures.get(), newSigns);
return message;
}
/** /**
* Sign the message and produce a <code>signatures</code> object that can directly be added to the object being signed. * Sign the message and produce a <code>signatures</code> object that can directly be added to the object being signed.
* *

View File

@@ -31,6 +31,7 @@ import io.kamax.mxisd.proxy.Response;
import io.kamax.mxisd.util.RestClientUtils; import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.util.HttpString; import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -48,10 +49,7 @@ import java.net.InetSocketAddress;
import java.net.URI; import java.net.URI;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Deque; import java.util.*;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
public abstract class BasicHttpHandler implements HttpHandler { public abstract class BasicHttpHandler implements HttpHandler {
@@ -122,6 +120,20 @@ public abstract class BasicHttpHandler implements HttpHandler {
return GsonUtil.parseObj(getBodyUtf8(exchange)); return GsonUtil.parseObj(getBodyUtf8(exchange));
} }
protected String getOrThrow(FormData data, String key) {
FormData.FormValue value = data.getFirst(key);
if (Objects.isNull(value)) {
throw new IllegalArgumentException("Form key " + key + " is missing");
}
String object = value.getValue();
if (Objects.isNull(object)) {
throw new IllegalArgumentException("Form key " + key + " does not have a value");
}
return object;
}
protected void putHeader(HttpServerExchange ex, String name, String value) { protected void putHeader(HttpServerExchange ex, String name, String value) {
ex.getResponseHeaders().put(HttpString.tryFromString(name), value); ex.getResponseHeaders().put(HttpString.tryFromString(name), value);
} }

View File

@@ -20,94 +20,36 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.SuccessStatusJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.session.ValidationResult; import io.kamax.mxisd.session.ValidationResult;
import io.kamax.mxisd.util.FileUtil; import org.apache.commons.lang3.StringUtils;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; public abstract class SessionValidateHandler extends BasicHttpHandler {
import java.net.MalformedURLException;
import java.net.URL;
public class SessionValidateHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken"; public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken";
private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class); private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class);
private SessionManager mgr; private SessionManager mgr;
private ServerConfig srvCfg;
private ViewConfig viewCfg;
public SessionValidateHandler(SessionManager mgr, ServerConfig srvCfg, ViewConfig viewCfg) { public SessionValidateHandler(SessionManager mgr) {
this.mgr = mgr; this.mgr = mgr;
this.srvCfg = srvCfg;
this.viewCfg = viewCfg;
} }
@Override protected ValidationResult handleRequest(String sid, String secret, String token) {
public void handleRequest(HttpServerExchange exchange) { if (StringUtils.isEmpty(sid)) {
String medium = getQueryParameter(exchange, "medium"); throw new IllegalArgumentException("sid is not set or is empty");
String sid = getQueryParameter(exchange, "sid");
String secret = getQueryParameter(exchange, "client_secret");
String token = getQueryParameter(exchange, "token");
boolean isHtmlRequest = false;
for (String v : exchange.getRequestHeaders().get("Accept")) {
if (StringUtils.startsWithIgnoreCase(v, "text/html")) {
isHtmlRequest = true;
break;
}
} }
if (isHtmlRequest) { if (StringUtils.isEmpty(secret)) {
handleHtmlRequest(exchange, medium, sid, secret, token); throw new IllegalArgumentException("client secret is not set or is empty");
} else {
handleJsonRequest(exchange, sid, secret, token);
} }
}
private void handleHtmlRequest(HttpServerExchange exchange, String medium, String sid, String secret, String token) { return mgr.validate(sid, secret, token);
log.info("Validating session {} for medium {}", sid, medium);
ValidationResult r = mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) {
String url = r.getNextUrl().get();
try {
url = new URL(url).toString();
} catch (MalformedURLException e) {
log.info("Session next URL {} is not a valid one, will prepend public URL {}", url, srvCfg.getPublicUrl());
url = srvCfg.getPublicUrl() + r.getNextUrl().get();
}
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
exchange.setStatusCode(302);
exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url);
} else {
try {
String data = FileUtil.load(viewCfg.getSession().getOnTokenSubmit().getSuccess());
writeBodyAsUtf8(exchange, data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void handleJsonRequest(HttpServerExchange exchange, String sid, String secret, String token) {
log.info("Requested: {}", exchange.getRequestURL());
mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid);
respondJson(exchange, new SuccessStatusJson(true));
} }
} }

View File

@@ -0,0 +1,82 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.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.http.undertow.handler.identity.v1;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.session.ValidationResult;
import io.kamax.mxisd.util.FileUtil;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class SessionValidationGetHandler extends SessionValidateHandler {
private transient final Logger log = LoggerFactory.getLogger(SessionValidationGetHandler.class);
private ServerConfig srvCfg;
private ViewConfig viewCfg;
public SessionValidationGetHandler(SessionManager mgr, MxisdConfig cfg) {
super(mgr);
this.srvCfg = cfg.getServer();
this.viewCfg = cfg.getView();
}
@Override
public void handleRequest(HttpServerExchange exchange) {
log.info("Handling GET request to validate session");
String sid = getQueryParameter(exchange, "sid");
String secret = getQueryParameter(exchange, "client_secret");
String token = getQueryParameter(exchange, "token");
ValidationResult r = handleRequest(sid, secret, token);
log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) {
String url = r.getNextUrl().get();
try {
url = new URL(url).toString();
} catch (MalformedURLException e) {
log.info("Session next URL {} is not a valid one, will prepend public URL {}", url, srvCfg.getPublicUrl());
url = srvCfg.getPublicUrl() + r.getNextUrl().get();
}
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
exchange.setStatusCode(302);
exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url);
} else {
try {
String data = FileUtil.load(viewCfg.getSession().getOnTokenSubmit().getSuccess());
writeBodyAsUtf8(exchange, data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.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.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.http.io.identity.SuccessStatusJson;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormParserFactory;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class SessionValidationPostHandler extends SessionValidateHandler {
private transient final Logger log = LoggerFactory.getLogger(SessionValidationPostHandler.class);
private FormParserFactory factory;
public SessionValidationPostHandler(SessionManager mgr) {
super(mgr);
factory = FormParserFactory.builder().build();
}
@Override
public void handleRequest(HttpServerExchange exchange) throws IOException {
log.info("Handling POST request to validate session");
String sid;
String secret;
String token;
String contentType = getContentType(exchange).orElseThrow(() -> new IllegalArgumentException("Content type header is not set"));
if (StringUtils.equals(contentType, "application/json")) { // FIXME use MIME parsing tools
log.info("Parsing as JSON data");
JsonObject body = parseJsonObject(exchange);
sid = GsonUtil.getStringOrThrow(body, "sid");
secret = GsonUtil.getStringOrThrow(body, "client_secret");
token = GsonUtil.getStringOrThrow(body, "token");
} else if (StringUtils.equals(contentType, "application/x-www-form-urlencoded")) { // FIXME use MIME parsing tools
log.info("Parsing as Form data");
FormData data = factory.createParser(exchange).parseBlocking();
sid = getOrThrow(data, "sid");
secret = getOrThrow(data, "client_secret");
token = getOrThrow(data, "token");
} else {
log.info("Unsupported Content type: {}", contentType);
throw new IllegalArgumentException("Unsupported Content type: " + contentType);
}
handleRequest(sid, secret, token);
respondJson(exchange, new SuccessStatusJson(true));
}
}

View File

@@ -21,9 +21,7 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.crypto.SignatureManager;
@@ -73,11 +71,8 @@ public class SingleLookupHandler extends LookupHandler {
respondJson(exchange, "{}"); respondJson(exchange, "{}");
} else { } else {
SingleLookupReply lookup = lookupOpt.get(); SingleLookupReply lookup = lookupOpt.get();
// FIXME signing should be done in the business model, not in the controller
JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup)); JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup));
obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(cfg.getName(), MatrixJson.encodeCanonical(obj))); signMgr.signMessageGson(cfg.getName(), obj);
respondJson(exchange, obj); respondJson(exchange, obj);
} }
} }

View File

@@ -30,7 +30,6 @@ import io.kamax.mxisd.config.InvitationConfig;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.*; import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
@@ -38,6 +37,7 @@ import io.kamax.mxisd.exception.ObjectNotFoundException;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
@@ -57,13 +57,10 @@ import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.Instant; import java.time.Instant;
@@ -85,7 +82,7 @@ public class InvitationManager {
private LookupStrategy lookupMgr; private LookupStrategy lookupMgr;
private KeyManager keyMgr; private KeyManager keyMgr;
private SignatureManager signMgr; private SignatureManager signMgr;
private FederationDnsOverwrite dns; private HomeserverFederationResolver resolver;
private NotificationManager notifMgr; private NotificationManager notifMgr;
private ProfileManager profileMgr; private ProfileManager profileMgr;
@@ -100,7 +97,7 @@ public class InvitationManager {
LookupStrategy lookupMgr, LookupStrategy lookupMgr,
KeyManager keyMgr, KeyManager keyMgr,
SignatureManager signMgr, SignatureManager signMgr,
FederationDnsOverwrite dns, HomeserverFederationResolver resolver,
NotificationManager notifMgr, NotificationManager notifMgr,
ProfileManager profileMgr ProfileManager profileMgr
) { ) {
@@ -110,15 +107,15 @@ public class InvitationManager {
this.lookupMgr = lookupMgr; this.lookupMgr = lookupMgr;
this.keyMgr = keyMgr; this.keyMgr = keyMgr;
this.signMgr = signMgr; this.signMgr = signMgr;
this.dns = dns; this.resolver = resolver;
this.notifMgr = notifMgr; this.notifMgr = notifMgr;
this.profileMgr = profileMgr; this.profileMgr = profileMgr;
log.info("Loading saved invites"); log.debug("Loading saved invites");
Collection<ThreePidInviteIO> ioList = storage.getInvites(); Collection<ThreePidInviteIO> ioList = storage.getInvites();
ioList.forEach(io -> { ioList.forEach(io -> {
io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs); io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs);
log.info("Processing invite {}", GsonUtil.get().toJson(io)); log.debug("Processing invite {}", GsonUtil.get().toJson(io));
ThreePidInvite invite = new ThreePidInvite( ThreePidInvite invite = new ThreePidInvite(
MatrixID.asAcceptable(io.getSender()), MatrixID.asAcceptable(io.getSender()),
io.getMedium(), io.getMedium(),
@@ -130,6 +127,7 @@ public class InvitationManager {
ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList()); ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList());
invitations.put(reply.getId(), reply); invitations.put(reply.getId(), reply);
}); });
log.info("Loaded saved invites");
// FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver // FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver
try { try {
@@ -207,56 +205,6 @@ public class InvitationManager {
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
} }
private String getSrvRecordName(String domain) {
return "_matrix._tcp." + domain;
}
// TODO use caching mechanism
// TODO export in matrix-java-sdk
private String findHomeserverForDomain(String domain) {
Optional<String> entryOpt = dns.findHost(domain);
if (entryOpt.isPresent()) {
String entry = entryOpt.get();
log.info("Found DNS overwrite for {} to {}", domain, entry);
try {
return new URL(entry).toString();
} catch (MalformedURLException e) {
log.warn("Skipping homeserver Federation DNS overwrite for {} - not a valid URL: {}", domain, entry);
}
}
log.debug("Performing SRV lookup for {}", domain);
String lookupDns = getSrvRecordName(domain);
log.info("Lookup name: {}", lookupDns);
try {
List<SRVRecord> srvRecords = new ArrayList<>();
Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run();
if (rawRecords != null && rawRecords.length > 0) {
for (Record record : rawRecords) {
if (Type.SRV == record.getType()) {
srvRecords.add((SRVRecord) record);
} else {
log.info("Got non-SRV record: {}", record.toString());
}
}
srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority));
for (SRVRecord record : srvRecords) {
log.info("Found SRV record: {}", record.toString());
return "https://" + record.getTarget().toString(true) + ":" + record.getPort();
}
} else {
log.info("No SRV record for {}", lookupDns);
}
} catch (TextParseException e) {
log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage());
}
log.info("Performing basic lookup using domain name {}", domain);
return "https://" + domain + ":8448";
}
private Optional<SingleLookupReply> lookup3pid(String medium, String address) { private Optional<SingleLookupReply> lookup3pid(String medium, String address) {
if (!cfg.getResolution().isRecursive()) { if (!cfg.getResolution().isRecursive()) {
log.warn("/!\\ /!\\ --- RECURSIVE INVITE RESOLUTION HAS BEEN DISABLED --- /!\\ /!\\"); log.warn("/!\\ /!\\ --- RECURSIVE INVITE RESOLUTION HAS BEEN DISABLED --- /!\\ /!\\");
@@ -413,7 +361,7 @@ public class InvitationManager {
continue; continue;
} }
log.info("Invite {} has expired at TS {} - Expiring and resolving to {}", targetTs, targetMxid); log.info("Invite {} has expired at TS {} - Expiring and resolving to {}", reply.getId(), targetTs, targetMxid);
publishMapping(reply, targetMxid); publishMapping(reply, targetMxid);
} catch (NumberFormatException | DateTimeException e) { } catch (NumberFormatException | DateTimeException e) {
log.warn("Invite {} has an invalid creation TS, setting to default value of {}", reply.getId(), defaultCreateTs); log.warn("Invite {} has an invalid creation TS, setting to default value of {}", reply.getId(), defaultCreateTs);
@@ -475,7 +423,7 @@ public class InvitationManager {
String address = reply.getInvite().getAddress(); String address = reply.getInvite().getAddress();
String domain = reply.getInvite().getSender().getDomain(); String domain = reply.getInvite().getSender().getDomain();
log.info("Discovering HS for domain {}", domain); log.info("Discovering HS for domain {}", domain);
String hsUrlOpt = findHomeserverForDomain(domain); String hsUrlOpt = resolver.resolve(domain).toString();
// TODO this is needed as this will block if called during authentication cycle due to synapse implementation // TODO this is needed as this will block if called during authentication cycle due to synapse implementation
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
@@ -564,6 +512,9 @@ public class InvitationManager {
publishMapping(reply, lookup.getMxid().getId()); publishMapping(reply, lookup.getMxid().getId());
} else { } else {
log.info("No mapping for pending invite {}", getIdForLog(reply)); log.info("No mapping for pending invite {}", getIdForLog(reply));
if (lookupMgr.getLocalProviders().isEmpty()) {
log.warn("No Identity store has been configured, this invite may never resolve");
}
} }
} catch (Throwable t) { } catch (Throwable t) {
log.error("Unable to process invite", t); log.error("Unable to process invite", t);

View File

@@ -0,0 +1,216 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.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.matrix;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.InvalidJsonException;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class HomeserverFederationResolver {
private static final Logger log = LoggerFactory.getLogger(HomeserverFederationResolver.class);
private FederationDnsOverwrite dns;
private CloseableHttpClient client;
public HomeserverFederationResolver(FederationDnsOverwrite dns, CloseableHttpClient client) {
this.dns = dns;
this.client = client;
}
private String getDefaultScheme() {
return "https";
}
private int getDefaultPort() {
return 8448;
}
private String getDnsSrvPrefix() {
return "_matrix._tcp.";
}
private String buildSrvRecordName(String domain) {
return getDnsSrvPrefix() + domain;
}
private Optional<URL> resolveOverwrite(String domain) {
Optional<String> entryOpt = dns.findHost(domain);
if (!entryOpt.isPresent()) {
log.info("No DNS overwrite for {}", domain);
return Optional.empty();
}
try {
return Optional.of(new URL(entryOpt.get()));
} catch (MalformedURLException e) {
log.warn("Skipping homeserver Federation DNS overwrite for {} - not a valid URL: {}", domain, entryOpt.get());
return Optional.empty();
}
}
private Optional<URL> resolveLiteral(String domain) {
if (domain.contains("[") && domain.contains("]")) {
// This is an IPv6
if (domain.contains("]:")) {
// With a custom port, we return as is
return Optional.of(build(domain));
} else {
return Optional.of(build(domain + ":" + getDefaultPort()));
}
}
if (domain.contains(":")) {
// This is a domain or IPv4 with an explicit port, we return as is
return Optional.of(build(domain));
}
// At this point, we do not account for the provided string to be an IPv4 without a port. We will therefore
// perform well-known lookup and SRV record. While this is not needed, we don't expect the SRV to return anything
// and the well-known shouldn't either, but it might, leading to a wrong destination potentially.
//
// We accept this risk as mxisd is not meant to be used without DNS domain as per FAQ. We also provide resolution
// override facilities. Therefore, we accept to not handle this case until we get report of such unwanted behaviour
// that still fix mxisd use case and can't be resolved via override.
return Optional.empty();
}
private Optional<URL> resolveWellKnown(String domain) {
log.debug("Performing Well-known lookup for {}", domain);
HttpGet wnReq = new HttpGet("https://" + domain + "/.well-known/matrix/server");
try (CloseableHttpResponse wnRes = client.execute(wnReq)) {
int status = wnRes.getStatusLine().getStatusCode();
if (status == 200) {
try {
JsonObject body = GsonUtil.parseObj(EntityUtils.toString(wnRes.getEntity()));
String server = GsonUtil.getStringOrNull(body, "m.server");
if (StringUtils.isNotBlank(server)) {
log.debug("Found well-known entry: {}", server);
return Optional.of(build(server));
}
} catch (InvalidJsonException e) {
log.info("Could not parse well-known resource: {}", e.getMessage());
}
} else {
log.info("Well-known did not return status code 200 but {}, ignoring", status);
}
return Optional.empty();
} catch (IOException e) {
throw new RuntimeException("Error while trying to lookup well-known for " + domain, e);
}
}
private Optional<URL> resolveDnsSrv(String domain) {
log.debug("Performing SRV lookup for {}", domain);
String lookupDns = buildSrvRecordName(domain);
log.debug("Lookup name: {}", lookupDns);
try {
List<SRVRecord> srvRecords = new ArrayList<>();
Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run();
if (Objects.isNull(rawRecords) || rawRecords.length == 0) {
log.debug("No SRV record for {}", domain);
return Optional.empty();
}
for (Record record : rawRecords) {
if (Type.SRV == record.getType()) {
srvRecords.add((SRVRecord) record);
} else {
log.debug("Ignoring non-SRV record: {}", record.toString());
}
}
if (srvRecords.size() < 1) {
log.warn("DNS SRV records were found for {} but none is usable", lookupDns);
return Optional.empty();
}
srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority));
SRVRecord record = srvRecords.get(0);
return Optional.of(build(record.getTarget().toString(true) + ":" + record.getPort()));
} catch (TextParseException e) {
log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage());
}
return Optional.empty();
}
public URL build(String authority) {
try {
return new URL(getDefaultScheme() + "://" + authority);
} catch (MalformedURLException e) {
throw new RuntimeException("Could not build URL for " + authority, e);
}
}
public URL resolve(String domain) {
Optional<URL> s1 = resolveOverwrite(domain);
if (s1.isPresent()) {
URL dest = s1.get();
log.info("Resolution of {} via DNS overwrite to {}", domain, dest);
return dest;
}
Optional<URL> s2 = resolveLiteral(domain);
if (s2.isPresent()) {
URL dest = s2.get();
log.info("Resolution of {} as IP literal or IP/hostname with explicit port to {}", domain, dest);
return dest;
}
Optional<URL> s3 = resolveWellKnown(domain);
if (s3.isPresent()) {
URL dest = s3.get();
log.info("Resolution of {} via well-known to {}", domain, dest);
return dest;
}
// The domain needs to be resolved
Optional<URL> s4 = resolveDnsSrv(domain);
if (s4.isPresent()) {
URL dest = s4.get();
log.info("Resolution of {} via DNS SRV record to {}", domain, dest);
return dest;
}
URL dest = build(domain + ":" + getDefaultPort());
log.info("Resolution of {} to {}", domain, dest);
return dest;
}
}

View File

@@ -38,9 +38,8 @@ import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -58,23 +57,18 @@ public class SessionManager {
private NotificationManager notifMgr; private NotificationManager notifMgr;
private LookupStrategy lookupMgr; private LookupStrategy lookupMgr;
// FIXME export into central class, set version
private CloseableHttpClient client;
public SessionManager( public SessionManager(
SessionConfig cfg, SessionConfig cfg,
MatrixConfig mxCfg, MatrixConfig mxCfg,
IStorage storage, IStorage storage,
NotificationManager notifMgr, NotificationManager notifMgr,
LookupStrategy lookupMgr, LookupStrategy lookupMgr
CloseableHttpClient client
) { ) {
this.cfg = cfg; this.cfg = cfg;
this.mxCfg = mxCfg; this.mxCfg = mxCfg;
this.storage = storage; this.storage = storage;
this.notifMgr = notifMgr; this.notifMgr = notifMgr;
this.lookupMgr = lookupMgr; this.lookupMgr = lookupMgr;
this.client = client;
} }
private ThreePidSession getSession(String sid, String secret) { private ThreePidSession getSession(String sid, String secret) {
@@ -128,7 +122,7 @@ public class SessionManager {
log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server);
storage.insertThreePidSession(session.getDao()); storage.insertThreePidSession(session.getDao());
log.info("Stored session {}", sessionId, tpid, server); log.info("Stored session {}", sessionId);
log.info("Session {} for {}: sending validation notification", sessionId, tpid); log.info("Session {} for {}: sending validation notification", sessionId, tpid);
notifMgr.sendForValidation(session); notifMgr.sendForValidation(session);
@@ -139,12 +133,13 @@ public class SessionManager {
} }
public ValidationResult validate(String sid, String secret, String token) { public ValidationResult validate(String sid, String secret, String token) {
log.info("Validating session {}", sid);
ThreePidSession session = getSession(sid, secret); ThreePidSession session = getSession(sid, secret);
log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); log.info("Session {} is from {}", session.getId(), session.getServer());
session.validate(token); session.validate(token);
storage.updateThreePidSession(session.getDao()); storage.updateThreePidSession(session.getDao());
log.info("Session {} has been validated locally", session.getId()); log.info("Session {} has been validated", session.getId());
ValidationResult r = new ValidationResult(session); ValidationResult r = new ValidationResult(session);
session.getNextLink().ifPresent(r::setNextUrl); session.getNextLink().ifPresent(r::setNextUrl);
@@ -195,6 +190,7 @@ public class SessionManager {
*/ */
log.warn("A remote host attempted to unbind without proper authorization. Request was denied"); log.warn("A remote host attempted to unbind without proper authorization. Request was denied");
log.warn("See https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy for more info");
if (!cfg.getPolicy().getUnbind().getFraudulent().getSendWarning()) { if (!cfg.getPolicy().getUnbind().getFraudulent().getSendWarning()) {
log.info("Not sending notification to 3PID owner as per configuration"); log.info("Not sending notification to 3PID owner as per configuration");

View File

@@ -0,0 +1,37 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.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.connector.email;
public class BlackholeEmailConnector implements EmailConnector {
public static final String ID = "none";
@Override
public String getId() {
return ID;
}
@Override
public void send(String senderAddress, String senderName, String recipient, String content) {
//dev/null
}
}

View File

@@ -33,6 +33,10 @@ public class BuiltInEmailConnectorSupplier implements EmailConnectorSupplier {
@Override @Override
public Optional<EmailConnector> apply(EmailConfig cfg, Mxisd mxisd) { public Optional<EmailConnector> apply(EmailConfig cfg, Mxisd mxisd) {
if (StringUtils.equals(BlackholeEmailConnector.ID, cfg.getConnector())) {
return Optional.of(new BlackholeEmailConnector());
}
if (StringUtils.equals(EmailSmtpConnector.ID, cfg.getConnector())) { if (StringUtils.equals(EmailSmtpConnector.ID, cfg.getConnector())) {
EmailSmtpConfig smtpCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(EmailSmtpConnector.ID, new JsonObject()), EmailSmtpConfig.class); EmailSmtpConfig smtpCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(EmailSmtpConnector.ID, new JsonObject()), EmailSmtpConfig.class);
return Optional.of(new EmailSmtpConnector(smtpCfg)); return Optional.of(new EmailSmtpConnector(smtpCfg));

View File

@@ -31,14 +31,17 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.mail.Header;
import javax.mail.Message; import javax.mail.Message;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.mail.Session; import javax.mail.Session;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.Enumeration;
import java.util.Properties; import java.util.Properties;
public class EmailSmtpConnector implements EmailConnector { public class EmailSmtpConnector implements EmailConnector {
@@ -66,6 +69,10 @@ public class EmailSmtpConnector implements EmailConnector {
sCfg.setProperty("mail.smtp.auth", "true"); sCfg.setProperty("mail.smtp.auth", "true");
} }
if (cfg.getTls() == 3) {
sCfg.setProperty("mail.smtp.ssl.enable", "true");
}
session = Session.getInstance(sCfg); session = Session.getInstance(sCfg);
} }
@@ -93,7 +100,16 @@ public class EmailSmtpConnector implements EmailConnector {
try { try {
InternetAddress sender = new InternetAddress(senderAddress, senderName); InternetAddress sender = new InternetAddress(senderAddress, senderName);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
msg.setHeader("X-Mailer", Mxisd.Agent);
// We must encode our headers ourselves as we have no guarantee that they were in the provided data.
// This is required to support UTF-8 characters from user display names or room names in the subject header per example
Enumeration<Header> headers = msg.getAllHeaders();
while (headers.hasMoreElements()) {
Header header = headers.nextElement();
msg.setHeader(header.getName(), MimeUtility.encodeText(header.getValue()));
}
msg.setHeader("X-Mailer", MimeUtility.encodeText(Mxisd.Agent));
msg.setSentDate(new Date()); msg.setSentDate(new Date());
msg.setFrom(sender); msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, recipient); msg.setRecipients(Message.RecipientType.TO, recipient);
@@ -101,8 +117,11 @@ public class EmailSmtpConnector implements EmailConnector {
log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1); if (cfg.getTls() < 3) {
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
}
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
if (StringUtils.isAllEmpty(cfg.getLogin(), cfg.getPassword())) { if (StringUtils.isAllEmpty(cfg.getLogin(), cfg.getPassword())) {

View File

@@ -24,14 +24,14 @@ public class BlackholePhoneConnector implements PhoneConnector {
public static final String ID = "none"; public static final String ID = "none";
@Override
public void send(String recipient, String content) {
//dev/null
}
@Override @Override
public String getId() { public String getId() {
return ID; return ID;
} }
@Override
public void send(String recipient, String content) {
//dev/null
}
} }

View File

@@ -33,15 +33,15 @@ public class BuiltInPhoneConnectorSupplier implements PhoneConnectorSupplier {
@Override @Override
public Optional<PhoneConnector> apply(PhoneConfig cfg, Mxisd mxisd) { public Optional<PhoneConnector> apply(PhoneConfig cfg, Mxisd mxisd) {
if (StringUtils.equals(BlackholePhoneConnector.ID, cfg.getConnector())) {
return Optional.of(new BlackholePhoneConnector());
}
if (StringUtils.equals(PhoneSmsTwilioConnector.ID, cfg.getConnector())) { if (StringUtils.equals(PhoneSmsTwilioConnector.ID, cfg.getConnector())) {
PhoneTwilioConfig cCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(PhoneSmsTwilioConnector.ID, new JsonObject()), PhoneTwilioConfig.class); PhoneTwilioConfig cCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(PhoneSmsTwilioConnector.ID, new JsonObject()), PhoneTwilioConfig.class);
return Optional.of(new PhoneSmsTwilioConnector(cCfg)); return Optional.of(new PhoneSmsTwilioConnector(cCfg));
} }
if (StringUtils.equals(BlackholePhoneConnector.ID, cfg.getConnector())) {
return Optional.of(new BlackholePhoneConnector());
}
return Optional.empty(); return Optional.empty();
} }

View File

@@ -68,6 +68,7 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
@Override @Override
public String getForReply(IThreePidInviteReply invite) { public String getForReply(IThreePidInviteReply invite) {
log.info("Generating notification content for 3PID invite"); log.info("Generating notification content for 3PID invite");
invite.getInvite().getProperties().putAll(cfg.getPlaceholder());
return populateForReply(invite, getTemplateContent(cfg.getInvite())); return populateForReply(invite, getTemplateContent(cfg.getInvite()));
} }

View File

@@ -27,14 +27,16 @@ import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils; import org.apache.commons.lang.WordUtils;
import org.apache.commons.lang3.StringUtils;
import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.RoomName; import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.RoomName;
import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.SenderDisplayName; import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.SenderDisplayName;
public abstract class PlaceholderNotificationGenerator { public abstract class PlaceholderNotificationGenerator {
public static final String RegisterUrl = "REGISTER_URL";
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
private ServerConfig srvCfg; private ServerConfig srvCfg;
@@ -44,6 +46,10 @@ public abstract class PlaceholderNotificationGenerator {
} }
protected String populateForCommon(ThreePid recipient, String input) { protected String populateForCommon(ThreePid recipient, String input) {
if (StringUtils.isBlank(input)) {
return input;
}
String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain()); String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain());
return input return input
@@ -54,6 +60,10 @@ public abstract class PlaceholderNotificationGenerator {
} }
protected String populateForInvite(IMatrixIdInvite invite, String input) { protected String populateForInvite(IMatrixIdInvite invite, String input) {
if (StringUtils.isBlank(input)) {
return input;
}
String senderName = invite.getProperties().getOrDefault(SenderDisplayName, ""); String senderName = invite.getProperties().getOrDefault(SenderDisplayName, "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getSender().getId()); String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getSender().getId());
String roomName = invite.getProperties().getOrDefault(RoomName, ""); String roomName = invite.getProperties().getOrDefault(RoomName, "");
@@ -70,14 +80,20 @@ public abstract class PlaceholderNotificationGenerator {
} }
protected String populateForReply(IThreePidInviteReply invite, String input) { protected String populateForReply(IThreePidInviteReply invite, String input) {
if (StringUtils.isBlank(input)) {
return input;
}
ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress()); ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress());
String senderName = invite.getInvite().getProperties().getOrDefault(SenderDisplayName, ""); String senderName = invite.getInvite().getProperties().getOrDefault(SenderDisplayName, "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault(RoomName, ""); String roomName = invite.getInvite().getProperties().getOrDefault(RoomName, "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId()); String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId());
String registerUrl = StringUtils.defaultIfBlank(invite.getInvite().getProperties().get(RegisterUrl), "https://" + mxCfg.getDomain());
return populateForCommon(tpid, input) return populateForCommon(tpid, input)
.replace("%" + RegisterUrl + "%", registerUrl)
.replace("%SENDER_ID%", invite.getInvite().getSender().getId()) .replace("%SENDER_ID%", invite.getInvite().getSender().getId())
.replace("%SENDER_NAME%", senderName) .replace("%SENDER_NAME%", senderName)
.replace("%SENDER_NAME_OR_ID%", senderNameOrId) .replace("%SENDER_NAME_OR_ID%", senderNameOrId)
@@ -89,6 +105,10 @@ public abstract class PlaceholderNotificationGenerator {
} }
protected String populateForValidation(IThreePidSession session, String input) { protected String populateForValidation(IThreePidSession session, String input) {
if (StringUtils.isBlank(input)) {
return input;
}
String validationLink = srvCfg.getPublicUrl() + IsAPIv1.getValidate( String validationLink = srvCfg.getPublicUrl() + IsAPIv1.getValidate(
session.getThreePid().getMedium(), session.getThreePid().getMedium(),
session.getId(), session.getId(),
@@ -102,10 +122,6 @@ public abstract class PlaceholderNotificationGenerator {
.replace("%NEXT_URL%", validationLink); .replace("%NEXT_URL%", validationLink);
} }
protected String populateForRemoteValidation(IThreePidSession session, String input) {
return populateForValidation(session, input);
}
protected String populateForFraudulentUndind(ThreePid tpid, String input) { protected String populateForFraudulentUndind(ThreePid tpid, String input) {
return populateForCommon(tpid, input); return populateForCommon(tpid, input);
} }

View File

@@ -20,7 +20,7 @@
package io.kamax.mxisd.threepid.notification; package io.kamax.mxisd.threepid.notification;
import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.Mxisd;
@@ -65,13 +65,18 @@ public class BuiltInNotificationHandlerSupplier implements NotificationHandlerSu
if (StringUtils.equals(EmailRawNotificationHandler.ID, handler)) { if (StringUtils.equals(EmailRawNotificationHandler.ID, handler)) {
Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.Email.getId()); Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.Email.getId());
if (Objects.nonNull(o)) { if (Objects.nonNull(o)) {
EmailConfig emailCfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), EmailConfig.class); EmailConfig emailCfg;
try {
emailCfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), EmailConfig.class);
} catch (JsonSyntaxException e) {
throw new ConfigurationException("Invalid configuration for threepid email notification");
}
if (org.apache.commons.lang.StringUtils.isBlank(emailCfg.getGenerator())) { if (StringUtils.isBlank(emailCfg.getGenerator())) {
throw new ConfigurationException("notification.email.generator"); throw new ConfigurationException("notification.email.generator");
} }
if (org.apache.commons.lang.StringUtils.isBlank(emailCfg.getConnector())) { if (StringUtils.isBlank(emailCfg.getConnector())) {
throw new ConfigurationException("notification.email.connector"); throw new ConfigurationException("notification.email.connector");
} }
@@ -94,9 +99,15 @@ public class BuiltInNotificationHandlerSupplier implements NotificationHandlerSu
} }
if (StringUtils.equals(EmailSendGridNotificationHandler.ID, handler)) { if (StringUtils.equals(EmailSendGridNotificationHandler.ID, handler)) {
JsonObject cfgJson = mxisd.getConfig().getNotification().getHandlers().get(EmailSendGridNotificationHandler.ID); Object cfgJson = mxisd.getConfig().getNotification().getHandlers().get(EmailSendGridNotificationHandler.ID);
if (Objects.nonNull(cfgJson)) { if (Objects.nonNull(cfgJson)) {
EmailSendGridConfig cfg = GsonUtil.get().fromJson(cfgJson, EmailSendGridConfig.class); EmailSendGridConfig cfg;
try {
cfg = GsonUtil.get().fromJson(GsonUtil.get().toJson(cfgJson), EmailSendGridConfig.class);
} catch (JsonSyntaxException e) {
throw new ConfigurationException("Invalid configuration for threepid email sendgrid handler");
}
NotificationHandlers.register(() -> new EmailSendGridNotificationHandler(mxisd.getConfig(), cfg)); NotificationHandlers.register(() -> new EmailSendGridNotificationHandler(mxisd.getConfig(), cfg));
} }
} }
@@ -107,7 +118,12 @@ public class BuiltInNotificationHandlerSupplier implements NotificationHandlerSu
if (StringUtils.equals(PhoneNotificationHandler.ID, handler)) { if (StringUtils.equals(PhoneNotificationHandler.ID, handler)) {
Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.PhoneNumber.getId()); Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.PhoneNumber.getId());
if (Objects.nonNull(o)) { if (Objects.nonNull(o)) {
PhoneConfig cfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), PhoneConfig.class); PhoneConfig cfg;
try {
cfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), PhoneConfig.class);
} catch (JsonSyntaxException e) {
throw new ConfigurationException("Invalid configuration for threepid msisdn notification");
}
List<PhoneGenerator> generators = StreamSupport List<PhoneGenerator> generators = StreamSupport
.stream(ServiceLoader.load(PhoneGeneratorSupplier.class).spliterator(), false) .stream(ServiceLoader.load(PhoneGeneratorSupplier.class).spliterator(), false)

View File

@@ -33,7 +33,7 @@ import io.kamax.mxisd.notification.NotificationHandler;
import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator; import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import io.kamax.mxisd.util.FileUtil; import io.kamax.mxisd.util.FileUtil;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -86,6 +86,9 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override @Override
public void sendForInvite(IMatrixIdInvite invite) { public void sendForInvite(IMatrixIdInvite invite) {
EmailTemplate template = cfg.getTemplates().getGeneric().get("matrixId"); EmailTemplate template = cfg.getTemplates().getGeneric().get("matrixId");
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for Matrix ID invite notifications");
}
Email email = getEmail(); Email email = getEmail();
email.setSubject(populateForInvite(invite, template.getSubject())); email.setSubject(populateForInvite(invite, template.getSubject()));
@@ -98,6 +101,10 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override @Override
public void sendForReply(IThreePidInviteReply invite) { public void sendForReply(IThreePidInviteReply invite) {
EmailTemplate template = cfg.getTemplates().getInvite(); EmailTemplate template = cfg.getTemplates().getInvite();
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for 3PID invite notifications");
}
Email email = getEmail(); Email email = getEmail();
email.setSubject(populateForReply(invite, template.getSubject())); email.setSubject(populateForReply(invite, template.getSubject()));
email.setText(populateForReply(invite, getFromFile(template.getBody().getText()))); email.setText(populateForReply(invite, getFromFile(template.getBody().getText())));
@@ -109,6 +116,10 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override @Override
public void sendForValidation(IThreePidSession session) { public void sendForValidation(IThreePidSession session) {
EmailTemplate template = cfg.getTemplates().getSession().getValidation(); EmailTemplate template = cfg.getTemplates().getSession().getValidation();
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for validation notifications");
}
Email email = getEmail(); Email email = getEmail();
email.setSubject(populateForValidation(session, template.getSubject())); email.setSubject(populateForValidation(session, template.getSubject()));
email.setText(populateForValidation(session, getFromFile(template.getBody().getText()))); email.setText(populateForValidation(session, getFromFile(template.getBody().getText())));
@@ -120,6 +131,10 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override @Override
public void sendForFraudulentUnbind(ThreePid tpid) { public void sendForFraudulentUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent(); EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent();
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for fraudulent unbind notifications");
}
Email email = getEmail(); Email email = getEmail();
email.setSubject(populateForCommon(tpid, template.getSubject())); email.setSubject(populateForCommon(tpid, template.getSubject()));
email.setText(populateForCommon(tpid, getFromFile(template.getBody().getText()))); email.setText(populateForCommon(tpid, getFromFile(template.getBody().getText())));

View File

@@ -1,2 +1,4 @@
org.slf4j.simpleLogger.logFile=System.out org.slf4j.simpleLogger.logFile=System.out
org.slf4j.simpleLogger.log.org.xnio=warn org.slf4j.simpleLogger.log.org.xnio=warn
org.slf4j.simpleLogger.log.com.j256.ormlite.table.TableUtils=warn
org.slf4j.simpleLogger.log.com.mchange.v2.log.MLog=warn

View File

@@ -10,7 +10,7 @@ Content-Disposition: inline
Hi, Hi,
%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on %SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on
Matrix. To join the conversation, register an account on https://%DOMAIN% Matrix. To join the conversation, register an account on %REGISTER_URL%
You can also register an account on a public server and get in touch with them. You can also register an account on a public server and get in touch with them.
@@ -39,26 +39,26 @@ Content-Disposition: inline
<html lang="en"> <html lang="en">
<head> <head>
<style type="text/css"> <style type="text/css">
body { body {
margin: 0px; margin: 0px;
} }
pre, code { pre, code {
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
#page { #page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545; font-color: #454545;
font-size: 12pt; font-size: 12pt;
width: 100%%; width: 100%%;
padding: 20px; padding: 20px;
} }
#inner { #inner {
width: 640px; width: 640px;
} }
</style> </style>
</head> </head>
<body> <body>
@@ -66,24 +66,24 @@ pre, code {
<tr> <tr>
<td> </td> <td> </td>
<td id="inner"> <td id="inner">
<p>Hi,</p> <p>Hi,</p>
<p>%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on <p>%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on
Matrix. To join the conversation, register an account on <a href="https://%DOMAIN%">%DOMAIN%</a>.</p> Matrix. To join the conversation, register an account on <a href="%REGISTER_URL%">%DOMAIN%</a>.</p>
<pYou can also register an account on a public server and get in touch with them.</p> <pYou can also register an account on a public server and get in touch with them.</p>
<br> <br>
<p>About Matrix:</p> <p>About Matrix:</p>
<p>Matrix is an open standard for interoperable, decentralised, real-time communication <p>Matrix is an open standard for interoperable, decentralised, real-time communication
over IP, supporting group chat, file transfer, voice and video calling, integrations to over IP, supporting group chat, file transfer, voice and video calling, integrations to
other apps, bridges to other communication systems and much more. It can be used to power other apps, bridges to other communication systems and much more. It can be used to power
Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication.</p> Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication.</p>
<p>Thanks,</p> <p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p> <p>%DOMAIN_PRETTY% Admins</p>
</td> </td>
<td> </td> <td> </td>
</tr> </tr>

View File

@@ -28,26 +28,26 @@ Content-Disposition: inline
<html lang="en"> <html lang="en">
<head> <head>
<style type="text/css"> <style type="text/css">
body { body {
margin: 0px; margin: 0px;
} }
pre, code { pre, code {
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
#page { #page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545; font-color: #454545;
font-size: 12pt; font-size: 12pt;
width: 100%%; width: 100%%;
padding: 20px; padding: 20px;
} }
#inner { #inner {
width: 640px; width: 640px;
} }
</style> </style>
</head> </head>
<body> <body>
@@ -55,13 +55,13 @@ pre, code {
<tr> <tr>
<td> </td> <td> </td>
<td id="inner"> <td id="inner">
<p>Hi,</p> <p>Hi,</p>
<p>%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on Matrix.</p> <p>%SENDER_NAME_OR_ID% has invited you into a room [%ROOM_NAME_OR_ID%] on Matrix.</p>
<p>Thanks,</p> <p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p> <p>%DOMAIN_PRETTY% Admins</p>
</td> </td>
<td> </td> <td> </td>
</tr> </tr>

View File

@@ -61,26 +61,26 @@ Content-Disposition: inline
<html lang="en"> <html lang="en">
<head> <head>
<style type="text/css"> <style type="text/css">
body { body {
margin: 0px; margin: 0px;
} }
pre, code { pre, code {
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
#page { #page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545; font-color: #454545;
font-size: 12pt; font-size: 12pt;
width: 100%%; width: 100%%;
padding: 20px; padding: 20px;
} }
#inner { #inner {
width: 640px; width: 640px;
} }
</style> </style>
</head> </head>
<body> <body>
@@ -88,42 +88,42 @@ pre, code {
<tr> <tr>
<td> </td> <td> </td>
<td id="inner"> <td id="inner">
<p>Hi,</p> <p>Hi,</p>
<p><b>THIS IS IMPORTANT, PLEASE READ CAREFULLY</b>.<br/> <p><b>THIS IS IMPORTANT, PLEASE READ CAREFULLY</b>.<br/>
If you are the system administrator of the Matrix installation, read the second section.</p> If you are the system administrator of the Matrix installation, read the second section.</p>
<p>This is a notification email that a possibly unauthorized entity has attempted to alter your <p>This is a notification email that a possibly unauthorized entity has attempted to alter your
3PIDs (email, phone numbers, etc.) settings. The request was denied and no change has been made.</p> 3PIDs (email, phone numbers, etc.) settings. The request was denied and no change has been made.</p>
<p>This is so you are aware of a possible failure in case you just tried to remove a 3PID from your account.</p> <p>This is so you are aware of a possible failure in case you just tried to remove a 3PID from your account.</p>
<p>If you do not understand this email, please forward it to your System administrator.</p> <p>If you do not understand this email, please forward it to your System administrator.</p>
<hr> <hr>
<p>As the system administrator:</p> <p>As the system administrator:</p>
<p>If you are using synapse as a Homeserver, this is a known issue related to <a href="https://github.com/matrix-org/matrix-doc/issues/1194">MSC1194</a> <p>If you are using synapse as a Homeserver, this is a known issue related to <a href="https://github.com/matrix-org/matrix-doc/issues/1194">MSC1194</a>
and abuse of separation of concerns. As a privacy-centric product and to protect your privacy, the request was actively and abuse of separation of concerns. As a privacy-centric product and to protect your privacy, the request was actively
blocked. We have written a more detailed explanation on our <a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy">Privacy wiki page</a> blocked. We have written a more detailed explanation on our <a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy">Privacy wiki page</a>
(<a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#msc1194-synapse-and-impacts-on-your-privacy">Direct link to section</a>) (<a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#msc1194-synapse-and-impacts-on-your-privacy">Direct link to section</a>)
so you can fully grasp the impact for you and your users.</p> so you can fully grasp the impact for you and your users.</p>
<p>We have open an issue on the synapse repos to reflect the related privacy concerns and GDPR violation(s) and would <p>We have open an issue on the synapse repos to reflect the related privacy concerns and GDPR violation(s) and would
appreciate if you could comment on it or simply adds a thumbs up so the concerns are finally dealt with by the synapse dev team.<br/> appreciate if you could comment on it or simply adds a thumbs up so the concerns are finally dealt with by the synapse dev team.<br/>
Issue: <a href="https://github.com/matrix-org/synapse/issues/4540">https://github.com/matrix-org/synapse/issues/4540</a></p> Issue: <a href="https://github.com/matrix-org/synapse/issues/4540">https://github.com/matrix-org/synapse/issues/4540</a></p>
<p>If you are using another Homeserver or this came following no action from your own users, then you have been the target <p>If you are using another Homeserver or this came following no action from your own users, then you have been the target
of an unbind attack from a rogue entity which was blocked. You may want to check your logs to see the exact source of of an unbind attack from a rogue entity which was blocked. You may want to check your logs to see the exact source of
the attack and take relevant actions following your policy.</p> the attack and take relevant actions following your policy.</p>
<p>If you would like to disable these notifications, please see the <p>If you would like to disable these notifications, please see the
<a href="https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#configuration">3PID sessions configuration documentation.</a></p> <a href="https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#configuration">3PID sessions configuration documentation.</a></p>
<p>Thanks,</p> <p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p> <p>%DOMAIN_PRETTY% Admins</p>
</td> </td>
<td> </td> <td> </td>
</tr> </tr>

View File

@@ -33,30 +33,30 @@ Content-Disposition: inline
<html lang="en"> <html lang="en">
<head> <head>
<style type="text/css"> <style type="text/css">
body { body {
margin: 0px; margin: 0px;
} }
pre, code { pre, code {
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
#page { #page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545; font-color: #454545;
font-size: 12pt; font-size: 12pt;
width: 100%%; width: 100%%;
padding: 20px; padding: 20px;
} }
#inner { #inner {
width: 640px; width: 640px;
} }
.notif_link a, .footer a { .notif_link a, .footer a {
color: #76CFA6 ! important; color: #76CFA6 ! important;
} }
</style> </style>
</head> </head>
<body> <body>

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd.test.crypto; package io.kamax.mxisd.test.crypto;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson; import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.crypto.Signature; import io.kamax.mxisd.crypto.Signature;
@@ -36,10 +37,14 @@ import org.junit.Test;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
public class SignatureManagerTest { public class SignatureManagerTest {
private static final String lookupData = "{\n" + " \"not_before\": 0,\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n"
+ " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n"
+ " \"not_after\": 253402300799000,\n" + " \"ts\": 1523482030147\n" + "}";
private static SignatureManager signMgr; private static SignatureManager signMgr;
private static SignatureManager build(String keySeed) { private static SignatureManager build(String keySeed) {
@@ -98,12 +103,19 @@ public class SignatureManagerTest {
@Test @Test
public void onIdentityLookup() { public void onIdentityLookup() {
String value = MatrixJson.encodeCanonical("{\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n" String value = MatrixJson.encodeCanonical(lookupData);
+ " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n"
+ " \"not_after\": 253402300799000,\n" + " \"not_before\": 0,\n" + " \"ts\": 1523482030147\n" + "}");
String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg"; String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg";
testSign(value, sign); testSign(value, sign);
} }
@Test
public void onIdentityLookupFull() {
JsonObject data = GsonUtil.parseObj(lookupData);
signMgr.signMessageGson("localhost", data);
JsonObject signatures = EventKey.Signatures.getObj(data);
JsonObject domainSign = GsonUtil.getObj(signatures, "localhost");
String sign = GsonUtil.getStringOrThrow(domainSign, "ed25519:0");
assertEquals(sign, "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg");
}
} }

View File

@@ -0,0 +1,64 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.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.test.matrix;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.junit.BeforeClass;
import org.junit.Test;
import java.net.URL;
import static org.junit.Assert.assertEquals;
public class HomeserverFederationResolverTest {
private static HomeserverFederationResolver resolver;
@BeforeClass
public static void beforeClass() {
CloseableHttpClient client = HttpClients.custom()
.setUserAgent(Mxisd.Agent)
.setMaxConnPerRoute(Integer.MAX_VALUE)
.setMaxConnTotal(Integer.MAX_VALUE)
.build();
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(new MxisdConfig().getDns().getOverwrite());
resolver = new HomeserverFederationResolver(fedDns, client);
}
@Test
public void hostnameWithoutPort() {
URL url = resolver.resolve("example.org");
assertEquals("https://example.org:8448", url.toString());
}
@Test
public void hostnameWithPort() {
URL url = resolver.resolve("example.org:443");
assertEquals("https://example.org:443", url.toString());
}
}

View File

@@ -32,7 +32,10 @@ import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.invitation.MatrixIdInvite; import io.kamax.mxisd.invitation.MatrixIdInvite;
import io.kamax.mxisd.invitation.ThreePidInvite;
import io.kamax.mxisd.invitation.ThreePidInviteReply;
import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector; import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector;
import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator;
import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;
import org.junit.After; import org.junit.After;
@@ -45,6 +48,7 @@ import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeMultipart;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
@@ -56,7 +60,8 @@ public class EmailNotificationTest {
private final String user = "mxisd"; private final String user = "mxisd";
private final String notifiee = "john"; private final String notifiee = "john";
private final String sender = user + "@" + domain; private final String sender = user + "@" + domain;
private final String senderEmail = "\"Mxisd Server (Unit Test)\" <" + sender + ">"; private final String senderName = "\"Mxisd Server (Unit Test)\" <" + sender + ">";
private final String senderNameEncoded = "=?UTF-8?Q?=22Mxisd_Server_=E3=81=82_=28Unit_T?= =?UTF-8?Q?est=29=22_=3Cmxisd=40localhost=3E?= <mxisd@localhost>";
private final String target = notifiee + "@" + domain; private final String target = notifiee + "@" + domain;
private Mxisd m; private Mxisd m;
@@ -72,7 +77,7 @@ public class EmailNotificationTest {
EmailConfig eCfg = new EmailConfig(); EmailConfig eCfg = new EmailConfig();
eCfg.setConnector(EmailSmtpConnector.ID); eCfg.setConnector(EmailSmtpConnector.ID);
eCfg.getIdentity().setFrom(sender); eCfg.getIdentity().setFrom(sender);
eCfg.getIdentity().setName("Mxisd Server (Unit Test)"); eCfg.getIdentity().setName(senderName);
eCfg.getConnectors().put(EmailSmtpConnector.ID, GsonUtil.makeObj(smtpCfg)); eCfg.getConnectors().put(EmailSmtpConnector.ID, GsonUtil.makeObj(smtpCfg));
MxisdConfig cfg = new MxisdConfig(); MxisdConfig cfg = new MxisdConfig();
@@ -114,10 +119,33 @@ public class EmailNotificationTest {
assertEquals(1, gm.getReceivedMessages().length); assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0]; MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length); assertEquals(1, msg.getFrom().length);
assertEquals(senderEmail, msg.getFrom()[0].toString()); assertEquals(senderNameEncoded, msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length); assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
} }
@Test
public void forThreepidInvite() throws MessagingException, IOException {
String registerUrl = "https://" + RandomStringUtils.randomAlphanumeric(20) + ".example.org/register";
gm.setUser(user, user);
_MatrixID sender = MatrixID.asAcceptable(user, domain);
ThreePidInvite inv = new ThreePidInvite(sender, ThreePidMedium.Email.getId(), target, "!rid:" + domain);
inv.getProperties().put(PlaceholderNotificationGenerator.RegisterUrl, registerUrl);
m.getNotif().sendForReply(new ThreePidInviteReply("a", inv, "b", "c", new ArrayList<>()));
assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length);
assertEquals(senderNameEncoded, msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
// We just check on the text/plain one. HTML is multipart and it's difficult so we skip
MimeMultipart content = (MimeMultipart) msg.getContent();
MimeBodyPart mbp = (MimeBodyPart) content.getBodyPart(0);
String mbpContent = mbp.getContent().toString();
assertTrue(mbpContent.contains(registerUrl));
}
@Test @Test
public void forValidation() throws MessagingException, IOException { public void forValidation() throws MessagingException, IOException {
gm.setUser(user, user); gm.setUser(user, user);
@@ -138,7 +166,7 @@ public class EmailNotificationTest {
assertEquals(1, gm.getReceivedMessages().length); assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0]; MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length); assertEquals(1, msg.getFrom().length);
assertEquals(senderEmail, msg.getFrom()[0].toString()); assertEquals(senderNameEncoded, msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length); assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
// We just check on the text/plain one. HTML is multipart and it's difficult so we skip // We just check on the text/plain one. HTML is multipart and it's difficult so we skip