From 55288d3346c8775dce9e144eac8670b78d568214 Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 18 Apr 2021 18:18:29 +0530 Subject: [PATCH] use picoServe library Signed-off-by: hrj --- .../java/org/limium/picoserve/Server.java | 251 ++++++++++++++++++ src/main/scala/lc/core/models.scala | 1 - src/main/scala/lc/server/Server.scala | 147 +++------- 3 files changed, 281 insertions(+), 118 deletions(-) create mode 100644 src/main/java/org/limium/picoserve/Server.java diff --git a/src/main/java/org/limium/picoserve/Server.java b/src/main/java/org/limium/picoserve/Server.java new file mode 100644 index 0000000..b1b2be5 --- /dev/null +++ b/src/main/java/org/limium/picoserve/Server.java @@ -0,0 +1,251 @@ +// Distributed under Apache 2 license +// Copyright 2021 github.com/hrj + +package org.limium.picoserve; + +import java.net.InetSocketAddress; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.LinkedList; +import java.util.concurrent.Executor; +import java.util.regex.Pattern; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; + +public final class Server { + private final HttpServer server; + + public static interface Response { + public int getCode(); + public byte[] getBytes(); + public Map> getResponseHeaders(); + } + + public static class ByteResponse implements Response { + private final int code; + private final byte[] bytes; + private final Map> responseHeaders; + + public ByteResponse(final int code, final byte[] bytes) { + this.code = code; + this.bytes = bytes; + this.responseHeaders = null; + } + + public ByteResponse(final int code, final byte[] bytes, final Map> responseHeaders) { + this.code = code; + this.bytes = bytes; + this.responseHeaders = responseHeaders; + } + + public int getCode() { return this.code; } + public byte[] getBytes() { return this.bytes; } + public Map> getResponseHeaders() { + return this.responseHeaders; + } + } + + public static class StringResponse extends ByteResponse { + public StringResponse(final int code, final String msg) { + super(code, msg.getBytes()); + } + + public StringResponse(final int code, final String msg, final Map> responseHeaders) { + super(code, msg.getBytes(), responseHeaders); + } + } + + public final class Request { + final HttpExchange exchange; + Request(final HttpExchange exchange) { + this.exchange = exchange; + } + public String getMethod() { + return exchange.getRequestMethod(); + } + public Map> getQueryParams() { + final var query = exchange.getRequestURI().getQuery(); + final var params = parseParams(query); + return params; + } + public byte[] getBody() { + try(final var bodyIS = exchange.getRequestBody()) { + final var bytes = bodyIS.readAllBytes(); + bodyIS.close(); + return bytes; + } catch (IOException ioe) { + return null; + } + } + public String getBodyString() { + return new String(getBody()); + } + } + + @FunctionalInterface + public static interface Processor { + public Response process(final Request request); + } + + public static class Handler { + public final String path; + public final Processor processor; + public final String[] methods; + public Handler(final String path, final Processor processor) { + this.path = path; + this.processor = processor; + this.methods = new String[] {}; + } + public Handler(final String path, final String methods, final Processor processor) { + this.path = path; + this.processor = processor; + this.methods = methods.split(","); + } + } + + public Server(final InetSocketAddress addr, final int backlog, final List handlers, final Executor executor) throws IOException { + this.server = HttpServer.create(addr, backlog); + this.server.setExecutor(executor); + for (final var handler: handlers) { + // System.out.println("Registering handler for " + handler.path); + this.server.createContext(handler.path, new HttpHandler() { + public void handle(final HttpExchange exchange) { + final var method = exchange.getRequestMethod(); + final Response errorResponse = checkMethods(handler.methods, method); + try(final var os = exchange.getResponseBody()) { + Response response; + if (errorResponse != null) { + response = errorResponse; + } else { + try { + response = handler.processor.process(new Request(exchange)); + } catch (final Exception e) { + e.printStackTrace(); + response = new StringResponse(500, "Error: " + e); + } + } + final var headersToSend = response.getResponseHeaders(); + if (headersToSend != null) { + final var responseHeaders = exchange.getResponseHeaders(); + responseHeaders.putAll(headersToSend); + } + final var bytes = response.getBytes(); + final var code = response.getCode(); + exchange.sendResponseHeaders(code, bytes.length); + os.write(bytes); + os.close(); + } catch (IOException ioe) { + System.out.println("Error: " + ioe); + } + } + }); + } + } + + public static Response checkMethods(final String[] methods, final String method) { + if (methods.length > 0) { + var found = false; + for (var m: methods) { + if (m.equals(method)) { + found = true; + break; + } + } + if (!found) { + return new StringResponse(404, "Method Not Accepted"); + } + } + return null; + } + + public void start() { + this.server.start(); + } + + public void stop(int delay) { + this.server.stop(delay); + } + + public static ServerBuilder builder() { + return new ServerBuilder(); + } + + // Adapted from https://stackoverflow.com/a/37368660 + private final static Pattern ampersandPattern = Pattern.compile("&"); + private final static Pattern equalPattern = Pattern.compile("="); + private final static Map> emptyMap = Map.of(); + private static Map> parseParams(final String query) { + if (query == null) { + return emptyMap; + } + final var params = ampersandPattern + .splitAsStream(query) + .map(s -> Arrays.copyOf(equalPattern.split(s, 2), 2)) + .collect(Collectors.groupingBy(s -> decode(s[0]), Collectors.mapping(s -> decode(s[1]), Collectors.toList()))); + return params; + } + + private static String decode(final String encoded) { + return Optional.ofNullable(encoded) + .map(e -> URLDecoder.decode(e, StandardCharsets.UTF_8)) + .orElse(null); + } + + public static class ServerBuilder { + private InetSocketAddress mAddress = new InetSocketAddress(9000); + private int backlog = 5; + private List handlers = new LinkedList(); + private Executor executor = null; + + public ServerBuilder port(final int port) { + mAddress = new InetSocketAddress(port); + return this; + } + public ServerBuilder backlog(final int backlog) { + this.backlog = backlog; + return this; + } + public ServerBuilder address(final InetSocketAddress addr) { + mAddress = addr; + return this; + } + public ServerBuilder handle(final Handler handler) { + handlers.add(handler); + return this; + } + public ServerBuilder GET(final String path, final Processor processor) { + handlers.add(new Handler(path, "GET", request -> processor.process(request))); + return this; + } + public ServerBuilder POST(final String path, final Processor processor) { + handlers.add(new Handler(path, "POST", request -> processor.process(request))); + return this; + } + public ServerBuilder PUT(final String path, final Processor processor) { + handlers.add(new Handler(path, "PUT", request -> processor.process(request))); + return this; + } + public ServerBuilder DELETE(final String path, final Processor processor) { + handlers.add(new Handler(path, "DELETE", request -> processor.process(request))); + return this; + } + public ServerBuilder HEAD(final String path, final Processor processor) { + handlers.add(new Handler(path, "HEAD", request -> processor.process(request))); + return this; + } + public ServerBuilder executor(final Executor executor) { + this.executor = executor; + return this; + } + public Server build() throws IOException { + return new Server(mAddress, backlog, handlers, executor); + } + } +} diff --git a/src/main/scala/lc/core/models.scala b/src/main/scala/lc/core/models.scala index b06a1aa..121a1bf 100644 --- a/src/main/scala/lc/core/models.scala +++ b/src/main/scala/lc/core/models.scala @@ -11,7 +11,6 @@ case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[ case class Answer(answer: String, id: String) case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } } case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } } -case class Response(statusCode: Int, message: Array[Byte]) case class CaptchaConfig( name: String, allowedLevels: List[String], diff --git a/src/main/scala/lc/server/Server.scala b/src/main/scala/lc/server/Server.scala index b5c2848..7c4cf6e 100644 --- a/src/main/scala/lc/server/Server.scala +++ b/src/main/scala/lc/server/Server.scala @@ -3,139 +3,52 @@ package lc.server import org.json4s.jackson.JsonMethods.parse import lc.core.Captcha import lc.core.ErrorMessageEnum -import lc.core.{Parameters, Id, Answer, Response, Error, ByteConvert} -import org.json4s.JsonAST.JValue -import com.sun.net.httpserver.{HttpServer, HttpExchange} -import java.net.InetSocketAddress +import lc.core.{Parameters, Id, Answer, Error, ByteConvert} import lc.core.Config.formats +import org.limium.picoserve +import org.limium.picoserve.Server.ByteResponse class Server(port: Int) { - - val server: HttpServer = HttpServer.create(new InetSocketAddress(port), 32) - server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool()) - - private def getRequestJson(ex: HttpExchange): JValue = { - val requestBody = ex.getRequestBody - val bytes = requestBody.readAllBytes - val string = new String(bytes) - parse(string) - } - - private val eqPattern = java.util.regex.Pattern.compile("=") - private def getPathParameter(ex: HttpExchange): Either[String, String] = { - try { - val query = ex.getRequestURI.getQuery - val param = eqPattern.split(query) - if (param(0) == "id") { - Right(param(1)) + val server = picoserve.Server.builder() + .port(8888) + .POST("/v1/captcha", (request) => { + val json = parse(request.getBodyString()) + val param = json.extract[Parameters] + val id = Captcha.getChallenge(param) + getResponse(id) + }) + .GET("/v1/media", (request) => { + val params = request.getQueryParams() + val result = if (params.containsKey("id")) { + val paramId = params.get("id").get(0) + val id = Id(paramId) + Captcha.getCaptcha(id) } else { - Left(ErrorMessageEnum.INVALID_PARAM.toString + "=> id") + Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id")) } - } catch { - case exception: ArrayIndexOutOfBoundsException => { - println(exception) - Left(ErrorMessageEnum.INVALID_PARAM.toString + "=> id") - } - } - } + getResponse(result) + }) + .POST("/v1/answer", (request) => { + val json = parse(request.getBodyString()) + val answer = json.extract[Answer] + val result = Captcha.checkAnswer(answer) + getResponse(result) + }) + .build() - private def sendResponse(statusCode: Int, response: Array[Byte], ex: HttpExchange): Unit = { - ex.sendResponseHeaders(statusCode, response.length) - val os = ex.getResponseBody - os.write(response) - os.close - } - - private def getException(exception: Exception): Response = { - println(exception) - val message = Error(exception.getMessage) - Response(500, message.toBytes()) - } - - private def getBadRequestError(): Response = { - val message = Error(ErrorMessageEnum.BAD_METHOD.toString) - Response(405, message.toBytes()) - } - - private def getResponse(response: Either[Error, ByteConvert]): Response = { + private def getResponse(response: Either[Error, ByteConvert]): ByteResponse = { response match { case Right(value) => { - Response(200, value.toBytes()) + new ByteResponse(200, value.toBytes()) } case Left(value) => { - Response(500, value.toBytes()) + new ByteResponse(500, value.toBytes()) } } } - private def makeApiWorker(path: String, f: (String, HttpExchange) => Response): Unit = { - server.createContext( - path, - ex => { - val requestMethod = ex.getRequestMethod - val response = - try { - f(requestMethod, ex) - } catch { - case exception: Exception => { - getException(exception) - } - } - sendResponse(statusCode = response.statusCode, response = response.message, ex = ex) - } - ) - } - def start(): Unit = { println("Starting server on port:" + port) server.start() } - - makeApiWorker( - "/v1/captcha", - (method: String, ex: HttpExchange) => { - if (method == "POST") { - val json = getRequestJson(ex) - val param = json.extract[Parameters] - val id = Captcha.getChallenge(param) - getResponse(id) - } else { - getBadRequestError() - } - } - ) - - makeApiWorker( - "/v1/media", - (method: String, ex: HttpExchange) => { - if (method == "GET") { - val param = getPathParameter(ex) - val result = param match { - case Right(value) => { - val id = Id(value) - Captcha.getCaptcha(id) - } - case Left(value) => Left(Error(value)) - } - getResponse(result) - } else { - getBadRequestError() - } - } - ) - - makeApiWorker( - "/v1/answer", - (method: String, ex: HttpExchange) => { - if (method == "POST") { - val json = getRequestJson(ex) - val answer = json.extract[Answer] - val result = Captcha.checkAnswer(answer) - getResponse(result) - } else { - getBadRequestError() - } - } - ) - }