implemented cookies, implemented local file delivery option (--base /path/to/static/content), refactoring static files
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -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
Normal file
19
de.srsoftware.cookies/build.gradle
Normal file
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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() {
|
||||||
@@ -13,17 +13,21 @@ 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 {
|
||||||
public static final String STATIC_PATH = "/web";
|
public static final String STATIC_PATH = "/web";
|
||||||
public static final String FIRST_USER = "admin";
|
public static final String FIRST_USER = "admin";
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -6,11 +6,21 @@ 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 ClassLoader loader;
|
private final Optional<Path> base;
|
||||||
|
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) {
|
||||||
}
|
}
|
||||||
@@ -18,15 +28,15 @@ public class StaticPages extends PathHandler {
|
|||||||
|
|
||||||
@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));
|
||||||
if (resource == null) return Optional.empty();
|
return resource;
|
||||||
var connection = resource.openConnection();
|
}
|
||||||
var contentType = connection.getContentType();
|
|
||||||
try (var in = connection.getInputStream()) {
|
private Optional<Response> loadFile(String language, String path) {
|
||||||
return Optional.of(new Response(contentType, in.readAllBytes()));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Normal file
7
de.srsoftware.oidc.web/src/main/resources/en/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const UNAUTHORIZED = 401;
|
||||||
|
|
||||||
|
function handleUser(response){
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(api+"/user").then(handleUser);
|
||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user