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 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()
@@ -103,8 +102,8 @@ public class HttpMxisd {
.post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity())))
.post(StoreInviteHandler.Path, storeInvHandler)
.post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession())))
.get(SessionValidateHandler.Path, sessValidateHandler)
.post(SessionValidateHandler.Path, sessValidateHandler)
.get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig())))
.post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(m.getSession())))
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite())))
.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.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@@ -48,10 +49,7 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.*;
public abstract class BasicHttpHandler implements HttpHandler {
@@ -122,6 +120,20 @@ public abstract class BasicHttpHandler implements HttpHandler {
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) {
ex.getResponseHeaders().put(HttpString.tryFromString(name), value);
}

View File

@@ -20,94 +20,36 @@
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.io.identity.SuccessStatusJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
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.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class SessionValidateHandler extends BasicHttpHandler {
public abstract class SessionValidateHandler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken";
private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class);
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.srvCfg = srvCfg;
this.viewCfg = viewCfg;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
String medium = getQueryParameter(exchange, "medium");
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;
}
protected ValidationResult handleRequest(String sid, String secret, String token) {
if (StringUtils.isEmpty(sid)) {
throw new IllegalArgumentException("sid is not set or is empty");
}
if (isHtmlRequest) {
handleHtmlRequest(exchange, medium, sid, secret, token);
} else {
handleJsonRequest(exchange, sid, secret, token);
if (StringUtils.isEmpty(secret)) {
throw new IllegalArgumentException("client secret is not set or is empty");
}
}
private void handleHtmlRequest(HttpServerExchange exchange, String medium, String sid, String secret, String 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));
return mgr.validate(sid, secret, token);
}
}

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.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -139,12 +139,13 @@ public class SessionManager {
}
public ValidationResult validate(String sid, String secret, String token) {
log.info("Validating session {}", sid);
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);
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);
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
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())) {
EmailSmtpConfig smtpCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(EmailSmtpConnector.ID, new JsonObject()), EmailSmtpConfig.class);
return Optional.of(new EmailSmtpConnector(smtpCfg));

View File

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

View File

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