diff --git a/build.gradle.kts b/build.gradle.kts index 5336942..9e94fa2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b24fe64..6a73656 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -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) \ No newline at end of file +- [libjf-web-v1](./libjf-web-v1.md) \ No newline at end of file diff --git a/docs/libjf-web-v0.md b/docs/libjf-web-v1.md similarity index 82% rename from docs/libjf-web-v0.md rename to docs/libjf-web-v1.md index b71a110..5a9e1b3 100644 --- a/docs/libjf-web-v0.md +++ b/docs/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: diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java index 06127c0..7023655 100644 --- a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java @@ -27,9 +27,8 @@ public class CoProcessManager implements ModInitializer { } private void stop() { - Iterator procs = coProcesses.iterator(); - while (procs.hasNext()) { - CoProcess coProcess = procs.next(); + for (Iterator 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(); } } } diff --git a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigCore.java b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigCore.java index fd97734..75112d0 100644 --- a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigCore.java +++ b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigCore.java @@ -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) ); diff --git a/libjf-config-core-v2/src/main/resources/fabric.mod.json b/libjf-config-core-v2/src/main/resources/fabric.mod.json index 624a8b6..3a54a89 100644 --- a/libjf-config-core-v2/src/main/resources/fabric.mod.json +++ b/libjf-config-core-v2/src/main/resources/fabric.mod.json @@ -1,6 +1,7 @@ { "schemaVersion": 1, "id": "libjf-config-core-v2", + "provides": ["libjf-config-core-v1"], "name": "LibJF Config", "version": "${version}", "authors": ["JFronny"], diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/ContentProvider.java b/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/ContentProvider.java deleted file mode 100644 index 1687919..0000000 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/ContentProvider.java +++ /dev/null @@ -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; -} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/SubServer.java b/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/SubServer.java deleted file mode 100644 index 15c6d76..0000000 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/SubServer.java +++ /dev/null @@ -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; -} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequest.java b/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequest.java deleted file mode 100644 index 6e3a09d..0000000 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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> header; - private final Map> headerLC; - private byte[] data; - - private String path = null; - private Map getParams = null; - private String getParamString = null; - - public HttpRequest(String method, String adress, String version, Map> header) { - this.method = method; - this.adress = adress; - this.version = version; - this.header = header; - this.headerLC = new HashMap<>(); - - for (Entry> e : header.entrySet()){ - Set 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> getHeader() { - return header; - } - - public Map> getLowercaseHeader() { - return headerLC; - } - - public Set getHeader(String key){ - Set headerValues = header.get(key); - if (headerValues == null) return Collections.emptySet(); - return headerValues; - } - - public Set getLowercaseHeader(String key){ - Set headerValues = headerLC.get(key.toLowerCase()); - if (headerValues == null) return Collections.emptySet(); - return headerValues; - } - - public String getPath() { - if (path == null) parseAdress(); - return path; - } - - public Map 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 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 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> headerMap = new HashMap>(); - for (String line : header){ - if (line.trim().isEmpty()) continue; - - String[] kv = line.split(":", 2); - if (kv.length < 2) continue; - - Set 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 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; - } - -} diff --git a/libjf-web-v0/src/main/resources/assets/libjf-web-v0/lang/en_us.json b/libjf-web-v0/src/main/resources/assets/libjf-web-v0/lang/en_us.json deleted file mode 100644 index 38a4c7b..0000000 --- a/libjf-web-v0/src/main/resources/assets/libjf-web-v0/lang/en_us.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/libjf-web-v0/build.gradle.kts b/libjf-web-v1/build.gradle.kts similarity index 92% rename from libjf-web-v0/build.gradle.kts rename to libjf-web-v1/build.gradle.kts index c5e453a..b83d6a5 100644 --- a/libjf-web-v0/build.gradle.kts +++ b/libjf-web-v1/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } base { - archivesName.set("libjf-web-v0") + archivesName.set("libjf-web-v1") } dependencies { diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/AdvancedSubServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/AdvancedSubServer.java similarity index 68% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/AdvancedSubServer.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/AdvancedSubServer.java index b570904..9748540 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/AdvancedSubServer.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/AdvancedSubServer.java @@ -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(); diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/ContentProvider.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/ContentProvider.java new file mode 100644 index 0000000..72d99e5 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/ContentProvider.java @@ -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; +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpRequest.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpRequest.java new file mode 100644 index 0000000..39f260c --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpRequest.java @@ -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> getHeader(); + Map> getLowercaseHeader(); + Set getHeader(String key); + Set getLowercaseHeader(String key); + String getPath(); + Map getQuery(); + String getQueryString(); + InputStream getData(); +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpResponse.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpResponse.java new file mode 100644 index 0000000..7020775 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpResponse.java @@ -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> getHeader(); + Set getHeader(String key); +} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpStatusCode.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpStatusCode.java similarity index 97% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpStatusCode.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpStatusCode.java index d25f391..d72b634 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpStatusCode.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/HttpStatusCode.java @@ -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"), diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/SubServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/SubServer.java new file mode 100644 index 0000000..816d035 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/SubServer.java @@ -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; +} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebInit.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebInit.java similarity index 59% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebInit.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebInit.java index 4853388..6a15e23 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebInit.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebInit.java @@ -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); diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebServer.java similarity index 73% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebServer.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebServer.java index 3654962..7038ec6 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/api/WebServer.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/api/v1/WebServer.java @@ -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() { diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java similarity index 88% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java index df20a97..17192a6 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/DefaultFileHost.java @@ -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; diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java similarity index 65% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java index 4ede783..9ecf1b8 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWeb.java @@ -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() { diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java similarity index 62% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java index 3d1e3b7..4a258ad 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebConfig.java @@ -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; } } diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java similarity index 75% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java index 3c4953e..34b3d1e 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/RequestHandler.java @@ -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; } diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/JfWebMixinPlugin.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/JfWebMixinPlugin.java new file mode 100644 index 0000000..ef2f214 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/JfWebMixinPlugin.java @@ -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 myTargets, Set otherTargets) { + } + + @Override + public List 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) { + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIo$1Mixin.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIo$1Mixin.java new file mode 100644 index 0000000..a2a8e02 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIo$1Mixin.java @@ -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()); + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIoMixin.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIoMixin.java new file mode 100644 index 0000000..1a8da9b --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/mixin/ServerNetworkIoMixin.java @@ -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.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.Claim claim : libjf$portClaim) claim.release(); + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/ClaimPool.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/ClaimPool.java new file mode 100644 index 0000000..b6e0c03 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/ClaimPool.java @@ -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 { + private final List 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); + } + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpRequestImpl.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpRequestImpl.java new file mode 100644 index 0000000..16f8aed --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpRequestImpl.java @@ -0,0 +1,249 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * 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> header; + private final Map> headerLC; + private byte[] data; + + private String path = null; + private Map queryParameters = null; + private String queryString = null; + + public HttpRequestImpl(String method, String address, String version, Map> header) { + this.method = method; + this.address = address; + this.version = version; + this.header = header; + this.headerLC = new HashMap<>(); + + for (Entry> e : header.entrySet()){ + Set 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> getHeader() { + return header; + } + + @Override + public Map> getLowercaseHeader() { + return headerLC; + } + + @Override + public Set getHeader(String key){ + Set headerValues = header.get(key); + if (headerValues == null) return Collections.emptySet(); + return headerValues; + } + + @Override + public Set getLowercaseHeader(String key){ + Set 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 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 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 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 extractHeaders(BufferedReader reader) throws IOException { + List 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 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> parseExtraHeaders(List header) { + Map> headerMap = new HashMap<>(); + for (String line : header) { + if (line.trim().isEmpty()) continue; + + String[] kv = line.split(":", 2); + if (kv.length < 2) continue; + + Set 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; + } +} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpResponse.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpResponseImpl.java similarity index 73% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpResponse.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpResponseImpl.java index 5460420..7a4dcde 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpResponse.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/HttpResponseImpl.java @@ -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> 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 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 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 { *
* 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> e : header.entrySet()){ + for (Entry> 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> getHeader() { return header; } + @Override public Set getHeader(String key){ Set headerValues = header.get(key); if (headerValues == null) return Collections.emptySet(); diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java similarity index 89% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java index 68051a2..1a30629 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/WebPaths.java @@ -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; diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/AbstractWebServer.java similarity index 66% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebServer.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/AbstractWebServer.java index 3049646..4318440 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/JfWebServer.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/AbstractWebServer.java @@ -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 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); } } diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HostedWebServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HostedWebServer.java new file mode 100644 index 0000000..defb94a --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HostedWebServer.java @@ -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(); + } +} diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpConnection.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpConnection.java similarity index 90% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpConnection.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpConnection.java index aaab0d8..181533d 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpConnection.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpConnection.java @@ -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( diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequestHandler.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpRequestHandler.java similarity index 82% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequestHandler.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpRequestHandler.java index 7f5d490..8e2cd9f 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpRequestHandler.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpRequestHandler.java @@ -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 { diff --git a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpServer.java similarity index 98% rename from libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpServer.java rename to libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpServer.java index cc0e9ac..c01c4fe 100644 --- a/libjf-web-v0/src/main/java/io/gitlab/jfronny/libjf/web/impl/util/bluemapcore/HttpServer.java +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/hosted/HttpServer.java @@ -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; diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/HttpDecoder.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/HttpDecoder.java new file mode 100644 index 0000000..fb87c97 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/HttpDecoder.java @@ -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 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 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); + } + } + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/SharedWebServer.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/SharedWebServer.java new file mode 100644 index 0000000..4892ba1 --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/SharedWebServer.java @@ -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 gamePort = new ClaimPool<>(); + public static final Set 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(); + } +} diff --git a/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/Trie.java b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/Trie.java new file mode 100644 index 0000000..7646c4e --- /dev/null +++ b/libjf-web-v1/src/main/java/io/gitlab/jfronny/libjf/web/impl/variant/shared/Trie.java @@ -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 { + public final Char2ObjectMap> next; + public T content; + + public Trie() { + this.next = new Char2ObjectArrayMap<>(); + } + + public void add(Map 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 Trie of(Map source) { + Trie root = new Trie<>(); + root.add(source); + return root; + } + + public static Trie of(List source) { + return of(source.stream().collect(Collectors.toMap(Function.identity(), Function.identity()))); + } +} diff --git a/libjf-web-v1/src/main/resources/assets/libjf-web-v1/lang/en_us.json b/libjf-web-v1/src/main/resources/assets/libjf-web-v1/lang/en_us.json new file mode 100644 index 0000000..b314596 --- /dev/null +++ b/libjf-web-v1/src/main/resources/assets/libjf-web-v1/lang/en_us.json @@ -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" +} \ No newline at end of file diff --git a/libjf-web-v0/src/main/resources/fabric.mod.json b/libjf-web-v1/src/main/resources/fabric.mod.json similarity index 90% rename from libjf-web-v0/src/main/resources/fabric.mod.json rename to libjf-web-v1/src/main/resources/fabric.mod.json index d9bb8e4..7af09db 100644 --- a/libjf-web-v0/src/main/resources/fabric.mod.json +++ b/libjf-web-v1/src/main/resources/fabric.mod.json @@ -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"], diff --git a/libjf-web-v1/src/main/resources/libjf-web-v1.mixins.json b/libjf-web-v1/src/main/resources/libjf-web-v1.mixins.json new file mode 100644 index 0000000..3bf60c3 --- /dev/null +++ b/libjf-web-v1/src/main/resources/libjf-web-v1.mixins.json @@ -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 + } +} diff --git a/libjf-web-v0/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java b/libjf-web-v1/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java similarity index 60% rename from libjf-web-v0/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java rename to libjf-web-v1/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java index a92f49f..9901bdc 100644 --- a/libjf-web-v0/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java +++ b/libjf-web-v1/src/testmod/java/io/gitlab/jfronny/libjf/web/test/WebTest.java @@ -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)); diff --git a/libjf-web-v0/src/testmod/resources/fabric.mod.json b/libjf-web-v1/src/testmod/resources/fabric.mod.json similarity index 90% rename from libjf-web-v0/src/testmod/resources/fabric.mod.json rename to libjf-web-v1/src/testmod/resources/fabric.mod.json index 01f70a7..bb77d58 100644 --- a/libjf-web-v0/src/testmod/resources/fabric.mod.json +++ b/libjf-web-v1/src/testmod/resources/fabric.mod.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "id": "libjf-web-v0-testmod", + "id": "libjf-web-v1-testmod", "name": "LibJF Web", "version": "1.0", "environment": "*", diff --git a/libjf-web-v0/src/testmod/resources/test.html b/libjf-web-v1/src/testmod/resources/test.html similarity index 100% rename from libjf-web-v0/src/testmod/resources/test.html rename to libjf-web-v1/src/testmod/resources/test.html diff --git a/settings.gradle.kts b/settings.gradle.kts index db592b1..ee0e582 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")