diff --git a/build.gradle b/build.gradle index 07652d2..37690ce 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,13 @@ plugins { id "com.diffplug.spotless" version "6.25.0" } + group = 'de.srsoftware' version = '1.0-SNAPSHOT' +jar.enabled = false +build.enabled = false + repositories { mavenCentral() } diff --git a/de.srsoftware.cookies/build.gradle b/de.srsoftware.cookies/build.gradle new file mode 100644 index 0000000..a55b584 --- /dev/null +++ b/de.srsoftware.cookies/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group = 'de.srsoftware' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java similarity index 81% rename from de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java rename to de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java index 5552215..ae97c98 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java +++ b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java @@ -1,10 +1,11 @@ /* © SRSoftware 2024 */ -package de.srsoftware.oidc.api; +package de.srsoftware.cookies; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import java.util.List; import java.util.Map; +import java.util.Optional; public abstract class Cookie implements Map.Entry { private final String key; @@ -34,8 +35,8 @@ public abstract class Cookie implements Map.Entry { return value; } - protected static List of(HttpExchange ex) { - return ex.getRequestHeaders().get("Cookie"); + protected static Optional> of(HttpExchange ex) { + return Optional.ofNullable(ex.getRequestHeaders().get("Cookie")); } @Override diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java similarity index 62% rename from de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java rename to de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java index 6ab8c23..672c84c 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java +++ b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java @@ -1,8 +1,9 @@ /* © SRSoftware 2024 */ -package de.srsoftware.oidc.api; +package de.srsoftware.cookies; import com.sun.net.httpserver.HttpExchange; +import java.util.List; import java.util.Optional; public class SessionToken extends Cookie { @@ -14,7 +15,7 @@ public class SessionToken extends Cookie { } public static Optional from(HttpExchange ex) { - return Cookie.of(ex).stream().filter(cookie -> cookie.startsWith("sessionToken=")).map(cookie -> cookie.split("=", 2)[1]).map(id -> new SessionToken(id)).findAny(); + return Cookie.of(ex).orElseGet(List::of).stream().filter(cookie -> cookie.startsWith("sessionToken=")).map(cookie -> cookie.split("=", 2)[1]).map(id -> new SessionToken(id)).findAny(); } public String sessionId() { diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index e8dde62..75eca43 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -13,17 +13,21 @@ import de.srsoftware.oidc.web.Forward; import de.srsoftware.oidc.web.StaticPages; import java.io.File; import java.net.InetSocketAddress; -import java.util.UUID; +import java.nio.file.Path; +import java.util.*; import java.util.concurrent.Executors; public class Application { - public static final String STATIC_PATH = "/web"; - public static final String FIRST_USER = "admin"; - public static final String FIRST_USER_PASS = "admin"; - public static final String FIRST_UUID = UUID.randomUUID().toString(); - public static final String INDEX = STATIC_PATH + "/index.html"; + public static final String STATIC_PATH = "/web"; + public static final String FIRST_USER = "admin"; + public static final String FIRST_USER_PASS = "admin"; + public static final String FIRST_UUID = UUID.randomUUID().toString(); + public static final String INDEX = STATIC_PATH + "/index.html"; + private static final String BASE_PATH = "basePath"; public static void main(String[] args) throws Exception { + var argMap = map(args); + Optional basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty(); var storageFile = new File("/tmp/lightoidc.json"); var passwordHasher = new UuidHasher(); var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); @@ -32,10 +36,28 @@ public class Application { UserService userService = fileStore; SessionService sessionService = fileStore; HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); - new StaticPages().bindPath(STATIC_PATH).on(server); + new StaticPages(basePath).bindPath(STATIC_PATH).on(server); new Forward(INDEX).bindPath("/").on(server); new Backend(sessionService, userService).bindPath("/api").on(server); server.setExecutor(Executors.newCachedThreadPool()); server.start(); } + + private static Map map(String[] args) { + var tokens = new ArrayList<>(List.of(args)); + var map = new HashMap(); + while (!tokens.isEmpty()) { + var token = tokens.remove(0); + switch (token) { + case "--base": + if (tokens.isEmpty()) throw new IllegalArgumentException("--path option requires second argument!"); + map.put(BASE_PATH, Path.of(tokens.remove(0))); + break; + default: + System.err.printf("Unknown option: %s\n", token); + } + } + + return map; + } } diff --git a/de.srsoftware.oidc.backend/build.gradle b/de.srsoftware.oidc.backend/build.gradle index cbc152a..4d47a02 100644 --- a/de.srsoftware.oidc.backend/build.gradle +++ b/de.srsoftware.oidc.backend/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' + implementation project(':de.srsoftware.cookies') implementation project(':de.srsoftware.oidc.api') implementation 'org.json:json:20240303' } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java index f46f1ef..7d3afd2 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java @@ -8,6 +8,7 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.nio.charset.StandardCharsets.UTF_8; import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.cookies.SessionToken; import de.srsoftware.oidc.api.*; import java.io.IOException; import java.util.Optional; @@ -43,19 +44,23 @@ public class Backend extends PathHandler { String method = ex.getRequestMethod(); System.out.printf("%s %s…", method, path); - var user = getSession(ex).map(Session::user); + var session = getSession(ex); if ("login".equals(path) && POST.equals(method)) { doLogin(ex); // TODO: prevent brute force return; } - if (user.isEmpty()) { + if (session.isEmpty()) { sendEmptyResponse(HTTP_UNAUTHORIZED, ex); System.err.println("unauthorized"); return; } + switch (path) { + case "user": + sendUserAndCookie(ex, session.get()); + return; + } System.err.println("not implemented"); - ex.sendResponseHeaders(HTTP_NOT_FOUND, 0); - ex.getResponseBody().close(); + sendEmptyResponse(HTTP_NOT_FOUND, ex); } private Optional getSession(HttpExchange ex) { diff --git a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java index b893e7f..84e14ed 100644 --- a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java +++ b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java @@ -6,11 +6,21 @@ import de.srsoftware.oidc.api.PathHandler; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; public class StaticPages extends PathHandler { private static final String DEFAULT_LANGUAGE = "en"; - private ClassLoader loader; + private final Optional base; + private ClassLoader loader; + + public StaticPages(Optional basePath) { + super(); + base = basePath; + } private record Response(String contentType, byte[] content) { } @@ -18,15 +28,15 @@ public class StaticPages extends PathHandler { @Override public void handle(HttpExchange ex) throws IOException { - String path = relativePath(ex); - String lang = language(ex).orElse(DEFAULT_LANGUAGE); - String method = ex.getRequestMethod(); + String relativePath = relativePath(ex); + String lang = language(ex).orElse(DEFAULT_LANGUAGE); + String method = ex.getRequestMethod(); - if (path.isBlank()) path = INDEX; + if (relativePath.isBlank()) relativePath = INDEX; System.out.printf("%s %s: ", method, ex.getRequestURI()); try { - System.out.printf("Loading %s for lagnuage %s…", path, lang); - var response = loadTemplate(lang, path).orElseThrow(() -> new FileNotFoundException()); + System.out.printf("Loading %s for lagnuage %s…", relativePath, lang); + Response response = loadFile(lang, relativePath).orElseThrow(() -> new FileNotFoundException()); ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType); ex.sendResponseHeaders(200, response.content.length); @@ -41,15 +51,37 @@ public class StaticPages extends PathHandler { } } - private Optional loadTemplate(String language, String path) throws IOException { + private URL getLocalUrl(Path base, String language, String path) { + var file = base.resolve(language).resolve(path); + if (!Files.isRegularFile(file)) { + file = base.resolve(DEFAULT_LANGUAGE).resolve(path); + if (!Files.isRegularFile(file)) return null; + } + try { + return file.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private URL getResource(String language, String path) { if (loader == null) loader = getClass().getClassLoader(); var resource = loader.getResource(String.join("/", language, path)); if (resource == null) resource = loader.getResource(String.join("/", DEFAULT_LANGUAGE, path)); - if (resource == null) return Optional.empty(); - var connection = resource.openConnection(); - var contentType = connection.getContentType(); - try (var in = connection.getInputStream()) { - return Optional.of(new Response(contentType, in.readAllBytes())); + return resource; + } + + private Optional loadFile(String language, String path) { + try { + var resource = base.map(b -> getLocalUrl(b, language, path)).orElseGet(() -> getResource(language, path)); + if (resource == null) return Optional.empty(); + var connection = resource.openConnection(); + var contentType = connection.getContentType(); + try (var in = connection.getInputStream()) { + return Optional.of(new Response(contentType, in.readAllBytes())); + } + } catch (IOException e) { + throw new RuntimeException(e); } } } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/index.html b/de.srsoftware.oidc.web/src/main/resources/en/index.html index a410308..16069ad 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/index.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/index.html @@ -2,10 +2,7 @@ Light OIDC - - +

Welcome!

diff --git a/de.srsoftware.oidc.web/src/main/resources/en/index.js b/de.srsoftware.oidc.web/src/main/resources/en/index.js new file mode 100644 index 0000000..5b558c9 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/index.js @@ -0,0 +1,7 @@ +const UNAUTHORIZED = 401; + +function handleUser(response){ + console.log(response); +} + +fetch(api+"/user").then(handleUser); \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js b/de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js deleted file mode 100644 index 53344bd..0000000 --- a/de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js +++ /dev/null @@ -1,43 +0,0 @@ -const UNAUTHORIZED = 401; - -function handleCheckUser(response){ - console.log(window.location.href); - if (response.status == UNAUTHORIZED){ - window.location.href = "login.html"; - return; - } -} -function checkUser(){ - fetch(api+"/user") - .then(handleCheckUser) - .catch((err) => console.log(err)); -} - -function handleLogin(response){ - if (response.status == 401){ - loadError("login-failed"); - return; - } - console.log(response); -} - -function loadError(page){ - fetch(web+"/"+page+".txt").then(resp => resp.text()).then(showError); -} - -function showError(content){ - document.getElementById("error").innerHTML = content; -} - -function tryLogin(){ - document.getElementById("error").innerHTML = ""; - var data = Object.fromEntries(new FormData(document.getElementById('login'))); - fetch(api+"/login",{ - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }).then(handleLogin); -} \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.html b/de.srsoftware.oidc.web/src/main/resources/en/login.html index ddb23a9..81a4195 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.html @@ -2,7 +2,7 @@ Light OIDC - +

Login

diff --git a/settings.gradle b/settings.gradle index 3ca87fe..1fafedf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,5 @@ include 'de.srsoftware.oidc.app' include 'de.srsoftware.oidc.web' include 'de.srsoftware.oidc.backend' include 'de.srsoftware.oidc.datastore.file' +include 'de.srsoftware.cookies'