Compare commits

..

6 Commits

Author SHA1 Message Date
Maxime Dor
3d39ee97dd Add 3PIDs at registration 2018-04-13 03:48:32 +02:00
Maxime Dor
94d60b6fc9 Auto-generate username 2018-04-13 03:48:32 +02:00
Maxime Dor
ac9881fa4b Fix login 2018-04-13 03:48:32 +02:00
Maxime Dor
547cafdd13 PoC of registration with Google 3PID 2018-04-13 03:48:32 +02:00
Maxime Dor
0da1af9869 registration handling skeleton 2018-04-13 03:48:32 +02:00
Maxime Dor
268df533f5 Research with Google auth integration 2018-04-13 03:48:32 +02:00
21 changed files with 541 additions and 100 deletions

View File

@@ -102,6 +102,9 @@ dependencies {
compile 'com.sun.mail:javax.mail:1.5.6' compile 'com.sun.mail:javax.mail:1.5.6'
compile 'javax.mail:javax.mail-api:1.5.6' compile 'javax.mail:javax.mail-api:1.5.6'
// Google Client APIs
compile 'com.google.api-client:google-api-client:1.23.0'
// Google Firebase Authentication backend // Google Firebase Authentication backend
compile 'com.google.firebase:firebase-admin:5.3.0' compile 'com.google.firebase:firebase-admin:5.3.0'

View File

@@ -44,7 +44,7 @@ Example: `/path/to/sqlite/file.db`
#### Others #### Others
```yaml ```yaml
sql.connection: //<HOST[:PORT]/DB?user=USER&password=PASS sql.connection: //<HOST[:PORT]/DB?username=USER&password=PASS
``` ```
Set the connection info for the database by replacing the following values: Set the connection info for the database by replacing the following values:
- `HOST`: Hostname of the SQL server - `HOST`: Hostname of the SQL server

View File

@@ -35,7 +35,7 @@ Example: `/path/to/synapse/sqliteFile.db`
### PostgreSQL ### PostgreSQL
```yaml ```yaml
synapseSql.connection: //<HOST[:PORT]/DB?user=USER&password=PASS synapseSql.connection: //<HOST[:PORT]/DB?username=USER&password=PASS
``` ```
Set the connection info for the database by replacing the following values: Set the connection info for the database by replacing the following values:
- `HOST`: Hostname of the SQL server - `HOST`: Hostname of the SQL server

View File

@@ -117,7 +117,6 @@ The following example of configuration (incomplete extract) shows which items ar
**IMPORTANT:** Most configuration items shown have default values and should not be included in your own configuration **IMPORTANT:** Most configuration items shown have default values and should not be included in your own configuration
file unless you want to specifically overwrite them. file unless you want to specifically overwrite them.
```yaml ```yaml
# CONFIGURATION EXAMPLE
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION # DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
session.policy.validation.enabled: true session.policy.validation.enabled: true
session.policy.validation.forLocal: session.policy.validation.forLocal:
@@ -133,7 +132,6 @@ session.policy.validation.forRemote:
enabled: true enabled: true
server: 'configExample' # Not to be included in config! Already present in default config! server: 'configExample' # Not to be included in config! Already present in default config!
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION # DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
# CONFIGURATION EXAMPLE
``` ```
`session.policy.validation` is the core configuration to control what users configured to use your Identity server `session.policy.validation` is the core configuration to control what users configured to use your Identity server
@@ -145,8 +143,8 @@ It is also divided into two sections: `forLocal` and `forRemote` which refers to
Each scope is divided into three parts: Each scope is divided into three parts:
- global on/off switch for 3PID sessions using `.enabled` - global on/off switch for 3PID sessions using `.enabled`
- `toLocal` allowing or not local 3PID session validations - `toLocal` allowing or not local 3PID session validations
- `toRemote` allowing or not remote 3PID session validations and to which server such sessions should be sent. - `toRemote` allowing or not remote 3PID session validations and to which server such sessions should be sent.
`.server` takes a Matrix Identity server list label. Only the first server in the list is currently used. `.server` takes a Matrix Identity server list label. Only the first server in the list is currently used.
If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID
locally validated. locally validated.

View File

@@ -43,8 +43,7 @@ public class AuthManager {
private Logger log = LoggerFactory.getLogger(AuthManager.class); private Logger log = LoggerFactory.getLogger(AuthManager.class);
@Autowired private List<AuthenticatorProvider> providers;
private List<AuthenticatorProvider> providers = new ArrayList<>();
@Autowired @Autowired
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
@@ -52,6 +51,10 @@ public class AuthManager {
@Autowired @Autowired
private InvitationManager invMgr; private InvitationManager invMgr;
public AuthManager(List<AuthenticatorProvider> providers) {
this.providers = new ArrayList<>(providers);
}
public UserAuthResult authenticate(String id, String password) { public UserAuthResult authenticate(String id, String password) {
_MatrixID mxid = MatrixID.asAcceptable(id); _MatrixID mxid = MatrixID.asAcceptable(id);
for (AuthenticatorProvider provider : providers) { for (AuthenticatorProvider provider : providers) {
@@ -59,6 +62,7 @@ public class AuthManager {
continue; continue;
} }
log.info("Attempting auth with " + provider.getClass().getSimpleName());
BackendAuthResult result = provider.authenticate(mxid, password); BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) { if (result.isSuccess()) {

View File

@@ -49,8 +49,9 @@ public class BackendAuthResult {
return r; return r;
} }
public void fail() { public BackendAuthResult fail() {
success = false; success = false;
return this;
} }
public static BackendAuthResult success(String id, UserIdType type, String displayName) { public static BackendAuthResult success(String id, UserIdType type, String displayName) {
@@ -63,10 +64,11 @@ public class BackendAuthResult {
return r; return r;
} }
public void succeed(String id, String type, String displayName) { public BackendAuthResult succeed(String id, String type, String displayName) {
this.success = true; this.success = true;
this.id = new UserID(type, id); this.id = new UserID(type, id);
this.profile.displayName = displayName; this.profile.displayName = displayName;
return this;
} }
private Boolean success; private Boolean success;

View File

@@ -0,0 +1,134 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.google;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import io.kamax.mxisd.config.GoogleConfig;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Component
public class GoogleProviderBackend implements AuthenticatorProvider {
private final Logger log = LoggerFactory.getLogger(GoogleProviderBackend.class);
private final GoogleConfig cfg;
private final LookupStrategy lookup;
private GoogleIdTokenVerifier verifier;
public Optional<GoogleIdToken> extractToken(String data) throws GeneralSecurityException, IOException {
return Optional.ofNullable(verifier.verify(data));
}
public List<ThreePid> extractThreepids(GoogleIdToken token) {
List<ThreePid> tpids = new ArrayList<>();
tpids.add(new ThreePid("io.kamax.google.id", token.getPayload().getSubject()));
if (token.getPayload().getEmailVerified()) {
tpids.add(new ThreePid("email", token.getPayload().getEmail()));
}
return tpids;
}
@Autowired
public GoogleProviderBackend(GoogleConfig cfg, LookupStrategy lookup) {
this.cfg = cfg;
this.lookup = lookup;
if (isEnabled()) {
try {
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
.setAudience(Collections.singletonList(cfg.getClient().getId()))
.build();
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
}
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
@Override
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
BackendAuthResult result = new BackendAuthResult();
try {
return extractToken(password).map(idToken -> {
GoogleIdToken.Payload payload = idToken.getPayload();
if (!payload.getEmailVerified()) { // We only want users who validated their email
return BackendAuthResult.failure();
}
// Get user identifier
String userId = payload.getSubject();
// We validate that the user who authenticated has his Google account associated already
return lookup.find("io.kamax.google.id", userId, false).map(r -> {
if (!r.getMxid().equals(mxid)) {
return result.fail();
}
// Get profile information from payload
extractThreepids(idToken).forEach(result::withThreePid);
String name = (String) payload.get("name");
payload.getUnknownKeys().keySet().forEach(key -> {
log.info("Unknown key in Google profile: {} -> ", key, payload.get(key));
});
return result.succeed(mxid.getId(), UserIdType.MatrixID.getId(), name);
}).orElse(BackendAuthResult.failure());
}).orElse(BackendAuthResult.failure());
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (IOException e) {
log.error("Unable to authenticate via Google due to network error", e);
return result.fail();
}
}
}

View File

@@ -59,6 +59,10 @@ public abstract class SqlThreePidProvider implements IThreePidProvider, ProfileP
this.mxCfg = mxCfg; this.mxCfg = mxCfg;
} }
protected Connection getConnection() throws SQLException {
return pool.get();
}
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return cfg.isEnabled(); return cfg.isEnabled();
@@ -119,7 +123,7 @@ public abstract class SqlThreePidProvider implements IThreePidProvider, ProfileP
List<_ThreePid> threepids = new ArrayList<>(); List<_ThreePid> threepids = new ArrayList<>();
String stmtSql = cfg.getProfile().getThreepid().getQuery(); String stmtSql = cfg.getProfile().getThreepid().getQuery();
try (Connection conn = pool.get()) { try (Connection conn = getConnection()) {
PreparedStatement stmt = conn.prepareStatement(stmtSql); PreparedStatement stmt = conn.prepareStatement(stmtSql);
stmt.setString(1, mxid.getId()); stmt.setString(1, mxid.getId());

View File

@@ -20,17 +20,50 @@
package io.kamax.mxisd.backend.sql; package io.kamax.mxisd.backend.sql;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.sql.synapse.SynapseSqlProviderConfig; import io.kamax.mxisd.config.sql.synapse.SynapseSqlProviderConfig;
import io.kamax.mxisd.profile.ProfileWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.Instant;
@Component @Component
public class SynapseSqlThreePidProvider extends SqlThreePidProvider { public class SynapseSqlThreePidProvider extends SqlThreePidProvider implements ProfileWriter {
private final Logger log = LoggerFactory.getLogger(SynapseSqlThreePidProvider.class);
@Autowired @Autowired
public SynapseSqlThreePidProvider(SynapseSqlProviderConfig cfg, MatrixConfig mxCfg) { public SynapseSqlThreePidProvider(SynapseSqlProviderConfig cfg, MatrixConfig mxCfg) {
super(cfg, mxCfg); super(cfg, mxCfg);
} }
@Override
public boolean addThreepid(_MatrixID mxid, ThreePid tpid) {
try (Connection conn = getConnection()) {
PreparedStatement stmt = conn.prepareStatement("INSERT INTO user_threepids (user_id, medium, address, validated_at, added_at) values (?,?,?,?,?)");
stmt.setString(1, mxid.getId());
stmt.setString(2, tpid.getMedium());
stmt.setString(3, tpid.getAddress());
stmt.setLong(4, Instant.now().toEpochMilli());
stmt.setLong(5, Instant.now().toEpochMilli());
int rows = stmt.executeUpdate();
if (rows != 1) {
log.error("Unable to update 3PID info. Modified row(s): {}", rows);
}
return true;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
} }

View File

@@ -0,0 +1,107 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.config;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("google")
public class GoogleConfig {
public static class Client {
private String id;
private String secret;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
private final Logger log = LoggerFactory.getLogger(GoogleConfig.class);
private boolean enabled;
private Client client = new Client();
private String medium = "io.kamax.google.id";
private String prefix = "google_";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
@PostConstruct
public void build() {
log.info("--- Google config ---");
log.info("Enabled: {}", isEnabled());
log.info("Client ID: {}", getClient().getId());
log.info("Client secret set? {}", StringUtils.isNotBlank(getClient().getSecret()));
log.info("3PID medium: {}", getMedium());
log.info("MXID prefix: {}", getPrefix());
}
}

View File

@@ -359,7 +359,6 @@ public abstract class LdapConfig {
log.info("Host: {}", connection.getHost()); log.info("Host: {}", connection.getHost());
log.info("Port: {}", connection.getPort()); log.info("Port: {}", connection.getPort());
log.info("TLS: {}", connection.isTls());
log.info("Bind DN: {}", connection.getBindDn()); log.info("Bind DN: {}", connection.getBindDn());
log.info("Base DN: {}", connection.getBaseDn()); log.info("Base DN: {}", connection.getBaseDn());

View File

@@ -0,0 +1,159 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.controller.auth.v1;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.backend.google.GoogleProviderBackend;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.GsonUtil;
import io.kamax.mxisd.util.RestClientUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
@RestController
@CrossOrigin
@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class RegistrationController {
private final Logger log = LoggerFactory.getLogger(RegistrationController.class);
private final String registerV1Url = "/_matrix/client/r0/register";
private GoogleProviderBackend google;
private ProfileManager pMgr;
private ClientDnsOverwrite dns;
private CloseableHttpClient client;
private Gson gson;
private GsonParser parser;
@Autowired
public RegistrationController(GoogleProviderBackend google, ProfileManager pMgr, ClientDnsOverwrite dns, CloseableHttpClient client) {
this.google = google;
this.pMgr = pMgr;
this.dns = dns;
this.client = client;
this.gson = GsonUtil.build();
this.parser = new GsonParser(gson);
}
private String resolveProxyUrl(HttpServletRequest req) {
URI target = URI.create(req.getRequestURL().toString());
URIBuilder builder = dns.transform(target);
String urlToLogin = builder.toString();
log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin);
return urlToLogin;
}
@RequestMapping(path = registerV1Url, method = RequestMethod.GET)
public String getLogin(HttpServletRequest req, HttpServletResponse res) {
try (CloseableHttpResponse hsResponse = client.execute(new HttpGet(resolveProxyUrl(req)))) {
res.setStatus(hsResponse.getStatusLine().getStatusCode());
return EntityUtils.toString(hsResponse.getEntity());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@RequestMapping(path = registerV1Url, method = RequestMethod.POST)
public String register(HttpServletRequest req, HttpServletResponse res) {
List<ThreePid> ids = new ArrayList<>();
try {
JsonObject reqJsonObject = parser.parse(req.getInputStream());
GsonUtil.findObj(reqJsonObject, "auth").ifPresent(auth -> {
GsonUtil.findPrimitive(auth, "type").ifPresent(type -> {
if (StringUtils.equals("io.kamax.google.auth", type.getAsString())) {
log.info("Got registration attempt with Google account");
if (!auth.has("googleId")) {
throw new IllegalArgumentException("Google ID is missing");
}
String gId = auth.get("googleId").getAsString();
try {
GoogleIdToken token = google.extractToken(reqJsonObject.get("password").getAsString()).orElseThrow(() -> new IllegalArgumentException("Google ID Token is missing or invalid"));
if (!StringUtils.equals(gId, token.getPayload().getSubject())) {
throw new IllegalArgumentException("Google ID does not match token");
}
log.info("Google ID: {}", gId);
ids.addAll(google.extractThreepids(token));
auth.addProperty("type", "m.login.dummy");
auth.remove("googleId");
reqJsonObject.addProperty("username", "g-" + gId);
reqJsonObject.addProperty("password", "");
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
});
});
log.info("Sending body: {}", gson.toJson(reqJsonObject));
HttpPost httpPost = RestClientUtils.post(resolveProxyUrl(req), gson, reqJsonObject);
try (CloseableHttpResponse httpResponse = client.execute(httpPost)) {
int sc = httpResponse.getStatusLine().getStatusCode();
String body = EntityUtils.toString(httpResponse.getEntity());
JsonObject json = parser.parse(body);
if (sc == 200 && json.has("user_id")) {
// Required here as synapse doesn't call pass provider on register
log.info("User was registered, adding 3PIDs");
_MatrixID mxid = new MatrixID(json.get("user_id").getAsString());
for (ThreePid tpid : ids) {
pMgr.addThreepid(mxid, tpid);
}
}
res.setStatus(sc);
return body;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -20,8 +20,8 @@
package io.kamax.mxisd.controller.directory.v1.io; package io.kamax.mxisd.controller.directory.v1.io;
import java.util.HashSet; import java.util.ArrayList;
import java.util.Set; import java.util.List;
public class UserDirectorySearchResult { public class UserDirectorySearchResult {
@@ -55,31 +55,10 @@ public class UserDirectorySearchResult {
this.userId = userId; this.userId = userId;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Result result = (Result) o;
if (displayName != null ? !displayName.equals(result.displayName) : result.displayName != null)
return false;
if (avatarUrl != null ? !avatarUrl.equals(result.avatarUrl) : result.avatarUrl != null) return false;
return userId.equals(result.userId);
}
@Override
public int hashCode() {
int result = displayName != null ? displayName.hashCode() : 0;
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
result = 31 * result + userId.hashCode();
return result;
}
} }
private boolean limited; private boolean limited;
private Set<Result> results = new HashSet<>(); private List<Result> results = new ArrayList<>();
public boolean isLimited() { public boolean isLimited() {
return limited; return limited;
@@ -89,11 +68,11 @@ public class UserDirectorySearchResult {
this.limited = limited; this.limited = limited;
} }
public Set<Result> getResults() { public List<Result> getResults() {
return results; return results;
} }
public void setResults(Set<Result> results) { public void setResults(List<Result> results) {
this.results = results; this.results = results;
} }

View File

@@ -23,7 +23,6 @@ package io.kamax.mxisd.lookup.provider;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.controller.identity.v1.ClientBulkLookupRequest; import io.kamax.mxisd.controller.identity.v1.ClientBulkLookupRequest;
import io.kamax.mxisd.exception.InvalidResponseJsonException; import io.kamax.mxisd.exception.InvalidResponseJsonException;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
@@ -34,20 +33,18 @@ import io.kamax.mxisd.matrix.IdentityServerUtils;
import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.RestClientUtils; import io.kamax.mxisd.util.RestClientUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils; import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -62,9 +59,6 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher
private Gson gson = new Gson(); private Gson gson = new Gson();
private GsonParser parser = new GsonParser(gson); private GsonParser parser = new GsonParser(gson);
@Autowired
private CloseableHttpClient client;
@Override @Override
public boolean isUsable(String remote) { public boolean isUsable(String remote) {
return IdentityServerUtils.isUsable(remote); return IdentityServerUtils.isUsable(remote);
@@ -75,40 +69,24 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher
log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote); log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote);
try { try {
URIBuilder b = new URIBuilder(remote); HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
b.setPath("/_matrix/identity/api/v1/lookup"); remote + "/_matrix/identity/api/v1/lookup?medium=" + request.getType() + "&address=" + request.getThreePid()
b.addParameter("medium", request.getType()); ).openConnection();
b.addParameter("address", request.getThreePid()); JsonObject obj = parser.parse(rootSrvConn.getInputStream());
HttpGet req = new HttpGet(b.build()); if (obj.has("address")) {
log.info("Found 3PID mapping: {}", gson.toJson(obj));
try (CloseableHttpResponse res = client.execute(req)) { return Optional.of(SingleLookupReply.fromRecursive(request, gson.toJson(obj)));
int statusCode = res.getStatusLine().getStatusCode();
String body = EntityUtils.toString(res.getEntity());
if (statusCode != 200) {
log.warn("Remote returned status code {}", statusCode);
log.warn("Body: {}", body);
return Optional.empty();
}
JsonObject obj = GsonUtil.parseObj(body);
if (obj.has("address")) {
log.debug("Found 3PID mapping: {}", gson.toJson(obj));
return Optional.of(SingleLookupReply.fromRecursive(request, gson.toJson(obj)));
}
log.info("Empty 3PID mapping from {}", remote);
return Optional.empty();
} }
log.info("Empty 3PID mapping from {}", remote);
return Optional.empty();
} catch (IOException e) { } catch (IOException e) {
log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage()); log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage());
return Optional.empty(); return Optional.empty();
} catch (JsonParseException e) { } catch (JsonParseException e) {
log.warn("Invalid JSON answer from {}", remote); log.warn("Invalid JSON answer from {}", remote);
return Optional.empty(); return Optional.empty();
} catch (URISyntaxException e) {
log.warn("Invalid remote address: {}", e.getMessage(), e);
return Optional.empty();
} }
} }
@@ -120,15 +98,12 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher
mappingRequest.setMappings(mappings); mappingRequest.setMappings(mappings);
String url = remote + "/_matrix/identity/api/v1/bulk_lookup"; String url = remote + "/_matrix/identity/api/v1/bulk_lookup";
CloseableHttpClient client = HttpClients.createDefault();
try { try {
HttpPost request = RestClientUtils.post(url, mappingRequest); HttpPost request = RestClientUtils.post(url, mappingRequest);
try (CloseableHttpResponse response = client.execute(request)) { try (CloseableHttpResponse response = client.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode(); if (response.getStatusLine().getStatusCode() != 200) {
String body = EntityUtils.toString(response.getEntity()); log.info("Could not perform lookup at {} due to HTTP return code: {}", url, response.getStatusLine().getStatusCode());
if (statusCode != 200) {
log.warn("Could not perform lookup at {} due to HTTP return code: {}", url, statusCode);
log.warn("Body: {}", body);
return mappingsFound; return mappingsFound;
} }

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.profile; package io.kamax.mxisd.profile;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid; import io.kamax.matrix._ThreePid;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -32,16 +33,21 @@ import java.util.stream.Collectors;
@Component @Component
public class ProfileManager { public class ProfileManager {
private List<ProfileProvider> providers; private List<ProfileProvider> readers;
private List<ProfileWriter> writers;
public ProfileManager(List<ProfileProvider> providers) { public ProfileManager(List<ProfileProvider> providers, List<ProfileWriter> writers) {
this.providers = providers.stream() this.readers = providers.stream()
.filter(ProfileProvider::isEnabled) .filter(ProfileProvider::isEnabled)
.collect(Collectors.toList()); .collect(Collectors.toList());
this.writers = writers.stream()
.filter(ProfileWriter::isEnabled)
.collect(Collectors.toList());
} }
public <T> List<T> get(Function<ProfileProvider, List<T>> function) { public <T> List<T> get(Function<ProfileProvider, List<T>> function) {
return providers.stream() return readers.stream()
.map(function) .map(function)
.flatMap(Collection::stream) .flatMap(Collection::stream)
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -55,4 +61,8 @@ public class ProfileManager {
return get(p -> p.getRoles(mxid)); return get(p -> p.getRoles(mxid));
} }
public void addThreepid(_MatrixID mxid, ThreePid tpid) {
writers.forEach(w -> w.addThreepid(mxid, tpid));
}
} }

View File

@@ -0,0 +1,32 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 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.profile;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID;
public interface ProfileWriter {
boolean isEnabled();
boolean addThreepid(_MatrixID mxid, ThreePid tpid);
}

View File

@@ -277,7 +277,8 @@ public class SessionMananger {
} }
String is = servers.get(0); String is = servers.get(0);
String url = IdentityServerUtils.findIsUrlForDomain(is).orElse(is); String url = IdentityServerUtils.findIsUrlForDomain(is)
.orElseThrow(() -> new InternalServerError(is + " could not be resolved to an Identity server"));
log.info("Will use IS endpoint {}", url); log.info("Will use IS endpoint {}", url);
String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16); String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16);

View File

@@ -30,11 +30,7 @@ public class CloseableHttpClientFactory {
@Bean @Bean
public CloseableHttpClient getClient() { public CloseableHttpClient getClient() {
return HttpClients.custom() return HttpClients.custom().setUserAgent("mxisd").build();
.setUserAgent("mxisd")
.setMaxConnPerRoute(Integer.MAX_VALUE)
.setMaxConnTotal(Integer.MAX_VALUE)
.build();
} }
} }

View File

@@ -24,7 +24,7 @@ import io.kamax.matrix.crypto.KeyFileStore;
import io.kamax.matrix.crypto.KeyManager; import io.kamax.matrix.crypto.KeyManager;
import io.kamax.matrix.crypto.SignatureManager; import io.kamax.matrix.crypto.SignatureManager;
import io.kamax.mxisd.config.KeyConfig; import io.kamax.mxisd.config.KeyConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.MatrixConfig;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -50,8 +50,8 @@ public class CryptoFactory {
} }
@Bean @Bean
public SignatureManager getSignatureManager(KeyManager keyMgr, ServerConfig cfg) { public SignatureManager getSignatureManager(KeyManager keyMgr, MatrixConfig mxCfg) {
return new SignatureManager(keyMgr, cfg.getName()); return new SignatureManager(keyMgr, mxCfg.getDomain());
} }
} }

View File

@@ -45,8 +45,8 @@ public class GsonParser {
this.gson = gson; this.gson = gson;
} }
public JsonObject parse(InputStream stream) throws IOException { public JsonObject parse(String raw) {
JsonElement el = parser.parse(IOUtils.toString(stream, StandardCharsets.UTF_8)); JsonElement el = parser.parse(raw);
if (!el.isJsonObject()) { if (!el.isJsonObject()) {
throw new InvalidResponseJsonException("Response body is not a JSON object"); throw new InvalidResponseJsonException("Response body is not a JSON object");
} }
@@ -54,6 +54,10 @@ public class GsonParser {
return el.getAsJsonObject(); return el.getAsJsonObject();
} }
public JsonObject parse(InputStream stream) throws IOException {
return parse(IOUtils.toString(stream, StandardCharsets.UTF_8));
}
public <T> T parse(HttpServletRequest req, Class<T> type) throws IOException { public <T> T parse(HttpServletRequest req, Class<T> type) throws IOException {
return gson.fromJson(parse(req.getInputStream()), type); return gson.fromJson(parse(req.getInputStream()), type);
} }

View File

@@ -33,7 +33,8 @@ import org.junit.Test;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.Assert.*; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class RestDirectoryProviderTest { public class RestDirectoryProviderTest {
@@ -88,8 +89,8 @@ public class RestDirectoryProviderTest {
UserDirectorySearchResult result = p.searchByDisplayName(byNameSearch); UserDirectorySearchResult result = p.searchByDisplayName(byNameSearch);
assertTrue(!result.isLimited()); assertTrue(!result.isLimited());
assertEquals(1, result.getResults().size()); assertTrue(result.getResults().size() == 1);
UserDirectorySearchResult.Result entry = result.getResults().iterator().next(); UserDirectorySearchResult.Result entry = result.getResults().get(0);
assertNotNull(entry); assertNotNull(entry);
assertTrue(StringUtils.equals(byNameAvatar, entry.getAvatarUrl())); assertTrue(StringUtils.equals(byNameAvatar, entry.getAvatarUrl()));
assertTrue(StringUtils.equals(byNameDisplay, entry.getDisplayName())); assertTrue(StringUtils.equals(byNameDisplay, entry.getDisplayName()));
@@ -131,8 +132,8 @@ public class RestDirectoryProviderTest {
UserDirectorySearchResult result = p.searchBy3pid(byThreepidSearch); UserDirectorySearchResult result = p.searchBy3pid(byThreepidSearch);
assertTrue(!result.isLimited()); assertTrue(!result.isLimited());
assertEquals(1, result.getResults().size()); assertTrue(result.getResults().size() == 1);
UserDirectorySearchResult.Result entry = result.getResults().iterator().next(); UserDirectorySearchResult.Result entry = result.getResults().get(0);
assertNotNull(entry); assertNotNull(entry);
assertTrue(StringUtils.equals(byThreepidAvatar, entry.getAvatarUrl())); assertTrue(StringUtils.equals(byThreepidAvatar, entry.getAvatarUrl()));
assertTrue(StringUtils.equals(byThreepidDisplay, entry.getDisplayName())); assertTrue(StringUtils.equals(byThreepidDisplay, entry.getDisplayName()));