Bye bye Groovy, you won't be missed :(
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* mxisd - Matrix Identity Server Daemon
|
* mxisd - Matrix Identity Server Daemon
|
||||||
* Copyright (C) 2017 Maxime Dor
|
* Copyright (C) 2017 Maxime Dor
|
||||||
@@ -20,7 +18,9 @@ import java.util.regex.Pattern
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
apply plugin: 'groovy'
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
apply plugin: 'java'
|
||||||
apply plugin: 'org.springframework.boot'
|
apply plugin: 'org.springframework.boot'
|
||||||
|
|
||||||
def confFileName = "application.example.yaml"
|
def confFileName = "application.example.yaml"
|
||||||
@@ -70,9 +70,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// We are a groovy project
|
|
||||||
compile 'org.codehaus.groovy:groovy-all:2.4.7'
|
|
||||||
|
|
||||||
// Easy file management
|
// Easy file management
|
||||||
compile 'commons-io:commons-io:2.5'
|
compile 'commons-io:commons-io:2.5'
|
||||||
|
|
||||||
|
@@ -1,193 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.backend.firebase
|
|
||||||
|
|
||||||
import com.google.firebase.FirebaseApp
|
|
||||||
import com.google.firebase.FirebaseOptions
|
|
||||||
import com.google.firebase.auth.*
|
|
||||||
import com.google.firebase.internal.NonNull
|
|
||||||
import com.google.firebase.tasks.OnFailureListener
|
|
||||||
import com.google.firebase.tasks.OnSuccessListener
|
|
||||||
import io.kamax.matrix.ThreePidMedium
|
|
||||||
import io.kamax.matrix._MatrixID
|
|
||||||
import io.kamax.mxisd.ThreePid
|
|
||||||
import io.kamax.mxisd.UserIdType
|
|
||||||
import io.kamax.mxisd.auth.provider.AuthenticatorProvider
|
|
||||||
import io.kamax.mxisd.auth.provider.BackendAuthResult
|
|
||||||
import org.apache.commons.lang.StringUtils
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
|
|
||||||
|
|
||||||
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk
|
|
||||||
|
|
||||||
private boolean isEnabled;
|
|
||||||
private String domain;
|
|
||||||
private FirebaseApp fbApp;
|
|
||||||
private FirebaseAuth fbAuth;
|
|
||||||
|
|
||||||
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
|
|
||||||
try {
|
|
||||||
l.await(timeout, unit);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.warn("Interrupted while waiting for " + purpose);
|
|
||||||
result.failure();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GoogleFirebaseAuthenticator(boolean isEnabled) {
|
|
||||||
this.isEnabled = isEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
|
|
||||||
this(true);
|
|
||||||
this.domain = domain;
|
|
||||||
try {
|
|
||||||
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
|
|
||||||
fbAuth = FirebaseAuth.getInstance(fbApp);
|
|
||||||
|
|
||||||
log.info("Google Firebase Authentication is ready");
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Error when initializing Firebase", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FirebaseCredential getCreds(String credsPath) throws IOException {
|
|
||||||
if (StringUtils.isNotBlank(credsPath)) {
|
|
||||||
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
|
|
||||||
} else {
|
|
||||||
return FirebaseCredentials.applicationDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
|
|
||||||
if (StringUtils.isBlank(db)) {
|
|
||||||
throw new IllegalArgumentException("Firebase database is not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FirebaseOptions.Builder()
|
|
||||||
.setCredential(getCreds(credsPath))
|
|
||||||
.setDatabaseUrl(db)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return isEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitOnLatch(CountDownLatch l) {
|
|
||||||
try {
|
|
||||||
l.await(30, TimeUnit.SECONDS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.warn("Interrupted while waiting for Firebase auth check");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
|
|
||||||
if (!isEnabled()) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Trying to authenticate {}", mxid);
|
|
||||||
|
|
||||||
BackendAuthResult result = BackendAuthResult.failure();
|
|
||||||
|
|
||||||
String localpart = m.group(1);
|
|
||||||
CountDownLatch l = new CountDownLatch(1);
|
|
||||||
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
|
|
||||||
@Override
|
|
||||||
void onSuccess(FirebaseToken token) {
|
|
||||||
try {
|
|
||||||
if (!StringUtils.equals(localpart, token.getUid())) {
|
|
||||||
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
|
|
||||||
result = BackendAuthResult.failure();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, token.getName());
|
|
||||||
log.info("{} was successfully authenticated", mxid);
|
|
||||||
log.info("Fetching profile for {}", mxid);
|
|
||||||
CountDownLatch userRecordLatch = new CountDownLatch(1);
|
|
||||||
fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() {
|
|
||||||
@Override
|
|
||||||
void onSuccess(UserRecord user) {
|
|
||||||
try {
|
|
||||||
if (StringUtils.isNotBlank(user.getEmail())) {
|
|
||||||
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
|
|
||||||
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
userRecordLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).addOnFailureListener(new OnFailureListener() {
|
|
||||||
@Override
|
|
||||||
void onFailure(@NonNull Exception e) {
|
|
||||||
try {
|
|
||||||
log.warn("Unable to fetch Firebase user profile for {}", mxid);
|
|
||||||
result = BackendAuthResult.failure();
|
|
||||||
} finally {
|
|
||||||
userRecordLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile");
|
|
||||||
} finally {
|
|
||||||
l.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).addOnFailureListener(new OnFailureListener() {
|
|
||||||
@Override
|
|
||||||
void onFailure(@NonNull Exception e) {
|
|
||||||
try {
|
|
||||||
if (e instanceof IllegalArgumentException) {
|
|
||||||
log.info("Failure to authenticate {}: invalid firebase token", mxid);
|
|
||||||
} else {
|
|
||||||
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
|
|
||||||
log.info("Exception", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = BackendAuthResult.failure();
|
|
||||||
} finally {
|
|
||||||
l.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.backend.ldap
|
|
||||||
|
|
||||||
import io.kamax.mxisd.config.MatrixConfig
|
|
||||||
import io.kamax.mxisd.lookup.SingleLookupReply
|
|
||||||
import io.kamax.mxisd.lookup.SingleLookupRequest
|
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
|
||||||
import io.kamax.mxisd.lookup.provider.IThreePidProvider
|
|
||||||
import org.apache.commons.lang.StringUtils
|
|
||||||
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException
|
|
||||||
import org.apache.directory.api.ldap.model.cursor.EntryCursor
|
|
||||||
import org.apache.directory.api.ldap.model.entry.Attribute
|
|
||||||
import org.apache.directory.api.ldap.model.entry.Entry
|
|
||||||
import org.apache.directory.api.ldap.model.message.SearchScope
|
|
||||||
import org.apache.directory.ldap.client.api.LdapConnection
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
|
|
||||||
|
|
||||||
public static final String UID = "uid"
|
|
||||||
public static final String MATRIX_ID = "mxid"
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class)
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MatrixConfig mxCfg
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean isEnabled() {
|
|
||||||
return getCfg().isEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUidAttribute() {
|
|
||||||
return getCfg().getAttribute().getUid().getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean isLocal() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int getPriority() {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> lookup(LdapConnection conn, String medium, String value) {
|
|
||||||
String uidAttribute = getUidAttribute()
|
|
||||||
|
|
||||||
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium)
|
|
||||||
if (!queryOpt.isPresent()) {
|
|
||||||
log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
|
|
||||||
return Optional.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
String searchQuery = queryOpt.get().replaceAll("%3pid", value)
|
|
||||||
EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)
|
|
||||||
try {
|
|
||||||
while (cursor.next()) {
|
|
||||||
Entry entry = cursor.get()
|
|
||||||
log.info("Found possible match, DN: {}", entry.getDn().getName())
|
|
||||||
|
|
||||||
Attribute attribute = entry.get(uidAttribute)
|
|
||||||
if (attribute == null) {
|
|
||||||
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
String data = attribute.get().toString()
|
|
||||||
if (data.length() < 1) {
|
|
||||||
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder matrixId = new StringBuilder()
|
|
||||||
// TODO Should we turn this block into a map of functions?
|
|
||||||
String uidType = getCfg().getAttribute().getUid().getType()
|
|
||||||
if (StringUtils.equals(UID, uidType)) {
|
|
||||||
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain())
|
|
||||||
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
|
|
||||||
matrixId.append(data)
|
|
||||||
} else {
|
|
||||||
log.warn("Bind was found but type {} is not supported", uidType)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("DN {} is a valid match", entry.getDn().getName())
|
|
||||||
return Optional.of(matrixId.toString())
|
|
||||||
}
|
|
||||||
} catch (CursorLdapReferralException e) {
|
|
||||||
log.warn("3PID {} is only available via referral, skipping", value)
|
|
||||||
} finally {
|
|
||||||
cursor.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> find(SingleLookupRequest request) {
|
|
||||||
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}")
|
|
||||||
|
|
||||||
LdapConnection conn = getConn()
|
|
||||||
try {
|
|
||||||
bind(conn)
|
|
||||||
|
|
||||||
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
|
|
||||||
if (mxid.isPresent()) {
|
|
||||||
return Optional.of(new SingleLookupReply(request, mxid.get()));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
conn.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("No match found")
|
|
||||||
return Optional.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
|
|
||||||
log.info("Looking up {} mappings", mappings.size())
|
|
||||||
List<ThreePidMapping> mappingsFound = new ArrayList<>()
|
|
||||||
|
|
||||||
LdapConnection conn = getConn()
|
|
||||||
try {
|
|
||||||
bind(conn)
|
|
||||||
|
|
||||||
for (ThreePidMapping mapping : mappings) {
|
|
||||||
try {
|
|
||||||
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue())
|
|
||||||
if (mxid.isPresent()) {
|
|
||||||
mapping.setMxid(mxid.get())
|
|
||||||
mappingsFound.add(mapping)
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
conn.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappingsFound
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.config
|
|
||||||
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.InitializingBean
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
|
|
||||||
class RecursiveLookupBridgeConfig implements InitializingBean {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class)
|
|
||||||
|
|
||||||
private boolean enabled
|
|
||||||
private boolean recursiveOnly
|
|
||||||
private String server
|
|
||||||
private Map<String, String> mappings = new HashMap<>()
|
|
||||||
|
|
||||||
boolean getEnabled() {
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean getRecursiveOnly() {
|
|
||||||
return recursiveOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
void setRecursiveOnly(boolean recursiveOnly) {
|
|
||||||
this.recursiveOnly = recursiveOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
String getServer() {
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
void setServer(String server) {
|
|
||||||
this.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> getMappings() {
|
|
||||||
return mappings
|
|
||||||
}
|
|
||||||
|
|
||||||
void setMappings(Map<String, String> mappings) {
|
|
||||||
this.mappings = mappings
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void afterPropertiesSet() throws Exception {
|
|
||||||
log.info("--- Bridge integration lookups config ---")
|
|
||||||
log.info("Enabled: {}", getEnabled())
|
|
||||||
if (getEnabled()) {
|
|
||||||
log.info("Recursive only: {}", getRecursiveOnly())
|
|
||||||
log.info("Fallback Server: {}", getServer())
|
|
||||||
log.info("Mappings: {}", mappings.size())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,129 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.config.ldap
|
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider
|
|
||||||
import org.apache.commons.lang.StringUtils
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
import javax.annotation.PostConstruct
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "ldap")
|
|
||||||
class LdapConfig {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(LdapConfig.class)
|
|
||||||
|
|
||||||
private boolean enabled
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private LdapConnectionConfig conn
|
|
||||||
private LdapAttributeConfig attribute
|
|
||||||
private LdapAuthConfig auth
|
|
||||||
private LdapIdentityConfig identity
|
|
||||||
|
|
||||||
boolean isEnabled() {
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
LdapConnectionConfig getConn() {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
void setConn(LdapConnectionConfig conn) {
|
|
||||||
this.conn = conn
|
|
||||||
}
|
|
||||||
|
|
||||||
LdapAttributeConfig getAttribute() {
|
|
||||||
return attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAttribute(LdapAttributeConfig attribute) {
|
|
||||||
this.attribute = attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
LdapAuthConfig getAuth() {
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAuth(LdapAuthConfig auth) {
|
|
||||||
this.auth = auth
|
|
||||||
}
|
|
||||||
|
|
||||||
LdapIdentityConfig getIdentity() {
|
|
||||||
return identity
|
|
||||||
}
|
|
||||||
|
|
||||||
void setIdentity(LdapIdentityConfig identity) {
|
|
||||||
this.identity = identity
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
void afterPropertiesSet() {
|
|
||||||
log.info("--- LDAP Config ---")
|
|
||||||
log.info("Enabled: {}", isEnabled())
|
|
||||||
|
|
||||||
if (!isEnabled()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(conn.getHost())) {
|
|
||||||
throw new IllegalStateException("LDAP Host must be configured!")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (1 > conn.getPort() || 65535 < conn.getPort()) {
|
|
||||||
throw new IllegalStateException("LDAP port is not valid")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(attribute.getUid().getType())) {
|
|
||||||
throw new IllegalStateException("Attribute UID Type cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(attribute.getUid().getValue())) {
|
|
||||||
throw new IllegalStateException("Attribute UID value cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
String uidType = attribute.getUid().getType();
|
|
||||||
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
|
|
||||||
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Host: {}", conn.getHost())
|
|
||||||
log.info("Port: {}", conn.getPort())
|
|
||||||
log.info("Bind DN: {}", conn.getBindDn())
|
|
||||||
log.info("Base DN: {}", conn.getBaseDn())
|
|
||||||
|
|
||||||
log.info("Attribute: {}", JsonOutput.toJson(attribute))
|
|
||||||
log.info("Auth: {}", JsonOutput.toJson(auth))
|
|
||||||
log.info("Identity: {}", JsonOutput.toJson(identity))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,121 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson
|
|
||||||
import io.kamax.mxisd.lookup.*
|
|
||||||
import io.kamax.mxisd.lookup.strategy.LookupStrategy
|
|
||||||
import io.kamax.mxisd.signature.SignatureManager
|
|
||||||
import org.apache.commons.lang.StringUtils
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
|
||||||
import org.springframework.web.bind.annotation.RestController
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
|
||||||
|
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.GET
|
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.POST
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@CrossOrigin
|
|
||||||
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
|
||||||
class MappingController {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(MappingController.class)
|
|
||||||
private JsonSlurper json = new JsonSlurper()
|
|
||||||
private Gson gson = new Gson()
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private LookupStrategy strategy
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private SignatureManager signMgr
|
|
||||||
|
|
||||||
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
|
|
||||||
lookupReq.setRequester(req.getRemoteAddr())
|
|
||||||
String xff = req.getHeader("X-FORWARDED-FOR")
|
|
||||||
lookupReq.setRecursive(StringUtils.isNotBlank(xff))
|
|
||||||
if (lookupReq.isRecursive()) {
|
|
||||||
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")))
|
|
||||||
}
|
|
||||||
|
|
||||||
lookupReq.setUserAgent(req.getHeader("USER-AGENT"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping(value = "/lookup", method = GET)
|
|
||||||
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
|
|
||||||
SingleLookupRequest lookupRequest = new SingleLookupRequest()
|
|
||||||
setRequesterInfo(lookupRequest, request)
|
|
||||||
lookupRequest.setType(medium)
|
|
||||||
lookupRequest.setThreePid(address)
|
|
||||||
|
|
||||||
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
|
|
||||||
|
|
||||||
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest)
|
|
||||||
if (!lookupOpt.isPresent()) {
|
|
||||||
log.info("No mapping was found, return empty JSON object")
|
|
||||||
return "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
SingleLookupReply lookup = lookupOpt.get()
|
|
||||||
if (lookup.isSigned()) {
|
|
||||||
log.info("Lookup is already signed, sending as-is")
|
|
||||||
return lookup.getBody();
|
|
||||||
} else {
|
|
||||||
log.info("Lookup is not signed, signing")
|
|
||||||
JsonObject obj = new Gson().toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject()
|
|
||||||
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)))
|
|
||||||
|
|
||||||
return gson.toJson(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping(value = "/bulk_lookup", method = POST)
|
|
||||||
String bulkLookup(HttpServletRequest request) {
|
|
||||||
BulkLookupRequest lookupRequest = new BulkLookupRequest()
|
|
||||||
setRequesterInfo(lookupRequest, request)
|
|
||||||
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
|
|
||||||
|
|
||||||
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(request.getInputStream().getText())
|
|
||||||
List<ThreePidMapping> mappings = new ArrayList<>()
|
|
||||||
for (List<String> mappingRaw : input.getThreepids()) {
|
|
||||||
ThreePidMapping mapping = new ThreePidMapping()
|
|
||||||
mapping.setMedium(mappingRaw.get(0))
|
|
||||||
mapping.setValue(mappingRaw.get(1))
|
|
||||||
mappings.add(mapping)
|
|
||||||
}
|
|
||||||
lookupRequest.setMappings(mappings)
|
|
||||||
|
|
||||||
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer()
|
|
||||||
answer.addAll(strategy.find(lookupRequest))
|
|
||||||
return JsonOutput.toJson(answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.key
|
|
||||||
|
|
||||||
import io.kamax.mxisd.config.KeyConfig
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
|
||||||
import net.i2p.crypto.eddsa.KeyPairGenerator
|
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec
|
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.springframework.beans.factory.InitializingBean
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.PrivateKey
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class KeyManager implements InitializingBean {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private KeyConfig keyCfg
|
|
||||||
|
|
||||||
private EdDSAParameterSpec keySpecs
|
|
||||||
private EdDSAEngine signEngine
|
|
||||||
private List<KeyPair> keys
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void afterPropertiesSet() throws Exception {
|
|
||||||
keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
|
|
||||||
signEngine = new EdDSAEngine(MessageDigest.getInstance(keySpecs.getHashAlgorithm()))
|
|
||||||
keys = new ArrayList<>()
|
|
||||||
|
|
||||||
Path privKey = Paths.get(keyCfg.getPath())
|
|
||||||
|
|
||||||
if (!Files.exists(privKey)) {
|
|
||||||
KeyPair pair = (new KeyPairGenerator()).generateKeyPair()
|
|
||||||
String keyEncoded = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded())
|
|
||||||
FileUtils.writeStringToFile(privKey.toFile(), keyEncoded, StandardCharsets.ISO_8859_1)
|
|
||||||
keys.add(pair)
|
|
||||||
} else {
|
|
||||||
if (Files.isDirectory(privKey)) {
|
|
||||||
throw new RuntimeException("Invalid path for private key: ${privKey.toString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.isReadable(privKey)) {
|
|
||||||
byte[] seed = Base64.getDecoder().decode(FileUtils.readFileToString(privKey.toFile(), StandardCharsets.ISO_8859_1))
|
|
||||||
EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, keySpecs)
|
|
||||||
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs)
|
|
||||||
keys.add(new KeyPair(new EdDSAPublicKey(pubKeySpec), new EdDSAPrivateKey(privKeySpec)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int getCurrentIndex() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyPair getKeys(int index) {
|
|
||||||
return keys.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivateKey getPrivateKey(int index) {
|
|
||||||
return getKeys(index).getPrivate()
|
|
||||||
}
|
|
||||||
|
|
||||||
EdDSAPublicKey getPublicKey(int index) {
|
|
||||||
return (EdDSAPublicKey) getKeys(index).getPublic()
|
|
||||||
}
|
|
||||||
|
|
||||||
EdDSAParameterSpec getSpecs() {
|
|
||||||
return keySpecs
|
|
||||||
}
|
|
||||||
|
|
||||||
String getPublicKeyBase64(int index) {
|
|
||||||
return Base64.getEncoder().encodeToString(getPublicKey(index).getAbyte())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.lookup.provider
|
|
||||||
|
|
||||||
import groovy.json.JsonException
|
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
import io.kamax.mxisd.controller.v1.ClientBulkLookupRequest
|
|
||||||
import io.kamax.mxisd.lookup.SingleLookupReply
|
|
||||||
import io.kamax.mxisd.lookup.SingleLookupRequest
|
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
|
||||||
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher
|
|
||||||
import io.kamax.mxisd.matrix.IdentityServerUtils
|
|
||||||
import org.apache.http.HttpEntity
|
|
||||||
import org.apache.http.HttpResponse
|
|
||||||
import org.apache.http.client.HttpClient
|
|
||||||
import org.apache.http.client.entity.EntityBuilder
|
|
||||||
import org.apache.http.client.methods.HttpPost
|
|
||||||
import org.apache.http.entity.ContentType
|
|
||||||
import org.apache.http.impl.client.HttpClients
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.context.annotation.Lazy
|
|
||||||
import org.springframework.context.annotation.Scope
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Scope("prototype")
|
|
||||||
@Lazy
|
|
||||||
public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(RemoteIdentityServerFetcher.class)
|
|
||||||
|
|
||||||
private JsonSlurper json = new JsonSlurper()
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean isUsable(String remote) {
|
|
||||||
return IdentityServerUtils.isUsable(remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> find(String remote, SingleLookupRequest request) {
|
|
||||||
log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote)
|
|
||||||
|
|
||||||
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
|
|
||||||
"${remote}/_matrix/identity/api/v1/lookup?medium=${request.getType()}&address=${request.getThreePid()}"
|
|
||||||
).openConnection()
|
|
||||||
|
|
||||||
try {
|
|
||||||
String outputRaw = rootSrvConn.getInputStream().getText()
|
|
||||||
def output = json.parseText(outputRaw)
|
|
||||||
if (output['address']) {
|
|
||||||
log.info("Found 3PID mapping: {}", output)
|
|
||||||
|
|
||||||
return Optional.of(SingleLookupReply.fromRecursive(request, outputRaw))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Empty 3PID mapping from {}", remote)
|
|
||||||
return Optional.empty()
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage())
|
|
||||||
return Optional.empty()
|
|
||||||
} catch (JsonException e) {
|
|
||||||
log.warn("Invalid JSON answer from {}", remote)
|
|
||||||
return Optional.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<ThreePidMapping> find(String remote, List<ThreePidMapping> mappings) {
|
|
||||||
List<ThreePidMapping> mappingsFound = new ArrayList<>()
|
|
||||||
|
|
||||||
ClientBulkLookupRequest mappingRequest = new ClientBulkLookupRequest()
|
|
||||||
mappingRequest.setMappings(mappings)
|
|
||||||
|
|
||||||
String url = "${remote}/_matrix/identity/api/v1/bulk_lookup"
|
|
||||||
HttpClient client = HttpClients.createDefault()
|
|
||||||
try {
|
|
||||||
HttpPost request = new HttpPost(url)
|
|
||||||
request.setEntity(
|
|
||||||
EntityBuilder.create()
|
|
||||||
.setText(JsonOutput.toJson(mappingRequest))
|
|
||||||
.setContentType(ContentType.APPLICATION_JSON)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
HttpResponse response = client.execute(request)
|
|
||||||
try {
|
|
||||||
if (response.getStatusLine().getStatusCode() != 200) {
|
|
||||||
log.info("Could not perform lookup at {} due to HTTP return code: {}", url, response.getStatusLine().getStatusCode())
|
|
||||||
return mappingsFound
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpEntity entity = response.getEntity()
|
|
||||||
if (entity != null) {
|
|
||||||
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(entity.getContent().getText())
|
|
||||||
for (List<String> mappingRaw : input.getThreepids()) {
|
|
||||||
ThreePidMapping mapping = new ThreePidMapping()
|
|
||||||
mapping.setMedium(mappingRaw.get(0))
|
|
||||||
mapping.setValue(mappingRaw.get(1))
|
|
||||||
mapping.setMxid(mappingRaw.get(2))
|
|
||||||
mappingsFound.add(mapping)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info("HTTP response from {} was empty", remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappingsFound
|
|
||||||
} finally {
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,210 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.lookup.strategy
|
|
||||||
|
|
||||||
import edazdarevic.commons.net.CIDRUtils
|
|
||||||
import io.kamax.mxisd.config.RecursiveLookupConfig
|
|
||||||
import io.kamax.mxisd.lookup.*
|
|
||||||
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher
|
|
||||||
import io.kamax.mxisd.lookup.provider.IThreePidProvider
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.InitializingBean
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
import java.util.function.Predicate
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBean {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(RecursivePriorityLookupStrategy.class)
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RecursiveLookupConfig recursiveCfg
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private List<IThreePidProvider> providers
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private IBridgeFetcher bridge
|
|
||||||
|
|
||||||
private List<CIDRUtils> allowedCidr = new ArrayList<>()
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void afterPropertiesSet() throws Exception {
|
|
||||||
log.info("Found ${providers.size()} providers")
|
|
||||||
|
|
||||||
providers.sort(new Comparator<IThreePidProvider>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int compare(IThreePidProvider o1, IThreePidProvider o2) {
|
|
||||||
return Integer.compare(o2.getPriority(), o1.getPriority())
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
log.info("Recursive lookup enabled: {}", recursiveCfg.isEnabled())
|
|
||||||
for (String cidr : recursiveCfg.getAllowedCidr()) {
|
|
||||||
log.info("{} is allowed for recursion", cidr)
|
|
||||||
allowedCidr.add(new CIDRUtils(cidr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isAllowedForRecursive(String source) {
|
|
||||||
boolean canRecurse = false
|
|
||||||
|
|
||||||
if (recursiveCfg.isEnabled()) {
|
|
||||||
log.debug("Checking {} CIDRs for recursion", allowedCidr.size())
|
|
||||||
for (CIDRUtils cidr : allowedCidr) {
|
|
||||||
if (cidr.isInRange(source)) {
|
|
||||||
log.debug("{} is in range {}, allowing recursion", source, cidr.getNetworkAddress())
|
|
||||||
canRecurse = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
log.debug("{} is not in range {}", source, cidr.getNetworkAddress())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return canRecurse
|
|
||||||
}
|
|
||||||
|
|
||||||
List<IThreePidProvider> listUsableProviders(ALookupRequest request) {
|
|
||||||
return listUsableProviders(request, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<IThreePidProvider> listUsableProviders(ALookupRequest request, boolean forceRecursive) {
|
|
||||||
List<IThreePidProvider> usableProviders = new ArrayList<>()
|
|
||||||
|
|
||||||
boolean canRecurse = forceRecursive || isAllowedForRecursive(request.getRequester())
|
|
||||||
|
|
||||||
log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse)
|
|
||||||
for (IThreePidProvider provider : providers) {
|
|
||||||
if (provider.isEnabled() && (provider.isLocal() || canRecurse || forceRecursive)) {
|
|
||||||
usableProviders.add(provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usableProviders
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<IThreePidProvider> getLocalProviders() {
|
|
||||||
return providers.stream().filter(new Predicate<IThreePidProvider>() {
|
|
||||||
@Override
|
|
||||||
boolean test(IThreePidProvider iThreePidProvider) {
|
|
||||||
return iThreePidProvider.isEnabled() && iThreePidProvider.isLocal()
|
|
||||||
}
|
|
||||||
}).collect(Collectors.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
List<IThreePidProvider> getRemoteProviders() {
|
|
||||||
return providers.stream().filter(new Predicate<IThreePidProvider>() {
|
|
||||||
@Override
|
|
||||||
boolean test(IThreePidProvider iThreePidProvider) {
|
|
||||||
return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal()
|
|
||||||
}
|
|
||||||
}).collect(Collectors.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SingleLookupRequest build(String medium, String address) {
|
|
||||||
SingleLookupRequest req = new SingleLookupRequest();
|
|
||||||
req.setType(medium)
|
|
||||||
req.setThreePid(address)
|
|
||||||
req.setRequester("Internal")
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
|
|
||||||
return find(build(medium, address), recursive)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> findLocal(String medium, String address) {
|
|
||||||
return find(build(medium, address), getLocalProviders())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> findRemote(String medium, String address) {
|
|
||||||
return find(build(medium, address), getRemoteProviders())
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
|
|
||||||
return find(request, listUsableProviders(request, forceRecursive));
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<SingleLookupReply> find(SingleLookupRequest request, List<IThreePidProvider> providers) {
|
|
||||||
for (IThreePidProvider provider : providers) {
|
|
||||||
Optional<SingleLookupReply> lookupDataOpt = provider.find(request)
|
|
||||||
if (lookupDataOpt.isPresent()) {
|
|
||||||
return lookupDataOpt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
recursiveCfg.getBridge() != null &&
|
|
||||||
recursiveCfg.getBridge().getEnabled() &&
|
|
||||||
(!recursiveCfg.getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
|
|
||||||
) {
|
|
||||||
log.info("Using bridge failover for lookup")
|
|
||||||
return bridge.find(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> find(SingleLookupRequest request) {
|
|
||||||
return find(request, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request) {
|
|
||||||
return find(request, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
List<ThreePidMapping> find(BulkLookupRequest request) {
|
|
||||||
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())
|
|
||||||
List<ThreePidMapping> mapFoundAll = new ArrayList<>()
|
|
||||||
|
|
||||||
for (IThreePidProvider provider : listUsableProviders(request)) {
|
|
||||||
if (mapToDo.isEmpty()) {
|
|
||||||
log.info("No more mappings to lookup")
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
log.info("{} mappings remaining overall", mapToDo.size())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Using provider {} for remaining mappings", provider.getClass().getSimpleName())
|
|
||||||
List<ThreePidMapping> mapFound = provider.populate(mapToDo)
|
|
||||||
log.info("Provider {} returned {} mappings", provider.getClass().getSimpleName(), mapFound.size())
|
|
||||||
mapFoundAll.addAll(mapFound)
|
|
||||||
mapToDo.removeAll(mapFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapFoundAll
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* mxisd - Matrix Identity Server Daemon
|
|
||||||
* Copyright (C) 2017 Maxime Dor
|
|
||||||
*
|
|
||||||
* https://max.kamax.io/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.kamax.mxisd.signature
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import io.kamax.mxisd.config.ServerConfig
|
|
||||||
import io.kamax.mxisd.key.KeyManager
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.springframework.beans.factory.InitializingBean
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class SignatureManager implements InitializingBean {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private KeyManager keyMgr
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ServerConfig srvCfg
|
|
||||||
|
|
||||||
private EdDSAEngine signEngine
|
|
||||||
|
|
||||||
private String sign(String message) {
|
|
||||||
byte[] signRaw = signEngine.signOneShot(message.getBytes())
|
|
||||||
return Base64.getEncoder().encodeToString(signRaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject signMessageJson(String message) {
|
|
||||||
String sign = sign(message)
|
|
||||||
|
|
||||||
JSONObject keySignature = new JSONObject()
|
|
||||||
keySignature.put("ed25519:${keyMgr.getCurrentIndex()}", sign)
|
|
||||||
JSONObject signature = new JSONObject()
|
|
||||||
signature.put("${srvCfg.getName()}", keySignature)
|
|
||||||
|
|
||||||
return signature
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject signMessageGson(String message) {
|
|
||||||
String sign = sign(message)
|
|
||||||
|
|
||||||
JsonObject keySignature = new JsonObject()
|
|
||||||
keySignature.addProperty("ed25519:${keyMgr.getCurrentIndex()}", sign)
|
|
||||||
JsonObject signature = new JsonObject()
|
|
||||||
signature.add("${srvCfg.getName()}", keySignature);
|
|
||||||
|
|
||||||
return signature
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void afterPropertiesSet() throws Exception {
|
|
||||||
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
|
|
||||||
signEngine.initSign(keyMgr.getPrivateKey(keyMgr.getCurrentIndex()))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -18,16 +18,16 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd
|
package io.kamax.mxisd;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
class MatrixIdentityServerApplication {
|
class MatrixIdentityServerApplication {
|
||||||
|
|
||||||
static void main(String[] args) throws Exception {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(MatrixIdentityServerApplication.class, args)
|
SpringApplication.run(MatrixIdentityServerApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -49,20 +49,27 @@ public class BackendAuthResult {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fail() {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
public static BackendAuthResult success(String id, UserIdType type, String displayName) {
|
public static BackendAuthResult success(String id, UserIdType type, String displayName) {
|
||||||
return success(id, type.getId(), displayName);
|
return success(id, type.getId(), displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BackendAuthResult success(String id, String type, String displayName) {
|
public static BackendAuthResult success(String id, String type, String displayName) {
|
||||||
BackendAuthResult r = new BackendAuthResult();
|
BackendAuthResult r = new BackendAuthResult();
|
||||||
r.success = true;
|
r.succeed(id, type, displayName);
|
||||||
r.id = new UserID(type, id);
|
|
||||||
r.profile = new BackendAuthProfile();
|
|
||||||
r.profile.displayName = displayName;
|
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void succeed(String id, String type, String displayName) {
|
||||||
|
this.success = true;
|
||||||
|
this.id = new UserID(type, id);
|
||||||
|
this.profile = new BackendAuthProfile();
|
||||||
|
this.profile.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
private Boolean success;
|
private Boolean success;
|
||||||
private UserID id;
|
private UserID id;
|
||||||
private BackendAuthProfile profile = new BackendAuthProfile();
|
private BackendAuthProfile profile = new BackendAuthProfile();
|
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.backend.firebase;
|
||||||
|
|
||||||
|
import com.google.firebase.FirebaseApp;
|
||||||
|
import com.google.firebase.FirebaseOptions;
|
||||||
|
import com.google.firebase.auth.FirebaseAuth;
|
||||||
|
import com.google.firebase.auth.FirebaseCredential;
|
||||||
|
import com.google.firebase.auth.FirebaseCredentials;
|
||||||
|
import io.kamax.matrix.ThreePidMedium;
|
||||||
|
import io.kamax.matrix._MatrixID;
|
||||||
|
import io.kamax.mxisd.ThreePid;
|
||||||
|
import io.kamax.mxisd.UserIdType;
|
||||||
|
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
|
||||||
|
import io.kamax.mxisd.auth.provider.BackendAuthResult;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
|
||||||
|
|
||||||
|
private boolean isEnabled;
|
||||||
|
private FirebaseApp fbApp;
|
||||||
|
private FirebaseAuth fbAuth;
|
||||||
|
|
||||||
|
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) {
|
||||||
|
try {
|
||||||
|
l.await(30, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.warn("Interrupted while waiting for " + purpose);
|
||||||
|
result.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleFirebaseAuthenticator(boolean isEnabled) {
|
||||||
|
this.isEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleFirebaseAuthenticator(String credsPath, String db) {
|
||||||
|
this(true);
|
||||||
|
try {
|
||||||
|
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
|
||||||
|
fbAuth = FirebaseAuth.getInstance(fbApp);
|
||||||
|
|
||||||
|
log.info("Google Firebase Authentication is ready");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Error when initializing Firebase", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FirebaseCredential getCreds(String credsPath) throws IOException {
|
||||||
|
if (StringUtils.isNotBlank(credsPath)) {
|
||||||
|
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
|
||||||
|
} else {
|
||||||
|
return FirebaseCredentials.applicationDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
|
||||||
|
if (StringUtils.isBlank(db)) {
|
||||||
|
throw new IllegalArgumentException("Firebase database is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FirebaseOptions.Builder()
|
||||||
|
.setCredential(getCreds(credsPath))
|
||||||
|
.setDatabaseUrl(db)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitOnLatch(CountDownLatch l) {
|
||||||
|
try {
|
||||||
|
l.await(30, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.warn("Interrupted while waiting for Firebase auth check");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Trying to authenticate {}", mxid);
|
||||||
|
|
||||||
|
final BackendAuthResult result = BackendAuthResult.failure();
|
||||||
|
|
||||||
|
String localpart = mxid.getLocalPart();
|
||||||
|
CountDownLatch l = new CountDownLatch(1);
|
||||||
|
fbAuth.verifyIdToken(password).addOnSuccessListener(token -> {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.equals(localpart, token.getUid())) {
|
||||||
|
log.info("Failure to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", mxid, localpart, token.getUid());
|
||||||
|
result.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.succeed(mxid.getId(), UserIdType.MatrixID.getId(), token.getName());
|
||||||
|
log.info("{} was successfully authenticated", mxid);
|
||||||
|
log.info("Fetching profile for {}", mxid);
|
||||||
|
CountDownLatch userRecordLatch = new CountDownLatch(1);
|
||||||
|
fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> {
|
||||||
|
try {
|
||||||
|
if (StringUtils.isNotBlank(user.getEmail())) {
|
||||||
|
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
|
||||||
|
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
userRecordLatch.countDown();
|
||||||
|
}
|
||||||
|
}).addOnFailureListener(e -> {
|
||||||
|
try {
|
||||||
|
log.warn("Unable to fetch Firebase user profile for {}", mxid);
|
||||||
|
result.fail();
|
||||||
|
} finally {
|
||||||
|
userRecordLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
waitOnLatch(result, userRecordLatch, "Firebase user profile");
|
||||||
|
} finally {
|
||||||
|
l.countDown();
|
||||||
|
}
|
||||||
|
}).addOnFailureListener(e -> {
|
||||||
|
try {
|
||||||
|
if (e instanceof IllegalArgumentException) {
|
||||||
|
log.info("Failure to authenticate {}: invalid firebase token", mxid);
|
||||||
|
} else {
|
||||||
|
log.info("Failure to authenticate {}: {}", mxid, e.getMessage(), e);
|
||||||
|
log.info("Exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.fail();
|
||||||
|
} finally {
|
||||||
|
l.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
waitOnLatch(result, l, "Firebase auth check");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -18,40 +18,40 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.backend.firebase
|
package io.kamax.mxisd.backend.firebase;
|
||||||
|
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp;
|
||||||
import com.google.firebase.FirebaseOptions
|
import com.google.firebase.FirebaseOptions;
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth;
|
||||||
import com.google.firebase.auth.FirebaseCredential
|
import com.google.firebase.auth.FirebaseCredential;
|
||||||
import com.google.firebase.auth.FirebaseCredentials
|
import com.google.firebase.auth.FirebaseCredentials;
|
||||||
import com.google.firebase.auth.UserRecord
|
import com.google.firebase.auth.UserRecord;
|
||||||
import com.google.firebase.internal.NonNull
|
import com.google.firebase.tasks.OnFailureListener;
|
||||||
import com.google.firebase.tasks.OnFailureListener
|
import com.google.firebase.tasks.OnSuccessListener;
|
||||||
import com.google.firebase.tasks.OnSuccessListener
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.matrix.ThreePidMedium
|
import io.kamax.matrix.ThreePidMedium;
|
||||||
import io.kamax.mxisd.lookup.SingleLookupReply
|
import io.kamax.mxisd.lookup.SingleLookupReply;
|
||||||
import io.kamax.mxisd.lookup.SingleLookupRequest
|
import io.kamax.mxisd.lookup.SingleLookupRequest;
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
import io.kamax.mxisd.lookup.provider.IThreePidProvider
|
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.io.FileInputStream;
|
||||||
import java.util.concurrent.TimeUnit
|
import java.io.IOException;
|
||||||
import java.util.function.Consumer
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Pattern
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class GoogleFirebaseProvider implements IThreePidProvider {
|
public class GoogleFirebaseProvider implements IThreePidProvider {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class);
|
private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class);
|
||||||
|
|
||||||
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)");
|
|
||||||
|
|
||||||
private boolean isEnabled;
|
private boolean isEnabled;
|
||||||
private String domain;
|
private String domain;
|
||||||
private FirebaseApp fbApp;
|
|
||||||
private FirebaseAuth fbAuth;
|
private FirebaseAuth fbAuth;
|
||||||
|
|
||||||
public GoogleFirebaseProvider(boolean isEnabled) {
|
public GoogleFirebaseProvider(boolean isEnabled) {
|
||||||
@@ -61,8 +61,9 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
|
|||||||
public GoogleFirebaseProvider(String credsPath, String db, String domain) {
|
public GoogleFirebaseProvider(String credsPath, String db, String domain) {
|
||||||
this(true);
|
this(true);
|
||||||
this.domain = domain;
|
this.domain = domain;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
|
FirebaseApp fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
|
||||||
fbAuth = FirebaseAuth.getInstance(fbApp);
|
fbAuth = FirebaseAuth.getInstance(fbApp);
|
||||||
|
|
||||||
log.info("Google Firebase Authentication is ready");
|
log.info("Google Firebase Authentication is ready");
|
||||||
@@ -91,7 +92,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getMxid(UserRecord record) {
|
private String getMxid(UserRecord record) {
|
||||||
return "@${record.getUid()}:${domain}";
|
return new MatrixID(record.getUid(), domain).getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -118,71 +119,59 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<UserRecord> findInternal(String medium, String address) {
|
private Optional<UserRecord> findInternal(String medium, String address) {
|
||||||
UserRecord r;
|
final UserRecord[] r = new UserRecord[1];
|
||||||
CountDownLatch l = new CountDownLatch(1);
|
CountDownLatch l = new CountDownLatch(1);
|
||||||
|
|
||||||
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() {
|
OnSuccessListener<UserRecord> success = result -> {
|
||||||
@Override
|
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid());
|
||||||
void onSuccess(UserRecord result) {
|
r[0] = result;
|
||||||
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid())
|
l.countDown();
|
||||||
r = result;
|
|
||||||
l.countDown()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
OnFailureListener failure = new OnFailureListener() {
|
OnFailureListener failure = e -> {
|
||||||
@Override
|
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage());
|
||||||
void onFailure(@NonNull Exception e) {
|
r[0] = null;
|
||||||
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage())
|
l.countDown();
|
||||||
r = null;
|
|
||||||
l.countDown()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ThreePidMedium.Email.is(medium)) {
|
if (ThreePidMedium.Email.is(medium)) {
|
||||||
log.info("Performing E-mail 3PID lookup for {}", address)
|
log.info("Performing E-mail 3PID lookup for {}", address);
|
||||||
fbAuth.getUserByEmail(address)
|
fbAuth.getUserByEmail(address)
|
||||||
.addOnSuccessListener(success)
|
.addOnSuccessListener(success)
|
||||||
.addOnFailureListener(failure);
|
.addOnFailureListener(failure);
|
||||||
waitOnLatch(l);
|
waitOnLatch(l);
|
||||||
} else if (ThreePidMedium.PhoneNumber.is(medium)) {
|
} else if (ThreePidMedium.PhoneNumber.is(medium)) {
|
||||||
log.info("Performing msisdn 3PID lookup for {}", address)
|
log.info("Performing msisdn 3PID lookup for {}", address);
|
||||||
fbAuth.getUserByPhoneNumber(address)
|
fbAuth.getUserByPhoneNumber(address)
|
||||||
.addOnSuccessListener(success)
|
.addOnSuccessListener(success)
|
||||||
.addOnFailureListener(failure);
|
.addOnFailureListener(failure);
|
||||||
waitOnLatch(l);
|
waitOnLatch(l);
|
||||||
} else {
|
} else {
|
||||||
log.info("{} is not a supported 3PID medium", medium);
|
log.info("{} is not a supported 3PID medium", medium);
|
||||||
r = null;
|
r[0] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.ofNullable(r);
|
return Optional.ofNullable(r[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
|
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
|
||||||
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid())
|
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid());
|
||||||
if (urOpt.isPresent()) {
|
return urOpt.map(userRecord -> new SingleLookupReply(request, getMxid(userRecord)));
|
||||||
return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get())));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
|
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
|
||||||
List<ThreePidMapping> results = new ArrayList<>();
|
List<ThreePidMapping> results = new ArrayList<>();
|
||||||
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() {
|
mappings.parallelStream().forEach(o -> {
|
||||||
@Override
|
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
|
||||||
void accept(ThreePidMapping o) {
|
if (urOpt.isPresent()) {
|
||||||
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
|
ThreePidMapping result = new ThreePidMapping();
|
||||||
if (urOpt.isPresent()) {
|
result.setMedium(o.getMedium());
|
||||||
ThreePidMapping result = new ThreePidMapping();
|
result.setValue(o.getValue());
|
||||||
result.setMedium(o.getMedium())
|
result.setMxid(getMxid(urOpt.get()));
|
||||||
result.setValue(o.getValue())
|
results.add(result);
|
||||||
result.setMxid(getMxid(urOpt.get()))
|
|
||||||
results.add(result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.backend.ldap;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.config.MatrixConfig;
|
||||||
|
import io.kamax.mxisd.exception.InternalServerError;
|
||||||
|
import io.kamax.mxisd.lookup.SingleLookupReply;
|
||||||
|
import io.kamax.mxisd.lookup.SingleLookupRequest;
|
||||||
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
|
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.apache.directory.api.ldap.model.cursor.CursorException;
|
||||||
|
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
|
||||||
|
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
|
||||||
|
import org.apache.directory.api.ldap.model.entry.Attribute;
|
||||||
|
import org.apache.directory.api.ldap.model.entry.Entry;
|
||||||
|
import org.apache.directory.api.ldap.model.exception.LdapException;
|
||||||
|
import org.apache.directory.api.ldap.model.message.SearchScope;
|
||||||
|
import org.apache.directory.ldap.client.api.LdapConnection;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
|
||||||
|
|
||||||
|
public static final String UID = "uid";
|
||||||
|
public static final String MATRIX_ID = "mxid";
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MatrixConfig mxCfg;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return getCfg().isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUidAttribute() {
|
||||||
|
return getCfg().getAttribute().getUid().getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> lookup(LdapConnection conn, String medium, String value) {
|
||||||
|
String uidAttribute = getUidAttribute();
|
||||||
|
|
||||||
|
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium);
|
||||||
|
if (!queryOpt.isPresent()) {
|
||||||
|
log.warn("{} is not a configured 3PID type for LDAP lookup", medium);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String searchQuery = queryOpt.get().replaceAll("%3pid", value);
|
||||||
|
try (EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)) {
|
||||||
|
while (cursor.next()) {
|
||||||
|
Entry entry = cursor.get();
|
||||||
|
log.info("Found possible match, DN: {}", entry.getDn().getName());
|
||||||
|
|
||||||
|
Attribute attribute = entry.get(uidAttribute);
|
||||||
|
if (attribute == null) {
|
||||||
|
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String data = attribute.get().toString();
|
||||||
|
if (data.length() < 1) {
|
||||||
|
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder matrixId = new StringBuilder();
|
||||||
|
// TODO Should we turn this block into a map of functions?
|
||||||
|
String uidType = getCfg().getAttribute().getUid().getType();
|
||||||
|
if (StringUtils.equals(UID, uidType)) {
|
||||||
|
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain());
|
||||||
|
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
|
||||||
|
matrixId.append(data);
|
||||||
|
} else {
|
||||||
|
log.warn("Bind was found but type {} is not supported", uidType);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("DN {} is a valid match", entry.getDn().getName());
|
||||||
|
return Optional.of(matrixId.toString());
|
||||||
|
}
|
||||||
|
} catch (CursorLdapReferralException e) {
|
||||||
|
log.warn("3PID {} is only available via referral, skipping", value);
|
||||||
|
} catch (IOException | LdapException | CursorException e) {
|
||||||
|
throw new InternalServerError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
|
||||||
|
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}");
|
||||||
|
|
||||||
|
try (LdapConnection conn = getConn()) {
|
||||||
|
bind(conn);
|
||||||
|
|
||||||
|
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid());
|
||||||
|
if (mxid.isPresent()) {
|
||||||
|
return Optional.of(new SingleLookupReply(request, mxid.get()));
|
||||||
|
}
|
||||||
|
} catch (LdapException | IOException e) {
|
||||||
|
throw new InternalServerError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("No match found");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
|
||||||
|
log.info("Looking up {} mappings", mappings.size());
|
||||||
|
List<ThreePidMapping> mappingsFound = new ArrayList<>();
|
||||||
|
|
||||||
|
try (LdapConnection conn = getConn()) {
|
||||||
|
bind(conn);
|
||||||
|
|
||||||
|
for (ThreePidMapping mapping : mappings) {
|
||||||
|
try {
|
||||||
|
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue());
|
||||||
|
if (mxid.isPresent()) {
|
||||||
|
mapping.setMxid(mxid.get());
|
||||||
|
mappingsFound.add(mapping);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (LdapException | IOException e) {
|
||||||
|
throw new InternalServerError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingsFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -85,7 +85,7 @@ public class FirebaseConfig {
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return new GoogleFirebaseAuthenticator(false);
|
return new GoogleFirebaseAuthenticator(false);
|
||||||
} else {
|
} else {
|
||||||
return new GoogleFirebaseAuthenticator(credentials, database, mxCfg.getDomain());
|
return new GoogleFirebaseAuthenticator(credentials, database);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -18,23 +18,26 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.config
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "forward")
|
@ConfigurationProperties(prefix = "forward")
|
||||||
class ForwardConfig {
|
public class ForwardConfig {
|
||||||
|
|
||||||
private List<String> servers = new ArrayList<>()
|
private List<String> servers = new ArrayList<>();
|
||||||
|
|
||||||
List<String> getServers() {
|
public List<String> getServers() {
|
||||||
return servers
|
return servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setServers(List<String> servers) {
|
public void setServers(List<String> servers) {
|
||||||
this.servers = servers
|
this.servers = servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -18,32 +18,33 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.config
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
import io.kamax.mxisd.exception.ConfigurationException
|
import io.kamax.mxisd.exception.ConfigurationException;
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.springframework.beans.factory.InitializingBean
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "key")
|
@ConfigurationProperties(prefix = "key")
|
||||||
class KeyConfig implements InitializingBean {
|
public class KeyConfig {
|
||||||
|
|
||||||
private String path
|
private String path;
|
||||||
|
|
||||||
void setPath(String path) {
|
public void setPath(String path) {
|
||||||
this.path = path
|
this.path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPath() {
|
public String getPath() {
|
||||||
return path
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@PostConstruct
|
||||||
void afterPropertiesSet() throws Exception {
|
public void build() {
|
||||||
if (StringUtils.isBlank(getPath())) {
|
if (StringUtils.isBlank(getPath())) {
|
||||||
throw new ConfigurationException("key.path")
|
throw new ConfigurationException("key.path");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
|
||||||
|
public class RecursiveLookupBridgeConfig {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class);
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
private boolean recursiveOnly;
|
||||||
|
private String server;
|
||||||
|
private Map<String, String> mappings = new HashMap<>();
|
||||||
|
|
||||||
|
public boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getRecursiveOnly() {
|
||||||
|
return recursiveOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecursiveOnly(boolean recursiveOnly) {
|
||||||
|
this.recursiveOnly = recursiveOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServer(String server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getMappings() {
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMappings(Map<String, String> mappings) {
|
||||||
|
this.mappings = mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void build() {
|
||||||
|
log.info("--- Bridge integration lookups config ---");
|
||||||
|
log.info("Enabled: {}", getEnabled());
|
||||||
|
if (getEnabled()) {
|
||||||
|
log.info("Recursive only: {}", getRecursiveOnly());
|
||||||
|
log.info("Fallback Server: {}", getServer());
|
||||||
|
log.info("Mappings: {}", mappings.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -18,41 +18,43 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.config
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "lookup.recursive")
|
@ConfigurationProperties(prefix = "lookup.recursive")
|
||||||
class RecursiveLookupConfig {
|
public class RecursiveLookupConfig {
|
||||||
|
|
||||||
private boolean enabled
|
private boolean enabled;
|
||||||
private List<String> allowedCidr
|
private List<String> allowedCidr;
|
||||||
private RecursiveLookupBridgeConfig bridge
|
private RecursiveLookupBridgeConfig bridge;
|
||||||
|
|
||||||
boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEnabled(boolean enabled) {
|
public void setEnabled(boolean enabled) {
|
||||||
this.enabled = enabled
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> getAllowedCidr() {
|
public List<String> getAllowedCidr() {
|
||||||
return allowedCidr
|
return allowedCidr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAllowedCidr(List<String> allowedCidr) {
|
public void setAllowedCidr(List<String> allowedCidr) {
|
||||||
this.allowedCidr = allowedCidr
|
this.allowedCidr = allowedCidr;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecursiveLookupBridgeConfig getBridge() {
|
public RecursiveLookupBridgeConfig getBridge() {
|
||||||
return bridge
|
return bridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBridge(RecursiveLookupBridgeConfig bridge) {
|
public void setBridge(RecursiveLookupBridgeConfig bridge) {
|
||||||
this.bridge = bridge
|
this.bridge = bridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -18,56 +18,59 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.config
|
package io.kamax.mxisd.config;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "server")
|
@ConfigurationProperties(prefix = "server")
|
||||||
class ServerConfig implements InitializingBean {
|
public class ServerConfig {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(ServerConfig.class);
|
private Logger log = LoggerFactory.getLogger(ServerConfig.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MatrixConfig mxCfg;
|
private MatrixConfig mxCfg;
|
||||||
|
|
||||||
private String name
|
private String name;
|
||||||
private int port
|
private int port;
|
||||||
private String publicUrl
|
private String publicUrl;
|
||||||
|
|
||||||
String getName() {
|
public String getName() {
|
||||||
return name
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setName(String name) {
|
public void setName(String name) {
|
||||||
this.name = name
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getPort() {
|
public int getPort() {
|
||||||
return port
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPort(int port) {
|
public void setPort(int port) {
|
||||||
this.port = port
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPublicUrl() {
|
public String getPublicUrl() {
|
||||||
return publicUrl
|
return publicUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublicUrl(String publicUrl) {
|
public void setPublicUrl(String publicUrl) {
|
||||||
this.publicUrl = publicUrl
|
this.publicUrl = publicUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@PostConstruct
|
||||||
void afterPropertiesSet() throws Exception {
|
public void build() {
|
||||||
log.info("--- Server config ---")
|
log.info("--- Server config ---");
|
||||||
|
|
||||||
if (StringUtils.isBlank(getName())) {
|
if (StringUtils.isBlank(getName())) {
|
||||||
setName(mxCfg.getDomain());
|
setName(mxCfg.getDomain());
|
||||||
@@ -75,21 +78,21 @@ class ServerConfig implements InitializingBean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(getPublicUrl())) {
|
if (StringUtils.isBlank(getPublicUrl())) {
|
||||||
setPublicUrl("https://${getName()}");
|
setPublicUrl("https://" + getName());
|
||||||
log.debug("Public URL is empty, generating from name");
|
log.debug("Public URL is empty, generating from name");
|
||||||
} else {
|
} else {
|
||||||
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
|
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(getPublicUrl())
|
new URL(getPublicUrl());
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"))
|
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Name: {}", getName())
|
log.info("Name: {}", getName());
|
||||||
log.info("Port: {}", getPort())
|
log.info("Port: {}", getPort());
|
||||||
log.info("Public URL: {}", getPublicUrl())
|
log.info("Public URL: {}", getPublicUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
131
src/main/java/io/kamax/mxisd/config/ldap/LdapConfig.java
Normal file
131
src/main/java/io/kamax/mxisd/config/ldap/LdapConfig.java
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.config.ldap;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "ldap")
|
||||||
|
public class LdapConfig {
|
||||||
|
|
||||||
|
private static Gson gson = new Gson();
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(LdapConfig.class);
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LdapConnectionConfig conn;
|
||||||
|
private LdapAttributeConfig attribute;
|
||||||
|
private LdapAuthConfig auth;
|
||||||
|
private LdapIdentityConfig identity;
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdapConnectionConfig getConn() {
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConn(LdapConnectionConfig conn) {
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdapAttributeConfig getAttribute() {
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttribute(LdapAttributeConfig attribute) {
|
||||||
|
this.attribute = attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdapAuthConfig getAuth() {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuth(LdapAuthConfig auth) {
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LdapIdentityConfig getIdentity() {
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentity(LdapIdentityConfig identity) {
|
||||||
|
this.identity = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void build() {
|
||||||
|
log.info("--- LDAP Config ---");
|
||||||
|
log.info("Enabled: {}", isEnabled());
|
||||||
|
|
||||||
|
if (!isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(conn.getHost())) {
|
||||||
|
throw new IllegalStateException("LDAP Host must be configured!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 > conn.getPort() || 65535 < conn.getPort()) {
|
||||||
|
throw new IllegalStateException("LDAP port is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(attribute.getUid().getType())) {
|
||||||
|
throw new IllegalStateException("Attribute UID Type cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(attribute.getUid().getValue())) {
|
||||||
|
throw new IllegalStateException("Attribute UID value cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String uidType = attribute.getUid().getType();
|
||||||
|
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Host: {}", conn.getHost());
|
||||||
|
log.info("Port: {}", conn.getPort());
|
||||||
|
log.info("Bind DN: {}", conn.getBindDn());
|
||||||
|
log.info("Base DN: {}", conn.getBaseDn());
|
||||||
|
|
||||||
|
log.info("Attribute: {}", gson.toJson(attribute));
|
||||||
|
log.info("Auth: {}", gson.toJson(auth));
|
||||||
|
log.info("Identity: {}", gson.toJson(identity));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -18,28 +18,31 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
|
|
||||||
class ClientBulkLookupRequest {
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
private List<List<String>> threepids = new ArrayList<>()
|
public class ClientBulkLookupRequest {
|
||||||
|
|
||||||
List<List<String>> getThreepids() {
|
private List<List<String>> threepids = new ArrayList<>();
|
||||||
return threepids
|
|
||||||
|
public List<List<String>> getThreepids() {
|
||||||
|
return threepids;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setThreepids(List<List<String>> threepids) {
|
public void setThreepids(List<List<String>> threepids) {
|
||||||
this.threepids = threepids
|
this.threepids = threepids;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMappings(List<ThreePidMapping> mappings) {
|
public void setMappings(List<ThreePidMapping> mappings) {
|
||||||
for (ThreePidMapping mapping : mappings) {
|
for (ThreePidMapping mapping : mappings) {
|
||||||
List<String> threepid = new ArrayList<>()
|
List<String> threepid = new ArrayList<>();
|
||||||
threepid.add(mapping.getMedium())
|
threepid.add(mapping.getMedium());
|
||||||
threepid.add(mapping.getValue())
|
threepid.add(mapping.getValue());
|
||||||
threepids.add(threepid)
|
threepids.add(threepid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -18,47 +18,49 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson;
|
||||||
import io.kamax.matrix.MatrixID
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.mxisd.config.ServerConfig
|
import io.kamax.mxisd.config.ServerConfig;
|
||||||
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO
|
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO;
|
||||||
import io.kamax.mxisd.invitation.IThreePidInvite
|
import io.kamax.mxisd.invitation.IThreePidInvite;
|
||||||
import io.kamax.mxisd.invitation.IThreePidInviteReply
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
import io.kamax.mxisd.invitation.InvitationManager
|
import io.kamax.mxisd.invitation.InvitationManager;
|
||||||
import io.kamax.mxisd.invitation.ThreePidInvite
|
import io.kamax.mxisd.invitation.ThreePidInvite;
|
||||||
import io.kamax.mxisd.key.KeyManager
|
import io.kamax.mxisd.key.KeyManager;
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.POST
|
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||||
class InvitationController {
|
class InvitationController {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(InvitationController.class)
|
private Logger log = LoggerFactory.getLogger(InvitationController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private InvitationManager mgr
|
private InvitationManager mgr;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private KeyManager keyMgr
|
private KeyManager keyMgr;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ServerConfig srvCfg
|
private ServerConfig srvCfg;
|
||||||
|
|
||||||
private Gson gson = new Gson()
|
private Gson gson = new Gson();
|
||||||
|
|
||||||
@RequestMapping(value = "/store-invite", method = POST)
|
@RequestMapping(value = "/store-invite", method = POST)
|
||||||
String store(
|
String store(
|
||||||
@@ -67,14 +69,14 @@ class InvitationController {
|
|||||||
@RequestParam String medium,
|
@RequestParam String medium,
|
||||||
@RequestParam String address,
|
@RequestParam String address,
|
||||||
@RequestParam("room_id") String roomId) {
|
@RequestParam("room_id") String roomId) {
|
||||||
Map<String, String> parameters = new HashMap<>()
|
Map<String, String> parameters = new HashMap<>();
|
||||||
for (String key : request.getParameterMap().keySet()) {
|
for (String key : request.getParameterMap().keySet()) {
|
||||||
parameters.put(key, request.getParameter(key));
|
parameters.put(key, request.getParameter(key));
|
||||||
}
|
}
|
||||||
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters)
|
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters);
|
||||||
IThreePidInviteReply reply = mgr.storeInvite(invite)
|
IThreePidInviteReply reply = mgr.storeInvite(invite);
|
||||||
|
|
||||||
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()))
|
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -18,64 +18,64 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson;
|
||||||
import groovy.json.JsonOutput
|
import com.google.gson.JsonObject;
|
||||||
import io.kamax.mxisd.controller.v1.io.KeyValidityJson
|
import io.kamax.mxisd.controller.v1.io.KeyValidityJson;
|
||||||
import io.kamax.mxisd.exception.BadRequestException
|
import io.kamax.mxisd.exception.BadRequestException;
|
||||||
import io.kamax.mxisd.key.KeyManager
|
import io.kamax.mxisd.key.KeyManager;
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.GET
|
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||||
class KeyController {
|
public class KeyController {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(KeyController.class)
|
private Logger log = LoggerFactory.getLogger(KeyController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private KeyManager keyMgr
|
private KeyManager keyMgr;
|
||||||
|
|
||||||
private Gson gson = new Gson();
|
private Gson gson = new Gson();
|
||||||
private String validKey = gson.toJson(new KeyValidityJson(true));
|
private String validKey = gson.toJson(new KeyValidityJson(true));
|
||||||
private String invalidKey = gson.toJson(new KeyValidityJson(false));
|
private String invalidKey = gson.toJson(new KeyValidityJson(false));
|
||||||
|
|
||||||
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET)
|
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET)
|
||||||
String getKey(@PathVariable String keyType, @PathVariable int keyId) {
|
public String getKey(@PathVariable String keyType, @PathVariable int keyId) {
|
||||||
if (!"ed25519".contentEquals(keyType)) {
|
if (!"ed25519".contentEquals(keyType)) {
|
||||||
throw new BadRequestException("Invalid algorithm: " + keyType)
|
throw new BadRequestException("Invalid algorithm: " + keyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Key {}:{} was requested", keyType, keyId)
|
log.info("Key {}:{} was requested", keyType, keyId);
|
||||||
return JsonOutput.toJson([
|
JsonObject obj = new JsonObject();
|
||||||
public_key: keyMgr.getPublicKeyBase64(keyId)
|
obj.addProperty("public_key", keyMgr.getPublicKeyBase64(keyId));
|
||||||
])
|
return gson.toJson(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
|
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
|
||||||
String checkEphemeralKeyValidity(HttpServletRequest request) {
|
public String checkEphemeralKeyValidity(HttpServletRequest request) {
|
||||||
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid")
|
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid");
|
||||||
|
|
||||||
return invalidKey
|
return invalidKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/pubkey/isvalid", method = GET)
|
@RequestMapping(value = "/pubkey/isvalid", method = GET)
|
||||||
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
|
public String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
|
||||||
log.info("Validating public key {}", pubKey)
|
log.info("Validating public key {}", pubKey);
|
||||||
|
|
||||||
// TODO do in manager
|
// TODO do in manager
|
||||||
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()))
|
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()));
|
||||||
return valid ? validKey : invalidKey
|
return valid ? validKey : invalidKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson;
|
||||||
|
import io.kamax.mxisd.exception.InternalServerError;
|
||||||
|
import io.kamax.mxisd.lookup.*;
|
||||||
|
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
||||||
|
import io.kamax.mxisd.signature.SignatureManager;
|
||||||
|
import io.kamax.mxisd.util.GsonParser;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||||
|
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@CrossOrigin
|
||||||
|
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||||
|
public class MappingController {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(MappingController.class);
|
||||||
|
private Gson gson = new Gson();
|
||||||
|
private GsonParser parser = new GsonParser(gson);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LookupStrategy strategy;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SignatureManager signMgr;
|
||||||
|
|
||||||
|
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
|
||||||
|
lookupReq.setRequester(req.getRemoteAddr());
|
||||||
|
String xff = req.getHeader("X-FORWARDED-FOR");
|
||||||
|
lookupReq.setRecursive(StringUtils.isNotBlank(xff));
|
||||||
|
if (lookupReq.isRecursive()) {
|
||||||
|
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupReq.setUserAgent(req.getHeader("USER-AGENT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/lookup", method = GET)
|
||||||
|
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
|
||||||
|
SingleLookupRequest lookupRequest = new SingleLookupRequest();
|
||||||
|
setRequesterInfo(lookupRequest, request);
|
||||||
|
lookupRequest.setType(medium);
|
||||||
|
lookupRequest.setThreePid(address);
|
||||||
|
|
||||||
|
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
|
||||||
|
|
||||||
|
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest);
|
||||||
|
if (!lookupOpt.isPresent()) {
|
||||||
|
log.info("No mapping was found, return empty JSON object");
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleLookupReply lookup = lookupOpt.get();
|
||||||
|
if (lookup.isSigned()) {
|
||||||
|
log.info("Lookup is already signed, sending as-is");
|
||||||
|
return lookup.getBody();
|
||||||
|
} else {
|
||||||
|
log.info("Lookup is not signed, signing");
|
||||||
|
JsonObject obj = gson.toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject();
|
||||||
|
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)));
|
||||||
|
|
||||||
|
return gson.toJson(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/bulk_lookup", method = POST)
|
||||||
|
String bulkLookup(HttpServletRequest request) {
|
||||||
|
BulkLookupRequest lookupRequest = new BulkLookupRequest();
|
||||||
|
setRequesterInfo(lookupRequest, request);
|
||||||
|
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientBulkLookupRequest input = parser.parse(request, ClientBulkLookupRequest.class);
|
||||||
|
List<ThreePidMapping> mappings = new ArrayList<>();
|
||||||
|
for (List<String> mappingRaw : input.getThreepids()) {
|
||||||
|
ThreePidMapping mapping = new ThreePidMapping();
|
||||||
|
mapping.setMedium(mappingRaw.get(0));
|
||||||
|
mapping.setValue(mappingRaw.get(1));
|
||||||
|
mappings.add(mapping);
|
||||||
|
}
|
||||||
|
lookupRequest.setMappings(mappings);
|
||||||
|
|
||||||
|
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer();
|
||||||
|
answer.addAll(strategy.find(lookupRequest));
|
||||||
|
return gson.toJson(answer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalServerError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -18,41 +18,44 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.controller.v1
|
package io.kamax.mxisd.controller.v1;
|
||||||
|
|
||||||
import io.kamax.mxisd.config.ServerConfig
|
import io.kamax.mxisd.config.ServerConfig;
|
||||||
import io.kamax.mxisd.config.ViewConfig
|
import io.kamax.mxisd.config.ViewConfig;
|
||||||
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1
|
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
|
||||||
import io.kamax.mxisd.session.SessionMananger
|
import io.kamax.mxisd.exception.InternalServerError;
|
||||||
import io.kamax.mxisd.session.ValidationResult
|
import io.kamax.mxisd.session.SessionMananger;
|
||||||
import org.slf4j.Logger
|
import io.kamax.mxisd.session.ValidationResult;
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.Logger;
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Controller
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.ui.Model
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping(path = IdentityAPIv1.BASE)
|
@RequestMapping(path = IdentityAPIv1.BASE)
|
||||||
class SessionController {
|
class SessionController {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(SessionController.class)
|
private Logger log = LoggerFactory.getLogger(SessionController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ServerConfig srvCfg;
|
private ServerConfig srvCfg;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SessionMananger mgr
|
private SessionMananger mgr;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ViewConfig viewCfg;
|
private ViewConfig viewCfg;
|
||||||
|
;
|
||||||
|
|
||||||
@RequestMapping(value = "/validate/{medium}/submitToken")
|
@RequestMapping(value = "/validate/{medium}/submitToken")
|
||||||
String validate(
|
public String validate(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
@RequestParam String sid,
|
@RequestParam String sid,
|
||||||
@@ -60,21 +63,27 @@ class SessionController {
|
|||||||
@RequestParam String token,
|
@RequestParam String token,
|
||||||
Model model
|
Model model
|
||||||
) {
|
) {
|
||||||
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
|
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString());
|
||||||
|
|
||||||
ValidationResult r = mgr.validate(sid, secret, token)
|
ValidationResult r = mgr.validate(sid, secret, token);
|
||||||
log.info("Session {} was validated", sid)
|
log.info("Session {} was validated", sid);
|
||||||
if (r.getNextUrl().isPresent()) {
|
if (r.getNextUrl().isPresent()) {
|
||||||
String url = srvCfg.getPublicUrl() + r.getNextUrl().get()
|
String url = srvCfg.getPublicUrl() + r.getNextUrl().get();
|
||||||
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url)
|
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
|
||||||
response.sendRedirect(url)
|
try {
|
||||||
|
response.sendRedirect(url);
|
||||||
|
return "";
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Unable to redirect user to {}", url);
|
||||||
|
throw new InternalServerError(e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (r.isCanRemote()) {
|
if (r.isCanRemote()) {
|
||||||
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
|
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
|
||||||
model.addAttribute("remoteSessionLink", url)
|
model.addAttribute("remoteSessionLink", url);
|
||||||
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess()
|
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess();
|
||||||
} else {
|
} else {
|
||||||
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess()
|
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -18,16 +18,16 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.exception
|
package io.kamax.mxisd.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
|
||||||
class BadRequestException extends RuntimeException {
|
public class BadRequestException extends RuntimeException {
|
||||||
|
|
||||||
BadRequestException(String s) {
|
public BadRequestException(String s) {
|
||||||
super(s)
|
super(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -43,6 +43,10 @@ public class InternalServerError extends MatrixException {
|
|||||||
this.internalReason = internalReason;
|
this.internalReason = internalReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InternalServerError(Throwable t) {
|
||||||
|
this(t.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
public String getReference() {
|
public String getReference() {
|
||||||
return reference;
|
return reference;
|
||||||
}
|
}
|
@@ -18,10 +18,10 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.kamax.mxisd.exception
|
package io.kamax.mxisd.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
@ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED)
|
@ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED)
|
||||||
public class NotImplementedException extends RuntimeException {
|
public class NotImplementedException extends RuntimeException {
|
@@ -21,6 +21,8 @@
|
|||||||
package io.kamax.mxisd.invitation;
|
package io.kamax.mxisd.invitation;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import io.kamax.matrix.MatrixID;
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.mxisd.config.DnsOverwrite;
|
import io.kamax.mxisd.config.DnsOverwrite;
|
||||||
import io.kamax.mxisd.config.DnsOverwriteEntry;
|
import io.kamax.mxisd.config.DnsOverwriteEntry;
|
||||||
@@ -45,8 +47,6 @@ import org.apache.http.entity.StringEntity;
|
|||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.ssl.SSLContextBuilder;
|
import org.apache.http.ssl.SSLContextBuilder;
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -153,13 +153,13 @@ public class InvitationManager {
|
|||||||
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
|
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSrvRecordName(String domain) {
|
private String getSrvRecordName(String domain) {
|
||||||
return "_matrix._tcp." + domain;
|
return "_matrix._tcp." + domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO use caching mechanism
|
// TODO use caching mechanism
|
||||||
// TODO export in matrix-java-sdk
|
// TODO export in matrix-java-sdk
|
||||||
String findHomeserverForDomain(String domain) {
|
private String findHomeserverForDomain(String domain) {
|
||||||
Optional<DnsOverwriteEntry> entryOpt = dns.findHost(domain);
|
Optional<DnsOverwriteEntry> entryOpt = dns.findHost(domain);
|
||||||
if (entryOpt.isPresent()) {
|
if (entryOpt.isPresent()) {
|
||||||
DnsOverwriteEntry entry = entryOpt.get();
|
DnsOverwriteEntry entry = entryOpt.get();
|
||||||
@@ -264,28 +264,28 @@ public class InvitationManager {
|
|||||||
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
|
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
|
||||||
HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind");
|
HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind");
|
||||||
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
|
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
|
||||||
JSONObject obj = new JSONObject(); // TODO use Gson instead
|
JsonObject obj = new JsonObject();
|
||||||
obj.put("mxid", mxid);
|
obj.addProperty("mxid", mxid);
|
||||||
obj.put("token", reply.getToken());
|
obj.addProperty("token", reply.getToken());
|
||||||
obj.put("signatures", signMgr.signMessageJson(obj.toString()));
|
obj.add("signatures", signMgr.signMessageGson(obj.toString()));
|
||||||
|
|
||||||
JSONObject objUp = new JSONObject();
|
JsonObject objUp = new JsonObject();
|
||||||
objUp.put("mxid", mxid);
|
objUp.addProperty("mxid", mxid);
|
||||||
objUp.put("medium", medium);
|
objUp.addProperty("medium", medium);
|
||||||
objUp.put("address", address);
|
objUp.addProperty("address", address);
|
||||||
objUp.put("sender", reply.getInvite().getSender().getId());
|
objUp.addProperty("sender", reply.getInvite().getSender().getId());
|
||||||
objUp.put("room_id", reply.getInvite().getRoomId());
|
objUp.addProperty("room_id", reply.getInvite().getRoomId());
|
||||||
objUp.put("signed", obj);
|
objUp.add("signed", obj);
|
||||||
|
|
||||||
JSONObject content = new JSONObject(); // TODO use Gson instead
|
JsonObject content = new JsonObject();
|
||||||
JSONArray invites = new JSONArray();
|
JsonArray invites = new JsonArray();
|
||||||
invites.put(objUp);
|
invites.add(objUp);
|
||||||
content.put("invites", invites);
|
content.add("invites", invites);
|
||||||
content.put("medium", medium);
|
content.addProperty("medium", medium);
|
||||||
content.put("address", address);
|
content.addProperty("address", address);
|
||||||
content.put("mxid", mxid);
|
content.addProperty("mxid", mxid);
|
||||||
|
|
||||||
content.put("signatures", signMgr.signMessageJson(content.toString()));
|
content.add("signatures", signMgr.signMessageGson(content.toString()));
|
||||||
|
|
||||||
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
|
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
|
||||||
entity.setContentType("application/json");
|
entity.setContentType("application/json");
|
||||||
@@ -313,7 +313,7 @@ public class InvitationManager {
|
|||||||
|
|
||||||
private IThreePidInviteReply reply;
|
private IThreePidInviteReply reply;
|
||||||
|
|
||||||
public MappingChecker(IThreePidInviteReply reply) {
|
MappingChecker(IThreePidInviteReply reply) {
|
||||||
this.reply = reply;
|
this.reply = reply;
|
||||||
}
|
}
|
||||||
|
|
115
src/main/java/io/kamax/mxisd/key/KeyManager.java
Normal file
115
src/main/java/io/kamax/mxisd/key/KeyManager.java
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2017 Maxime Dor
|
||||||
|
*
|
||||||
|
* https://max.kamax.io/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.kamax.mxisd.key;
|
||||||
|
|
||||||
|
import io.kamax.mxisd.config.KeyConfig;
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||||
|
import net.i2p.crypto.eddsa.KeyPairGenerator;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class KeyManager {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private KeyConfig keyCfg;
|
||||||
|
|
||||||
|
private EdDSAParameterSpec keySpecs;
|
||||||
|
private EdDSAEngine signEngine;
|
||||||
|
private List<KeyPair> keys;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void build() {
|
||||||
|
try {
|
||||||
|
keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);
|
||||||
|
signEngine = new EdDSAEngine(MessageDigest.getInstance(keySpecs.getHashAlgorithm()));
|
||||||
|
keys = new ArrayList<>();
|
||||||
|
|
||||||
|
Path privKey = Paths.get(keyCfg.getPath());
|
||||||
|
|
||||||
|
if (!Files.exists(privKey)) {
|
||||||
|
KeyPair pair = (new KeyPairGenerator()).generateKeyPair();
|
||||||
|
String keyEncoded = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded());
|
||||||
|
FileUtils.writeStringToFile(privKey.toFile(), keyEncoded, StandardCharsets.ISO_8859_1);
|
||||||
|
keys.add(pair);
|
||||||
|
} else {
|
||||||
|
if (Files.isDirectory(privKey)) {
|
||||||
|
throw new RuntimeException("Invalid path for private key: " + privKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isReadable(privKey)) {
|
||||||
|
byte[] seed = Base64.getDecoder().decode(FileUtils.readFileToString(privKey.toFile(), StandardCharsets.ISO_8859_1));
|
||||||
|
EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, keySpecs);
|
||||||
|
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs);
|
||||||
|
keys.add(new KeyPair(new EdDSAPublicKey(pubKeySpec), new EdDSAPrivateKey(privKeySpec)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException | IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentIndex() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPair getKeys(int index) {
|
||||||
|
return keys.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getPrivateKey(int index) {
|
||||||
|
return getKeys(index).getPrivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EdDSAPublicKey getPublicKey(int index) {
|
||||||
|
return (EdDSAPublicKey) getKeys(index).getPublic();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EdDSAParameterSpec getSpecs() {
|
||||||
|
return keySpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublicKeyBase64(int index) {
|
||||||
|
return Base64.getEncoder().encodeToString(getPublicKey(index).getAbyte());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user