Fix handling various GET and POST content types/logic for submitToken

- Properly support Form-encoded POST
- Fix #167
This commit is contained in:
Max Dor
2019-04-26 08:41:06 +02:00
parent 9d4680f55a
commit 39447b8b8b
10 changed files with 244 additions and 87 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.session.ValidationResult;
import io.kamax.mxisd.util.FileUtil;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class SessionValidationGetHandler extends SessionValidateHandler {
private transient final Logger log = LoggerFactory.getLogger(SessionValidationGetHandler.class);
private ServerConfig srvCfg;
private ViewConfig viewCfg;
public SessionValidationGetHandler(SessionManager mgr, MxisdConfig cfg) {
super(mgr);
this.srvCfg = cfg.getServer();
this.viewCfg = cfg.getView();
}
@Override
public void handleRequest(HttpServerExchange exchange) {
log.info("Handling GET request to validate session");
String sid = getQueryParameter(exchange, "sid");
String secret = getQueryParameter(exchange, "client_secret");
String token = getQueryParameter(exchange, "token");
ValidationResult r = handleRequest(sid, secret, token);
log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) {
String url = r.getNextUrl().get();
try {
url = new URL(url).toString();
} catch (MalformedURLException e) {
log.info("Session next URL {} is not a valid one, will prepend public URL {}", url, srvCfg.getPublicUrl());
url = srvCfg.getPublicUrl() + r.getNextUrl().get();
}
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
exchange.setStatusCode(302);
exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url);
} else {
try {
String data = FileUtil.load(viewCfg.getSession().getOnTokenSubmit().getSuccess());
writeBodyAsUtf8(exchange, data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.http.io.identity.SuccessStatusJson;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormParserFactory;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class SessionValidationPostHandler extends SessionValidateHandler {
private transient final Logger log = LoggerFactory.getLogger(SessionValidationPostHandler.class);
private FormParserFactory factory;
public SessionValidationPostHandler(SessionManager mgr) {
super(mgr);
factory = FormParserFactory.builder().build();
}
@Override
public void handleRequest(HttpServerExchange exchange) throws IOException {
log.info("Handling POST request to validate session");
String sid;
String secret;
String token;
String contentType = getContentType(exchange).orElseThrow(() -> new IllegalArgumentException("Content type header is not set"));
if (StringUtils.equals(contentType, "application/json")) { // FIXME use MIME parsing tools
log.info("Parsing as JSON data");
JsonObject body = parseJsonObject(exchange);
sid = GsonUtil.getStringOrThrow(body, "sid");
secret = GsonUtil.getStringOrThrow(body, "client_secret");
token = GsonUtil.getStringOrThrow(body, "token");
} else if (StringUtils.equals(contentType, "application/x-www-form-urlencoded")) { // FIXME use MIME parsing tools
log.info("Parsing as Form data");
FormData data = factory.createParser(exchange).parseBlocking();
sid = getOrThrow(data, "sid");
secret = getOrThrow(data, "client_secret");
token = getOrThrow(data, "token");
} else {
log.info("Unsupported Content type: {}", contentType);
throw new IllegalArgumentException("Unsupported Content type: " + contentType);
}
handleRequest(sid, secret, token);
respondJson(exchange, new SuccessStatusJson(true));
}
}

View File

@@ -38,8 +38,8 @@ import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -139,12 +139,13 @@ public class SessionManager {
} }
public ValidationResult validate(String sid, String secret, String token) { public ValidationResult validate(String sid, String secret, String token) {
log.info("Validating session {}", sid);
ThreePidSession session = getSession(sid, secret); ThreePidSession session = getSession(sid, secret);
log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); log.info("Session {} is from {}", session.getId(), session.getServer());
session.validate(token); session.validate(token);
storage.updateThreePidSession(session.getDao()); storage.updateThreePidSession(session.getDao());
log.info("Session {} has been validated locally", session.getId()); log.info("Session {} has been validated", session.getId());
ValidationResult r = new ValidationResult(session); ValidationResult r = new ValidationResult(session);
session.getNextLink().ifPresent(r::setNextUrl); session.getNextLink().ifPresent(r::setNextUrl);

View File

@@ -0,0 +1,37 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.connector.email;
public class BlackholeEmailConnector implements EmailConnector {
public static final String ID = "none";
@Override
public String getId() {
return ID;
}
@Override
public void send(String senderAddress, String senderName, String recipient, String content) {
//dev/null
}
}

View File

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

View File

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

View File

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