package vn.sis.appointment; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.Executors; final class AppointmentWebServer { private final HttpServer server; private final SisApiClient apiClient = new SisApiClient(); AppointmentWebServer(int port) throws IOException { server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0); server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); server.createContext("/api/health", exchange -> { if (!requireMethod(exchange, "GET")) { return; } sendJson(exchange, 200, Json.stringify(Map.of( "ok", true, "application", "SisAppointmentJavaWeb", "javaVersion", System.getProperty("java.version")))); }); server.createContext("/api/token", postHandler((form, config) -> apiClient.checkToken(config))); server.createContext("/api/doctors", postHandler((form, config) -> apiClient.getDoctors(config))); server.createContext("/api/schedule", postHandler((form, config) -> apiClient.getSchedule(config, form.getOrDefault("doctorCode", "")))); server.createContext("/api/clear-token", postHandler((form, config) -> { apiClient.clearToken(config); return new ProxyResponse(true, 200, "OK", "-", "-", 0, "", "", "", null, "Da xoa access token trong bo nho may chu."); })); server.createContext("/", new StaticHandler()); } void start() { server.start(); } private HttpHandler postHandler(ApiOperation operation) { return exchange -> { if (!requireMethod(exchange, "POST")) { return; } try { Map form = FormData.read(exchange); ApiConfig config = ApiConfig.fromForm(form); ProxyResponse result = operation.execute(form, config); sendJson(exchange, 200, result.toJson()); } catch (IllegalArgumentException ex) { ProxyResponse error = new ProxyResponse(false, 400, "Bad Request", "-", "-", 0, "", "", "", null, ex.getMessage()); sendJson(exchange, 400, error.toJson()); } catch (Exception ex) { ProxyResponse error = new ProxyResponse(false, 500, "Internal Server Error", "-", "-", 0, "", "", "", null, ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage()); sendJson(exchange, 500, error.toJson()); } }; } private static boolean requireMethod(HttpExchange exchange, String expected) throws IOException { if (exchange.getRequestMethod().equalsIgnoreCase(expected)) { return true; } exchange.getResponseHeaders().set("Allow", expected); sendJson(exchange, 405, Json.stringify(Map.of( "ok", false, "message", "Phuong thuc khong duoc ho tro."))); return false; } private static void sendJson(HttpExchange exchange, int status, String json) throws IOException { byte[] data = json.getBytes(StandardCharsets.UTF_8); applySecurityHeaders(exchange.getResponseHeaders()); exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); exchange.getResponseHeaders().set("Cache-Control", "no-store"); exchange.sendResponseHeaders(status, data.length); exchange.getResponseBody().write(data); exchange.close(); } private static void applySecurityHeaders(Headers headers) { headers.set("X-Content-Type-Options", "nosniff"); headers.set("X-Frame-Options", "DENY"); headers.set("Referrer-Policy", "no-referrer"); headers.set("Content-Security-Policy", "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; connect-src 'self'"); } @FunctionalInterface private interface ApiOperation { ProxyResponse execute(Map form, ApiConfig config); } private static final class StaticHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) { exchange.getResponseHeaders().set("Allow", "GET"); exchange.sendResponseHeaders(405, -1); exchange.close(); return; } String path = exchange.getRequestURI().getPath(); if (path.equals("/")) { path = "/index.html"; } if (path.contains("..")) { exchange.sendResponseHeaders(400, -1); exchange.close(); return; } String resource = "web" + path; try (InputStream input = AppointmentWebServer.class.getClassLoader().getResourceAsStream(resource)) { if (input == null) { exchange.sendResponseHeaders(404, -1); exchange.close(); return; } byte[] data = input.readAllBytes(); Headers headers = exchange.getResponseHeaders(); applySecurityHeaders(headers); headers.set("Content-Type", contentType(path)); headers.set("Cache-Control", path.endsWith("index.html") ? "no-cache" : "public, max-age=3600"); exchange.sendResponseHeaders(200, data.length); exchange.getResponseBody().write(data); exchange.close(); } } private static String contentType(String path) { if (path.endsWith(".html")) return "text/html; charset=utf-8"; if (path.endsWith(".css")) return "text/css; charset=utf-8"; if (path.endsWith(".js")) return "application/javascript; charset=utf-8"; if (path.endsWith(".svg")) return "image/svg+xml"; if (path.endsWith(".png")) return "image/png"; return "application/octet-stream"; } } }