22 changed files with 173 additions and 401 deletions
@ -1,17 +1,23 @@
@@ -1,17 +1,23 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.server; |
||||
package de.srsoftware.oidc.app; |
||||
|
||||
|
||||
import com.sun.net.httpserver.HttpServer; |
||||
import de.srsoftware.oidc.backend.Backend; |
||||
import de.srsoftware.oidc.web.Forward; |
||||
import de.srsoftware.oidc.web.StaticPages; |
||||
import java.net.InetSocketAddress; |
||||
import java.util.concurrent.Executors; |
||||
|
||||
public class Application { |
||||
public static final String STATIC_PATH = "/web"; |
||||
public static final String INDEX = STATIC_PATH + "/index.html"; |
||||
|
||||
public static void main(String[] args) throws Exception { |
||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); |
||||
new StaticPages().bindPath("/static").on(server); |
||||
new LanguageDirector("/static").bindPath("/").on(server); |
||||
new StaticPages().bindPath(STATIC_PATH).on(server); |
||||
new Forward(INDEX).bindPath("/").on(server); |
||||
new Backend().bindPath("/api").on(server); |
||||
server.setExecutor(Executors.newCachedThreadPool()); |
||||
server.start(); |
||||
} |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.backend; |
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND; |
||||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.oidc.api.PathHandler; |
||||
import java.io.IOException; |
||||
import java.util.Optional; |
||||
|
||||
public class Backend extends PathHandler { |
||||
@Override |
||||
public void handle(HttpExchange ex) throws IOException { |
||||
String path = relativePath(ex); |
||||
String method = ex.getRequestMethod(); |
||||
System.out.printf("%s %s…", method, path); |
||||
|
||||
if ("login".equals(path)) { |
||||
doLogin(ex); |
||||
return; |
||||
} |
||||
var token = getAuthToken(ex); |
||||
if (token.isEmpty()) { |
||||
emptyResponse(HTTP_UNAUTHORIZED, ex); |
||||
System.err.println("unauthorized"); |
||||
return; |
||||
} |
||||
System.err.println("not implemented"); |
||||
ex.sendResponseHeaders(HTTP_NOT_FOUND, 0); |
||||
ex.getResponseBody().close(); |
||||
} |
||||
|
||||
private void doLogin(HttpExchange ex) throws IOException { |
||||
Optional<String> user = getHeader(ex, "login-username"); |
||||
Optional<String> pass = getHeader(ex, "login-password"); |
||||
System.out.printf("%s : %s", user, pass); |
||||
emptyResponse(HTTP_UNAUTHORIZED, ex); |
||||
} |
||||
|
||||
private Optional<String> getAuthToken(HttpExchange ex) { |
||||
return getHeader(ex, "Authorization"); |
||||
} |
||||
} |
@ -1,20 +0,0 @@
@@ -1,20 +0,0 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.light; |
||||
|
||||
public class Constants { |
||||
public static final String BODY = "body"; |
||||
public static final String EMAIL = "email"; |
||||
public static final String ERR_PAGE_NOT_FOUND = "page_not_found"; |
||||
public static final String ERR_REDIRECT_FAILED = "redirect_failed"; |
||||
public static final String HEAD = "head"; |
||||
public static final String MESSAGES = "messages.txt"; |
||||
public static final String PAGE_LOGIN = "login"; |
||||
public static final String PAGE_START = "start"; |
||||
public static final String PAGE_WELCOME = "welcome"; |
||||
public static final String PASSWORD = "password"; |
||||
public static final String TARGET = "target"; |
||||
public static final String TITLE = "title"; |
||||
public static final String TITLE_LOGIN = "title_login"; |
||||
public static final String TITLE_WELCOME = "title_welcom"; |
||||
public static final String USER = "user"; |
||||
} |
@ -1,178 +0,0 @@
@@ -1,178 +0,0 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.light; |
||||
|
||||
import static de.srsoftware.oidc.light.Constants.*; |
||||
import static de.srsoftware.oidc.light.Templates.braced; |
||||
|
||||
import de.srsoftware.oidc.api.User; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.annotation.WebServlet; |
||||
import jakarta.servlet.http.HttpServlet; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import jakarta.servlet.http.HttpSession; |
||||
import java.io.IOException; |
||||
import java.util.*; |
||||
import java.util.stream.Collectors; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
@WebServlet("/web") |
||||
public class LightOICD extends HttpServlet { |
||||
private static final Logger LOG = LoggerFactory.getLogger(LightOICD.class); |
||||
private static final Templates templates; |
||||
|
||||
static { |
||||
try { |
||||
templates = Templates.singleton(); |
||||
} catch (IOException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { |
||||
var path = relativePath(req); |
||||
var optUser = loadUser(req); |
||||
handleGet(path, optUser, req, resp).ifPresent(resp.getWriter()::println); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
||||
var path = relativePath(req); |
||||
var optUser = loadUser(req); |
||||
handlePost(path, optUser, req, resp).ifPresent(resp.getWriter()::println); |
||||
} |
||||
|
||||
public List<String> relativePath(HttpServletRequest req) { |
||||
var cp = req.getContextPath(); |
||||
var uri = req.getRequestURI(); |
||||
if (uri.startsWith(cp)) uri = uri.substring(cp.length()); // strip context path → relative path!
|
||||
var path = Arrays.stream(uri.split("/")).skip(1).collect(Collectors.toList()); |
||||
if (path.isEmpty()) path.add(PAGE_START); |
||||
return path; |
||||
} |
||||
|
||||
private Optional<String> handleGet(List<String> path, Optional<User> optUser, HttpServletRequest req, HttpServletResponse resp) { |
||||
String token = path.remove(0); |
||||
if (optUser.isPresent()) { |
||||
var user = optUser.get(); |
||||
switch (token) { |
||||
case PAGE_START: |
||||
return pageStart(user, req, resp); |
||||
case PAGE_WELCOME: |
||||
return pageWelcome(user, req, resp); |
||||
} |
||||
} |
||||
|
||||
switch (token) { |
||||
case PAGE_LOGIN: |
||||
return pageLogin(req, resp); |
||||
case PAGE_START: |
||||
case PAGE_WELCOME: |
||||
return redirect(resp, PAGE_LOGIN); |
||||
} |
||||
return templates.message(ERR_PAGE_NOT_FOUND); |
||||
} |
||||
|
||||
|
||||
private Optional<String> handlePost(List<String> path, Optional<User> optUser, HttpServletRequest req, HttpServletResponse resp) { |
||||
String token = path.remove(0); |
||||
if (optUser.isPresent()) { |
||||
var user = optUser.get(); |
||||
switch (token) { |
||||
case PAGE_START: |
||||
return pageStart(user, req, resp); |
||||
} |
||||
} |
||||
|
||||
switch (token) { |
||||
case PAGE_LOGIN: |
||||
return postLogin(req, resp); |
||||
case PAGE_START: |
||||
return redirect(resp, PAGE_LOGIN); |
||||
} |
||||
return templates.message(ERR_PAGE_NOT_FOUND); |
||||
} |
||||
|
||||
private Optional<User> loadUser(HttpServletRequest req) { |
||||
HttpSession session = req.getSession(); |
||||
if (session.getAttribute(USER) instanceof User user) return Optional.of(user); |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
|
||||
private Optional<String> pageLogin(HttpServletRequest req, HttpServletResponse resp) { |
||||
LOG.debug("pageLogin(…)"); |
||||
try { |
||||
var title = templates.message(TITLE_LOGIN).orElse(TITLE_LOGIN); |
||||
var head = templates.get("head.snippet", Map.of(TITLE, title)).get(); |
||||
var login = templates.get("login.snippet", Map.of(USER, "Darling", EMAIL, "", PASSWORD, "")).get(); |
||||
var page = templates.get("scaffold.html", Map.of(BODY, login, HEAD, head)).get(); |
||||
resp.setContentType("text/html"); |
||||
resp.getWriter().println(page); |
||||
return Optional.empty(); |
||||
} catch (Exception e) { |
||||
return Optional.of(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
private Optional<String> pageStart(User user, HttpServletRequest req, HttpServletResponse resp) { |
||||
LOG.debug("pageStart(…)"); |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
private Optional<String> pageWelcome(User user, HttpServletRequest req, HttpServletResponse resp) { |
||||
LOG.debug("pageWelcome(…)"); |
||||
try { |
||||
var title = templates.message(TITLE_WELCOME).orElse(TITLE_WELCOME); |
||||
var head = templates.get("head.snippet", Map.of(TITLE, title)).get(); |
||||
var login = templates.get("welcome.snippet", Map.of(USER, "Darling", EMAIL, "", PASSWORD, "")).get(); |
||||
var page = templates.get("scaffold.html", Map.of(BODY, login, HEAD, head)).get(); |
||||
resp.setContentType("text/html"); |
||||
resp.getWriter().println(page); |
||||
return Optional.empty(); |
||||
} catch (Exception e) { |
||||
return Optional.of(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
private Optional<String> postLogin(HttpServletRequest req, HttpServletResponse resp) { |
||||
LOG.debug("postLogin(…)"); |
||||
var email = req.getParameter(EMAIL); |
||||
if (braced(EMAIL).equals(email)) email = ""; |
||||
var pass = req.getParameter(PASSWORD); |
||||
var user = tryLogin(email, pass); |
||||
if (user.isPresent()) { |
||||
req.getSession().setAttribute(USER, user.get()); |
||||
return redirect(resp, PAGE_WELCOME); |
||||
} |
||||
try { |
||||
var title = templates.message(TITLE_LOGIN).orElse(TITLE_LOGIN); |
||||
var head = templates.get("head.snippet", Map.of(TITLE, title)).get(); |
||||
var login = templates.get("login.snippet", Map.of(USER, "Darling", EMAIL, email, PASSWORD, "")).get(); |
||||
var page = templates.get("scaffold.html", Map.of(BODY, login, HEAD, head)).get(); |
||||
resp.setContentType("text/html"); |
||||
resp.getWriter().println(page); |
||||
return Optional.empty(); |
||||
} catch (Exception e) { |
||||
return Optional.of(e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
private Optional<User> tryLogin(String email, String pass) { |
||||
if (email == null || pass == null) return Optional.empty(); |
||||
if (email.equals(pass)) return Optional.of(new User()); |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
private Optional<String> redirect(HttpServletResponse resp, String path) { |
||||
try { |
||||
resp.sendRedirect(path); |
||||
} catch (IOException e) { |
||||
return templates.message(ERR_REDIRECT_FAILED, Map.of(TARGET, path)); |
||||
} |
||||
return Optional.empty(); |
||||
} |
||||
} |
@ -1,94 +0,0 @@
@@ -1,94 +0,0 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.light; |
||||
|
||||
import static de.srsoftware.oidc.light.Constants.MESSAGES; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
public class Templates { |
||||
private static Templates singleton = null; |
||||
private static Logger LOG = LoggerFactory.getLogger(Templates.class); |
||||
private Path dir = searchTemplates(); |
||||
private Map<String, String> messages = null; |
||||
|
||||
public Templates() throws FileNotFoundException { |
||||
} |
||||
|
||||
private static Path searchTemplates() throws FileNotFoundException { |
||||
return searchTemplates(new File(System.getProperty("user.dir"))).map(File::toPath).orElseThrow(() -> new FileNotFoundException("Missing template directory")); |
||||
} |
||||
|
||||
private static Optional<File> searchTemplates(File dir) { |
||||
if (dir.isDirectory()) { |
||||
var children = dir.listFiles(); |
||||
for (File child : children) { |
||||
if (child.isDirectory()) { |
||||
if (child.getName().equals("templates")) return Optional.of(child); |
||||
var inner = searchTemplates(child); |
||||
if (inner.isPresent()) return inner; |
||||
} |
||||
} |
||||
} |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
public static Templates singleton() throws IOException { |
||||
if (singleton == null) singleton = new Templates(); |
||||
return singleton; |
||||
} |
||||
|
||||
public Optional<String> get(String path) { |
||||
return get(path, Map.of()); |
||||
} |
||||
|
||||
public Optional<String> get(String path, Map<String, String> replacements) { |
||||
var file = dir.resolve(path); |
||||
try { |
||||
return Optional.of(Files.readString(file)).map(s -> replaceKeys(s, replacements)); |
||||
} catch (IOException e) { |
||||
LOG.warn("Failed to read {}", path, e); |
||||
return Optional.empty(); |
||||
} |
||||
} |
||||
|
||||
private String replaceKeys(String text, Map<String, String> replacements) { |
||||
for (Map.Entry<String, String> replacement : replacements.entrySet()) text = text.replace(braced(replacement.getKey()), replacement.getValue()); |
||||
return text; |
||||
} |
||||
|
||||
public Optional<String> message(String code) { |
||||
return message(code, Map.of()); |
||||
} |
||||
|
||||
public Optional<String> message(String code, Map<String, String> replacements) { |
||||
if (this.messages == null) { |
||||
get(MESSAGES).map(s -> s.split("\n")).ifPresent(this::setMessages); |
||||
} |
||||
return Optional.ofNullable(messages.get(code)).map(text -> replaceKeys(text, replacements)); |
||||
} |
||||
|
||||
private void setMessages(String[] lines) { |
||||
this.messages = new HashMap<>(); |
||||
for (String line : lines) { |
||||
var parts = line.split(" ", 2); |
||||
if (parts.length < 2) { |
||||
LOG.warn("Invalid format in {} file, skipped {}", MESSAGES, line); |
||||
continue; |
||||
} |
||||
messages.put(parts[0], parts[1]); |
||||
} |
||||
} |
||||
|
||||
public static String braced(String key) { |
||||
return String.join(key, "{", "}"); |
||||
} |
||||
} |
@ -1,16 +0,0 @@
@@ -1,16 +0,0 @@
|
||||
<meta charset="UTF-8"> |
||||
<title>{title}</title> |
||||
<style> |
||||
body{ |
||||
background-color: #555; |
||||
color: #eeffee; |
||||
} |
||||
label{ |
||||
display: block; |
||||
} |
||||
fieldset{ |
||||
border-radius: 10px; |
||||
display: inline; |
||||
margin: 0 auto; |
||||
} |
||||
</style> |
@ -1,5 +0,0 @@
@@ -1,5 +0,0 @@
|
||||
<html> |
||||
<body> |
||||
Hallo {user}, dies ist die Index-Seite! |
||||
</body> |
||||
</html> |
@ -1,16 +0,0 @@
@@ -1,16 +0,0 @@
|
||||
<h1>Login</h1> |
||||
|
||||
<form action="login" method="POST"> |
||||
<fieldset> |
||||
<legend>Light OIDC Login</legend> |
||||
<label> |
||||
<input type="text" name="email" value="{email}" /> |
||||
Email address |
||||
</label> |
||||
<label> |
||||
<input type="password" name="password" value="{password}" /> |
||||
Password |
||||
</label> |
||||
<button type="submit">Login</button> |
||||
</fieldset> |
||||
</form> |
@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
|
||||
title_login LightOIDC Login |
||||
title_welcome Willkommen bei LightOIDC! |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
{head} |
||||
<body> |
||||
{body} |
||||
</html> |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
Willkommen! |
@ -1,27 +0,0 @@
@@ -1,27 +0,0 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.server; |
||||
|
||||
import com.sun.net.httpserver.Headers; |
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.oidc.api.PathHandler; |
||||
import java.io.IOException; |
||||
import java.util.Arrays; |
||||
|
||||
public class LanguageDirector extends PathHandler { |
||||
private static final String DEFAULT_LANG = "de"; |
||||
private final String path; |
||||
|
||||
public LanguageDirector(String pathTo) { |
||||
path = pathTo; |
||||
} |
||||
|
||||
@Override |
||||
public void handle(HttpExchange t) throws IOException { |
||||
Headers headers = t.getRequestHeaders(); |
||||
String lang = headers.get("Accept-Language").stream().flatMap(s -> Arrays.stream(s.split(","))).findFirst().orElse(DEFAULT_LANG); |
||||
|
||||
t.getResponseHeaders().add("Location", String.join(lang, "/static/", "/de/index.html")); |
||||
t.sendResponseHeaders(301, 0); |
||||
t.getResponseBody().close(); |
||||
} |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* © SRSoftware 2024 */ |
||||
package de.srsoftware.oidc.web; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import de.srsoftware.oidc.api.PathHandler; |
||||
import java.io.IOException; |
||||
|
||||
public class Forward extends PathHandler { |
||||
private final int CODE = 302; |
||||
private final String toPath; |
||||
|
||||
public Forward(String toPath) { |
||||
this.toPath = toPath; |
||||
} |
||||
|
||||
@Override |
||||
public void handle(HttpExchange ex) throws IOException { |
||||
System.out.printf("Forwarding (%d) %s to %s…\n", CODE, ex.getRequestURI(), toPath); |
||||
ex.getResponseHeaders().add("Location", toPath); |
||||
ex.sendResponseHeaders(CODE, 0); |
||||
ex.getResponseBody().close(); |
||||
} |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
var api = "/api"; |
@ -1,9 +1,13 @@
@@ -1,9 +1,13 @@
|
||||
<html> |
||||
<head> |
||||
<title>{title}</title> |
||||
<script src="lightoidc.js" /> |
||||
<title>Light OIDC</title> |
||||
<script src="config.js"></script> |
||||
<script src="lightoidc.js"></script> |
||||
<script> |
||||
checkUser(); |
||||
</script> |
||||
</head> |
||||
<body> |
||||
{body} |
||||
<h1>Welcome!</h1> |
||||
</body> |
||||
</html> |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
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 submitForm(formId){ |
||||
var data = Object.fromEntries(new FormData(document.getElementById(formId))); |
||||
fetch(api+"/login",{ |
||||
headers: { |
||||
'login-username': data.user, |
||||
'login-password': data.pass, // TODO: send via body?
|
||||
Accept: 'application/json', |
||||
'Content-Type': 'application/json' |
||||
} |
||||
}); |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
<html> |
||||
<head> |
||||
<title>Light OIDC</title> |
||||
<script src="config.js"></script> |
||||
<script src="lightoidc.js"></script> |
||||
</head> |
||||
<body> |
||||
<h1>Login</h1> |
||||
<form id="form"> |
||||
<fieldset> |
||||
<legend>User credentials</legend> |
||||
<label> |
||||
Username |
||||
<input type="text" name="user" /> |
||||
</label> |
||||
<label> |
||||
Password |
||||
<input type="password" name="pass" /> |
||||
</label> |
||||
<button type="button" onClick="submitForm('form')">Login</button> |
||||
</fieldset> |
||||
</form> |
||||
</body> |
||||
</html> |
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
rootProject.name = 'LightOIDC' |
||||
include 'de.srsoftware.oidc.api' |
||||
include 'de.srsoftware.oidc.light' |
||||
include 'de.srsoftware.oidc.server' |
||||
include 'de.srsoftware.oidc.app' |
||||
include 'de.srsoftware.oidc.web' |
||||
include 'de.srsoftware.oidc.backend' |
||||
|
||||
|
Loading…
Reference in new issue