feat(web): support hosting on game port
This commit is contained in:
parent
09466ce897
commit
f54f8d59c4
|
@ -16,7 +16,7 @@ val javapoetVersion by extra("1.13.0")
|
|||
|
||||
jfMod {
|
||||
minecraftVersion = "23w33a"
|
||||
yarn("build.3")
|
||||
yarn("build.7")
|
||||
loaderVersion = "0.14.22"
|
||||
|
||||
modrinth {
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
- [libjf-data-manipulation-v0](./libjf-data-manipulation-v0.md)
|
||||
- [libjf-translate-v1](./libjf-translate-v1.md)
|
||||
- [libjf-unsafe-v0](./libjf-unsafe-v0.md)
|
||||
- [libjf-web-v0](./libjf-web-v0.md)
|
||||
- [libjf-web-v1](./libjf-web-v1.md)
|
|
@ -1,7 +1,7 @@
|
|||
# libjf-web-v0
|
||||
libjf-web-v0 provides an HTTP web server you can use in your serverside (and technically also clientside) mods
|
||||
# libjf-web-v1
|
||||
libjf-web-v1 provides an HTTP web server you can use in your serverside (and technically also clientside) mods
|
||||
to serve web content through a unified port.
|
||||
libjf-web-v0 depends on libjf-config-core-v2 to provide its config, libjf-base, fabric-lifecycle-events-v1 and fabric-command-api-v1
|
||||
libjf-web-v1 depends on libjf-config-core-v2 to provide its config, libjf-base, fabric-lifecycle-events-v1 and fabric-command-api-v1
|
||||
|
||||
### Getting started
|
||||
Implement WebInit and register it as a libjf:web entrypoint. To enable the server, also add the following to your fabric.mod.json:
|
|
@ -27,9 +27,8 @@ public class CoProcessManager implements ModInitializer {
|
|||
}
|
||||
|
||||
private void stop() {
|
||||
Iterator<CoProcess> procs = coProcesses.iterator();
|
||||
while (procs.hasNext()) {
|
||||
CoProcess coProcess = procs.next();
|
||||
for (Iterator<CoProcess> iter = coProcesses.iterator(); iter.hasNext(); ) {
|
||||
CoProcess coProcess = iter.next();
|
||||
coProcess.stop();
|
||||
if (coProcess instanceof Closeable cl) {
|
||||
try {
|
||||
|
@ -38,7 +37,7 @@ public class CoProcessManager implements ModInitializer {
|
|||
LibJf.LOGGER.error("Could not close co-process", e);
|
||||
}
|
||||
}
|
||||
procs.remove();
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.gitlab.jfronny.libjf.config.impl;
|
||||
|
||||
import io.gitlab.jfronny.libjf.config.api.v2.ConfigHolder;
|
||||
import io.gitlab.jfronny.libjf.config.api.v2.ConfigInstance;
|
||||
import io.gitlab.jfronny.libjf.config.api.v2.dsl.DSL;
|
||||
|
||||
|
@ -10,6 +11,7 @@ public class ConfigCore {
|
|||
public static final ConfigInstance CONFIG_INSTANCE;
|
||||
|
||||
static {
|
||||
ConfigHolder.getInstance().migrateFiles(MOD_ID);
|
||||
CONFIG_INSTANCE = DSL.create(MOD_ID).register(builder -> builder
|
||||
.value("watchForChanges", watchForChanges, () -> watchForChanges, b -> watchForChanges = b)
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "libjf-config-core-v2",
|
||||
"provides": ["libjf-config-core-v1"],
|
||||
"name": "LibJF Config",
|
||||
"version": "${version}",
|
||||
"authors": ["JFronny"],
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package io.gitlab.jfronny.libjf.web.api;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpRequest;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface ContentProvider {
|
||||
HttpResponse handle(HttpRequest request) throws IOException;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package io.gitlab.jfronny.libjf.web.api;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpRequest;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface SubServer {
|
||||
HttpResponse handle(HttpRequest request, String[] segments) throws IOException;
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
/*
|
||||
* This file is part of BlueMap, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class HttpRequest {
|
||||
|
||||
private static final Pattern REQUEST_PATTERN = Pattern.compile("^(\\w+) (\\S+) (.+)$");
|
||||
|
||||
private final String method;
|
||||
private final String adress;
|
||||
private final String version;
|
||||
private final Map<String, Set<String>> header;
|
||||
private final Map<String, Set<String>> headerLC;
|
||||
private byte[] data;
|
||||
|
||||
private String path = null;
|
||||
private Map<String, String> getParams = null;
|
||||
private String getParamString = null;
|
||||
|
||||
public HttpRequest(String method, String adress, String version, Map<String, Set<String>> header) {
|
||||
this.method = method;
|
||||
this.adress = adress;
|
||||
this.version = version;
|
||||
this.header = header;
|
||||
this.headerLC = new HashMap<>();
|
||||
|
||||
for (Entry<String, Set<String>> e : header.entrySet()){
|
||||
Set<String> values = new HashSet<>();
|
||||
for (String v : e.getValue()){
|
||||
values.add(v.toLowerCase());
|
||||
}
|
||||
|
||||
headerLC.put(e.getKey().toLowerCase(), values);
|
||||
}
|
||||
|
||||
this.data = new byte[0];
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public String getAdress(){
|
||||
return adress;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Map<String, Set<String>> getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
public Map<String, Set<String>> getLowercaseHeader() {
|
||||
return headerLC;
|
||||
}
|
||||
|
||||
public Set<String> getHeader(String key){
|
||||
Set<String> headerValues = header.get(key);
|
||||
if (headerValues == null) return Collections.emptySet();
|
||||
return headerValues;
|
||||
}
|
||||
|
||||
public Set<String> getLowercaseHeader(String key){
|
||||
Set<String> headerValues = headerLC.get(key.toLowerCase());
|
||||
if (headerValues == null) return Collections.emptySet();
|
||||
return headerValues;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
if (path == null) parseAdress();
|
||||
return path;
|
||||
}
|
||||
|
||||
public Map<String, String> getGETParams() {
|
||||
if (getParams == null) parseAdress();
|
||||
return Collections.unmodifiableMap(getParams);
|
||||
}
|
||||
|
||||
public String getGETParamString() {
|
||||
if (getParamString == null) parseAdress();
|
||||
return getParamString;
|
||||
}
|
||||
|
||||
private void parseAdress() {
|
||||
String adress = this.adress;
|
||||
if (adress.isEmpty()) adress = "/";
|
||||
String[] adressParts = adress.split("\\?", 2);
|
||||
String path = adressParts[0];
|
||||
this.getParamString = adressParts.length > 1 ? adressParts[1] : "";
|
||||
|
||||
Map<String, String> getParams = new HashMap<>();
|
||||
for (String getParam : this.getParamString.split("&")){
|
||||
if (getParam.isEmpty()) continue;
|
||||
String[] kv = getParam.split("=", 2);
|
||||
String key = kv[0];
|
||||
String value = kv.length > 1 ? kv[1] : "";
|
||||
getParams.put(key, value);
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.getParams = getParams;
|
||||
}
|
||||
|
||||
public InputStream getData(){
|
||||
return new ByteArrayInputStream(data);
|
||||
}
|
||||
|
||||
public static HttpRequest read(InputStream in) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
List<String> header = new ArrayList<>(20);
|
||||
while(header.size() < 1000){
|
||||
String headerLine = readLine(reader);
|
||||
if (headerLine.isEmpty()) break;
|
||||
header.add(headerLine);
|
||||
}
|
||||
|
||||
if (header.isEmpty()) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
Matcher m = REQUEST_PATTERN.matcher(header.remove(0));
|
||||
if (!m.find()) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String method = m.group(1);
|
||||
if (method == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String adress = m.group(2);
|
||||
if (adress == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String version = m.group(3);
|
||||
if (version == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
Map<String, Set<String>> headerMap = new HashMap<String, Set<String>>();
|
||||
for (String line : header){
|
||||
if (line.trim().isEmpty()) continue;
|
||||
|
||||
String[] kv = line.split(":", 2);
|
||||
if (kv.length < 2) continue;
|
||||
|
||||
Set<String> values = new HashSet<>();
|
||||
if (kv[0].trim().equalsIgnoreCase("If-Modified-Since")){
|
||||
values.add(kv[1].trim());
|
||||
} else {
|
||||
for(String v : kv[1].split(",")){
|
||||
values.add(v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
headerMap.put(kv[0].trim(), values);
|
||||
}
|
||||
|
||||
HttpRequest request = new HttpRequest(method, adress, version, headerMap);
|
||||
|
||||
if (request.getLowercaseHeader("Transfer-Encoding").contains("chunked")){
|
||||
try {
|
||||
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
|
||||
while (dataStream.size() < 1000000){
|
||||
String hexSize = reader.readLine();
|
||||
int chunkSize = Integer.parseInt(hexSize, 16);
|
||||
if (chunkSize <= 0) break;
|
||||
byte[] data = new byte[chunkSize];
|
||||
in.read(data);
|
||||
dataStream.write(data);
|
||||
}
|
||||
|
||||
if (dataStream.size() >= 1000000) {
|
||||
throw new HttpConnection.InvalidRequestException();
|
||||
}
|
||||
|
||||
request.data = dataStream.toByteArray();
|
||||
|
||||
return request;
|
||||
} catch (NumberFormatException ex){
|
||||
return request;
|
||||
}
|
||||
} else {
|
||||
Set<String> clSet = request.getLowercaseHeader("Content-Length");
|
||||
if (clSet.isEmpty()){
|
||||
return request;
|
||||
} else {
|
||||
try {
|
||||
int cl = Integer.parseInt(clSet.iterator().next());
|
||||
byte[] data = new byte[cl];
|
||||
in.read(data);
|
||||
request.data = data;
|
||||
return request;
|
||||
} catch (NumberFormatException ex){
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String readLine(BufferedReader in) throws IOException {
|
||||
String line = in.readLine();
|
||||
if (line == null){
|
||||
throw new HttpConnection.ConnectionClosedException();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"libjf-web-v0.jfconfig.title": "LibJF Web v0",
|
||||
"libjf-web-v0.jfconfig.serverIp": "Server IP",
|
||||
"libjf-web-v0.jfconfig.serverIp.tooltip": "The public IP/host name to send to clients",
|
||||
"libjf-web-v0.jfconfig.port": "Port",
|
||||
"libjf-web-v0.jfconfig.port.tooltip": "The port to host content on",
|
||||
"libjf-web-v0.jfconfig.portOverride": "Port Override",
|
||||
"libjf-web-v0.jfconfig.portOverride.tooltip": "The port to send to clients (for reverse proxies, -1 to disable)",
|
||||
"libjf-web-v0.jfconfig.maxConnections": "Max. Connections",
|
||||
"libjf-web-v0.jfconfig.maxConnections.tooltip": "The maximum number of concurrent connections to this server",
|
||||
"libjf-web-v0.jfconfig.enableFileHost": "Enable File Host",
|
||||
"libjf-web-v0.jfconfig.enableFileHost.tooltip": "Whether files from config/wwwroot should be hosted as static resources"
|
||||
}
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
}
|
||||
|
||||
base {
|
||||
archivesName.set("libjf-web-v0")
|
||||
archivesName.set("libjf-web-v1")
|
||||
}
|
||||
|
||||
dependencies {
|
|
@ -1,4 +1,4 @@
|
|||
package io.gitlab.jfronny.libjf.web.api;
|
||||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
public interface AdvancedSubServer extends SubServer {
|
||||
void onStop();
|
|
@ -0,0 +1,7 @@
|
|||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface ContentProvider {
|
||||
HttpResponse handle(HttpRequest request) throws IOException;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpRequestImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public interface HttpRequest {
|
||||
static HttpRequest read(InputStream in) throws IOException {
|
||||
return HttpRequestImpl.read(in);
|
||||
}
|
||||
|
||||
default HttpResponse createResponse(HttpStatusCode statusCode) {
|
||||
return HttpResponse.create(statusCode);
|
||||
}
|
||||
|
||||
String getMethod();
|
||||
String getAddress();
|
||||
String getVersion();
|
||||
Map<String, Set<String>> getHeader();
|
||||
Map<String, Set<String>> getLowercaseHeader();
|
||||
Set<String> getHeader(String key);
|
||||
Set<String> getLowercaseHeader(String key);
|
||||
String getPath();
|
||||
Map<String, String> getQuery();
|
||||
String getQueryString();
|
||||
InputStream getData();
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpResponseImpl;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public interface HttpResponse extends Closeable {
|
||||
static HttpResponse create(HttpStatusCode statusCode) {
|
||||
return new HttpResponseImpl(statusCode);
|
||||
}
|
||||
|
||||
HttpResponse addHeader(String key, String value);
|
||||
HttpResponse removeHeader(String key, String value);
|
||||
HttpResponse setData(InputStream data);
|
||||
HttpResponse setData(String data);
|
||||
void write(OutputStream out) throws IOException;
|
||||
|
||||
HttpStatusCode getStatusCode();
|
||||
String getVersion();
|
||||
Map<String, Set<String>> getHeader();
|
||||
Set<String> getHeader(String key);
|
||||
}
|
|
@ -22,7 +22,7 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
public enum HttpStatusCode {
|
||||
CONTINUE (100, "Continue"),
|
|
@ -0,0 +1,7 @@
|
|||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface SubServer {
|
||||
HttpResponse handle(HttpRequest request, String[] segments) throws IOException;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package io.gitlab.jfronny.libjf.web.api;
|
||||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
public interface WebInit {
|
||||
void register(WebServer api);
|
|
@ -1,4 +1,4 @@
|
|||
package io.gitlab.jfronny.libjf.web.api;
|
||||
package io.gitlab.jfronny.libjf.web.api.v1;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.JfWeb;
|
||||
|
||||
|
@ -7,14 +7,14 @@ import java.nio.file.Path;
|
|||
|
||||
public interface WebServer {
|
||||
String register(String webPath, ContentProvider provider);
|
||||
String registerFile(String webPath, Path file, Boolean readOnSend) throws IOException;
|
||||
String registerFile(String webPath, Path file, boolean readOnSend) throws IOException;
|
||||
String registerFile(String webPath, byte[] data, String contentType);
|
||||
String registerDir(String webPath, Path dir, Boolean readOnSend) throws IOException;
|
||||
String registerDir(String webPath, Path dir, boolean readOnSend) throws IOException;
|
||||
String registerSubServer(String webPath, SubServer subServer);
|
||||
String registerSubServer(String webPath, AdvancedSubServer subServer);
|
||||
String getServerRoot();
|
||||
void stop();
|
||||
void restart();
|
||||
void queueRestart(Runnable callback);
|
||||
boolean isActive();
|
||||
|
||||
static WebServer getInstance() {
|
|
@ -1,8 +1,8 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.WebInit;
|
||||
import io.gitlab.jfronny.libjf.web.api.WebServer;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.WebInit;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.WebServer;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.io.IOException;
|
|
@ -1,31 +1,50 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import io.gitlab.jfronny.commons.ref.R;
|
||||
import io.gitlab.jfronny.libjf.Flags;
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.coprocess.CoProcess;
|
||||
import io.gitlab.jfronny.libjf.web.api.WebServer;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.WebServer;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.hosted.HostedWebServer;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.shared.SharedWebServer;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.text.Text;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.time.chrono.IsoEra;
|
||||
|
||||
import static net.minecraft.server.command.CommandManager.literal;
|
||||
|
||||
public class JfWeb implements CoProcess, ModInitializer {
|
||||
private static final RequestHandler handler;
|
||||
public static final WebServer SERVER;
|
||||
|
||||
static {
|
||||
JfWebConfig.ensureValidPort();
|
||||
SERVER = new JfWebServer(JfWebConfig.port, JfWebConfig.maxConnections);
|
||||
handler = new RequestHandler();
|
||||
if (JfWebConfig.port != -1) SERVER = new HostedWebServer(handler, JfWebConfig.port, JfWebConfig.maxConnections);
|
||||
else if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
|
||||
SERVER = new HostedWebServer(handler, 0, JfWebConfig.maxConnections);
|
||||
else SERVER = new SharedWebServer(handler);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static RequestHandler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (isEnabled()) SERVER.restart();
|
||||
if (isEnabled()) SERVER.queueRestart(R::nop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (isEnabled()) SERVER.stop();
|
||||
if (!(SERVER instanceof SharedWebServer)) SERVER.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -43,7 +62,9 @@ public class JfWeb implements CoProcess, ModInitializer {
|
|||
}).then(literal("restart").executes(context -> {
|
||||
try {
|
||||
context.getSource().sendFeedback(() -> Text.literal("Restarting LibWeb"), true);
|
||||
SERVER.restart();
|
||||
SERVER.queueRestart(() -> {
|
||||
context.getSource().sendFeedback(() -> Text.literal("LibWeb restarted"), true);
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
LibJf.LOGGER.error("Failed to run restart command", e);
|
||||
|
@ -53,7 +74,6 @@ public class JfWeb implements CoProcess, ModInitializer {
|
|||
}))));
|
||||
});
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(SERVER::stop));
|
||||
}
|
||||
|
||||
private boolean isEnabled() {
|
|
@ -9,19 +9,24 @@ import java.net.ServerSocket;
|
|||
@JfConfig
|
||||
public class JfWebConfig {
|
||||
@Entry public static String serverIp = "http://127.0.0.1";
|
||||
@Entry(min = 0, max = 35535) public static int port = 0;
|
||||
@Entry(min = -1, max = 35535) public static int port = 0;
|
||||
@Entry(min = -1, max = 35535) public static int portOverride = -1;
|
||||
@Entry(min = 8, max = 64) public static int maxConnections = 20;
|
||||
@Entry public static boolean enableFileHost = false;
|
||||
|
||||
public static void ensureValidPort() {
|
||||
if (port == 0) {
|
||||
try (ServerSocket socket = new ServerSocket(0)) {
|
||||
port = socket.getLocalPort();
|
||||
} catch (IOException e) {
|
||||
LibJf.LOGGER.error("Could not bind port to identify available", e);
|
||||
}
|
||||
ConfigHolder.getInstance().getRegistered().get("libjf-web-v0").write();
|
||||
port = findAvailablePort();
|
||||
ConfigHolder.getInstance().getRegistered().get("libjf-web-v1").write();
|
||||
}
|
||||
}
|
||||
|
||||
public static int findAvailablePort() {
|
||||
try (ServerSocket socket = new ServerSocket(0)) {
|
||||
return socket.getLocalPort();
|
||||
} catch (IOException e) {
|
||||
LibJf.LOGGER.error("Could not bind port to identify available", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.AdvancedSubServer;
|
||||
import io.gitlab.jfronny.libjf.web.api.ContentProvider;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.WebPaths;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.*;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.hosted.HttpRequestHandler;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
@ -17,7 +16,7 @@ public class RequestHandler implements HttpRequestHandler {
|
|||
HttpResponse resp = null;
|
||||
try {
|
||||
String webPath = WebPaths.simplify(request.getPath());
|
||||
if (webPath.length() == 0)
|
||||
if (webPath.isEmpty())
|
||||
webPath = "index.html";
|
||||
if (contentProviders.containsKey(webPath)) {
|
||||
resp = contentProviders.get(webPath).handle(request);
|
||||
|
@ -32,16 +31,16 @@ public class RequestHandler implements HttpRequestHandler {
|
|||
}
|
||||
}
|
||||
if (resp == null) {
|
||||
resp = new HttpResponse(HttpStatusCode.NOT_FOUND);
|
||||
resp = new HttpResponseImpl(HttpStatusCode.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LibJf.LOGGER.error("Caught error while sending", e);
|
||||
resp = new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
|
||||
resp = new HttpResponseImpl(HttpStatusCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
if (resp.getHeader("Cache-Control").size() == 0)
|
||||
if (resp.getHeader("Cache-Control").isEmpty())
|
||||
resp.addHeader("Cache-Control", "no-cache");
|
||||
if (resp.getHeader("Server").size() == 0)
|
||||
if (resp.getHeader("Server").isEmpty())
|
||||
resp.addHeader("Server", "LibWeb using BlueMapCore");
|
||||
return resp;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.mixin;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.JfWebConfig;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class JfWebMixinPlugin implements IMixinConfigPlugin {
|
||||
private static final String MIXIN_PACKAGE = "io.gitlab.jfronny.libjf.web.impl.mixin.";
|
||||
|
||||
@Override
|
||||
public void onLoad(String mixinPackage) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRefMapperConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
|
||||
return switch (mixinClassName) {
|
||||
case MIXIN_PACKAGE + "ServerNetworkIoMixin", MIXIN_PACKAGE + "ServerNetworkIo$1Mixin" -> JfWebConfig.port == -1;
|
||||
default -> throw new IllegalArgumentException("Unexpected mixin: " + mixinClassName);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getMixins() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.mixin;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.shared.HttpDecoder;
|
||||
import io.netty.channel.Channel;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(targets = "net.minecraft.server.ServerNetworkIo$1")
|
||||
public class ServerNetworkIo$1Mixin {
|
||||
@Inject(method = "initChannel(Lio/netty/channel/Channel;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;addHandlers(Lio/netty/channel/ChannelPipeline;Lnet/minecraft/network/NetworkSide;Lnet/minecraft/network/handler/PacketSizeLogger;)V"))
|
||||
private void inject(Channel channel, CallbackInfo ci) {
|
||||
channel.pipeline().addAfter("legacy_query", "libjf_http", new HttpDecoder());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.mixin;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.ClaimPool;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.shared.SharedWebServer;
|
||||
import net.minecraft.server.ServerNetworkIo;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Mixin(ServerNetworkIo.class)
|
||||
public class ServerNetworkIoMixin {
|
||||
@Unique private final Set<ClaimPool<Integer>.Claim> libjf$portClaim = new HashSet<>();
|
||||
|
||||
@Inject(method = "bind(Ljava/net/InetAddress;I)V", at = @At("HEAD"))
|
||||
void onBind(InetAddress address, int port, CallbackInfo ci) {
|
||||
libjf$portClaim.add(SharedWebServer.gamePort.claim(port));
|
||||
SharedWebServer.emitActive();
|
||||
}
|
||||
|
||||
@Inject(method = "stop()V", at = @At("HEAD"))
|
||||
void onStop(CallbackInfo ci) {
|
||||
for (ClaimPool<Integer>.Claim claim : libjf$portClaim) claim.release();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ClaimPool<T> {
|
||||
private final List<Claim> content = new LinkedList<>();
|
||||
|
||||
public Claim claim(T value) {
|
||||
return new Claim(value);
|
||||
}
|
||||
|
||||
public T getTopmost() {
|
||||
return content.isEmpty() ? null : content.get(content.size() - 1).value;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return content.isEmpty();
|
||||
}
|
||||
|
||||
public class Claim {
|
||||
private final T value;
|
||||
private final AtomicBoolean active = new AtomicBoolean(true);
|
||||
|
||||
private Claim(T value) {
|
||||
this.value = value;
|
||||
content.add(this);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (!active.getAndSet(false))
|
||||
throw new UnsupportedOperationException("Cannot release claim that is already released");
|
||||
content.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* This file is part of BlueMap, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.hosted.HttpConnection;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class HttpRequestImpl implements HttpRequest {
|
||||
private static final Pattern REQUEST_PATTERN = Pattern.compile("^(\\w+) (\\S+) (.+)$");
|
||||
|
||||
private final String method;
|
||||
private final String address;
|
||||
private final String version;
|
||||
private final Map<String, Set<String>> header;
|
||||
private final Map<String, Set<String>> headerLC;
|
||||
private byte[] data;
|
||||
|
||||
private String path = null;
|
||||
private Map<String, String> queryParameters = null;
|
||||
private String queryString = null;
|
||||
|
||||
public HttpRequestImpl(String method, String address, String version, Map<String, Set<String>> header) {
|
||||
this.method = method;
|
||||
this.address = address;
|
||||
this.version = version;
|
||||
this.header = header;
|
||||
this.headerLC = new HashMap<>();
|
||||
|
||||
for (Entry<String, Set<String>> e : header.entrySet()){
|
||||
Set<String> values = new HashSet<>();
|
||||
for (String v : e.getValue()){
|
||||
values.add(v.toLowerCase());
|
||||
}
|
||||
|
||||
headerLC.put(e.getKey().toLowerCase(), values);
|
||||
}
|
||||
|
||||
this.data = new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAddress(){
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Set<String>> getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Set<String>> getLowercaseHeader() {
|
||||
return headerLC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getHeader(String key){
|
||||
Set<String> headerValues = header.get(key);
|
||||
if (headerValues == null) return Collections.emptySet();
|
||||
return headerValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getLowercaseHeader(String key){
|
||||
Set<String> headerValues = headerLC.get(key.toLowerCase());
|
||||
if (headerValues == null) return Collections.emptySet();
|
||||
return headerValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
if (path == null) parseAddress();
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getQuery() {
|
||||
if (queryParameters == null) parseAddress();
|
||||
return Collections.unmodifiableMap(queryParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryString() {
|
||||
if (queryString == null) parseAddress();
|
||||
return queryString;
|
||||
}
|
||||
|
||||
private void parseAddress() {
|
||||
String adress = this.address;
|
||||
if (adress.isEmpty()) adress = "/";
|
||||
String[] addressParts = adress.split("\\?", 2);
|
||||
String path = addressParts[0];
|
||||
this.queryString = addressParts.length > 1 ? addressParts[1] : "";
|
||||
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
for (String queryParam : this.queryString.split("&")){
|
||||
if (queryParam.isEmpty()) continue;
|
||||
String[] kv = queryParam.split("=", 2);
|
||||
String key = kv[0];
|
||||
String value = kv.length > 1 ? kv[1] : "";
|
||||
queryParams.put(key, value);
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.queryParameters = queryParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getData(){
|
||||
return new ByteArrayInputStream(data);
|
||||
}
|
||||
|
||||
public static HttpRequestImpl read(InputStream in) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
HttpRequestImpl request = fromHeaders(extractHeaders(reader));
|
||||
readData(in, request, reader);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static void readData(InputStream in, HttpRequestImpl request, BufferedReader reader) throws IOException {
|
||||
if (request.getLowercaseHeader("Transfer-Encoding").contains("chunked")) {
|
||||
try {
|
||||
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
|
||||
while (dataStream.size() < 1000000) {
|
||||
String hexSize = reader.readLine();
|
||||
int chunkSize = Integer.parseInt(hexSize, 16);
|
||||
if (chunkSize <= 0) break;
|
||||
byte[] data = new byte[chunkSize];
|
||||
in.read(data);
|
||||
dataStream.write(data);
|
||||
}
|
||||
|
||||
if (dataStream.size() >= 1000000) {
|
||||
throw new HttpConnection.InvalidRequestException();
|
||||
}
|
||||
|
||||
request.data = dataStream.toByteArray();
|
||||
} catch (NumberFormatException ex) {
|
||||
}
|
||||
} else {
|
||||
Set<String> clSet = request.getLowercaseHeader("Content-Length");
|
||||
if (clSet.isEmpty()) return;
|
||||
try {
|
||||
int cl = Integer.parseInt(clSet.iterator().next());
|
||||
byte[] data = new byte[cl];
|
||||
in.read(data);
|
||||
request.data = data;
|
||||
} catch (NumberFormatException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> extractHeaders(BufferedReader reader) throws IOException {
|
||||
List<String> headers = new ArrayList<>(20);
|
||||
while (headers.size() < 1000) {
|
||||
String headerLine = readLine(reader);
|
||||
if (headerLine.isEmpty()) break;
|
||||
headers.add(headerLine);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static HttpRequestImpl fromHeaders(List<String> headers) throws HttpConnection.InvalidRequestException {
|
||||
if (headers.isEmpty()) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
Matcher m = REQUEST_PATTERN.matcher(headers.remove(0));
|
||||
if (!m.find()) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String method = m.group(1);
|
||||
if (method == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String adress = m.group(2);
|
||||
if (adress == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
String version = m.group(3);
|
||||
if (version == null) throw new HttpConnection.InvalidRequestException();
|
||||
|
||||
return new HttpRequestImpl(method, adress, version, parseExtraHeaders(headers));
|
||||
}
|
||||
|
||||
private static Map<String, Set<String>> parseExtraHeaders(List<String> header) {
|
||||
Map<String, Set<String>> headerMap = new HashMap<>();
|
||||
for (String line : header) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
|
||||
String[] kv = line.split(":", 2);
|
||||
if (kv.length < 2) continue;
|
||||
|
||||
Set<String> values = new HashSet<>();
|
||||
if (kv[0].trim().equalsIgnoreCase("If-Modified-Since")) {
|
||||
values.add(kv[1].trim());
|
||||
} else {
|
||||
for (String v : kv[1].split(",")) {
|
||||
values.add(v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
headerMap.put(kv[0].trim(), values);
|
||||
}
|
||||
return headerMap;
|
||||
}
|
||||
|
||||
private static String readLine(BufferedReader in) throws IOException {
|
||||
String line = in.readLine();
|
||||
if (line == null) {
|
||||
throw new HttpConnection.ConnectionClosedException();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
|
@ -22,8 +22,10 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
package io.gitlab.jfronny.libjf.web.impl.util;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpResponse;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpStatusCode;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
|
@ -31,13 +33,14 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class HttpResponse implements Closeable {
|
||||
public class HttpResponseImpl implements HttpResponse {
|
||||
private final String version;
|
||||
private final HttpStatusCode statusCode;
|
||||
private final Map<String, Set<String>> header;
|
||||
private boolean closed = false;
|
||||
private InputStream data;
|
||||
|
||||
public HttpResponse(HttpStatusCode statusCode) {
|
||||
public HttpResponseImpl(HttpStatusCode statusCode) {
|
||||
this.version = "HTTP/1.1";
|
||||
this.statusCode = statusCode;
|
||||
|
||||
|
@ -46,24 +49,32 @@ public class HttpResponse implements Closeable {
|
|||
addHeader("Connection", "keep-alive");
|
||||
}
|
||||
|
||||
public HttpResponse addHeader(String key, String value){
|
||||
@Override
|
||||
public HttpResponseImpl addHeader(String key, String value) {
|
||||
ensureOpen();
|
||||
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
|
||||
valueSet.add(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse removeHeader(String key, String value){
|
||||
@Override
|
||||
public HttpResponseImpl removeHeader(String key, String value) {
|
||||
ensureOpen();
|
||||
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
|
||||
valueSet.remove(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse setData(InputStream dataStream){
|
||||
@Override
|
||||
public HttpResponseImpl setData(InputStream dataStream) {
|
||||
ensureOpen();
|
||||
this.data = dataStream;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse setData(String data){
|
||||
@Override
|
||||
public HttpResponseImpl setData(String data) {
|
||||
ensureOpen();
|
||||
setData(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
|
||||
return this;
|
||||
}
|
||||
|
@ -73,17 +84,18 @@ public class HttpResponse implements Closeable {
|
|||
* <br>
|
||||
* This method closes the data-Stream of this response so it can't be used again!
|
||||
*/
|
||||
public HttpResponse write(OutputStream out) throws IOException {
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException {
|
||||
OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
|
||||
if (data != null){
|
||||
if (data != null) {
|
||||
addHeader("Transfer-Encoding", "chunked");
|
||||
} else {
|
||||
addHeader("Content-Length", "0");
|
||||
}
|
||||
|
||||
writeLine(writer, version + " " + statusCode.getCode() + " " + statusCode.getMessage());
|
||||
for (Entry<String, Set<String>> e : header.entrySet()){
|
||||
for (Entry<String, Set<String>> e : header.entrySet()) {
|
||||
if (e.getValue().isEmpty()) continue;
|
||||
writeLine(writer, e.getKey() + ": " + StringUtils.join(e.getValue(), ", "));
|
||||
}
|
||||
|
@ -91,17 +103,20 @@ public class HttpResponse implements Closeable {
|
|||
writeLine(writer, "");
|
||||
writer.flush();
|
||||
|
||||
if(data != null){
|
||||
if (data != null) {
|
||||
boolean markSupported = data.markSupported();
|
||||
if (markSupported) data.mark(Integer.MAX_VALUE);
|
||||
chunkedPipe(data, out);
|
||||
out.flush();
|
||||
data.close();
|
||||
if (markSupported) data.reset();
|
||||
else close();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
data.close();
|
||||
if (data != null) data.close();
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
private void writeLine(OutputStreamWriter writer, String line) throws IOException {
|
||||
|
@ -119,18 +134,26 @@ public class HttpResponse implements Closeable {
|
|||
output.write("0\r\n\r\n".getBytes());
|
||||
}
|
||||
|
||||
private void ensureOpen() {
|
||||
if (closed) throw new UnsupportedOperationException("Response cannot be changed after being closed or written");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatusCode getStatusCode(){
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion(){
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Set<String>> getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getHeader(String key){
|
||||
Set<String> headerValues = header.get(key);
|
||||
if (headerValues == null) return Collections.emptySet();
|
|
@ -1,5 +1,7 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.util;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.impl.JfWebConfig;
|
||||
|
||||
public class WebPaths {
|
||||
public static String concat(String s1, String s2) {
|
||||
return simplify(s1) + "/" + simplify(s2);
|
||||
|
@ -13,6 +15,12 @@ public class WebPaths {
|
|||
return simplify(s.toString());
|
||||
}
|
||||
|
||||
public static String getHttp(String ip) {
|
||||
if (!ip.startsWith("http"))
|
||||
ip = "http://" + ip;
|
||||
return simplify(ip);
|
||||
}
|
||||
|
||||
public static String simplify(String s) {
|
||||
boolean http = false;
|
||||
boolean https = false;
|
|
@ -1,10 +1,11 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl;
|
||||
package io.gitlab.jfronny.libjf.web.impl.variant;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.WebPaths;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.*;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import javax.management.openmbean.KeyAlreadyExistsException;
|
||||
import java.io.*;
|
||||
|
@ -13,15 +14,17 @@ import java.nio.file.Path;
|
|||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class JfWebServer implements WebServer {
|
||||
private HttpServer server = null;
|
||||
private final RequestHandler handler = new RequestHandler();
|
||||
private final int port;
|
||||
private final int maxConnections;
|
||||
public abstract class AbstractWebServer implements WebServer {
|
||||
protected final RequestHandler handler;
|
||||
private final DefaultFileHost dfh = new DefaultFileHost();
|
||||
public JfWebServer(int port, int maxConnections) {
|
||||
this.port = port;
|
||||
this.maxConnections = maxConnections;
|
||||
|
||||
protected AbstractWebServer(RequestHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public RequestHandler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -34,12 +37,12 @@ public class JfWebServer implements WebServer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String registerFile(String webPath, Path file, Boolean readOnSend) throws IOException {
|
||||
public String registerFile(String webPath, Path file, boolean readOnSend) throws IOException {
|
||||
if (readOnSend) {
|
||||
if (!Files.exists(file))
|
||||
throw new FileNotFoundException();
|
||||
register(webPath, s -> {
|
||||
HttpResponse resp = new HttpResponse(HttpStatusCode.OK);
|
||||
HttpResponse resp = HttpResponse.create(HttpStatusCode.OK);
|
||||
resp.addHeader("Content-Type", Files.probeContentType(file));
|
||||
resp.addHeader("Content-Length", String.valueOf(Files.size(file)));
|
||||
FileInputStream fs = new FileInputStream(file.toFile());
|
||||
|
@ -56,7 +59,7 @@ public class JfWebServer implements WebServer {
|
|||
@Override
|
||||
public String registerFile(String webPath, byte[] data, String contentType) {
|
||||
return register(webPath, s -> {
|
||||
HttpResponse resp = new HttpResponse(HttpStatusCode.OK);
|
||||
HttpResponse resp = HttpResponse.create(HttpStatusCode.OK);
|
||||
resp.addHeader("Content-Type", contentType);
|
||||
resp.addHeader("Content-Length", String.valueOf(data.length));
|
||||
ByteArrayInputStream fs = new ByteArrayInputStream(data);
|
||||
|
@ -66,7 +69,7 @@ public class JfWebServer implements WebServer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String registerDir(String webPath, Path dir, Boolean readOnSend) throws IOException {
|
||||
public String registerDir(String webPath, Path dir, boolean readOnSend) throws IOException {
|
||||
try (Stream<Path> contentPath = Files.walk(dir)) {
|
||||
if (readOnSend) {
|
||||
return registerSubServer(webPath, (s, t) -> {
|
||||
|
@ -89,13 +92,13 @@ public class JfWebServer implements WebServer {
|
|||
});
|
||||
HttpResponse resp;
|
||||
if (c[0]) {
|
||||
resp = new HttpResponse(HttpStatusCode.OK);
|
||||
resp = HttpResponse.create(HttpStatusCode.OK);
|
||||
resp.addHeader("Content-Type", Files.probeContentType(p_f[0]));
|
||||
resp.addHeader("Content-Length", String.valueOf(Files.size(p_f[0])));
|
||||
FileInputStream fs = new FileInputStream(p_f[0].toFile());
|
||||
resp.setData(fs);
|
||||
} else {
|
||||
resp = new HttpResponse(HttpStatusCode.NOT_FOUND);
|
||||
resp = HttpResponse.create(HttpStatusCode.NOT_FOUND);
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
|
@ -122,12 +125,10 @@ public class JfWebServer implements WebServer {
|
|||
return registerSubServer(webPath, new AdvancedSubServer() {
|
||||
@Override
|
||||
public void onStop() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -146,61 +147,16 @@ public class JfWebServer implements WebServer {
|
|||
return WebPaths.concat(getServerRoot(), webPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerRoot() {
|
||||
String ip = JfWebConfig.serverIp;
|
||||
if (!ip.startsWith("http"))
|
||||
ip = "http://" + ip;
|
||||
if (JfWebConfig.portOverride != -1) {
|
||||
return WebPaths.simplify(ip) + ":" + JfWebConfig.portOverride;
|
||||
}
|
||||
return WebPaths.simplify(ip) + ":" + JfWebConfig.port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) subServer.onStop();
|
||||
if (server != null) {
|
||||
try {
|
||||
server.close();
|
||||
server.join();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
//It is most likely already dead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void restart() {
|
||||
JfWebConfig.ensureValidPort();
|
||||
int tmpPort = port;
|
||||
if (server != null) {
|
||||
tmpPort = server.getPort();
|
||||
stop();
|
||||
}
|
||||
handler.clear();
|
||||
server = new HttpServer(null, tmpPort, maxConnections, handler, () -> {
|
||||
if (JfWebConfig.enableFileHost) dfh.register(this);
|
||||
FabricLoader.getInstance().getEntrypointContainers(LibJf.MOD_ID + ":web", WebInit.class).forEach(entrypoint -> {
|
||||
WebInit init = entrypoint.getEntrypoint();
|
||||
init.register(this);
|
||||
});
|
||||
protected void performRegistrations() {
|
||||
if (JfWebConfig.enableFileHost) dfh.register(this);
|
||||
FabricLoader.getInstance().getEntrypointContainers(LibJf.MOD_ID + ":web", WebInit.class).forEach(entrypoint -> {
|
||||
WebInit init = entrypoint.getEntrypoint();
|
||||
init.register(this);
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
server.waitUntilReady();
|
||||
} catch (InterruptedException e) {
|
||||
stop();
|
||||
LibJf.LOGGER.error("Server could not be readied", e);
|
||||
}
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) {
|
||||
subServer.onStart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return server != null && server.isAlive();
|
||||
protected String getServerRoot(int hostedPort) {
|
||||
return WebPaths.getHttp(JfWebConfig.serverIp) + ":"
|
||||
+ (JfWebConfig.portOverride != -1 ? JfWebConfig.portOverride : hostedPort);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.variant.hosted;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.AdvancedSubServer;
|
||||
import io.gitlab.jfronny.libjf.web.impl.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.AbstractWebServer;
|
||||
|
||||
public class HostedWebServer extends AbstractWebServer {
|
||||
private HttpServer server = null;
|
||||
private final int port;
|
||||
private final int maxConnections;
|
||||
|
||||
public HostedWebServer(RequestHandler handler, int port, int maxConnections) {
|
||||
super(handler);
|
||||
this.port = port;
|
||||
this.maxConnections = maxConnections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerRoot() {
|
||||
return getServerRoot(server.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) subServer.onStop();
|
||||
if (server != null) {
|
||||
try {
|
||||
server.close();
|
||||
server.join();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
//It is most likely already dead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueRestart(Runnable callback) {
|
||||
int tmpPort = port;
|
||||
if (server != null) {
|
||||
tmpPort = server.getPort();
|
||||
stop();
|
||||
} else if (tmpPort == 0) tmpPort = JfWebConfig.findAvailablePort();
|
||||
handler.clear();
|
||||
server = new HttpServer(null, tmpPort, maxConnections, handler, this::performRegistrations);
|
||||
server.start();
|
||||
try {
|
||||
server.waitUntilReady();
|
||||
} catch (InterruptedException e) {
|
||||
stop();
|
||||
LibJf.LOGGER.error("Server could not be readied", e);
|
||||
}
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) {
|
||||
subServer.onStart();
|
||||
}
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return server != null && server.isAlive();
|
||||
}
|
||||
}
|
|
@ -22,9 +22,12 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
package io.gitlab.jfronny.libjf.web.impl.variant.hosted;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpRequestImpl;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpResponseImpl;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
@ -64,8 +67,9 @@ public class HttpConnection implements Runnable {
|
|||
sendResponse(response);
|
||||
} catch (InvalidRequestException e){
|
||||
try {
|
||||
sendResponse(new HttpResponse(HttpStatusCode.BAD_REQUEST));
|
||||
} catch (IOException e1) {}
|
||||
sendResponse(HttpResponse.create(HttpStatusCode.BAD_REQUEST));
|
||||
} catch (IOException e1) {
|
||||
}
|
||||
break;
|
||||
} catch (SocketTimeoutException | SocketException | ConnectionClosedException e) {
|
||||
break;
|
||||
|
@ -82,7 +86,7 @@ public class HttpConnection implements Runnable {
|
|||
}
|
||||
}
|
||||
|
||||
private void log(HttpRequest request, HttpResponse response) {
|
||||
private void log(HttpRequestImpl request, HttpResponseImpl response) {
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
|
||||
Date date = new Date();
|
||||
LibJf.LOGGER.info(
|
|
@ -22,7 +22,12 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
package io.gitlab.jfronny.libjf.web.impl.variant.hosted;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpRequest;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpResponse;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpRequestImpl;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpResponseImpl;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HttpRequestHandler {
|
|
@ -22,7 +22,7 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package io.gitlab.jfronny.libjf.web.impl.util.bluemapcore;
|
||||
package io.gitlab.jfronny.libjf.web.impl.variant.hosted;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpRequest;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.HttpResponse;
|
||||
import io.gitlab.jfronny.libjf.web.impl.JfWeb;
|
||||
import io.gitlab.jfronny.libjf.web.impl.RequestHandler;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpRequestImpl;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpResponseImpl;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class HttpDecoder extends ChannelInboundHandlerAdapter {
|
||||
private static final Trie<String> METHOD = Trie.of(List.of(
|
||||
"GET",
|
||||
"HEAD",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
|
||||
"OPTIONS", "TRACE", "CONNECT"
|
||||
));
|
||||
|
||||
@Override
|
||||
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object msg) throws Exception {
|
||||
ByteBuf buf = (ByteBuf) msg;
|
||||
buf.markReaderIndex();
|
||||
boolean passOn = true;
|
||||
try {
|
||||
Trie<String> current = METHOD;
|
||||
while (buf.isReadable()
|
||||
&& current != null
|
||||
&& current.content == null)
|
||||
current = current.next.get((char) buf.readByte());
|
||||
if (current == null || current.content == null) return;
|
||||
buf.resetReaderIndex();
|
||||
byte[] data = new byte[buf.readableBytes()];
|
||||
buf.readBytes(data);
|
||||
try (ByteArrayInputStream is = new ByteArrayInputStream(data);
|
||||
HttpResponse response = JfWeb.getHandler().handle(HttpRequest.read(is));
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
|
||||
response.write(os);
|
||||
os.flush();
|
||||
ctx.pipeline()
|
||||
.firstContext()
|
||||
.writeAndFlush(Unpooled.wrappedBuffer(os.toByteArray()))
|
||||
.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
buf.release();
|
||||
passOn = false;
|
||||
} catch (RuntimeException re) {
|
||||
LibJf.LOGGER.error("Could not process HTTP", re);
|
||||
} finally {
|
||||
if (passOn) {
|
||||
buf.resetReaderIndex();
|
||||
ctx.channel().pipeline().remove(this);
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
|
||||
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.AdvancedSubServer;
|
||||
import io.gitlab.jfronny.libjf.web.impl.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.ClaimPool;
|
||||
import io.gitlab.jfronny.libjf.web.impl.variant.AbstractWebServer;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SharedWebServer extends AbstractWebServer {
|
||||
public static final ClaimPool<Integer> gamePort = new ClaimPool<>();
|
||||
public static final Set<Runnable> onActive = new LinkedHashSet<>();
|
||||
|
||||
public static void emitActive() {
|
||||
for (Runnable runnable : onActive) runnable.run();
|
||||
}
|
||||
|
||||
public SharedWebServer(RequestHandler handler) {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerRoot() {
|
||||
Integer gamePort = this.gamePort.getTopmost();
|
||||
if (gamePort == null) throw new UnsupportedOperationException("Attempted to get server root on unhosted server");
|
||||
else return getServerRoot(gamePort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
throw new UnsupportedOperationException("A shared server cannot be stopped");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueRestart(Runnable callback) {
|
||||
onActive.add(() -> {
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) subServer.onStop();
|
||||
handler.clear();
|
||||
performRegistrations();
|
||||
for (AdvancedSubServer subServer : handler.subServers.values()) subServer.onStart();
|
||||
callback.run();
|
||||
});
|
||||
if (isActive()) emitActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return !gamePort.isEmpty();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.gitlab.jfronny.libjf.web.impl.variant.shared;
|
||||
|
||||
import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap;
|
||||
import it.unimi.dsi.fastutil.chars.Char2ObjectMap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Trie<T> {
|
||||
public final Char2ObjectMap<Trie<T>> next;
|
||||
public T content;
|
||||
|
||||
public Trie() {
|
||||
this.next = new Char2ObjectArrayMap<>();
|
||||
}
|
||||
|
||||
public void add(Map<String, T> next) {
|
||||
next.forEach(this::add);
|
||||
}
|
||||
|
||||
public void add(String key, T value) {
|
||||
if (key.isEmpty()) this.content = value;
|
||||
else this.next.computeIfAbsent(key.charAt(0), k -> new Trie<>())
|
||||
.add(key.substring(1), value);
|
||||
}
|
||||
|
||||
public static <T> Trie<T> of(Map<String, T> source) {
|
||||
Trie<T> root = new Trie<>();
|
||||
root.add(source);
|
||||
return root;
|
||||
}
|
||||
|
||||
public static Trie<String> of(List<String> source) {
|
||||
return of(source.stream().collect(Collectors.toMap(Function.identity(), Function.identity())));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"libjf-web-v1.jfconfig.title": "LibJF Web v0",
|
||||
"libjf-web-v1.jfconfig.serverIp": "Server IP",
|
||||
"libjf-web-v1.jfconfig.serverIp.tooltip": "The public IP/host name to send to clients",
|
||||
"libjf-web-v1.jfconfig.port": "Port",
|
||||
"libjf-web-v1.jfconfig.port.tooltip": "The port to host content on, 0 to choose a random one or -1 to reuse the minecraft port on servers (requires restart)",
|
||||
"libjf-web-v1.jfconfig.portOverride": "Port Override",
|
||||
"libjf-web-v1.jfconfig.portOverride.tooltip": "The port to send to clients (for reverse proxies, -1 to disable)",
|
||||
"libjf-web-v1.jfconfig.maxConnections": "Max. Connections",
|
||||
"libjf-web-v1.jfconfig.maxConnections.tooltip": "The maximum number of concurrent connections to this server",
|
||||
"libjf-web-v1.jfconfig.enableFileHost": "Enable File Host",
|
||||
"libjf-web-v1.jfconfig.enableFileHost.tooltip": "Whether files from config/wwwroot should be hosted as static resources"
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "libjf-web-v0",
|
||||
"id": "libjf-web-v1",
|
||||
"provides": ["libjf-web-v0"],
|
||||
"name": "LibJF Web",
|
||||
"version": "${version}",
|
||||
"authors": [
|
||||
|
@ -14,6 +15,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"environment": "*",
|
||||
"mixins": ["libjf-web-v1.mixins.json"],
|
||||
"entrypoints": {
|
||||
"main": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
|
||||
"libjf:coprocess": ["io.gitlab.jfronny.libjf.web.impl.JfWeb"],
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "io.gitlab.jfronny.libjf.web.impl.mixin",
|
||||
"compatibilityLevel": "JAVA_16",
|
||||
"plugin": "io.gitlab.jfronny.libjf.web.impl.mixin.JfWebMixinPlugin",
|
||||
"server": [
|
||||
"ServerNetworkIo$1Mixin",
|
||||
"ServerNetworkIoMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
package io.gitlab.jfronny.libjf.web.test;
|
||||
|
||||
import io.gitlab.jfronny.libjf.LibJf;
|
||||
import io.gitlab.jfronny.libjf.web.api.WebInit;
|
||||
import io.gitlab.jfronny.libjf.web.api.WebServer;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpResponse;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.bluemapcore.HttpStatusCode;
|
||||
import io.gitlab.jfronny.libjf.web.api.v1.*;
|
||||
import io.gitlab.jfronny.libjf.web.impl.util.HttpResponseImpl;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -14,8 +12,11 @@ import java.nio.file.Path;
|
|||
public class WebTest implements WebInit {
|
||||
@Override
|
||||
public void register(WebServer api) {
|
||||
Path sourcePath = FabricLoader.getInstance().getModContainer("libjf-web-v0-testmod").get().findPath("test.html").get();
|
||||
LibJf.LOGGER.info(api.register("/test/0.html", request -> new HttpResponse(HttpStatusCode.OK).setData(Files.readString(sourcePath))));
|
||||
Path sourcePath = FabricLoader.getInstance()
|
||||
.getModContainer("libjf-web-v1-testmod")
|
||||
.flatMap(modContainer -> modContainer.findPath("test.html"))
|
||||
.orElseThrow();
|
||||
LibJf.LOGGER.info(api.register("/test/0.html", request -> request.createResponse(HttpStatusCode.OK).setData(Files.readString(sourcePath))));
|
||||
try {
|
||||
LibJf.LOGGER.info(api.registerFile("/test/1.html", sourcePath, false));
|
||||
LibJf.LOGGER.info(api.registerFile("/test/2.html", sourcePath, true));
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "libjf-web-v0-testmod",
|
||||
"id": "libjf-web-v1-testmod",
|
||||
"name": "LibJF Web",
|
||||
"version": "1.0",
|
||||
"environment": "*",
|
|
@ -22,6 +22,6 @@ include("libjf-data-manipulation-v0")
|
|||
include("libjf-devutil")
|
||||
include("libjf-translate-v1")
|
||||
include("libjf-unsafe-v0")
|
||||
include("libjf-web-v0")
|
||||
include("libjf-web-v1")
|
||||
|
||||
include("libjf-config-compiler-plugin-v2")
|
||||
|
|
Loading…
Reference in New Issue