From 833c6c0f4fdb594185c1c65771238d5125b1c2aa Mon Sep 17 00:00:00 2001 From: Antti Date: Mon, 29 May 2023 00:14:55 +0300 Subject: [PATCH] feat: better web pages & some fixes --- Authentication/build.gradle.kts | 4 +- .../AuthenticationDatabase.java | 1 - .../authentication/AuthenticationPlugin.java | 7 +- .../AuthenticationWebServer.java | 23 ++-- .../antti/auth/authentication}/Database.java | 3 +- .../src/main/resources/{ => web}/login.html | 6 +- Authorization/build.gradle.kts | 2 +- .../authorization/AuthorizationPlugin.java | 7 +- .../authorization/AuthorizationWebServer.java | 34 +++--- .../src/main/resources/unauthorized.html | 49 --------- .../src/main/resources/web/unauthorized.html | 100 ++++++++++++++++++ Common/build.gradle.kts | 1 - .../codes/antti/auth/common/WebServer.java | 80 +++++++++++--- README.md | 18 +++- 14 files changed, 235 insertions(+), 100 deletions(-) rename {Common/src/main/java/codes/antti/auth/common => Authentication/src/main/java/codes/antti/auth/authentication}/Database.java (97%) rename Authentication/src/main/resources/{ => web}/login.html (96%) delete mode 100644 Authorization/src/main/resources/unauthorized.html create mode 100644 Authorization/src/main/resources/web/unauthorized.html diff --git a/Authentication/build.gradle.kts b/Authentication/build.gradle.kts index 7fd5941..84b8e22 100644 --- a/Authentication/build.gradle.kts +++ b/Authentication/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { compileOnly ("org.spigotmc:spigot-api:1.13.2-R0.1-SNAPSHOT") compileOnly ("org.jetbrains:annotations:23.0.0") implementation ("codes.antti.auth:Common") + implementation ("org.xerial:sqlite-jdbc:3.41.2.1") } val javaTarget = 11 @@ -60,7 +61,8 @@ tasks.shadowJar { destinationDirectory.set(file("../build")) archiveClassifier.set("") - relocate ("org.sqlite", "codes.antti.shadow.sqlite") + relocate ("codes.antti.auth.common", "codes.antti.auth.authentication.shadow.common") + relocate ("org.sqlite", "codes.antti.auth.authentication.shadow.sqlite") } modrinth { diff --git a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationDatabase.java b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationDatabase.java index de842a9..8c65258 100644 --- a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationDatabase.java +++ b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationDatabase.java @@ -1,6 +1,5 @@ package codes.antti.auth.authentication; -import codes.antti.auth.common.Database; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationPlugin.java b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationPlugin.java index 731d0c3..73d4875 100644 --- a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationPlugin.java +++ b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationPlugin.java @@ -7,6 +7,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.io.IOException; import java.sql.SQLException; import java.util.Objects; @@ -18,7 +19,11 @@ public final class AuthenticationPlugin extends JavaPlugin implements CommandExe @Override public void onEnable() { this.saveDefaultConfig(); - if (!getDataFolder().toPath().resolve("login.html").toFile().exists()) this.saveResource("login.html", false); + File webRoot = getDataFolder().toPath().resolve("web").toFile(); + if (!webRoot.exists()) { + boolean _ignored = webRoot.mkdirs(); + } + if (!webRoot.toPath().resolve("login.html").toFile().exists()) this.saveResource("web/login.html", true); try { this.db = new AuthenticationDatabase(this); diff --git a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationWebServer.java b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationWebServer.java index f305be9..e37f6ea 100644 --- a/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationWebServer.java +++ b/Authentication/src/main/java/codes/antti/auth/authentication/AuthenticationWebServer.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Objects; public class AuthenticationWebServer { @@ -15,10 +16,11 @@ public class AuthenticationWebServer { private final WebServer http; private final AuthenticationPlugin plugin; - private String loginPage = null; + private final Path loginPagePath; public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOException { this.plugin = plugin; + this.loginPagePath = this.plugin.getDataFolder().toPath().resolve("web/login.html"); FileConfiguration config = plugin.getConfig(); this.http = new WebServer(Objects.requireNonNull(config.getString("ip", "0.0.0.0")), config.getInt("port", 8200)); final String root = "/"; @@ -70,6 +72,8 @@ public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOEx request.respond(200); }); + this.http.serveStatic("/login/", this.plugin.getDataFolder().toPath().resolve("web")); + this.http.handle("/logout", request -> { @@ -113,16 +117,15 @@ public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOEx } private String formatLoginPage(@NotNull String authToken) { - if (this.loginPage == null) { - try { - this.loginPage = Files.readString(this.plugin.getDataFolder().toPath().resolve("login.html")); - } catch (IOException e) { - this.loginPage = "Authenticate using: /auth {{auth_token}}"; - this.plugin.getLogger().severe("Couldn't load login page, using a plain one"); - e.printStackTrace(); - } + String loginPage; + try { + loginPage = Files.readString(this.loginPagePath); + } catch (IOException e) { + loginPage = "Authenticate using: /auth {{auth_token}}"; + this.plugin.getLogger().severe("Couldn't load login page, using a plain one"); + e.printStackTrace(); } - return this.loginPage.replaceAll("\\{\\{auth_token}}", authToken); + return loginPage.replaceAll("\\{\\{auth_token}}", authToken); } public void close() { diff --git a/Common/src/main/java/codes/antti/auth/common/Database.java b/Authentication/src/main/java/codes/antti/auth/authentication/Database.java similarity index 97% rename from Common/src/main/java/codes/antti/auth/common/Database.java rename to Authentication/src/main/java/codes/antti/auth/authentication/Database.java index c71598a..bcf2ba3 100644 --- a/Common/src/main/java/codes/antti/auth/common/Database.java +++ b/Authentication/src/main/java/codes/antti/auth/authentication/Database.java @@ -1,5 +1,4 @@ -package codes.antti.auth.common; - +package codes.antti.auth.authentication; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; diff --git a/Authentication/src/main/resources/login.html b/Authentication/src/main/resources/web/login.html similarity index 96% rename from Authentication/src/main/resources/login.html rename to Authentication/src/main/resources/web/login.html index 07076e8..aae4cfb 100644 --- a/Authentication/src/main/resources/login.html +++ b/Authentication/src/main/resources/web/login.html @@ -21,7 +21,7 @@ .container { display: flex; background-color: #d4d4d8; - border-radius: 3px; + border-radius: 5px; padding: 12px; } .container p { @@ -33,7 +33,7 @@ font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; margin: 0; padding: 6px; - border-radius: 3px; + border-radius: 5px; position: relative; } #tooltip { @@ -45,7 +45,7 @@ transform: translateX(-50%); z-index: 1; background-color: #a1a1aa; - border-radius: 3px; + border-radius: 5px; padding: 8px; } diff --git a/Authorization/build.gradle.kts b/Authorization/build.gradle.kts index dc8f110..03b0b0a 100644 --- a/Authorization/build.gradle.kts +++ b/Authorization/build.gradle.kts @@ -61,7 +61,7 @@ tasks.shadowJar { destinationDirectory.set(file("../build")) archiveClassifier.set("") - relocate ("org.sqlite", "codes.antti.shadow.sqlite") + relocate ("codes.antti.auth.common", "codes.antti.auth.authorization.shadow.common") } modrinth { diff --git a/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationPlugin.java b/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationPlugin.java index 79b95b9..322d23c 100644 --- a/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationPlugin.java +++ b/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationPlugin.java @@ -2,6 +2,7 @@ import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; import java.io.IOException; public final class AuthorizationPlugin extends JavaPlugin { @@ -9,7 +10,11 @@ public final class AuthorizationPlugin extends JavaPlugin { @Override public void onEnable() { this.saveDefaultConfig(); - if (!getDataFolder().toPath().resolve("unauthorized.html").toFile().exists()) this.saveResource("unauthorized.html", false); + File webRoot = getDataFolder().toPath().resolve("web").toFile(); + if (!webRoot.exists()) { + boolean _ignored = webRoot.mkdirs(); + } + if (!webRoot.toPath().resolve("unauthorized.html").toFile().exists()) this.saveResource("web/unauthorized.html", true); try { this.server = new AuthorizationWebServer(this); diff --git a/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationWebServer.java b/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationWebServer.java index 79ff542..69fa8bb 100644 --- a/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationWebServer.java +++ b/Authorization/src/main/java/codes/antti/auth/authorization/AuthorizationWebServer.java @@ -10,13 +10,14 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; public class AuthorizationWebServer { private final WebServer http; private final UserManager userManager; - private String unauthorizedPage = null; + private final Path unauthorizedPagePath; private final HashMap permissionCache = new HashMap<>(); private final HashMap permissionCacheExpiry = new HashMap<>(); private static final long CACHE_SECONDS = 15; @@ -26,6 +27,7 @@ public AuthorizationWebServer(AuthorizationPlugin plugin) throws IOException { this.http = new WebServer(Objects.requireNonNull(config.getString("ip", "0.0.0.0")), config.getInt("port", 8300)); LuckPerms lp = LuckPermsProvider.get(); this.userManager = lp.getUserManager(); + this.unauthorizedPagePath = plugin.getDataFolder().toPath().resolve("web/unauthorized.html"); this.http.handle("/auth", request -> { String host = request.httpExchange.getRequestHeaders().get("host").get(0); @@ -58,21 +60,29 @@ public AuthorizationWebServer(AuthorizationPlugin plugin) throws IOException { return; } } - - if (this.unauthorizedPage == null) { - try { - this.unauthorizedPage = Files.readString(plugin.getDataFolder().toPath().resolve("unauthorized.html")); - } catch (IOException e) { - this.unauthorizedPage = "You are not authorized to use this application."; - plugin.getLogger().severe("Couldn't load unauthorized page, using a plain one"); - e.printStackTrace(); - } + String playerName = playerUuid != null ? plugin.getServer().getOfflinePlayer(UUID.fromString(playerUuid)).getName() : "null"; + + String rawUnauthorizedPage; + try { + rawUnauthorizedPage = Files.readString(this.unauthorizedPagePath); + } catch (IOException e) { + rawUnauthorizedPage = "You are not authorized to use this application."; + plugin.getLogger().severe("Couldn't load unauthorized page, using a plain one"); + e.printStackTrace(); } - - request.setBody(this.unauthorizedPage, "text/html"); + String unauthorizedPage = + rawUnauthorizedPage + .replaceAll("\\{\\{host}}", host) + .replaceAll("\\{\\{permission}}", String.valueOf(permissionNode)) + .replaceAll("\\{\\{uuid}}", String.valueOf(playerUuid)) + .replaceAll("\\{\\{name}}", String.valueOf(playerName)); + + request.setBody(unauthorizedPage, "text/html"); request.respond(200); }); + this.http.serveStatic("/unauthorized/", plugin.getDataFolder().toPath().resolve("web")); + this.http.start(); diff --git a/Authorization/src/main/resources/unauthorized.html b/Authorization/src/main/resources/unauthorized.html deleted file mode 100644 index 68ba5be..0000000 --- a/Authorization/src/main/resources/unauthorized.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - Authorization - - - -
-

- You are not authorized to use this application. -

- -
- - diff --git a/Authorization/src/main/resources/web/unauthorized.html b/Authorization/src/main/resources/web/unauthorized.html new file mode 100644 index 0000000..6db7dc6 --- /dev/null +++ b/Authorization/src/main/resources/web/unauthorized.html @@ -0,0 +1,100 @@ + + + + + + + Authorization + + + +
+
+
+ {{name}}'s player head {{name}} +
+
+

+ You are not authorized to use the application at {{host}}.
+ This requires the {{permission}} permission node.
+ +

+
+ + + diff --git a/Common/build.gradle.kts b/Common/build.gradle.kts index f3452a8..cacf6d9 100644 --- a/Common/build.gradle.kts +++ b/Common/build.gradle.kts @@ -13,7 +13,6 @@ repositories { dependencies { compileOnly ("org.jetbrains:annotations:23.0.0") - implementation ("org.xerial:sqlite-jdbc:3.41.2.1") } val javaTarget = 11 diff --git a/Common/src/main/java/codes/antti/auth/common/WebServer.java b/Common/src/main/java/codes/antti/auth/common/WebServer.java index e4e6ea0..e97f130 100644 --- a/Common/src/main/java/codes/antti/auth/common/WebServer.java +++ b/Common/src/main/java/codes/antti/auth/common/WebServer.java @@ -5,11 +5,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.sql.SQLException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; public final class WebServer { @@ -21,7 +21,7 @@ public WebServer(@NotNull String ip, int port) throws IOException { }); } - public class Request { + public static class Request { public final HttpExchange httpExchange; private final HashMap cookies = new HashMap<>(); private final ArrayList responseCookies = new ArrayList<>(); @@ -44,9 +44,6 @@ public Request(HttpExchange httpExchange) { } } - public void setBody(@NotNull String body) { - this.setBody(body, null); - } public void setBody(@NotNull String body, @Nullable String bodyType) { this.bodyType = bodyType; this.body = body; @@ -54,12 +51,10 @@ public void setBody(@NotNull String body, @Nullable String bodyType) { public void respond(int statusCode) throws IOException { if (this.responseCookies.size() > 0) setHeader("Set-Cookie", String.join("; ", responseCookies)); - if (this.body != null) { + if (this.body != null) this.setHeader("Content-Type", Optional.ofNullable(bodyType).orElse("text/plain") + "; charset=UTF-8"); + if (this.body != null && this.getMethod().equals("GET")) { byte[] response = body.getBytes(StandardCharsets.UTF_8); - - httpExchange.getResponseHeaders().add("Content-Type", Optional.ofNullable(bodyType).orElse("text/plain") + "; charset=UTF-8"); httpExchange.sendResponseHeaders(statusCode, response.length); - OutputStream out = httpExchange.getResponseBody(); out.write(response); out.close(); @@ -84,7 +79,7 @@ private String formatCookie(@NotNull String name, @NotNull String value, long ex return name + "=" + value + "; " + "Expires=" + new Date(expires * 1000L) + "; " + "HttpOnly; Secure; " + - "SameSite=Strict;" + + "SameSite=Strict; " + "Path=/"; } @@ -92,6 +87,10 @@ public HashMap getCookies() { return cookies; } + public String getMethod() { + return this.httpExchange.getRequestMethod(); + } + public void setCookie(@NotNull String key, @NotNull String value, long expires) { responseCookies.add(formatCookie(key, value, expires)); } @@ -107,7 +106,12 @@ public interface Handler { } public void handle(@NotNull String path, @NotNull Handler handler) { - this.internal.createContext(path, httpExchange -> { + this.internal.createContext(path.endsWith("*") ? path.substring(0, path.length() - 1) : path, httpExchange -> { + String method = httpExchange.getRequestMethod(); + if (!method.equals("GET") && !method.equals("HEAD")) { + httpExchange.sendResponseHeaders(400, -1); + return; + } String requestPath = httpExchange.getRequestURI().getPath(); if (!path.endsWith("*") && !path.equals(requestPath)) { httpExchange.sendResponseHeaders(404, -1); @@ -123,6 +127,56 @@ public void handle(@NotNull String path, @NotNull Handler handler) { }); } + public void serveStatic(@NotNull String path, @NotNull Path root) { + String rootString = root.toFile().getAbsolutePath(); + if (!path.startsWith("/")) throw new RuntimeException("Path should start with a slash"); + if (!path.endsWith("/")) throw new RuntimeException("Path should end with a slash"); + this.internal.createContext(path, httpExchange -> { + String method = httpExchange.getRequestMethod(); + if (!method.equals("GET") && !method.equals("HEAD")) { + httpExchange.sendResponseHeaders(400, -1); + return; + } + String wholePath = httpExchange.getRequestURI().getPath(); + if (wholePath.endsWith("/")) wholePath += "index.html"; + String fsPath = wholePath.substring(path.length()); + File file; + try { + file = root.resolve(fsPath).toFile().getCanonicalFile(); + } catch (IOException ex) { + httpExchange.sendResponseHeaders(400, -1); + return; + } + if (!file.getPath().startsWith(rootString)) { + httpExchange.sendResponseHeaders(400, -1); + return; + } + try (FileInputStream in = new FileInputStream(file)) { + String mimeType; + try { + mimeType = Optional.ofNullable(Files.probeContentType(file.toPath())).orElse("text/plain"); + } catch (Exception ignored) { + mimeType = "text/plain"; + }; + httpExchange.getResponseHeaders().set("Content-Type", mimeType); + if (method.equals("GET")) { + httpExchange.sendResponseHeaders(200, file.length()); + OutputStream out = httpExchange.getResponseBody(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) >= 0) { + out.write(buf, 0, n); + } + out.close(); + } else { + httpExchange.sendResponseHeaders(200, -1); + } + } catch (FileNotFoundException e) { + httpExchange.sendResponseHeaders(404, -1); + } + }); + } + public String getAddress() { return this.internal.getAddress().toString(); } diff --git a/README.md b/README.md index e717631..e8dd447 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ The default `31` means sessions expire after a month and then players need to re #### Customizing The default login page is very bland. -You can customize it to your branding by editing the `login.html` in `./plugins/Authentication/login.html`. -Additional assets aren't currently supported, so the page needs to be self-contained. -The string `{{auth_token}}` in the file is replaced by the auth token. +You can customize it to your branding by editing the `login.html` in `./plugins/Authentication/web/login.html`. +You can create additional assets in the web folder. They will be available under the `/login/*` path of the webserver. +The placeholder `{{auth_token}}` in the `login.html` file is replaced by the authentication token. #### Permissions @@ -119,8 +119,16 @@ the player needs to have the `auth.map` permission node. The default unauthorized page is very bland. You can customize it to your branding by editing the `unauthorized.html` in `./plugins/Authorization/unauthorized.html`. You should probably consider adding instructions on how to get access or explain why the user can't access the page. -Additional assets aren't currently supported, so the page needs to be self-contained. -There are currently no placeholder variables that could be utilized. +You can create additional assets in the web folder. They will be available under the `/unauthorized/*` path of the webserver. +There are couple placeholders available for the `unauthorized.html` file: + +| Variable | Value | +|------------------|------------------------------------| +| `{{uuid}}` | Logged in player's uuid | +| `{{name}}` | Logged in player's name | +| `{{host}}` | The hostname of the target website | +| `{{permission}}` | The required permission node | + ## Example Setup