Compare commits

...

18 Commits

Author SHA1 Message Date
Max Dor
3e240fe34d Improve fraudulent unbind notification 2019-02-01 15:41:44 +01:00
Max Dor
635f6fdbe7 Implementation for blocking fraudulent 3PID /unbind attempts 2019-02-01 02:34:52 +01:00
Max Dor
4237eeb3b6 Skeleton for blocking fraudulent 3PID /unbind attempts 2019-01-30 00:29:51 +01:00
Max Dor
a0e91e7896 Use proper return codes for session errors 2019-01-30 00:28:55 +01:00
Max Dor
aab0b86646 Talk about projects using mxisd under the hood 2019-01-23 18:50:00 +01:00
Max Dor
3e22301af7 Properly handle /v1/store-invite 2019-01-16 02:57:40 +01:00
Max Dor
2b202323c0 Catch and handle more exceptions in Base HTTP handler 2019-01-16 02:57:40 +01:00
Max Dor
4ec05f518e Properly handle v1 of 3pid/bind 2019-01-16 02:57:40 +01:00
Max Dor
6da68298b0 Fix invalid paths 2019-01-16 02:57:40 +01:00
Max Dor
aecaafdeca Set theme jekyll for github pages 2019-01-16 01:31:21 +01:00
Max Dor
d885932f45 Fix loading failures of JDBC drivers for SQL-based Identity stores 2019-01-15 06:22:03 +01:00
Max Dor
c689a3f161 Fix classpath resources config 2019-01-13 00:30:52 +01:00
Max Dor
7805112548 Fix #110 2019-01-11 23:07:58 +01:00
Max Dor
3e89f0bc5e Fix #109 2019-01-11 23:07:52 +01:00
Max Dor
c6b8f7d48e Better handle of File reading / Input Streams 2019-01-11 23:02:57 +01:00
Max Dor
83377ebee0 Protect against NPE 2019-01-11 22:08:35 +01:00
Max Dor
2aa6e4d142 Fix missing .html from Spring to Undertow port 2019-01-11 22:08:22 +01:00
Max Dor
82a1a3df68 Fix invalid parsing of 3PID medium configs 2019-01-11 21:44:51 +01:00
53 changed files with 1031 additions and 129 deletions

View File

@@ -8,6 +8,7 @@ mxisd - Federated Matrix Identity Server
- [Getting Started](#getting-started)
- [Support](#support)
- [Contribute](#contribute)
- [Powered by mxisd](#powered-by-mxisd)
- [FAQ](#faq)
- [Contact](#contact)
@@ -96,6 +97,11 @@ You can contribute as an organisation/corporation by:
maintained regularly and you get direct access to the support team.
- Sponsoring new features or bug fixes. [Get in touch](#contact) so we can discuss it further.
# Powered by mxisd
The following projects use mxisd under the hood for some or all their features. Check them out!
- [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy)
- [matrix-register-bot](https://github.com/krombel/matrix-register-bot)
# FAQ
See the [dedicated document](docs/faq.md)

View File

@@ -129,7 +129,7 @@ dependencies {
compile 'org.xerial:sqlite-jdbc:3.20.0'
// PostgreSQL
compile 'org.postgresql:postgresql:42.1.4'
compile 'org.postgresql:postgresql:42.2.5'
// MariaDB/MySQL
compile 'org.mariadb.jdbc:mariadb-java-client:2.1.2'

View File

@@ -1 +1 @@
theme: jekyll-theme-cayman
theme: jekyll-theme-hacker

View File

@@ -10,6 +10,15 @@ Following these quick start instructions, you will have a basic setup that can p
talk to the central Matrix.org Identity server.
This will be a good ground work for further integration with features and your existing Identity stores.
---
If you would like a more fully integrated setup out of the box, the [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy)
project provides a turn-key full-stack solution, including LDAP and the various mxisd features enabled and ready.
We work closely with the project owner so the latest mxisd version is always supported.
If you choose to use it, this Getting Started guide is not applicable - See the project documentation. You may then
directly go to the [Next steps](#next-steps).
## Preparation
You will need:
- Working Homeserver, ideally with working federation

View File

@@ -268,3 +268,10 @@ Structure with example values:
}
```
The base `profile` key is mandatory. `display_name`, `threepids` and `roles` are only to be returned on the relevant request.
If there is no profile, the following response is expected:
```json
{
"profile": {}
}
```

View File

@@ -36,4 +36,10 @@ notification:
body:
text: <Path to file containing the raw text part of the email. Do not set to not use one>
html: <Path to file containing the HTML part of the email. Do not set to not use one>
unbind:
fraudulent:
subject: <Subject of the email notification sent for potentially fraudulent 3PID unbinds>
body:
text: <Path to file containing the raw text part of the email. Do not set to not use one>
html: <Path to file containing the raw text part of the email. Do not set to not use one>
```

View File

@@ -20,7 +20,9 @@ threepid:
session:
validation:
local: '/path/to/validate-local-template.eml'
remote: 'path/to/validate-remote-template.eml'
remote: '/path/to/validate-remote-template.eml'
unbind:
frandulent: '/path/to/unbind-fraudulent-template.eml'
generic:
matrixId: '/path/to/mxid-invite-template.eml'
```

View File

@@ -149,6 +149,9 @@ session:
toRemote:
enabled: true
server: 'configExample' # Not to be included in config! Already present in default config!
unbind:
fraudulent:
sendWarning: true
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
# CONFIGURATION EXAMPLE
```
@@ -168,6 +171,14 @@ Each scope is divided into three parts:
If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID
locally validated.
---
`unbind.fraudulent` controls warning notifications if an illegal/fraudulent 3PID removal is attempted on the Identity server.
This is directly related to synapse disregard for privacy and new GDPR laws in Europe in an attempt to inform users about
potential privacy leaks.
For more information, see the corresponding [synapse issue](https://github.com/matrix-org/synapse/issues/4540).
### Web views
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted.
If the session or token is invalid, an error page is displayed.

View File

@@ -85,6 +85,7 @@ public class HttpMxisd {
.post(SessionValidateHandler.Path, sessValidateHandler)
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager())))
.post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession())))
.get(RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN, SaneHandler.around(new RemoteSessionStartHandler(m.getSession(), m.getConfig().getView())))
.get(RemoteIdentityAPIv1.SESSION_CHECK, SaneHandler.around(new RemoteSessionCheckHandler(m.getSession(), m.getConfig().getView())))

View File

@@ -47,7 +47,7 @@ import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.profile.ProfileProviders;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqliteStorage;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
@@ -88,7 +88,7 @@ public class Mxisd {
srvFetcher = new RemoteIdentityServerFetcher(httpClient);
store = new OrmLiteSqliteStorage(cfg);
store = new OrmLiteSqlStorage(cfg);
keyMgr = CryptoFactory.getKeyManager(cfg.getKey());
signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer());
ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite());
@@ -102,7 +102,7 @@ public class Mxisd {
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionMananger(cfg.getSession(), cfg.getMatrix(), store, notifMgr, httpClient);
sessMgr = new SessionMananger(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient);
invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());

View File

@@ -60,7 +60,9 @@ public class GoogleFirebaseBackend {
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
try (FileInputStream is = new FileInputStream(credsPath)) {
return FirebaseCredentials.fromCertificate(is);
}
} else {
return FirebaseCredentials.applicationDefault();
}

View File

@@ -0,0 +1,55 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* 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.backend.sql;
import org.apache.commons.lang3.StringUtils;
public class BuiltInDriverLoader implements DriverLoader {
@Override
public void accept(String s) {
String className = null;
if (StringUtils.equals("sqlite", s)) {
className = "org.sqlite.JDBC";
}
if (StringUtils.equals("postgresql", s)) {
className = "org.postgresql.Driver";
}
if (StringUtils.equals("mariadb", s)) {
className = "org.mariadb.jdbc.Driver";
}
if (StringUtils.equals("mysql", s)) {
className = "org.mariadb.jdbc.Driver";
}
if (StringUtils.isNotEmpty(className)) {
try {
Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* 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.backend.sql;
import java.util.function.Consumer;
public interface DriverLoader extends Consumer<String> {
}

View File

@@ -0,0 +1,38 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* 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.backend.sql;
import java.util.Objects;
import java.util.ServiceLoader;
public class Drivers {
private static ServiceLoader<DriverLoader> svcLoader;
public static void load(String type) {
if (Objects.isNull(svcLoader)) {
svcLoader = ServiceLoader.load(DriverLoader.class);
}
svcLoader.iterator().forEachRemaining(drv -> drv.accept(type));
}
}

View File

@@ -37,11 +37,15 @@ public class SqlConnectionPool {
private ComboPooledDataSource ds;
public SqlConnectionPool(SqlConfig cfg) {
Drivers.load(cfg.getType());
ds = new ComboPooledDataSource();
ds.setJdbcUrl("jdbc:" + cfg.getType() + ":" + cfg.getConnection());
ds.setMinPoolSize(1);
ds.setMaxPoolSize(10);
ds.setAcquireIncrement(2);
ds.setAcquireRetryAttempts(10);
ds.setAcquireRetryDelay(1000);
}
public Connection get() throws SQLException {

View File

@@ -125,6 +125,34 @@ public class SessionConfig {
}
public static class PolicyUnbind {
public static class PolicyUnbindFraudulent {
private boolean sendWarning = true;
public boolean getSendWarning() {
return sendWarning;
}
public void setSendWarning(boolean sendWarning) {
this.sendWarning = sendWarning;
}
}
private PolicyUnbindFraudulent fraudulent = new PolicyUnbindFraudulent();
public PolicyUnbindFraudulent getFraudulent() {
return fraudulent;
}
public void setFraudulent(PolicyUnbindFraudulent fraudulent) {
this.fraudulent = fraudulent;
}
}
public Policy() {
validation.enabled = true;
validation.forLocal.enabled = true;
@@ -139,6 +167,7 @@ public class SessionConfig {
}
private PolicyTemplate validation = new PolicyTemplate();
private PolicyUnbind unbind = new PolicyUnbind();
public PolicyTemplate getValidation() {
return validation;
@@ -148,6 +177,14 @@ public class SessionConfig {
this.validation = validation;
}
public PolicyUnbind getUnbind() {
return unbind;
}
public void setUnbind(PolicyUnbind unbind) {
this.unbind = unbind;
}
}
private Policy policy = new Policy();

View File

@@ -95,17 +95,17 @@ public class ViewConfig {
private Remote remote = new Remote();
public Session() {
local.onTokenSubmit.success = "session/local/tokenSubmitSuccess";
local.onTokenSubmit.failure = "session/local/tokenSubmitFailure";
local.onTokenSubmit.success = "classpath:/templates/session/local/tokenSubmitSuccess.html";
local.onTokenSubmit.failure = "classpath:/templates/session/local/tokenSubmitFailure.html";
localRemote.onTokenSubmit.success = "session/localRemote/tokenSubmitSuccess";
localRemote.onTokenSubmit.failure = "session/local/tokenSubmitFailure";
localRemote.onTokenSubmit.success = "classpath:/templates/session/localRemote/tokenSubmitSuccess.html";
localRemote.onTokenSubmit.failure = "classpath:/templates/session/local/tokenSubmitFailure.html";
remote.onRequest.success = "session/remote/requestSuccess";
remote.onRequest.failure = "session/remote/requestFailure";
remote.onRequest.success = "classpath:/templates/session/remote/requestSuccess.html";
remote.onRequest.failure = "classpath:/templates/session/remote/requestFailure.html";
remote.onCheck.success = "session/remote/checkSuccess";
remote.onCheck.failure = "session/remote/checkFailure";
remote.onCheck.success = "classpath:/templates/session/remote/checkSuccess.html";
remote.onCheck.failure = "classpath:/templates/session/remote/checkFailure.html";
}
public Local getLocal() {

View File

@@ -37,8 +37,10 @@ public class YamlConfigLoader {
rep.getPropertyUtils().setAllowReadOnlyProperties(true);
rep.getPropertyUtils().setSkipMissingProperties(true);
Yaml yaml = new Yaml(new Constructor(MxisdConfig.class), rep);
Object o = yaml.load(new FileInputStream(path));
return GsonUtil.get().fromJson(GsonUtil.get().toJson(o), MxisdConfig.class);
try (FileInputStream is = new FileInputStream(path)) {
Object o = yaml.load(is);
return GsonUtil.get().fromJson(GsonUtil.get().toJson(o), MxisdConfig.class);
}
}
public static Optional<MxisdConfig> tryLoadFromFile(String path) {

View File

@@ -34,8 +34,8 @@ public class RestBackendConfig {
public static class IdentityEndpoints {
private String single = "/_mxisd/backend/api/v1/identity/lookup/single";
private String bulk = "/_mxisd/backend/api/v1/identity/lookup/bulk";
private String single = "/_mxisd/backend/api/v1/identity/single";
private String bulk = "/_mxisd/backend/api/v1/identity/bulk";
public String getSingle() {
return single;

View File

@@ -20,7 +20,6 @@
package io.kamax.mxisd.config.threepid;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
@@ -31,18 +30,18 @@ import java.util.Map;
public class ThreePidConfig {
private Map<String, JsonObject> medium = new HashMap<>();
private Map<String, Object> medium = new HashMap<>();
public ThreePidConfig() {
public ThreePidConfig() { // TODO Check if this is still needed
medium.put(ThreePidMedium.Email.getId(), GsonUtil.makeObj(new EmailConfig()));
medium.put(ThreePidMedium.PhoneNumber.getId(), GsonUtil.makeObj(new PhoneConfig()));
}
public Map<String, JsonObject> getMedium() {
public Map<String, Object> getMedium() {
return medium;
}
public void setMedium(Map<String, JsonObject> medium) {
public void setMedium(Map<String, Object> medium) {
this.medium = medium;
}

View File

@@ -115,7 +115,7 @@ public class EmailSendGridConfig {
public static class Templates {
public static class TemplateSession {
public static class TemplateSessionValidation {
private EmailTemplate local = new EmailTemplate();
private EmailTemplate remote = new EmailTemplate();
@@ -137,6 +137,43 @@ public class EmailSendGridConfig {
}
}
public static class TemplateSessionUnbind {
private EmailTemplate fraudulent = new EmailTemplate();
public EmailTemplate getFraudulent() {
return fraudulent;
}
public void setFraudulent(EmailTemplate fraudulent) {
this.fraudulent = fraudulent;
}
}
public static class TemplateSession {
private TemplateSessionValidation validation = new TemplateSessionValidation();
private TemplateSessionUnbind unbind = new TemplateSessionUnbind();
public TemplateSessionValidation getValidation() {
return validation;
}
public void setValidation(TemplateSessionValidation validation) {
this.validation = validation;
}
public TemplateSessionUnbind getUnbind() {
return unbind;
}
public void setUnbind(TemplateSessionUnbind unbind) {
this.unbind = unbind;
}
}
private EmailTemplate invite = new EmailTemplate();
private TemplateSession session = new TemplateSession();
private Map<String, EmailTemplate> generic = new HashMap<>();

View File

@@ -32,6 +32,7 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().getValidation().setLocal("classpath:/threepids/email/validate-local-template.eml");
getSession().getValidation().setRemote("classpath:/threepids/email/validate-remote-template.eml");
getSession().getUnbind().setFraudulent("classpath:/threepids/email/unbind-fraudulent.eml");
}
public EmailTemplateConfig build() {

View File

@@ -62,7 +62,22 @@ public class GenericTemplateConfig {
}
public static class SessionUnbind {
private String fraudulent;
public String getFraudulent() {
return fraudulent;
}
public void setFraudulent(String fraudulent) {
this.fraudulent = fraudulent;
}
}
private SessionValidation validation = new SessionValidation();
private SessionUnbind unbind = new SessionUnbind();
public SessionValidation getValidation() {
return validation;
@@ -72,6 +87,14 @@ public class GenericTemplateConfig {
this.validation = validation;
}
public SessionUnbind getUnbind() {
return unbind;
}
public void setUnbind(SessionUnbind unbind) {
this.unbind = unbind;
}
}
private String invite;

View File

@@ -32,6 +32,7 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().getValidation().setLocal("classpath:/threepids/sms/validate-local-template.txt");
getSession().getValidation().setRemote("classpath:/threepids/sms/validate-remote-template.txt");
getSession().getUnbind().setFraudulent("classpath:/threepids/sms/unbind-fraudulent.txt");
}
public PhoneSmsTemplateConfig build() {

View File

@@ -28,7 +28,7 @@ public class ConfigurationException extends RuntimeException {
private String detailedMsg;
public ConfigurationException(String key) {
super("Invalid or empty value for configuration item " + key);
super("Invalid or empty value for configuration item: " + key);
}
public ConfigurationException(Throwable t) {

View File

@@ -25,7 +25,7 @@ import org.apache.http.HttpStatus;
public class SessionNotValidatedException extends HttpMatrixException {
public SessionNotValidatedException() {
super(HttpStatus.SC_OK, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed");
super(HttpStatus.SC_BAD_REQUEST, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed");
}
}

View File

@@ -20,6 +20,8 @@
package io.kamax.mxisd.exception;
import org.apache.http.HttpStatus;
public class SessionUnknownException extends HttpMatrixException {
public SessionUnknownException() {
@@ -27,7 +29,7 @@ public class SessionUnknownException extends HttpMatrixException {
}
public SessionUnknownException(String error) {
super(200, "M_NO_VALID_SESSION", error);
super(HttpStatus.SC_NOT_FOUND, "M_NO_VALID_SESSION", error);
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.io.identity;
import com.google.gson.annotations.SerializedName;
public class BindRequest {
public static class Keys {
public static final String SessionID = "sid";
public static final String Secret = "client_secret";
public static final String UserID = "mxid";
}
@SerializedName(Keys.SessionID)
private String sid;
@SerializedName(Keys.Secret)
private String secret;
@SerializedName(Keys.UserID)
private String userId;
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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.io.identity;
import com.google.gson.annotations.SerializedName;
public class StoreInviteRequest {
// Available keys from Spec + HS implementations reverse-engineering
//
// Synapse: https://github.com/matrix-org/synapse/blob/a219ce87263ad9be887cf039a04b4a1f06b7b0b8/synapse/handlers/room_member.py#L826
public static class Keys {
public static final String Medium = "medium";
public static final String Address = "address";
public static final String RoomID = "room_id";
public static final String RoomAlias = "room_alias"; // Not in the spec, arbitrary
public static final String RoomAvatarURL = "room_avatar_url"; // Not in the spec, arbitrary
public static final String RoomJoinRules = "room_join_rules"; // Not in the spec, arbitrary
public static final String RoomName = "room_name"; // Not in the spec, arbitrary
public static final String Sender = "sender";
public static final String SenderDisplayName = "sender_display_name"; // Not in the spec, arbitrary
public static final String SenderAvatarURL = "sender_avatar_url"; // Not in the spec, arbitrary
public static final String GuestAccessToken = "guest_access_token"; // Not in the spec, arbitrary
public static final String GuestUserID = "guest_user_id"; // Not in the spec, arbitrary
}
@SerializedName(Keys.Medium)
private String medium;
@SerializedName(Keys.Address)
private String address;
@SerializedName(Keys.RoomID)
private String roomId;
@SerializedName(Keys.RoomAlias)
private String roomAlias;
@SerializedName(Keys.RoomAvatarURL)
private String roomAvatarUrl;
@SerializedName(Keys.RoomJoinRules)
private String roomJoinRules;
@SerializedName(Keys.RoomName)
private String roomName;
@SerializedName(Keys.Sender)
private String sender;
@SerializedName(Keys.SenderDisplayName)
private String senderDisplayName;
@SerializedName(Keys.SenderAvatarURL)
private String senderAvatarUrl;
@SerializedName(Keys.GuestAccessToken)
private String guestAccessToken;
@SerializedName(Keys.GuestUserID)
private String guestUserId;
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getRoomId() {
return roomId;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public String getRoomAlias() {
return roomAlias;
}
public void setRoomAlias(String roomAlias) {
this.roomAlias = roomAlias;
}
public String getRoomAvatarUrl() {
return roomAvatarUrl;
}
public void setRoomAvatarUrl(String roomAvatarUrl) {
this.roomAvatarUrl = roomAvatarUrl;
}
public String getRoomJoinRules() {
return roomJoinRules;
}
public void setRoomJoinRules(String roomJoinRules) {
this.roomJoinRules = roomJoinRules;
}
public String getRoomName() {
return roomName;
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getSenderDisplayName() {
return senderDisplayName;
}
public void setSenderDisplayName(String senderDisplayName) {
this.senderDisplayName = senderDisplayName;
}
public String getSenderAvatarUrl() {
return senderAvatarUrl;
}
public void setSenderAvatarUrl(String senderAvatarUrl) {
this.senderAvatarUrl = senderAvatarUrl;
}
public String getGuestAccessToken() {
return guestAccessToken;
}
public void setGuestAccessToken(String guestAccessToken) {
this.guestAccessToken = guestAccessToken;
}
public String getGuestUserId() {
return guestUserId;
}
public void setGuestUserId(String guestUserId) {
this.guestUserId = guestUserId;
}
}

View File

@@ -30,6 +30,7 @@ import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,7 +39,10 @@ import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
public abstract class BasicHttpHandler implements HttpHandler {
@@ -49,8 +53,16 @@ public abstract class BasicHttpHandler implements HttpHandler {
}
protected String getQueryParameter(HttpServerExchange exchange, String name) {
return getQueryParameter(exchange.getQueryParameters(), name);
}
protected String getQueryParameter(Map<String, Deque<String>> parms, String name) {
try {
String raw = exchange.getQueryParameters().getOrDefault(name, new LinkedList<>()).peekFirst();
String raw = parms.getOrDefault(name, new LinkedList<>()).peekFirst();
if (StringUtils.isEmpty(raw)) {
return raw;
}
return URLDecoder.decode(raw, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new InternalServerError(e);
@@ -61,25 +73,32 @@ public abstract class BasicHttpHandler implements HttpHandler {
return getQueryParameter(exchange, name);
}
protected Optional<String> getContentType(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Content-Type"));
}
protected void writeBodyAsUtf8(HttpServerExchange exchange, String body) {
exchange.getResponseSender().send(body, StandardCharsets.UTF_8);
}
protected <T> T parseJsonTo(HttpServerExchange exchange, Class<T> type) {
protected String getBodyUtf8(HttpServerExchange exchange) {
try {
return GsonUtil.get().fromJson(IOUtils.toString(exchange.getInputStream(), StandardCharsets.UTF_8), type);
return IOUtils.toString(exchange.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected <T> T parseJsonTo(HttpServerExchange exchange, Class<T> type) {
return GsonUtil.get().fromJson(getBodyUtf8(exchange), type);
}
protected JsonObject parseJsonObject(HttpServerExchange exchange, String key) {
try {
JsonObject base = GsonUtil.parseObj(IOUtils.toString(exchange.getInputStream(), StandardCharsets.UTF_8));
return GsonUtil.getObj(base, key);
} catch (IOException e) {
throw new RuntimeException(e);
}
return GsonUtil.getObj(parseJsonObject(exchange), key);
}
protected JsonObject parseJsonObject(HttpServerExchange exchange) {
return GsonUtil.parseObj(getBodyUtf8(exchange));
}
protected void respond(HttpServerExchange ex, int statusCode, JsonElement bodyJson) {

View File

@@ -21,7 +21,9 @@
package io.kamax.mxisd.http.undertow.handler;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.MalformedJsonException;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.InvalidJsonException;
import io.kamax.mxisd.exception.*;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
@@ -63,8 +65,10 @@ public class SaneHandler extends BasicHttpHandler {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_ALREADY_EXISTS", e.getMessage());
} catch (JsonMemberNotFoundException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_JSON_MISSING_KEYS", e.getMessage());
} catch (InvalidResponseJsonException | JsonSyntaxException e) {
} catch (InvalidResponseJsonException | JsonSyntaxException | MalformedJsonException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, "M_INVALID_JSON", e.getMessage());
} catch (InvalidJsonException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, e.getErrorCode(), e.getError());
} catch (InvalidCredentialsException e) {
respond(exchange, HttpStatus.SC_UNAUTHORIZED, "M_UNAUTHORIZED", e.getMessage());
} catch (ObjectNotFoundException e) {

View File

@@ -24,11 +24,8 @@ import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.util.FileUtil;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.io.IOUtils;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
public class RemoteSessionCheckHandler extends BasicHttpHandler {
@@ -45,18 +42,15 @@ public class RemoteSessionCheckHandler extends BasicHttpHandler {
String sid = getQueryParameter(exchange, "sid");
String secret = getQueryParameter(exchange, "client_secret");
String viewData;
try {
FileInputStream f = new FileInputStream(viewCfg.getSession().getRemote().getOnCheck().getSuccess());
String viewData = IOUtils.toString(f, StandardCharsets.UTF_8);
mgr.validateRemote(sid, secret);
writeBodyAsUtf8(exchange, viewData);
viewData = FileUtil.load(viewCfg.getSession().getRemote().getOnCheck().getSuccess());
} catch (SessionNotValidatedException e) {
FileInputStream f = new FileInputStream(viewCfg.getSession().getRemote().getOnCheck().getFailure());
String viewData = IOUtils.toString(f, StandardCharsets.UTF_8);
writeBodyAsUtf8(exchange, viewData);
viewData = FileUtil.load(viewCfg.getSession().getRemote().getOnCheck().getFailure());
}
writeBodyAsUtf8(exchange, viewData);
}
}

View File

@@ -24,11 +24,8 @@ import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import io.kamax.mxisd.util.FileUtil;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.io.IOUtils;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
public class RemoteSessionStartHandler extends BasicHttpHandler {
@@ -46,8 +43,7 @@ public class RemoteSessionStartHandler extends BasicHttpHandler {
String secret = getQueryParameter(exchange, "client_secret");
IThreePidSession session = mgr.createRemote(sid, secret);
FileInputStream f = new FileInputStream(viewCfg.getSession().getRemote().getOnRequest().getSuccess());
String rawData = IOUtils.toString(f, StandardCharsets.UTF_8);
String rawData = FileUtil.load(viewCfg.getSession().getRemote().getOnRequest().getSuccess());
String data = rawData.replace("${checkLink}", RemoteIdentityAPIv1.getSessionCheck(session.getId(), session.getSecret()));
writeBodyAsUtf8(exchange, data);
}

View File

@@ -23,14 +23,21 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.BindRequest;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.session.SessionMananger;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.QueryParameterUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Map;
public class SessionTpidBindHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/3pid/bind";
@@ -47,12 +54,27 @@ public class SessionTpidBindHandler extends BasicHttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) {
String sid = getQueryParameter(exchange, "sid");
String secret = getQueryParameter(exchange, "client_secret");
String mxid = getQueryParameter(exchange, "mxid");
BindRequest bindReq = new BindRequest();
bindReq.setSid(getQueryParameter(exchange, BindRequest.Keys.SessionID));
bindReq.setSecret(getQueryParameter(exchange, BindRequest.Keys.Secret));
bindReq.setUserId(getQueryParameter(exchange, BindRequest.Keys.UserID));
String reqContentType = getContentType(exchange).orElse("application/octet-stream");
if (StringUtils.equals("application/x-www-form-urlencoded", reqContentType)) {
String body = getBodyUtf8(exchange);
Map<String, Deque<String>> parms = QueryParameterUtils.parseQueryString(body, StandardCharsets.UTF_8.name());
bindReq.setSid(getQueryParameter(parms, BindRequest.Keys.SessionID));
bindReq.setSecret(getQueryParameter(parms, BindRequest.Keys.Secret));
bindReq.setUserId(getQueryParameter(parms, BindRequest.Keys.UserID));
} else if (StringUtils.equals("application/json", reqContentType)) {
bindReq = parseJsonTo(exchange, BindRequest.class);
} else {
log.warn("Unknown encoding in 3PID session bind: {}", reqContentType);
log.warn("The request will most likely fail");
}
try {
mgr.bind(sid, secret, mxid);
mgr.bind(bindReq.getSid(), bindReq.getSecret(), bindReq.getUserId());
respond(exchange, new JsonObject());
} catch (BadRequestException e) {
log.info("requested session was not validated");

View File

@@ -0,0 +1,50 @@
/*
* 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.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionMananger;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SessionTpidUnbindHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/3pid/unbind";
private static final Logger log = LoggerFactory.getLogger(SessionTpidUnbindHandler.class);
private final SessionMananger sessMgr;
public SessionTpidUnbindHandler(SessionMananger sessMgr) {
this.sessMgr = sessMgr;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
JsonObject body = parseJsonObject(exchange);
sessMgr.unbind(body);
writeBodyAsUtf8(exchange, "{}");
}
}

View File

@@ -27,18 +27,16 @@ import io.kamax.mxisd.http.io.identity.SuccessStatusJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.session.ValidationResult;
import io.kamax.mxisd.util.FileUtil;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class SessionValidateHandler extends BasicHttpHandler {
@@ -95,16 +93,13 @@ public class SessionValidateHandler extends BasicHttpHandler {
exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url);
} else {
try {
String rawData = FileUtil.load(viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess());
if (r.isCanRemote()) {
FileInputStream f = new FileInputStream(viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess());
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
String rawData = IOUtils.toString(f, StandardCharsets.UTF_8);
String data = rawData.replace("${remoteSessionLink}", url);
writeBodyAsUtf8(exchange, data);
} else {
FileInputStream f = new FileInputStream(viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess());
String data = IOUtils.toString(f, StandardCharsets.UTF_8);
writeBodyAsUtf8(exchange, data);
writeBodyAsUtf8(exchange, rawData);
}
} catch (IOException e) {
throw new RuntimeException(e);

View File

@@ -20,10 +20,16 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.crypto.KeyManager;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.StoreInviteRequest;
import io.kamax.mxisd.http.io.identity.ThreePidInviteReplyIO;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.invitation.IThreePidInvite;
@@ -31,11 +37,13 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.invitation.ThreePidInvite;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.QueryParameterUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class StoreInviteHandler extends BasicHttpHandler {
@@ -53,21 +61,39 @@ public class StoreInviteHandler extends BasicHttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) {
Map<String, String> parameters = new HashMap<>();
String reqContentType = getContentType(exchange).orElse("application/octet-stream");
JsonObject invJson = new JsonObject();
for (Map.Entry<String, Deque<String>> entry : exchange.getQueryParameters().entrySet()) {
if (Objects.nonNull(entry.getValue().peekFirst())) {
parameters.put(entry.getKey(), entry.getValue().peekFirst());
}
if (StringUtils.startsWith(reqContentType, "application/json")) {
invJson = parseJsonObject(exchange);
}
// TODO test with missing parameters to see behaviour
String sender = parameters.get("sender");
String medium = parameters.get("medium");
String address = parameters.get("address");
String roomId = parameters.get("room_id");
// Backward compatibility for pre-r0.1.0 implementations
else if (StringUtils.startsWith(reqContentType, "application/x-www-form-urlencoded")) {
String body = getBodyUtf8(exchange);
Map<String, Deque<String>> parms = QueryParameterUtils.parseQueryString(body, StandardCharsets.UTF_8.name());
for (Map.Entry<String, Deque<String>> entry : parms.entrySet()) {
if (entry.getValue().size() == 0) {
return;
}
IThreePidInvite invite = new ThreePidInvite(MatrixID.asAcceptable(sender), medium, address, roomId, parameters);
if (entry.getValue().size() > 1) {
throw new BadRequestException("key " + entry.getKey() + " has more than one value");
}
invJson.addProperty(entry.getKey(), entry.getValue().peekFirst());
}
} else {
throw new BadRequestException("Unsupported Content-Type: " + reqContentType);
}
Type parmType = new TypeToken<Map<String, String>>() {
}.getType();
Map<String, String> parameters = GsonUtil.get().fromJson(invJson, parmType);
StoreInviteRequest inv = GsonUtil.get().fromJson(invJson, StoreInviteRequest.class);
_MatrixID sender = MatrixID.asAcceptable(inv.getSender());
IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters);
IThreePidInviteReply reply = invMgr.storeInvite(invite);
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), cfg.getPublicUrl()));

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.notification;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
@@ -38,4 +39,6 @@ public interface NotificationHandler {
void sendForRemoteValidation(IThreePidSession session);
void sendForFraudulentUnbind(ThreePid tpid);
}

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.notification;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.threepid.notification.NotificationConfig;
import io.kamax.mxisd.exception.NotImplementedException;
@@ -81,4 +82,8 @@ public class NotificationManager {
ensureMedium(session.getThreePid().getMedium()).sendForRemoteValidation(session);
}
public void sendForFraudulentUnbind(ThreePid tpid) throws NotImplementedException {
ensureMedium(tpid.getMedium()).sendForFraudulentUnbind(tpid);
}
}

View File

@@ -28,12 +28,15 @@ import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.SessionConfig;
import io.kamax.mxisd.exception.*;
import io.kamax.mxisd.http.io.identity.RequestTokenResponse;
import io.kamax.mxisd.http.undertow.handler.identity.v1.RemoteIdentityAPIv1;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.matrix.IdentityServerUtils;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
@@ -71,6 +74,7 @@ public class SessionMananger {
private MatrixConfig mxCfg;
private IStorage storage;
private NotificationManager notifMgr;
private LookupStrategy lookupMgr;
private GsonParser parser = new GsonParser();
private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); // FIXME refactor for sessions handling their own stuff
@@ -78,11 +82,19 @@ public class SessionMananger {
// FIXME export into central class, set version
private CloseableHttpClient client;
public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, NotificationManager notifMgr, CloseableHttpClient client) {
public SessionMananger(
SessionConfig cfg,
MatrixConfig mxCfg,
IStorage storage,
NotificationManager notifMgr,
LookupStrategy lookupMgr,
CloseableHttpClient client
) {
this.cfg = cfg;
this.mxCfg = mxCfg;
this.storage = storage;
this.notifMgr = notifMgr;
this.lookupMgr = lookupMgr;
this.client = client;
}
@@ -214,6 +226,10 @@ public class SessionMananger {
}
public void bind(String sid, String secret, String mxidRaw) {
if (StringUtils.isEmpty(mxidRaw)) {
throw new IllegalArgumentException("No Matrix User ID provided");
}
_MatrixID mxid = MatrixID.asAcceptable(mxidRaw);
ThreePidSession session = getSessionIfValidated(sid, secret);
@@ -255,6 +271,54 @@ public class SessionMananger {
}
}
public void unbind(JsonObject reqData) {
// TODO also check for HS header to know which domain attempting the unbind
if (reqData.entrySet().size() == 2 && reqData.has("mxid") && reqData.has("threepid")) {
/* This is a HS request to remove a 3PID and is considered:
* - An attack on user privacy
* - A baffling spec breakage requiring IS and HS 3PID info to be independent [1]
* - A baffling spec breakage that 3PID (un)bind is only one way [2]
*
* Given the lack of response on our extensive feedback on the proposal [3] which has not landed in the spec yet [4],
* We'll be denying such unbind requests and will inform users using their 3PID that a fraudulent attempt of
* removing their 3PID binding has been attempted and blocked.
*
* [1]: https://matrix.org/docs/spec/client_server/r0.4.0.html#adding-account-administrative-contact-information
* [2]: https://matrix.org/docs/spec/identity_service/r0.1.0.html#privacy
* [3]: https://docs.google.com/document/d/135g2muVxmuml0iUnLoTZxk8M2ZSt3kJzg81chGh51yg/edit
* [4]: https://github.com/matrix-org/matrix-doc/issues/1194
*/
log.warn("A remote host attempted to unbind without proper authorization. Request was denied");
if (!cfg.getPolicy().getUnbind().getFraudulent().getSendWarning()) {
log.info("Not sending notification to 3PID owner as per configuration");
} else {
log.info("Sending notification to 3PID owner as per configuration");
ThreePid tpid = GsonUtil.get().fromJson(GsonUtil.getObj(reqData, "threepid"), ThreePid.class);
Optional<SingleLookupReply> lookup = lookupMgr.findLocal(tpid.getMedium(), tpid.getAddress());
if (!lookup.isPresent()) {
log.info("No 3PID owner found, not sending any notification");
} else {
log.info("3PID owner found, sending notification");
try {
notifMgr.sendForFraudulentUnbind(tpid);
log.info("Notification sent");
} catch (NotImplementedException e) {
log.warn("Unable to send notification: {}", e.getMessage());
} catch (RuntimeException e) {
log.warn("Unable to send notification due to unknown error. See stacktrace below", e);
}
}
}
}
log.info("Denying request");
throw new NotAllowedException("You have attempted to alter 3PID bindings, which can only be done by the 3PID owner directly. " +
"We have informed the 3PID owner of your fraudulent attempt.");
}
public IThreePidSession createRemote(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid);

View File

@@ -40,7 +40,6 @@ import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
@@ -49,9 +48,9 @@ import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class OrmLiteSqliteStorage implements IStorage {
public class OrmLiteSqlStorage implements IStorage {
private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqliteStorage.class);
private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqlStorage.class);
@FunctionalInterface
private interface Getter<T> {
@@ -71,11 +70,11 @@ public class OrmLiteSqliteStorage implements IStorage {
private Dao<ThreePidSessionDao, String> sessionDao;
private Dao<ASTransactionDao, String> asTxnDao;
public OrmLiteSqliteStorage(MxisdConfig cfg) {
public OrmLiteSqlStorage(MxisdConfig cfg) {
this(cfg.getStorage().getBackend(), cfg.getStorage().getProvider().getSqlite().getDatabase());
}
public OrmLiteSqliteStorage(String backend, String path) {
public OrmLiteSqlStorage(String backend, String path) {
if (StringUtils.isBlank(backend)) {
throw new ConfigurationException("storage.backend");
}
@@ -85,13 +84,6 @@ public class OrmLiteSqliteStorage implements IStorage {
}
withCatcher(() -> {
if (path.startsWith("/") && !path.startsWith("//")) {
File parent = new File(path).getParentFile();
if (!parent.mkdirs() && !parent.isDirectory()) {
throw new RuntimeException("Unable to create DB parent directory: " + parent);
}
}
ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path);
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.threepid.generator;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ServerConfig;
@@ -27,16 +28,12 @@ import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.io.IOUtils;
import io.kamax.mxisd.util.FileUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
public abstract class GenericTemplateNotificationGenerator extends PlaceholderNotificationGenerator implements NotificationGenerator {
@@ -51,14 +48,7 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
private String getTemplateContent(String location) {
try {
URI loc = URI.create(location);
InputStream is;
if (StringUtils.equals("classpath", loc.getScheme())) {
is = getClass().getResourceAsStream(loc.getSchemeSpecificPart());
} else {
is = new FileInputStream(location);
}
return IOUtils.toString(is, StandardCharsets.UTF_8);
return FileUtil.load(location);
} catch (IOException e) {
throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage());
}
@@ -93,4 +83,10 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
return populateForRemoteValidation(session, getTemplateContent(cfg.getSession().getValidation().getRemote()));
}
@Override
public String getForFraudulentUnbind(ThreePid tpid) {
log.info("Generating notification content for fraudulent unbind");
return populateForFraudulentUndind(tpid, getTemplateContent(cfg.getSession().getUnbind().getFraudulent()));
}
}

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.threepid.generator;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
@@ -38,4 +39,6 @@ public interface NotificationGenerator {
String getForRemoteValidation(IThreePidSession session);
String getForFraudulentUnbind(ThreePid tpid);
}

View File

@@ -30,6 +30,9 @@ import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.RoomName;
import static io.kamax.mxisd.http.io.identity.StoreInviteRequest.Keys.SenderDisplayName;
public abstract class PlaceholderNotificationGenerator {
private MatrixConfig mxCfg;
@@ -51,9 +54,9 @@ public abstract class PlaceholderNotificationGenerator {
}
protected String populateForInvite(IMatrixIdInvite invite, String input) {
String senderName = invite.getProperties().getOrDefault("sender_display_name", "");
String senderName = invite.getProperties().getOrDefault(SenderDisplayName, "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getSender().getId());
String roomName = invite.getProperties().getOrDefault("room_name", "");
String roomName = invite.getProperties().getOrDefault(RoomName, "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getRoomId());
return populateForCommon(new ThreePid(invite.getMedium(), invite.getAddress()), input)
@@ -69,9 +72,9 @@ public abstract class PlaceholderNotificationGenerator {
protected String populateForReply(IThreePidInviteReply invite, String input) {
ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress());
String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", "");
String senderName = invite.getInvite().getProperties().getOrDefault(SenderDisplayName, "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault("room_name", "");
String roomName = invite.getInvite().getProperties().getOrDefault(RoomName, "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId());
return populateForCommon(tpid, input)
@@ -103,4 +106,8 @@ public abstract class PlaceholderNotificationGenerator {
return populateForValidation(session, input);
}
protected String populateForFraudulentUndind(ThreePid tpid, String input) {
return populateForCommon(tpid, input);
}
}

View File

@@ -26,6 +26,7 @@ import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
import io.kamax.mxisd.threepid.generator.GenericTemplateNotificationGenerator;
import org.apache.commons.lang3.StringUtils;
public class GenericEmailNotificationGenerator extends GenericTemplateNotificationGenerator implements EmailGenerator {
@@ -46,8 +47,8 @@ public class GenericEmailNotificationGenerator extends GenericTemplateNotificati
@Override
protected String populateForCommon(ThreePid recipient, String body) {
body = super.populateForCommon(recipient, body);
body = body.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom());
body = body.replace("%FROM_NAME%", cfg.getIdentity().getName());
body = body.replace("%FROM_EMAIL%", StringUtils.defaultIfEmpty(cfg.getIdentity().getFrom(), ""));
body = body.replace("%FROM_NAME%", StringUtils.defaultIfEmpty(cfg.getIdentity().getName(), ""));
return body;
}

View File

@@ -63,9 +63,9 @@ public class BuiltInNotificationHandlerSupplier implements NotificationHandlerSu
private void acceptEmail(String handler, Mxisd mxisd) {
if (StringUtils.equals(EmailRawNotificationHandler.ID, handler)) {
JsonObject emailCfgJson = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.Email.getId());
if (Objects.nonNull(emailCfgJson)) {
EmailConfig emailCfg = GsonUtil.get().fromJson(emailCfgJson, EmailConfig.class);
Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.Email.getId());
if (Objects.nonNull(o)) {
EmailConfig emailCfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), EmailConfig.class);
if (org.apache.commons.lang.StringUtils.isBlank(emailCfg.getGenerator())) {
throw new ConfigurationException("notification.email.generator");
@@ -105,9 +105,9 @@ public class BuiltInNotificationHandlerSupplier implements NotificationHandlerSu
private void acceptPhone(String handler, Mxisd mxisd) {
if (StringUtils.equals(PhoneNotificationHandler.ID, handler)) {
JsonObject cfgJson = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.PhoneNumber.getId());
if (Objects.nonNull(cfgJson)) {
PhoneConfig cfg = GsonUtil.get().fromJson(cfgJson, PhoneConfig.class);
Object o = mxisd.getConfig().getThreepid().getMedium().get(ThreePidMedium.PhoneNumber.getId());
if (Objects.nonNull(o)) {
PhoneConfig cfg = GsonUtil.get().fromJson(GsonUtil.makeObj(o), PhoneConfig.class);
List<PhoneGenerator> generators = StreamSupport
.stream(ServiceLoader.load(PhoneGeneratorSupplier.class).spliterator(), false)

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.threepid.notification;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
@@ -76,4 +77,9 @@ public abstract class GenericNotificationHandler<A extends ThreePidConnector, B
send(connector, session.getThreePid().getAddress(), generator.getForRemoteValidation(session));
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
send(connector, tpid.getAddress(), generator.getForFraudulentUnbind(tpid));
}
}

View File

@@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.notification.email;
import com.sendgrid.SendGrid;
import com.sendgrid.SendGridException;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.MxisdConfig;
@@ -31,14 +32,12 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.notification.NotificationHandler;
import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.io.IOUtils;
import io.kamax.mxisd.util.FileUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.sendgrid.SendGrid.Email;
import static com.sendgrid.SendGrid.Response;
@@ -78,7 +77,7 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
private String getFromFile(String path) {
try {
return IOUtils.toString(new FileInputStream(path), StandardCharsets.UTF_8);
return FileUtil.load(path);
} catch (IOException e) {
throw new RuntimeException("Couldn't create notification content using file " + path, e);
}
@@ -109,7 +108,7 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override
public void sendForValidation(IThreePidSession session) {
EmailTemplate template = cfg.getTemplates().getSession().getLocal();
EmailTemplate template = cfg.getTemplates().getSession().getValidation().getLocal();
Email email = getEmail();
email.setSubject(populateForValidation(session, template.getSubject()));
email.setText(populateForValidation(session, getFromFile(template.getBody().getText())));
@@ -120,7 +119,7 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override
public void sendForRemoteValidation(IThreePidSession session) {
EmailTemplate template = cfg.getTemplates().getSession().getLocal();
EmailTemplate template = cfg.getTemplates().getSession().getValidation().getRemote();
Email email = getEmail();
email.setSubject(populateForRemoteValidation(session, template.getSubject()));
email.setText(populateForRemoteValidation(session, getFromFile(template.getBody().getText())));
@@ -129,6 +128,17 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
send(session.getThreePid().getAddress(), email);
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent();
Email email = getEmail();
email.setSubject(populateForCommon(tpid, template.getSubject()));
email.setText(populateForCommon(tpid, getFromFile(template.getBody().getText())));
email.setHtml(populateForCommon(tpid, getFromFile(template.getBody().getHtml())));
send(tpid.getAddress(), email);
}
private void send(String recipient, Email email) {
if (StringUtils.isBlank(cfg.getIdentity().getFrom())) {
throw new FeatureNotAvailable("3PID Email identity: sender address is empty - " +

View File

@@ -0,0 +1,57 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* 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.util;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class FileUtil {
public static String load(String loc) throws IOException {
URI uri = URI.create(loc);
InputStream is;
if (StringUtils.equals("classpath", uri.getScheme())) {
String resource = uri.getSchemeSpecificPart();
is = FileUtil.class.getResourceAsStream(resource);
if (Objects.isNull(is)) {
throw new FileNotFoundException("No classpath resource: " + resource);
}
} else {
is = new FileInputStream(loc);
}
try {
return IOUtils.toString(is, StandardCharsets.UTF_8);
} finally {
is.close();
}
}
}

View File

@@ -0,0 +1 @@
io.kamax.mxisd.backend.sql.BuiltInDriverLoader

View File

@@ -0,0 +1,135 @@
Subject: IMPORTANT - %DOMAIN% Matrix Identity Server - Unauthorized 3PID unbind blocked
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ"
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Hi,
**THIS IS IMPORTANT, PLEASE READ CAREFULLY**.
If you are the system administrator of the Matrix installation, read the second section.
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.
This is so you are aware of a possible failure in case you just tried to remove a 3PID from your account.
If you do not understand this email, please forward it to your System administrator.
-----------
As the system administrator:
If you are using synapse as a Homeserver, this is a known issue related to MSC1194 [1] 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 Privacy wiki page [2] (Direct link [3]) so you can fully grasp the impact for you and your users.
We have open an issue [4] 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.
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
the attack and take relevant actions following your policy.
If you would like to disable these notifications, please see the 3PID sessions configuration documentation [5].
Thanks,
%DOMAIN_PRETTY% Admins
---
[1] https://github.com/matrix-org/matrix-doc/issues/1194
[2] https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy
[3] https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#msc1194-synapse-and-impacts-on-your-privacy
[4] https://github.com/matrix-org/synapse/issues/4540
[5] https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#configuration
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: multipart/related;
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
type="text/html"
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
Content-Type: text/html; charset=UTF-8
Content-Disposition: inline
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td> </td>
<td id="inner">
<p>Hi,</p>
<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>
<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>
<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>
<hr>
<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>
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>
(<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>
<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/>
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
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>
<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>
<p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td> </td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--

View File

@@ -0,0 +1 @@
INFORMATIONAL ONLY - Someone attempted to change your Matrix 3PIDs, with a potential data leak. Please contact your system administrator.

View File

@@ -20,23 +20,23 @@
package io.kamax.mxisd.test.storage;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqliteStorage;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage;
import org.junit.Test;
import java.time.Instant;
public class OrmLiteSqliteStorageTest {
public class OrmLiteSqlStorageTest {
@Test
public void insertAsTxnDuplicate() {
OrmLiteSqliteStorage store = new OrmLiteSqliteStorage("sqlite", ":memory:");
OrmLiteSqlStorage store = new OrmLiteSqlStorage("sqlite", ":memory:");
store.insertTransactionResult("mxisd", "1", Instant.now(), "{}");
store.insertTransactionResult("mxisd", "2", Instant.now(), "{}");
}
@Test(expected = RuntimeException.class)
public void insertAsTxnSame() {
OrmLiteSqliteStorage store = new OrmLiteSqliteStorage("sqlite", ":memory:");
OrmLiteSqlStorage store = new OrmLiteSqlStorage("sqlite", ":memory:");
store.insertTransactionResult("mxisd", "1", Instant.now(), "{}");
store.insertTransactionResult("mxisd", "1", Instant.now(), "{}");
}