You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
6.2 KiB
191 lines
6.2 KiB
/* © SRSoftware 2024 */ |
|
package de.srsoftware.http; |
|
|
|
|
|
import static de.srsoftware.utils.Optionals.nullable; |
|
import static java.lang.System.Logger.Level.*; |
|
import static java.net.HttpURLConnection.*; |
|
import static java.nio.charset.StandardCharsets.UTF_8; |
|
|
|
import com.sun.net.httpserver.HttpExchange; |
|
import com.sun.net.httpserver.HttpHandler; |
|
import com.sun.net.httpserver.HttpServer; |
|
import java.io.IOException; |
|
import java.util.*; |
|
import java.util.stream.Collectors; |
|
import java.util.stream.Stream; |
|
import org.json.JSONObject; |
|
|
|
public abstract class PathHandler implements HttpHandler { |
|
public static final String AUTHORIZATION = "Authorization"; |
|
public static final String CONTENT_TYPE = "Content-Type"; |
|
public static final String DEFAULT_LANGUAGE = "en"; |
|
public static final String DELETE = "DELETE"; |
|
private static final String FORWARDED_HOST = "x-forwarded-host"; |
|
public static final String GET = "GET"; |
|
public static final String HOST = "host"; |
|
public static final String JSON = "application/json"; |
|
public static System.Logger LOG = System.getLogger(PathHandler.class.getSimpleName()); |
|
public static final String POST = "POST"; |
|
|
|
private String[] paths; |
|
|
|
public record BasicAuth(String userId, String pass) { |
|
} |
|
|
|
public class Bond { |
|
Bond(String[] paths) { |
|
PathHandler.this.paths = paths; |
|
} |
|
public PathHandler on(HttpServer server) { |
|
for (var path : paths) server.createContext(path, PathHandler.this); |
|
return PathHandler.this; |
|
} |
|
} |
|
|
|
public static boolean badRequest(HttpExchange ex, byte[] bytes) throws IOException { |
|
return sendContent(ex, HTTP_BAD_REQUEST, bytes); |
|
} |
|
|
|
public static boolean badRequest(HttpExchange ex, Object o) throws IOException { |
|
return sendContent(ex, HTTP_BAD_REQUEST, o); |
|
} |
|
|
|
public Bond bindPath(String... path) { |
|
return new Bond(path); |
|
} |
|
|
|
public boolean doDelete(String path, HttpExchange ex) throws IOException { |
|
return notFound(ex); |
|
} |
|
|
|
public boolean doGet(String path, HttpExchange ex) throws IOException { |
|
return notFound(ex); |
|
} |
|
|
|
public boolean doPost(String path, HttpExchange ex) throws IOException { |
|
return notFound(ex); |
|
} |
|
|
|
@Override |
|
public void handle(HttpExchange ex) throws IOException { |
|
String path = relativePath(ex); |
|
String method = ex.getRequestMethod(); |
|
LOG.log(INFO, "{0} {1}", method, path); |
|
boolean ignored = switch (method) { |
|
case DELETE -> doDelete(path,ex); |
|
case GET -> doGet(path,ex); |
|
case POST -> doPost(path,ex); |
|
default -> false; |
|
}; |
|
ex.getResponseBody().close(); |
|
} |
|
|
|
public String relativePath(HttpExchange ex) { |
|
var requestPath = ex.getRequestURI().toString(); |
|
for (var path : paths){ |
|
if (requestPath.startsWith(path)) { |
|
requestPath = requestPath.substring(path.length()); |
|
break; |
|
} |
|
} |
|
if (!requestPath.startsWith("/")) requestPath = "/" + requestPath; |
|
var pos = requestPath.indexOf("?"); |
|
if (pos >= 0) requestPath = requestPath.substring(0, pos); |
|
return requestPath; |
|
} |
|
|
|
/******* begin of static methods *************/ |
|
|
|
public static String body(HttpExchange ex) throws IOException { |
|
return new String(ex.getRequestBody().readAllBytes(), UTF_8); |
|
} |
|
|
|
public static Optional<String> getAuthToken(HttpExchange ex) { |
|
return getHeader(ex, AUTHORIZATION); |
|
} |
|
|
|
public static Optional<BasicAuth> getBasicAuth(HttpExchange ex) { |
|
return getAuthToken(ex) |
|
.filter(token -> token.startsWith("Basic ")) // |
|
.map(token -> token.substring(6)) |
|
.map(Base64.getDecoder()::decode) |
|
.map(bytes -> new String(bytes, UTF_8)) |
|
.map(token -> token.split(":", 2)) |
|
.map(arr -> new BasicAuth(arr[0], arr[1])); |
|
} |
|
|
|
public static Optional<String> getBearer(HttpExchange ex) { |
|
return getAuthToken(ex).filter(token -> token.startsWith("Bearer ")).map(token -> token.substring(7)); |
|
} |
|
|
|
public static Optional<String> getHeader(HttpExchange ex, String key) { |
|
return nullable(ex.getRequestHeaders().get(key)).map(List::stream).flatMap(Stream::findFirst); |
|
} |
|
|
|
public static String hostname(HttpExchange ex) { |
|
var headers = ex.getRequestHeaders(); |
|
var host = headers.getFirst(FORWARDED_HOST); |
|
if (host == null) host = headers.getFirst(HOST); |
|
return host == null ? null : "https://" + host; |
|
} |
|
|
|
public static JSONObject json(HttpExchange ex) throws IOException { |
|
return new JSONObject(body(ex)); |
|
} |
|
|
|
public static String language(HttpExchange ex) { |
|
return getHeader(ex, "Accept-Language") // |
|
.map(s -> Arrays.stream(s.split(","))) |
|
.flatMap(Stream::findFirst) |
|
.orElse(DEFAULT_LANGUAGE); |
|
} |
|
|
|
public static boolean notFound(HttpExchange ex) throws IOException { |
|
LOG.log(ERROR, "not implemented"); |
|
return sendEmptyResponse(HTTP_NOT_FOUND, ex); |
|
} |
|
|
|
public Map<String, String> queryParam(HttpExchange ex) { |
|
return Arrays |
|
.stream(ex.getRequestURI().getQuery().split("&")) // |
|
.map(s -> s.split("=", 2)) |
|
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1])); |
|
} |
|
|
|
public static boolean sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException { |
|
ex.sendResponseHeaders(statusCode, 0); |
|
return false; |
|
} |
|
|
|
public static boolean sendRedirect(HttpExchange ex, String url) throws IOException { |
|
ex.getResponseHeaders().add("Location", url); |
|
return sendEmptyResponse(HTTP_MOVED_TEMP, ex); |
|
} |
|
|
|
public static boolean sendContent(HttpExchange ex, int status, byte[] bytes) throws IOException { |
|
LOG.log(DEBUG, "sending {0} response…", status); |
|
ex.sendResponseHeaders(status, bytes.length); |
|
ex.getResponseBody().write(bytes); |
|
return true; |
|
} |
|
|
|
public static boolean sendContent(HttpExchange ex, int status, Object o) throws IOException { |
|
if (o instanceof Map map) o = new JSONObject(map); |
|
if (o instanceof JSONObject) ex.getResponseHeaders().add(CONTENT_TYPE, JSON); |
|
return sendContent(ex, status, o.toString().getBytes(UTF_8)); |
|
} |
|
|
|
|
|
public static boolean sendContent(HttpExchange ex, byte[] bytes) throws IOException { |
|
return sendContent(ex, HTTP_OK, bytes); |
|
} |
|
|
|
public static boolean sendContent(HttpExchange ex, Object o) throws IOException { |
|
return sendContent(ex, HTTP_OK, o); |
|
} |
|
|
|
public static String url(HttpExchange ex) { |
|
return hostname(ex) + ex.getRequestURI(); |
|
} |
|
}
|
|
|