Refactored the request body PR stuff

- Made sure to not break compatibility with discord bots that use the old
  authentication via query parameters
- Query parameters now accept no-value keys
- Added access control checks for the errors endpoints
This commit is contained in:
Risto Lahtela 2021-07-10 09:11:39 +03:00
parent a3f5298617
commit c523e40b7f
7 changed files with 93 additions and 37 deletions

View File

@ -34,29 +34,29 @@ public final class Request {
private final URIQuery query;
private final WebUser user;
private final Map<String, String> headers;
private final byte[] body;
private final byte[] requestBody;
/**
* Constructor.
*
* @param method HTTP method, GET, PUT, POST, etc
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param body Raw body as bytes, if present
* @param method HTTP method, GET, PUT, POST, etc
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param requestBody Raw body as bytes, if present
*/
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, byte[] body) {
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, byte[] requestBody) {
this.method = method;
this.path = path;
this.query = query;
this.user = user;
this.headers = headers;
this.body = body;
this.requestBody = requestBody;
}
/**
* Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no form body.
* Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no request body.
*/
public Request(String method, String target, WebUser user, Map<String, String> headers) {
this.method = method;
@ -70,7 +70,7 @@ public final class Request {
}
this.user = user;
this.headers = headers;
this.body = new byte[0];
this.requestBody = new byte[0];
}
/**
@ -106,7 +106,7 @@ public final class Request {
* @return byte[].
*/
public byte[] getRequestBody() {
return body;
return requestBody;
}
/**
@ -129,7 +129,7 @@ public final class Request {
}
public Request omitFirstInPath() {
return new Request(method, path.omitFirst(), query, user, headers, body);
return new Request(method, path.omitFirst(), query, user, headers, requestBody);
}
@Override
@ -140,7 +140,7 @@ public final class Request {
", query=" + query +
", user=" + user +
", headers=" + headers +
", body=" + body.length +
", body=" + requestBody.length +
'}';
}
}

View File

@ -56,19 +56,35 @@ public final class URIQuery {
}
String[] keyAndValue = StringUtils.split(kv, "=", 2);
if (keyAndValue.length >= 2) {
try {
parameters.put(
URLDecoder.decode(keyAndValue[0], StandardCharsets.UTF_8.name()),
URLDecoder.decode(keyAndValue[1], StandardCharsets.UTF_8.name())
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
parseAndPutKeyValuePair(parameters, keyAndValue);
} else if (keyAndValue.length == 1) {
parseAndPutKeyEmptyValue(parameters, keyAndValue[0]);
}
}
return parameters;
}
private void parseAndPutKeyValuePair(Map<String, String> parameters, String[] keyAndValue) {
try {
parameters.put(
URLDecoder.decode(keyAndValue[0], StandardCharsets.UTF_8.name()),
URLDecoder.decode(keyAndValue[1], StandardCharsets.UTF_8.name())
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
}
private void parseAndPutKeyEmptyValue(Map<String, String> parameters, String s) {
try {
parameters.put(
URLDecoder.decode(s, StandardCharsets.UTF_8.name()), ""
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
}
}
/**
* Obtain an URI parameter by key.
*
@ -97,6 +113,8 @@ public final class URIQuery {
@Override
public String toString() {
Map<String, String> byKey = new HashMap<>(this.byKey);
byKey.remove("password");
return "URIQuery{" +
"byKey=" + byKey +
'}';

View File

@ -20,6 +20,11 @@ import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
public class RequestBodyConverter {
private RequestBodyConverter() {
/* Static utility class */
}
/**
* Get the body of a request as an url-encoded form.
*
@ -27,8 +32,8 @@ public class RequestBodyConverter {
*/
public static URIQuery formBody(Request request) {
if (
"POST".equalsIgnoreCase(request.getMethod()) &&
"application/x-www-form-urlencoded".equalsIgnoreCase(request.getHeader("Content-type").orElse(""))
"POST".equalsIgnoreCase(request.getMethod()) &&
"application/x-www-form-urlencoded".equalsIgnoreCase(request.getHeader("Content-type").orElse(""))
) {
return new URIQuery(new String(request.getRequestBody()));
} else {

View File

@ -189,20 +189,23 @@ public class RequestHandler implements HttpHandler {
String requestMethod = exchange.getRequestMethod();
URIPath path = new URIPath(exchange.getRequestURI().getPath());
URIQuery query = new URIQuery(exchange.getRequestURI().getRawQuery());
byte[] requestBody = new byte[0];
byte[] requestBody = readRequestBody(exchange);
WebUser user = getWebUser(exchange);
Map<String, String> headers = getRequestHeaders(exchange);
return new Request(requestMethod, path, query, user, headers, requestBody);
}
private byte[] readRequestBody(HttpExchange exchange) {
try (ByteArrayOutputStream buf = new ByteArrayOutputStream(512)) {
int b;
while ((b = exchange.getRequestBody().read()) != -1) {
buf.write((byte) b);
}
requestBody = buf.toByteArray();
return buf.toByteArray();
} catch (IOException ignored) {
// requestBody stays empty
return new byte[0];
}
WebUser user = getWebUser(exchange);
Map<String, String> headers = getRequestHeaders(exchange);
return new Request(requestMethod, path, query, user, headers, requestBody);
}
private WebUser getWebUser(HttpExchange exchange) {

View File

@ -71,8 +71,9 @@ public class LoginResolver implements NoAuthResolver {
public User getUser(Request request) {
URIQuery form = RequestBodyConverter.formBody(request);
String username = form.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
String password = form.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));
URIQuery query = request.getQuery();
String username = getUser(form, query);
String password = getPassword(form, query);
User user = dbSystem.getDatabase().query(WebUserQueries.fetchUser(username))
.orElseThrow(() -> new WebUserAuthException(FailReason.USER_PASS_MISMATCH));
@ -82,4 +83,16 @@ public class LoginResolver implements NoAuthResolver {
}
return user;
}
private String getPassword(URIQuery form, URIQuery query) {
return form.get("password")
.orElseGet(() -> query.get("password")
.orElseThrow(() -> new BadRequestException("'password' parameter not defined")));
}
private String getUser(URIQuery form, URIQuery query) {
return form.get("user")
.orElseGet(() -> query.get("user")
.orElseThrow(() -> new BadRequestException("'user' parameter not defined")));
}
}

View File

@ -57,12 +57,12 @@ public class RegisterResolver implements NoAuthResolver {
}
URIQuery form = RequestBodyConverter.formBody(request);
String username = form.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
String username = getUser(form, query);
boolean alreadyExists = dbSystem.getDatabase().query(WebUserQueries.fetchUser(username)).isPresent();
if (alreadyExists) throw new BadRequestException("User already exists!");
String password = form.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));
String password = getPassword(form, query);
try {
String code = RegistrationBin.addInfoForRegistration(username, password);
return Response.builder()
@ -77,4 +77,15 @@ public class RegisterResolver implements NoAuthResolver {
}
}
private String getPassword(URIQuery form, URIQuery query) {
return form.get("password")
.orElseGet(() -> query.get("password")
.orElseThrow(() -> new BadRequestException("'password' parameter not defined")));
}
private String getUser(URIQuery form, URIQuery query) {
return form.get("user")
.orElseGet(() -> query.get("user")
.orElseThrow(() -> new BadRequestException("'user' parameter not defined")));
}
}

View File

@ -192,7 +192,9 @@ public class AccessControlTest {
"/v1/players,200",
"/query,200",
"/v1/filters,200",
"/v1/query,400"
"/v1/query,400",
"/v1/errors,200",
"/errors,200",
})
void levelZeroCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel0);
@ -250,7 +252,9 @@ public class AccessControlTest {
"/v1/players,200",
"/query,200",
"/v1/filters,200",
"/v1/query,400"
"/v1/query,400",
"/v1/errors,403",
"/errors,403",
})
void levelOneCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel1);
@ -308,7 +312,9 @@ public class AccessControlTest {
"/v1/players,403",
"/query,403",
"/v1/filters,403",
"/v1/query,403"
"/v1/query,403",
"/v1/errors,403",
"/errors,403",
})
void levelTwoCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel2);