Initial draft of ResolverService

Affects issues:
- #1288
This commit is contained in:
Rsl1122 2020-02-09 15:22:48 +02:00 committed by Risto Lahtela
parent 62f3f46678
commit e7da714f55
9 changed files with 510 additions and 0 deletions

View File

@ -4,6 +4,7 @@ plugins {
dependencies {
compileOnly group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
compileOnly "com.google.code.gson:gson:$gsonVersion"
}
ext.apiVersion = '5.0-R0.3'

View File

@ -0,0 +1,71 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Service for modifying webserver request resolution.
* <p>
* It is recommended to use plugin based namespace in your custom targets,
* eg. "/flyplugin/flying" to avoid collisions with other plugins.
* You can also use {@link #getResolver(String)} to check if target is already resolved.
*
* @author Rsl1122
*/
public interface ResolverService {
/**
* Register a new resolver.
*
* @param pluginName Name of the plugin that is registering (For error messages)
* @param start Start of the target to match against, eg "/example" will send "/example/target" etc to the Resolver.
* @param resolver {@link Resolver} to use for this
* @throws IllegalArgumentException If pluginName is null or empty.
*/
void registerResolver(String pluginName, String start, Resolver resolver);
/**
* Register a new resolver with regex that maches start of target.
* <p>
* NOTICE: It is recommended to avoid too generic regex like "/.*" to not override existing resolvers.
* <p>
* Parameters (?param=value) are not included in the regex matching.
*
* @param pluginName Name of the plugin that is registering (For error messages)
* @param pattern Regex Pattern, "/example.*" will send "/exampletarget" etc to the Resolver.
* @param resolver {@link Resolver} to use for this.
* @throws IllegalArgumentException If pluginName is null or empty.
*/
void registerResolverForMatches(String pluginName, Pattern pattern, Resolver resolver);
/**
* Obtain a {@link Resolver} for a target.
* <p>
* First matching resolver will be returned.
* {@link #registerResolver} resolvers have higher priority than {@link #registerResolverForMatches}.
* <p>
* Can be used when making Resolver middleware.
*
* @param target "/example/target"
* @return Resolver if registered or empty.
*/
Optional<Resolver> getResolver(String target);
}

View File

@ -0,0 +1,35 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
public final class MimeType {
public static final String HTML = "text/html";
public static final String CSS = "text/css";
public static final String JSON = "application/json";
public static final String JS = "application/javascript";
public static final String IMAGE = "image/gif";
public static final String FAVICON = "image/x-icon";
public static final String FONT_TFF = "application/x-font-ttf";
public static final String FONT_WOFF = "application/font-woff";
public static final String FONT_WOFF2 = "application/font-woff2";
public static final String FONT_EOT = "application/vnd.ms-fontobject";
public static final String FONT_BYTESTREAM = "application/octet-stream";
private MimeType() {
// Static variable class
}
}

View File

@ -0,0 +1,44 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import java.util.Map;
import java.util.Optional;
/**
* Represents URI parameters described with {@code ?param=value&param2=value2} in the URL.
*
* @author Rsl1122
*/
public final class Parameters {
private final Map<String, String> byKey;
public Parameters(Map<String, String> byKey) {
this.byKey = byKey;
}
/**
* Obtain an URI parameter by key.
*
* @param key Case-sensitive key, eg. 'param' in {@code ?param=value&param2=value2}
* @return The value in the URL or empty if key is not specified in the URL.
*/
public Optional<String> get(String key) {
return Optional.ofNullable(byKey.get(key));
}
}

View File

@ -0,0 +1,49 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import java.util.Optional;
public interface Resolver {
/**
* Implement access control if authorization is enabled.
* <p>
* Is not called when access control is not active.
*
* @param permissions WebUser that is accessing this page.
* @param target Target that is being accessed, /example/target
* @param parameters Parameters in the URL, ?param=value etc.
* @return true if allowed, false if response should be 403 (forbidden)
*/
boolean canAccess(WebUser permissions, URLTarget target, Parameters parameters);
/**
* Implement request resolution.
*
* @param target Target that is being accessed, /example/target
* @param parameters Parameters in the URL, ?param=value etc.
* @return Response or empty if the response should be 404 (not found).
* @see Response for return value
*/
Optional<Response> resolve(URLTarget target, Parameters parameters);
default ResponseBuilder newResponseBuilder() {
return new ResponseBuilder();
}
}

View File

@ -0,0 +1,57 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Represents a response that will be sent over HTTP.
*
* @author Rsl1122
* @see MimeType for MIME types that are commonly used.
*/
public final class Response {
final Map<String, String> headers;
int code = 200;
byte[] bytes;
Charset charset; // can be null (raw bytes)
Response() {
headers = new HashMap<>();
}
public byte[] getBytes() {
return bytes;
}
public int getCode() {
return code;
}
public Map<String, String> getHeaders() {
return headers;
}
public Optional<Charset> getCharset() {
return Optional.ofNullable(charset);
}
}

View File

@ -0,0 +1,142 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import com.google.gson.Gson;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class ResponseBuilder {
private final Response response;
ResponseBuilder() {
this.response = new Response();
}
/**
* Set MIME Type of the Response.
*
* @param mimeType MIME type of the Response https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
* @return this builder.
* @see MimeType for common MIME types.
*/
public ResponseBuilder setMimeType(String mimeType) {
return setHeader("Content-Type", mimeType);
}
/**
* Set HTTP Status code.
* <p>
* Default status code is 200 (OK) if not set.
*
* @param code 1xx, 2xx, 3xx, 4xx, 5xx, https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @return this builder.
*/
public ResponseBuilder setStatus(int code) {
return this;
}
/**
* Set HTTP Response header.
*
* @param header Key of the header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param value Value for the header.
* @return this builder.
*/
public ResponseBuilder setHeader(String header, Object value) {
response.headers.put(header, value.toString());
return this;
}
/**
* Utility method for building redirects.
*
* @param url URL to redirect the client to with 302 Found.
* @return https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location
*/
public ResponseBuilder redirectTo(String url) {
return setStatus(302).setHeader("Location", url).setContent(new byte[]{});
}
public ResponseBuilder setContent(byte[] bytes) {
response.bytes = bytes;
return setHeader("Content-Length", bytes.length);
}
public ResponseBuilder setContent(String utf8String) {
return setContent(utf8String, StandardCharsets.UTF_8);
}
public ResponseBuilder setContent(String content, Charset charset) {
String mimeType = response.headers.get("Content-Type");
response.charset = charset;
if (mimeType != null) {
String[] parts = mimeType.split(";");
if (parts.length == 1) {
setMimeType(parts[0] + "; charset=" + charset.name().toLowerCase());
}
}
return setContent(content.getBytes(charset));
}
public ResponseBuilder setJSONContent(Object objectToSerialize) {
return setJSONContent(new Gson().toJson(objectToSerialize));
}
public ResponseBuilder setJSONContent(String json) {
setContent(json);
return setMimeType(MimeType.JSON);
}
/**
* Finish building.
*
* @return Response.
* @throws InvalidResponseException if content was not defined (not even empty byte array).
* @throws InvalidResponseException if content has bytes, but MIME-type is not defined.
* @throws InvalidResponseException if status code is outside range 100-599.
* @see #setMimeType(String) to set MIME-type.
*/
public Response build() {
byte[] content = response.bytes;
exceptionIf(content == null, "Content not defined for Response");
String mimeType = response.getHeaders().get("Content-Type");
exceptionIf(content.length > 0 && mimeType == null, "MIME Type not defined for Response");
exceptionIf(content.length > 0 && mimeType.isEmpty(), "MIME Type empty for Response");
exceptionIf(response.code < 100 || response.code >= 600, "HTTP Status code out of bounds (" + response.code + ")");
return response;
}
private void exceptionIf(boolean value, String errorMsg) {
if (value) throw new InvalidResponseException(errorMsg);
}
/**
* Thrown when {@link ResponseBuilder} is missing some parameters.
*
* @author Rsl1122
*/
public static class InvalidResponseException extends IllegalStateException {
public InvalidResponseException(String s) {
super(s);
}
}
}

View File

@ -0,0 +1,66 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public final class URLTarget {
// Internal representation
private final List<String> parts;
private final String full;
public URLTarget(String target) {
this.parts = parse(target);
full = target;
}
private List<String> parse(String target) {
String[] partArray = target.split("/");
// Ignores index 0, assuming target starts with /
return Arrays.asList(partArray)
.subList(1, partArray.length);
}
/**
* Obtain the full target.
*
* @return Example: "/target/path/in/url"
*/
public String asString() {
return full;
}
/**
* Obtain part of the target by index of slashes in the URL.
*
* @param index Index from root, eg. /0/1/2/3 etc
* @return part after a '/' in the target, Example "target" for 0 in "/target/example", "" for 0 in "/", "" for 1 in "/example/"
*/
public Optional<String> getPart(int index) {
if (index >= parts.size()) {
return Optional.empty();
}
return Optional.of(parts.get(index));
}
public boolean endsWith(String suffix) {
return full.endsWith(suffix);
}
}

View File

@ -0,0 +1,45 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public final class WebUser {
private final String name;
private final Set<String> permissions;
public WebUser(String name) {
this.name = name;
this.permissions = new HashSet<>();
}
public WebUser(String name, String... permissions) {
this(name);
this.permissions.addAll(Arrays.asList(permissions));
}
public boolean hasPermission(String permission) {
return permissions.contains(permission);
}
public String getName() {
return name;
}
}