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.
|
The server includes a (working but very WIP) HTTP API for third-party clients and a web interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
### Server
|
||||||
|
- [x] Game scanner
|
||||||
|
- [ ] Automatically deindex removed game
|
||||||
|
- [x] SQLite DB
|
||||||
|
- [x] Metadata
|
||||||
|
|
||||||
### HTTP API
|
### HTTP API
|
||||||
- [ ] Authentication
|
- [x] Authentication
|
||||||
- [x] Library info
|
- [x] Library info
|
||||||
- [x] Game info
|
- [x] Game info
|
||||||
- [x] Downloads
|
- [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] Deindex game
|
||||||
- [x] Trigger new scan
|
- [x] Trigger new scan
|
||||||
- [x] Downloads (from the API)
|
- [x] Downloads (from the API)
|
||||||
- [x] User Management
|
- [ ] User Management
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- **Freemarker** for web rendering
|
- **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) {
|
public void setUsers(Map<String, UserConfig> users) {
|
||||||
this.users = (users != null) ? users : new HashMap<>();
|
this.users = (users != null) ? users : new HashMap<>();
|
||||||
}
|
}
|
||||||
|
public void addUser(String name, UserConfig config) { this.users.put(name, config); }
|
||||||
|
|
||||||
public void hashPlaintextPasswords() {
|
public void hashPlaintextPasswords() {
|
||||||
if (users == null) return;
|
if (users == null) return;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ public class ConfigLoader {
|
||||||
|
|
||||||
private static AppConfig config;
|
private static AppConfig config;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static AppConfig load(String path) throws Exception {
|
public static AppConfig load(String path) throws Exception {
|
||||||
if (config != null) return config;
|
if (config != null) return config;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package org.adrianvictor.livingroom.http;
|
package org.adrianvictor.livingroom.http;
|
||||||
|
|
||||||
import org.adrianvictor.livingroom.http.handlers.*;
|
import org.adrianvictor.livingroom.http.handlers.*;
|
||||||
import org.adrianvictor.livingroom.web.pages.Remove;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class Handlers {
|
public class Handlers {
|
||||||
private static HashMap<Handler, String> map = new HashMap<>();
|
private static final HashMap<Handler, String> map = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
map.put(new CatalogHandler(), "/catalog");
|
map.put(new CatalogHandler(), "/catalog");
|
||||||
|
|
@ -14,6 +13,8 @@ public class Handlers {
|
||||||
map.put(new FileHandler(), "/download");
|
map.put(new FileHandler(), "/download");
|
||||||
map.put(new WebHandler(), "/web");
|
map.put(new WebHandler(), "/web");
|
||||||
map.put(new StaticWebHandler(), "/static");
|
map.put(new StaticWebHandler(), "/static");
|
||||||
|
map.put(new WebRedirectHandler(), "/");
|
||||||
|
map.put(new LoginHandler(), "/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashMap<Handler, String> getAll() {
|
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) {
|
public static HttpResponse ok(byte[] body, String contentType) {
|
||||||
return ok(body, contentType, new HashMap<>());
|
return ok(body, contentType, new HashMap<>());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import com.sun.net.httpserver.HttpHandler;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import org.adrianvictor.livingroom.Logger;
|
import org.adrianvictor.livingroom.Logger;
|
||||||
import org.adrianvictor.livingroom.Main;
|
import org.adrianvictor.livingroom.Main;
|
||||||
import org.adrianvictor.livingroom.http.handlers.FileHandler;
|
import org.adrianvictor.livingroom.auth.Session;
|
||||||
import org.adrianvictor.livingroom.http.handlers.ImageHandler;
|
import org.adrianvictor.livingroom.http.handlers.*;
|
||||||
import org.adrianvictor.livingroom.http.handlers.StaticWebHandler;
|
import org.adrianvictor.livingroom.http.utils.AuthenticationHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
@ -42,7 +42,7 @@ public class Server {
|
||||||
public void registerHandler(Handler handler, String path) {
|
public void registerHandler(Handler handler, String path) {
|
||||||
httpServer.createContext(path, new HttpHandler() {
|
httpServer.createContext(path, new HttpHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void handle(HttpExchange exchange) throws IOException {
|
public void handle(HttpExchange exchange) {
|
||||||
try {
|
try {
|
||||||
String fullPath = exchange.getRequestURI().getPath();
|
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-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
|
||||||
if (handler instanceof FileHandler) {
|
if (!(handler instanceof WebHandler) && !(handler instanceof LoginHandler)) {
|
||||||
((FileHandler) handler).handleStream(exchange, decoded);
|
Session session = AuthenticationHelper.getAuthenticatedSession(exchange);
|
||||||
} else if (handler instanceof ImageHandler) {
|
if (session == null) {
|
||||||
((ImageHandler) handler).handleStream(exchange, decoded);
|
try {
|
||||||
} else if (handler instanceof StaticWebHandler) {
|
AuthenticationHelper.sendUnauthorized(exchange, null);
|
||||||
((StaticWebHandler) handler).handleStream(exchange, decoded);
|
} catch (IOException ignored) {}
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
HttpResponse result = handler.result(baseAddress, decoded, exchange);
|
||||||
|
|
||||||
for (Map.Entry<String, String> header : result.headers().entrySet()) {
|
for (Map.Entry<String, String> header : result.headers().entrySet()) {
|
||||||
|
|
@ -87,10 +94,11 @@ public class Server {
|
||||||
os.write(result.body());
|
os.write(result.body());
|
||||||
os.close();
|
os.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.error("Handler error: " + e.getMessage());
|
Logger.error("Handler error: " + e.getMessage());
|
||||||
e.printStackTrace();
|
//e.printStackTrace();
|
||||||
try {
|
try {
|
||||||
exchange.sendResponseHeaders(500, 0);
|
exchange.sendResponseHeaders(500, 0);
|
||||||
exchange.close();
|
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)) {
|
if ("POST".equals(method)) {
|
||||||
try {
|
try {
|
||||||
// Read POST body
|
|
||||||
String body = new String(exchange.getRequestBody().readAllBytes());
|
String body = new String(exchange.getRequestBody().readAllBytes());
|
||||||
String[] params = body.split("&");
|
String[] params = body.split("&");
|
||||||
String username = null;
|
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;
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class Pages {
|
public class Pages {
|
||||||
private static HashMap<String, Page> map = new HashMap<>();
|
private static final HashMap<String, Page> map = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Index index = new Index();
|
Index index = new Index();
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ public class Login implements Page {
|
||||||
@Override
|
@Override
|
||||||
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
public String result(Configuration cfg, String baseAddress, String path, Map<String, Object> data, HttpExchange exchange) {
|
||||||
try {
|
try {
|
||||||
// Check if this is a POST request
|
|
||||||
// For now, just show the login form
|
|
||||||
|
|
||||||
freemarker.template.Template template = cfg.getTemplate("login.ftl");
|
freemarker.template.Template template = cfg.getTemplate("login.ftl");
|
||||||
|
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,10 @@ package org.adrianvictor.livingroom.web.pages;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import org.adrianvictor.livingroom.Logger;
|
import org.adrianvictor.livingroom.Logger;
|
||||||
import org.adrianvictor.livingroom.Main;
|
|
||||||
import org.adrianvictor.livingroom.data.Database;
|
import org.adrianvictor.livingroom.data.Database;
|
||||||
import org.adrianvictor.livingroom.data.Indexer;
|
|
||||||
import org.adrianvictor.livingroom.web.Page;
|
import org.adrianvictor.livingroom.web.Page;
|
||||||
import org.adrianvictor.livingroom.web.QuickResponses;
|
import org.adrianvictor.livingroom.web.QuickResponses;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class Remove implements Page {
|
public class Remove implements Page {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue