Add authentication on HTTP API.
Add LoginHandler and register. Add method to create user in AppConfig. Add login check in Server.
This commit is contained in:
parent
98cd823999
commit
d54826b66b
14 changed files with 184 additions and 33 deletions
10
README.md
10
README.md
|
|
@ -7,8 +7,14 @@ This is a work-in-progress kinda of game launcher. LivingRoom is a server softwa
|
|||
The server includes a (working but very WIP) HTTP API for third-party clients and a web interface.
|
||||
|
||||
## Features
|
||||
### Server
|
||||
- [x] Game scanner
|
||||
- [ ] Automatically deindex removed game
|
||||
- [x] SQLite DB
|
||||
- [x] Metadata
|
||||
|
||||
### HTTP API
|
||||
- [ ] Authentication
|
||||
- [x] Authentication
|
||||
- [x] Library info
|
||||
- [x] Game info
|
||||
- [x] Downloads
|
||||
|
|
@ -24,7 +30,7 @@ The server includes a (working but very WIP) HTTP API for third-party clients an
|
|||
- [x] Deindex game
|
||||
- [x] Trigger new scan
|
||||
- [x] Downloads (from the API)
|
||||
- [x] User Management
|
||||
- [ ] User Management
|
||||
|
||||
## Stack
|
||||
- **Freemarker** for web rendering
|
||||
|
|
|
|||
9
shell.nix
Normal file
9
shell.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# shell.nix
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
javaPackages.compiler.openjdk21
|
||||
];
|
||||
JAVA_HOME = pkgs.javaPackages.compiler.openjdk21 + "/lib/openjdk";
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ public class AppConfig {
|
|||
public void setUsers(Map<String, UserConfig> users) {
|
||||
this.users = (users != null) ? users : new HashMap<>();
|
||||
}
|
||||
public void addUser(String name, UserConfig config) { this.users.put(name, config); }
|
||||
|
||||
public void hashPlaintextPasswords() {
|
||||
if (users == null) return;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ public class ConfigLoader {
|
|||
|
||||
private static AppConfig config;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static AppConfig load(String path) throws Exception {
|
||||
if (config != null) return config;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package org.adrianvictor.livingroom.http;
|
||||
|
||||
import org.adrianvictor.livingroom.http.handlers.*;
|
||||
import org.adrianvictor.livingroom.web.pages.Remove;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class Handlers {
|
||||
private static HashMap<Handler, String> map = new HashMap<>();
|
||||
private static final HashMap<Handler, String> map = new HashMap<>();
|
||||
|
||||
static {
|
||||
map.put(new CatalogHandler(), "/catalog");
|
||||
|
|
@ -14,6 +13,8 @@ public class Handlers {
|
|||
map.put(new FileHandler(), "/download");
|
||||
map.put(new WebHandler(), "/web");
|
||||
map.put(new StaticWebHandler(), "/static");
|
||||
map.put(new WebRedirectHandler(), "/");
|
||||
map.put(new LoginHandler(), "/login");
|
||||
}
|
||||
|
||||
public static HashMap<Handler, String> getAll() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ public record HttpResponse(
|
|||
);
|
||||
}
|
||||
|
||||
public static HttpResponse text(int status, String text) {
|
||||
return new HttpResponse(
|
||||
400,
|
||||
text.getBytes(),
|
||||
Map.of()
|
||||
);
|
||||
}
|
||||
|
||||
public static HttpResponse ok(byte[] body, String contentType) {
|
||||
return ok(body, contentType, new HashMap<>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import com.sun.net.httpserver.HttpHandler;
|
|||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.http.handlers.FileHandler;
|
||||
import org.adrianvictor.livingroom.http.handlers.ImageHandler;
|
||||
import org.adrianvictor.livingroom.http.handlers.StaticWebHandler;
|
||||
import org.adrianvictor.livingroom.auth.Session;
|
||||
import org.adrianvictor.livingroom.http.handlers.*;
|
||||
import org.adrianvictor.livingroom.http.utils.AuthenticationHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
|
@ -42,7 +42,7 @@ public class Server {
|
|||
public void registerHandler(Handler handler, String path) {
|
||||
httpServer.createContext(path, new HttpHandler() {
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
public void handle(HttpExchange exchange) {
|
||||
try {
|
||||
String fullPath = exchange.getRequestURI().getPath();
|
||||
|
||||
|
|
@ -68,13 +68,20 @@ public class Server {
|
|||
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (handler instanceof FileHandler) {
|
||||
((FileHandler) handler).handleStream(exchange, decoded);
|
||||
} else if (handler instanceof ImageHandler) {
|
||||
((ImageHandler) handler).handleStream(exchange, decoded);
|
||||
} else if (handler instanceof StaticWebHandler) {
|
||||
((StaticWebHandler) handler).handleStream(exchange, decoded);
|
||||
} else {
|
||||
if (!(handler instanceof WebHandler) && !(handler instanceof LoginHandler)) {
|
||||
Session session = AuthenticationHelper.getAuthenticatedSession(exchange);
|
||||
if (session == null) {
|
||||
try {
|
||||
AuthenticationHelper.sendUnauthorized(exchange, null);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
switch (handler) {
|
||||
case FileHandler fileHandler -> fileHandler.handleStream(exchange, decoded);
|
||||
case ImageHandler imageHandler -> imageHandler.handleStream(exchange, decoded);
|
||||
case StaticWebHandler staticWebHandler -> staticWebHandler.handleStream(exchange, decoded);
|
||||
default -> {
|
||||
HttpResponse result = handler.result(baseAddress, decoded, exchange);
|
||||
|
||||
for (Map.Entry<String, String> header : result.headers().entrySet()) {
|
||||
|
|
@ -87,10 +94,11 @@ public class Server {
|
|||
os.write(result.body());
|
||||
os.close();
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
} catch (Exception e) {
|
||||
Logger.error("Handler error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
//e.printStackTrace();
|
||||
try {
|
||||
exchange.sendResponseHeaders(500, 0);
|
||||
exchange.close();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.auth.Session;
|
||||
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
import org.adrianvictor.livingroom.http.utils.AuthenticationHelper;
|
||||
import org.adrianvictor.livingroom.services.UserService;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class LoginHandler implements Handler {
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
return HttpResponse.text(400, "Not post.");
|
||||
}
|
||||
|
||||
Session session = AuthenticationHelper.getAuthenticatedSession(exchange);
|
||||
|
||||
if (session != null) {
|
||||
return HttpResponse.text(400, "You're already logged in.");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
byte[] body = exchange.getRequestBody().readAllBytes();
|
||||
String bodyString = new String(body, StandardCharsets.UTF_8);
|
||||
|
||||
JSONObject json = (JSONObject) parser.parse(bodyString);
|
||||
String username = (String) json.get("username");
|
||||
String password = (String) json.get("password");
|
||||
|
||||
if (password == null || username == null) {
|
||||
return HttpResponse.text(400, "You must provide an username and password.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (UserService.getInstance().getUser(username).auth(password)) {
|
||||
Session s = SessionManager.getInstance().createSession(username);
|
||||
|
||||
exchange.getResponseHeaders().add("Set-Cookie",
|
||||
"SESSIONID=" + s.getSessionId() + "; Path=/; HttpOnly; SameSite=Strict");
|
||||
|
||||
String jsonResponse = "{\"success\": true, \"sessionId\": \"" + s.getSessionId() + "\"}";
|
||||
return HttpResponse.ok(jsonResponse.getBytes(StandardCharsets.UTF_8), "application/json");
|
||||
} else {
|
||||
return HttpResponse.text(401, "Invalid username or password");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error("User lookup error: " + e.getMessage());
|
||||
return HttpResponse.text(401, "Invalid username or password");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.error("Error reading request body: " + e.getMessage());
|
||||
return HttpResponse.text(400, "Invalid request");
|
||||
} catch (ParseException e) {
|
||||
Logger.error("Error parsing JSON: " + e.getMessage());
|
||||
return HttpResponse.text(400, "Invalid JSON format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +83,6 @@ public class WebHandler implements Handler {
|
|||
|
||||
if ("POST".equals(method)) {
|
||||
try {
|
||||
// Read POST body
|
||||
String body = new String(exchange.getRequestBody().readAllBytes());
|
||||
String[] params = body.split("&");
|
||||
String username = null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.adrianvictor.livingroom.http.handlers;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.http.Handler;
|
||||
import org.adrianvictor.livingroom.http.HttpResponse;
|
||||
|
||||
public class WebRedirectHandler implements Handler {
|
||||
|
||||
@Override
|
||||
public HttpResponse result(String baseAddress, String path, HttpExchange exchange) {
|
||||
return HttpResponse.redirect("/web");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package org.adrianvictor.livingroom.http.utils;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import org.adrianvictor.livingroom.auth.Session;
|
||||
import org.adrianvictor.livingroom.auth.SessionManager;
|
||||
import org.adrianvictor.livingroom.utils.net.Cookie;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthenticationHelper {
|
||||
public static Session getAuthenticatedSession(HttpExchange exchange) {
|
||||
String id = Cookie.getSessionIdFromCookies(exchange);
|
||||
if (id != null) {
|
||||
Session session = SessionManager.getInstance().getSession(id);
|
||||
if (session != null) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
String bearerToken = getBearerToken(exchange);
|
||||
if (bearerToken != null) {
|
||||
Session session = SessionManager.getInstance().getSession(bearerToken);
|
||||
if (session != null) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getBearerToken(HttpExchange exchange) {
|
||||
String auth = exchange.getRequestHeaders().getFirst("Authorization");
|
||||
if (auth != null && auth.startsWith("Bearer ")) {
|
||||
return auth.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void sendUnauthorized(HttpExchange exchange, String message) throws IOException {
|
||||
byte[] response = (message == null) ? "Not authorized.".getBytes() : message.getBytes();
|
||||
exchange.sendResponseHeaders(401, response.length);
|
||||
exchange.getResponseBody().write(response);
|
||||
exchange.getResponseBody().close();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import org.adrianvictor.livingroom.web.pages.*;
|
|||
import java.util.HashMap;
|
||||
|
||||
public class Pages {
|
||||
private static HashMap<String, Page> map = new HashMap<>();
|
||||
private static final HashMap<String, Page> map = new HashMap<>();
|
||||
|
||||
static {
|
||||
Index index = new Index();
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ public class Login implements Page {
|
|||
@Override
|
||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||
try {
|
||||
// Check if this is a POST request
|
||||
// For now, just show the login form
|
||||
|
||||
freemarker.template.Template template = cfg.getTemplate("login.ftl");
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package org.adrianvictor.livingroom.web.pages;
|
|||
import com.sun.net.httpserver.HttpExchange;
|
||||
import freemarker.template.Configuration;
|
||||
import org.adrianvictor.livingroom.Logger;
|
||||
import org.adrianvictor.livingroom.Main;
|
||||
import org.adrianvictor.livingroom.data.Database;
|
||||
import org.adrianvictor.livingroom.data.Indexer;
|
||||
import org.adrianvictor.livingroom.web.Page;
|
||||
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class Remove implements Page {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue