Browse Source

implemented cookies, implemented local file delivery option (--base /path/to/static/content), refactoring static files

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 5 months ago
parent
commit
9ee963924d
  1. 4
      build.gradle
  2. 19
      de.srsoftware.cookies/build.gradle
  3. 7
      de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java
  4. 5
      de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java
  5. 26
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  6. 1
      de.srsoftware.oidc.backend/build.gradle
  7. 13
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java
  8. 42
      de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java
  9. 5
      de.srsoftware.oidc.web/src/main/resources/en/index.html
  10. 7
      de.srsoftware.oidc.web/src/main/resources/en/index.js
  11. 43
      de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js
  12. 2
      de.srsoftware.oidc.web/src/main/resources/en/login.html
  13. 1
      settings.gradle

4
build.gradle

@ -3,9 +3,13 @@ plugins {
id "com.diffplug.spotless" version "6.25.0" id "com.diffplug.spotless" version "6.25.0"
} }
group = 'de.srsoftware' group = 'de.srsoftware'
version = '1.0-SNAPSHOT' version = '1.0-SNAPSHOT'
jar.enabled = false
build.enabled = false
repositories { repositories {
mavenCentral() mavenCentral()
} }

19
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()
}

7
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java → de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java

@ -1,10 +1,11 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api; package de.srsoftware.cookies;
import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
public abstract class Cookie implements Map.Entry<String, String> { public abstract class Cookie implements Map.Entry<String, String> {
private final String key; private final String key;
@ -34,8 +35,8 @@ public abstract class Cookie implements Map.Entry<String, String> {
return value; return value;
} }
protected static List<String> of(HttpExchange ex) { protected static Optional<List<String>> of(HttpExchange ex) {
return ex.getRequestHeaders().get("Cookie"); return Optional.ofNullable(ex.getRequestHeaders().get("Cookie"));
} }
@Override @Override

5
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java → de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java

@ -1,8 +1,9 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api; package de.srsoftware.cookies;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public class SessionToken extends Cookie { public class SessionToken extends Cookie {
@ -14,7 +15,7 @@ public class SessionToken extends Cookie {
} }
public static Optional<SessionToken> from(HttpExchange ex) { public static Optional<SessionToken> 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() { public String sessionId() {

26
de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java

@ -13,7 +13,8 @@ import de.srsoftware.oidc.web.Forward;
import de.srsoftware.oidc.web.StaticPages; import de.srsoftware.oidc.web.StaticPages;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.UUID; import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class Application { public class Application {
@ -22,8 +23,11 @@ public class Application {
public static final String FIRST_USER_PASS = "admin"; public static final String FIRST_USER_PASS = "admin";
public static final String FIRST_UUID = UUID.randomUUID().toString(); public static final String FIRST_UUID = UUID.randomUUID().toString();
public static final String INDEX = STATIC_PATH + "/index.html"; public static final String INDEX = STATIC_PATH + "/index.html";
private static final String BASE_PATH = "basePath";
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
var argMap = map(args);
Optional<Path> basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty();
var storageFile = new File("/tmp/lightoidc.json"); var storageFile = new File("/tmp/lightoidc.json");
var passwordHasher = new UuidHasher(); var passwordHasher = new UuidHasher();
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
@ -32,10 +36,28 @@ public class Application {
UserService userService = fileStore; UserService userService = fileStore;
SessionService sessionService = fileStore; SessionService sessionService = fileStore;
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); 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 Forward(INDEX).bindPath("/").on(server);
new Backend(sessionService, userService).bindPath("/api").on(server); new Backend(sessionService, userService).bindPath("/api").on(server);
server.setExecutor(Executors.newCachedThreadPool()); server.setExecutor(Executors.newCachedThreadPool());
server.start(); server.start();
} }
private static Map<String, Object> map(String[] args) {
var tokens = new ArrayList<>(List.of(args));
var map = new HashMap<String, Object>();
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;
}
} }

1
de.srsoftware.oidc.backend/build.gradle

@ -12,6 +12,7 @@ repositories {
dependencies { dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter'
implementation project(':de.srsoftware.cookies')
implementation project(':de.srsoftware.oidc.api') implementation project(':de.srsoftware.oidc.api')
implementation 'org.json:json:20240303' implementation 'org.json:json:20240303'
} }

13
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 static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.cookies.SessionToken;
import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.*;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
@ -43,19 +44,23 @@ public class Backend extends PathHandler {
String method = ex.getRequestMethod(); String method = ex.getRequestMethod();
System.out.printf("%s %s…", method, path); 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)) { if ("login".equals(path) && POST.equals(method)) {
doLogin(ex); // TODO: prevent brute force doLogin(ex); // TODO: prevent brute force
return; return;
} }
if (user.isEmpty()) { if (session.isEmpty()) {
sendEmptyResponse(HTTP_UNAUTHORIZED, ex); sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
System.err.println("unauthorized"); System.err.println("unauthorized");
return; return;
} }
switch (path) {
case "user":
sendUserAndCookie(ex, session.get());
return;
}
System.err.println("not implemented"); System.err.println("not implemented");
ex.sendResponseHeaders(HTTP_NOT_FOUND, 0); sendEmptyResponse(HTTP_NOT_FOUND, ex);
ex.getResponseBody().close();
} }
private Optional<Session> getSession(HttpExchange ex) { private Optional<Session> getSession(HttpExchange ex) {

42
de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java

@ -6,27 +6,37 @@ import de.srsoftware.oidc.api.PathHandler;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; 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; import java.util.Optional;
public class StaticPages extends PathHandler { public class StaticPages extends PathHandler {
private static final String DEFAULT_LANGUAGE = "en"; private static final String DEFAULT_LANGUAGE = "en";
private final Optional<Path> base;
private ClassLoader loader; private ClassLoader loader;
public StaticPages(Optional<Path> basePath) {
super();
base = basePath;
}
private record Response(String contentType, byte[] content) { private record Response(String contentType, byte[] content) {
} }
private static final String INDEX = "en/index.html"; private static final String INDEX = "en/index.html";
@Override @Override
public void handle(HttpExchange ex) throws IOException { public void handle(HttpExchange ex) throws IOException {
String path = relativePath(ex); String relativePath = relativePath(ex);
String lang = language(ex).orElse(DEFAULT_LANGUAGE); String lang = language(ex).orElse(DEFAULT_LANGUAGE);
String method = ex.getRequestMethod(); String method = ex.getRequestMethod();
if (path.isBlank()) path = INDEX; if (relativePath.isBlank()) relativePath = INDEX;
System.out.printf("%s %s: ", method, ex.getRequestURI()); System.out.printf("%s %s: ", method, ex.getRequestURI());
try { try {
System.out.printf("Loading %s for lagnuage %s…", path, lang); System.out.printf("Loading %s for lagnuage %s…", relativePath, lang);
var response = loadTemplate(lang, path).orElseThrow(() -> new FileNotFoundException()); Response response = loadFile(lang, relativePath).orElseThrow(() -> new FileNotFoundException());
ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType); ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType);
ex.sendResponseHeaders(200, response.content.length); ex.sendResponseHeaders(200, response.content.length);
@ -41,15 +51,37 @@ public class StaticPages extends PathHandler {
} }
} }
private Optional<Response> 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(); if (loader == null) loader = getClass().getClassLoader();
var resource = loader.getResource(String.join("/", language, path)); var resource = loader.getResource(String.join("/", language, path));
if (resource == null) resource = loader.getResource(String.join("/", DEFAULT_LANGUAGE, path)); if (resource == null) resource = loader.getResource(String.join("/", DEFAULT_LANGUAGE, path));
return resource;
}
private Optional<Response> 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(); if (resource == null) return Optional.empty();
var connection = resource.openConnection(); var connection = resource.openConnection();
var contentType = connection.getContentType(); var contentType = connection.getContentType();
try (var in = connection.getInputStream()) { try (var in = connection.getInputStream()) {
return Optional.of(new Response(contentType, in.readAllBytes())); return Optional.of(new Response(contentType, in.readAllBytes()));
} }
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
} }

5
de.srsoftware.oidc.web/src/main/resources/en/index.html

@ -2,10 +2,7 @@
<head> <head>
<title>Light OIDC</title> <title>Light OIDC</title>
<script src="config.js"></script> <script src="config.js"></script>
<script src="lightoidc.js"></script> <script src="index.js"></script>
<script>
checkUser();
</script>
</head> </head>
<body> <body>
<h1>Welcome!</h1> <h1>Welcome!</h1>

7
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);

43
de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js

@ -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);
}

2
de.srsoftware.oidc.web/src/main/resources/en/login.html

@ -2,7 +2,7 @@
<head> <head>
<title>Light OIDC</title> <title>Light OIDC</title>
<script src="config.js"></script> <script src="config.js"></script>
<script src="lightoidc.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>
<h1>Login</h1> <h1>Login</h1>

1
settings.gradle

@ -4,4 +4,5 @@ include 'de.srsoftware.oidc.app'
include 'de.srsoftware.oidc.web' include 'de.srsoftware.oidc.web'
include 'de.srsoftware.oidc.backend' include 'de.srsoftware.oidc.backend'
include 'de.srsoftware.oidc.datastore.file' include 'de.srsoftware.oidc.datastore.file'
include 'de.srsoftware.cookies'

Loading…
Cancel
Save