diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4875a77..675b804 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,3 +1,9 @@ +<<<<<<< v2 +# Set update schedule for GitHub Actions + +version: 2 +updates: +======= version: 2 updates: - package-ecosystem: "github-actions" @@ -9,13 +15,19 @@ updates: master-actions: patterns: - "*" +>>>>>>> master - package-ecosystem: "github-actions" directory: "/" schedule: +<<<<<<< v2 + # Check for updates to GitHub Actions every week + interval: "daily" +======= interval: "weekly" target-branch: "docs/master" groups: docs-actions: patterns: - "*" +>>>>>>> master diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml index 3752e3d..90f6d91 100644 --- a/.github/workflows/build_artifacts.yml +++ b/.github/workflows/build_artifacts.yml @@ -12,28 +12,41 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 with: cache-read-only: false + - name: Store short commit hash + run: echo "short_commit_hash=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" + - name: Build with Gradle run: ./gradlew build env: + PRESERVE_PRERELEASE_VERSION: true DISABLE_PROPERTIES_UPDATE: true + VERSION_SUFFIX: ${{ env.short_commit_hash }} - - name: Delete common libs - run: rm -r ./common/build/libs + - name: Publish to Maven + run: ./gradlew publishMavenPublicationToOffsetMonkey538Repository + env: + PRESERVE_PRERELEASE_VERSION: true + DISABLE_PROPERTIES_UPDATE: true + VERSION_SUFFIX: ${{ env.short_commit_hash }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: Artifacts - path: ./*/build/libs/ + name: Build Artifacts + path: ./loader/*/*/build/libs/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 90e8b34..4333922 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,26 +14,33 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 with: cache-read-only: false - name: Build with Gradle run: ./gradlew build env: + IS_RELEASE: true DISABLE_PROPERTIES_UPDATE: true + VERSION_SUFFIX: "" - name: Upload to Modrinth run: ./gradlew modrinth env: + IS_RELEASE: true + DISABLE_PROPERTIES_UPDATE: true + VERSION_SUFFIX: "" MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} VERSION_NAME: ${{ github.event.release.name }} VERSION_IS_PRERELEASE: ${{ github.event.release.prerelease }} @@ -42,13 +49,13 @@ jobs: - name: Publish to Maven run: ./gradlew publishMavenPublicationToOffsetMonkey538Repository env: + IS_RELEASE: true + DISABLE_PROPERTIES_UPDATE: true + VERSION_SUFFIX: "" MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - - name: Delete common libs - run: rm -r ./common/build/libs - - name: Upload to GitHub uses: softprops/action-gh-release@v2 with: - files: ./*/build/libs/*.jar + files: ./loader/*/*/build/libs/*.jar diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cc7fe46 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "buildSrc"] + path = buildSrc + url = https://github.com/OffsetMods538/multiversion-buildscripts diff --git a/LICENSE b/LICENSE index 9e03ee7..6b9c8b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024,2025 OffsetMonkey538 +Copyright (c) 2024-2026 OffsetMonkey538 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index efd06c5..a3b7c63 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ This class has to implement the `HttpHandler` interface, this will look somethin public class MyHttpHandler implements HttpHandler { @Override - public void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception { + public void handleRequest(@NonNull ChannelHandlerContext ctx, @NonNull FullHttpRequest request) throws Exception { // Logic will go here } } @@ -64,7 +64,7 @@ public class MyHttpHandler implements HttpHandler { Now we'll need to actually implement the handler. You can google "HTTP Netty" for more info on how to handle HTTP requests with Netty. ```java -public void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception { +public void handleRequest(@NonNull ChannelHandlerContext ctx, @NonNull FullHttpRequest request) throws Exception { // Write "Hello, World!" to a buffer, encoded in UTF-8 final ByteBuf content = Unpooled.copiedBuffer("Hello, World!", StandardCharsets.UTF_8); // Create a response with said buffer diff --git a/build.gradle b/build.gradle index 3fc62e3..221c435 100644 --- a/build.gradle +++ b/build.gradle @@ -1,101 +1,73 @@ -import dex.plugins.outlet.v2.util.ReleaseType - plugins { - id 'fabric-loom' version '1.9-SNAPSHOT' apply false - id 'io.github.dexman545.outlet' version '1.6.1' apply false - id 'com.modrinth.minotaur' version '2.+' apply false - id 'maven-publish' -} - -ext { - debugVersion = System.currentTimeMillis() + id 'multiloader-root' + id 'java' + id 'java-library' + id 'net.neoforged.moddev' version "${neoforged_moddev}" apply false + id 'fabric-loom' version "${fabric_loom}" apply false + id 'io.papermc.paperweight.userdev' version "${papermc_paperweight_userdev}" apply false + id 'xyz.jpenilla.run-paper' version "${jpenilla_run_task}" apply false + id 'com.gradleup.shadow' version "${gradleup_shadow}" apply false + id 'xyz.jpenilla.resource-factory' version "${jpenilla_resource_factory}" apply false + id 'io.github.dexman545.outlet' version "${dexman_outlet}" apply false + id 'com.modrinth.minotaur' version "${modrinth_minotaur}" apply false + id 'hamburg.janove.elevator-music' version "0.1" } -allprojects { - group = "top.offsetmonkey538.meshlib" -} +//elevatorMusic { +// if (Boolean.parseBoolean(System.getenv("DISABLE_MUSIC"))) return +// waitMusic = file("${rootProject.projectDir}/veryImportant/music.wav") +// successSound = file("${rootProject.projectDir}/veryImportant/done.wav") +//} subprojects { - apply plugin: "maven-publish" - apply plugin: "java" - apply plugin: "java-library" - - archivesBaseName = "mesh-lib-${project.nameSuffix}" - version = "${rootProject.mod_version}+${rootProject.minecraft_version}" - - tasks.named("javadoc", Javadoc) { - options.addFileOption('-add-stylesheet', rootProject.file("javadoc-stylesheet.css")) - } - - java { - withSourcesJar() - withJavadocJar() - } - - jar { - from("${rootProject.projectDir}/LICENSE") { - rename { "${it}" } - } - } - - dependencies { - compileOnly "org.jetbrains:annotations:24.0.0" - compileOnly "org.slf4j:slf4j-api:2.0.16" - } - - publishing { - repositories { - maven { - name = "OffsetMonkey538" - url = "https://maven.offsetmonkey538.top/releases" - credentials { - username = providers.gradleProperty("OffsetMonkey538Username").getOrElse(System.getenv("MAVEN_USERNAME")) - password = providers.gradleProperty("OffsetMonkey538Password").getOrElse(System.getenv("MAVEN_PASSWORD")) - } - authentication { - basic(BasicAuthentication) - } - } - } - publications { - maven(MavenPublication) { - artifactId = project.archivesBaseName - - from(components["java"]) - } - } - } - tasks.publishMavenPublicationToMavenLocal.doLast { - if (System.getenv("IS_DEBUG") == "true") System.out.println("Version: " + version) - } + apply plugin: "java-library" + + repositories { + mavenCentral() + mavenLocal() + exclusiveContent { + forRepository { + maven { + name = "OffsetMods538" + url = "https://maven.offsetmonkey538.top/releases" + } + } + filter { + includeGroupAndSubgroups "top.offsetmonkey538" + } + } + } + + dependencies { + compileOnlyApi "org.jspecify:jspecify:${rootProject.jspecify_version}" + } } -configure(subprojects.findAll { it.name != "common" }) { - apply plugin: 'com.modrinth.minotaur' - apply plugin: 'io.github.dexman545.outlet' - - if (System.getenv("IS_DEBUG") == "true") version = "${version}-${rootProject.debugVersion}" - - outlet { - mcVersionRange = rootProject.supported_minecraft_versions - allowedReleaseTypes = Set.of(ReleaseType.RELEASE) - } - - modrinth { - token = System.getenv("MODRINTH_TOKEN") - projectId = "mesh-lib" - def customVersionName = System.getenv("VERSION_NAME") - if (customVersionName != null) versionName = customVersionName - versionNumber = "${project.version}" - versionType = "alpha" - def isPreRelease = System.getenv("VERSION_IS_PRERELEASE") - versionType = !"false".equals(isPreRelease) ? "beta" : "release" - additionalFiles = [sourcesJar.archiveFile] - gameVersions = outlet.mcVersions() - syncBodyFrom = rootProject.file("README.md").text - def changelogEnv = System.getenv("VERSION_CHANGELOG") - if (changelogEnv != null) changelog = changelogEnv - } - - tasks.modrinth.dependsOn(tasks.modrinthSyncBody) +configure(subprojects.findAll { it.hasProperty("project_name") }) { + apply plugin: "maven-publish" + + publishing { + repositories { + maven { + name = "OffsetMonkey538" + url = "https://maven.offsetmonkey538.top/releases" + credentials { + username = providers.gradleProperty("OffsetMonkey538Username").getOrElse(System.getenv("MAVEN_USERNAME")) + password = providers.gradleProperty("OffsetMonkey538Password").getOrElse(System.getenv("MAVEN_PASSWORD")) + } + authentication { + basic(BasicAuthentication) + } + } + } + publications { + register("maven", MavenPublication) { + afterEvaluate { + artifactId base.archivesName.get() + } + groupId "top.offsetmonkey538.meshlib" + from components.java + } + } + } } diff --git a/buildSrc b/buildSrc new file mode 160000 index 0000000..31a2373 --- /dev/null +++ b/buildSrc @@ -0,0 +1 @@ +Subproject commit 31a2373f007a4ef7aaaabdcf4d28fd283ef42175 diff --git a/common/build.gradle b/common/build.gradle index 84516a6..1034bef 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,14 +1,25 @@ plugins { - id 'com.gradleup.shadow' version '9.0.0-beta4' - id 'maven-publish' + id 'multiloader-base' } repositories { - mavenCentral() + maven { + name = "Mojang Libraries" + url = "https://libraries.minecraft.net" + content { + includeGroup "com.mojang" + } + } } dependencies { - // Netty - api "io.netty:netty-codec-http:${project.netty_version}" + compileOnlyApi("top.offsetmonkey538.monkeylib538:monkeylib538-common:${rootProject.monkeylib538_version}+common") { + exclude(group: "net.kyori") + } + compileOnlyApi "io.netty:netty-codec-http:${rootProject.netty_version}" + compileOnlyApi "io.netty:netty-transport-classes-epoll:${rootProject.netty_version}" + compileOnly "com.google.guava:guava:${rootProject.guava_version}" + compileOnly "com.mojang:brigadier:${rootProject.brigadier_version}" + + compileOnlyApi "net.kyori:adventure-api:${project.adventure_api_version}" } -tasks.build.dependsOn(shadowJar) diff --git a/common/gradle.properties b/common/gradle.properties index 9e33a89..6d3b82c 100644 --- a/common/gradle.properties +++ b/common/gradle.properties @@ -1,5 +1,5 @@ -## Netty, only used for netty-codec-http. Hopefully this version won't conflict with other versions of Netty -netty_version = 4.1.82.Final +project_name = common - -nameSuffix = api +# Dependencies +## Adventure +adventure_api_version = 4.26.1 \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java b/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java deleted file mode 100644 index 0316b26..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/MESHLib.java +++ /dev/null @@ -1,23 +0,0 @@ -package top.offsetmonkey538.meshlib; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Just used to store some constants - */ -public final class MESHLib { - /** - * Private constructor as this class shouldn't be instanced - */ - private MESHLib() {} - - /** - * String modid for this mod - */ - public static final String MOD_ID = "mesh-lib"; - /** - * Logger instance used by this mod - */ - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java deleted file mode 100644 index c6aa42e..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandler.java +++ /dev/null @@ -1,75 +0,0 @@ -package top.offsetmonkey538.meshlib.api; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.util.CharsetUtil; -import top.offsetmonkey538.meshlib.example.SimpleHttpHandler; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; -import static top.offsetmonkey538.meshlib.MESHLib.LOGGER; - -/** - * An http handler for you to implement :D - *

- * This class also provides some utility methods for making your life easier. - *
- * For example {@link #sendError(ChannelHandlerContext, HttpResponseStatus)} {@link #sendError(ChannelHandlerContext, HttpResponseStatus, String)} - *

- * Look at {@link SimpleHttpHandler SimpleHttpHandler} for an example - * - * @see HttpHandlerRegistry - */ -@FunctionalInterface -public interface HttpHandler { - - /** - * This is called when an HTTP request is received for this handler. - *
- * - * - * @param ctx the current channel handler context - * @param request the received request - * @throws Exception when anything goes wrong - */ - void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception; - - /** - * Sends the requester an error code. - * - * @param ctx the current channel handler context - * @param status the received request - * @see #sendError(ChannelHandlerContext, HttpResponseStatus, String) - */ - static void sendError(@NotNull ChannelHandlerContext ctx, @NotNull HttpResponseStatus status) { - sendError(ctx, status, null); - } - - /** - * Sends the requester an error code and optionally a reason with it. - * - * @param ctx the current channel handler context - * @param status the received request - * @param reason reason to display for the error, may be null or empty - * @see #sendError(ChannelHandlerContext, HttpResponseStatus) - */ - static void sendError(@NotNull ChannelHandlerContext ctx, @NotNull HttpResponseStatus status, @Nullable String reason) { - final String message = String.format("Failure: %s\r\n%s",status, (reason == null || reason.isBlank() ? "" : "Reason: " + reason + "\r\n")); - - LOGGER.error(message); - - final ByteBuf byteBuf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8); - final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, byteBuf); - - response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); - } -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java deleted file mode 100644 index 7c05d3c..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/api/HttpHandlerRegistry.java +++ /dev/null @@ -1,49 +0,0 @@ -package top.offsetmonkey538.meshlib.api; - -import org.jetbrains.annotations.NotNull; -import top.offsetmonkey538.meshlib.impl.HttpHandlerRegistryImpl; - -/** - * Registry for the handlers - *

- * Each handler will only be able to listen to requests on *its* sub-path. - *
- * If your handler's id is {@code testmod}, then your handler will only receive requests on {@code server.com/testmod/} - * @see HttpHandler - */ -public interface HttpHandlerRegistry { - /** - * Instance - */ - HttpHandlerRegistry INSTANCE = new HttpHandlerRegistryImpl(); - - /** - * Method for registering a {@link HttpHandler} - *
- * THE ID SHOULD NOT BE EMPTY - * - * @param id your handler or mod's id - * @param handler the {@link HttpHandler} to be registered - * @throws IllegalArgumentException when the provided id is empty or a handler with this id is already registered - * @see HttpHandler - */ - void register(@NotNull String id, @NotNull HttpHandler handler) throws IllegalArgumentException; - - /** - * Method for getting a registered {@link HttpHandler} - * - * @param id the handler's id - * @return the {@link HttpHandler} for the provided id - * @throws IllegalStateException when there is no {@link HttpHandler} registered for the provided id - */ - @NotNull - HttpHandler get(@NotNull String id) throws IllegalStateException; - - /** - * Returns true if handler with provided id is registered, false otherwise - * - * @param id the id to check - * @return true if handler with provided id is registered, false otherwise - */ - boolean has(@NotNull String id); -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java new file mode 100644 index 0000000..fc841be --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/MESHLib.java @@ -0,0 +1,86 @@ +package top.offsetmonkey538.meshlib.common; + +import top.offsetmonkey538.meshlib.common.api.MESHLibApi; +import top.offsetmonkey538.meshlib.common.api.example.ExampleMain; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticContentHandler; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticDirectoryHandler; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticFileHandler; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouter; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.rules.DomainHttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.rules.PathHttpRule; +import top.offsetmonkey538.meshlib.common.config.MESHLibConfig; +import top.offsetmonkey538.meshlib.common.config.RouterConfigHandler; +import top.offsetmonkey538.meshlib.common.netty.NettyServer; +import top.offsetmonkey538.meshlib.common.platform.PlatformUtil; +import top.offsetmonkey538.monkeylib538.common.api.command.CommandRegistrationApi; +import top.offsetmonkey538.monkeylib538.common.api.command.ConfigCommandApi; +import top.offsetmonkey538.monkeylib538.common.api.lifecycle.ServerLifecycleApi; +import top.offsetmonkey538.monkeylib538.common.api.telemetry.TelemetryRegistry; +import top.offsetmonkey538.offsetutils538.api.config.ConfigHolder; +import top.offsetmonkey538.offsetutils538.api.config.ConfigManager; +import top.offsetmonkey538.offsetutils538.api.config.event.JanksonConfigurationEvent; +import top.offsetmonkey538.offsetutils538.api.log.OffsetLogger; + +import java.util.ServiceLoader; + +public final class MESHLib { + /** + * Private constructor as this class shouldn't be instanced + */ + private MESHLib() {} + + /** + * String modid for this mod + */ + public static final String MOD_ID = "meshlib"; + /** + * Logger instance used by this mod + */ + public static final OffsetLogger LOGGER = OffsetLogger.create(MOD_ID); + + public static final ConfigHolder CONFIG = ConfigManager.init(ConfigHolder.create(MESHLibConfig::new, LOGGER)); + + + public static void initialize() { + TelemetryRegistry.register(MOD_ID); + + PlatformUtil.enableVanillaHandler(); + ExampleMain.onInitialize(); + + ConfigCommandApi.registerConfigCommand(CONFIG, () -> { + MESHLibApi.reload(); + MESHLibApi.initialize(); + }, MOD_ID, "config"); + CommandRegistrationApi.registerCommand(RouterConfigHandler.createExampleConfigCommand()); + + JanksonConfigurationEvent.JANKSON_CONFIGURATION_EVENT.listen(HttpRouter::configureJankson); + + HttpRuleTypeRegistry.HTTP_RULE_REGISTRATION_EVENT.listen(DomainHttpRule::register); + HttpRuleTypeRegistry.HTTP_RULE_REGISTRATION_EVENT.listen(PathHttpRule::register); + + HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticContentHandler::register); + HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticFileHandler::register); + HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(StaticDirectoryHandler::register); + + HttpRouterRegistry.HTTP_ROUTER_REGISTRATION_EVENT.listen(RouterConfigHandler::init); + + ServerLifecycleApi.STARTING.listen(MESHLibApi::reload); + ServerLifecycleApi.STARTED.listen(MESHLibApi::initialize); + } + + public static void disableAllHandlers() { + PlatformUtil.disableVanillaHandler(); + NettyServer.stop(); + } + + + public static T load(Class clazz) { + LOGGER.info("Loading service for: %s", clazz); + return ServiceLoader.load(clazz, MESHLib.class.getClassLoader()) + .findFirst() + .orElseThrow(() -> new RuntimeException("Failed to load service for " + clazz.getName())); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java new file mode 100644 index 0000000..6801643 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/MESHLibApi.java @@ -0,0 +1,42 @@ +package top.offsetmonkey538.meshlib.common.api; + +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.meshlib.common.impl.MESHLibApiImpl; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +/** + * Api for interacting with mesh lib + */ +public interface MESHLibApi { + @Internal + MESHLibApi INSTANCE = new MESHLibApiImpl(); + + /** + * Reloads MESH Lib. + */ + static void reload() { + INSTANCE.reloadImpl(); + } + + /** + * Initializes. + */ + static void initialize() { + INSTANCE.initializeImpl(); + } + + /** + * Provides the external port as defined in the MESH Lib config. + *

{@code null} value indicates that MESH Lib has not been set up correctly and isn't running.

+ * + * @return the port the server is accessible from externally + */ + static @Nullable Integer getExternalPort() { + return INSTANCE.getExternalPortImpl(); + } + + + @Internal void reloadImpl(); + @Internal void initializeImpl(); + @Internal @Nullable Integer getExternalPortImpl(); +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java new file mode 100644 index 0000000..7d5f3e5 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleHttpHandler.java @@ -0,0 +1,49 @@ +package top.offsetmonkey538.meshlib.common.api.example; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +/** + * An example {@link HttpHandler} implementation to learn from + */ +public record ExampleHttpHandler(String baseContent) implements HttpHandler { + + @Override + public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception { + // Calculate response using super amazing and hard math™ + final String responseText = superCoolMethodForRunningTheHardAndAmazingCalculationForCalculationinatingTheResponseTM(request.uri()); + + // Magical utility for sending a plain-text string + HttpResponseUtil.sendString(ctx, request, responseText); + } + + private String superCoolMethodForRunningTheHardAndAmazingCalculationForCalculationinatingTheResponseTM(String requestUri) { + final String requestedPath = requestUri.substring(requestUri.indexOf('/')); + + return this.baseContent + requestedPath; + } + + public static void register(final HttpHandlerTypeRegistry registry) { + registry.register("example-http", Data.class, ExampleHttpHandler.class, handler -> new Data(handler.baseContent), data -> new ExampleHttpHandler(data.content)); + } + + @Internal + private static final class Data { + private String content; + + @SuppressWarnings("unused") + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + public Data() { + + } + + public Data(final String content) { + this.content = content; + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java new file mode 100644 index 0000000..a47045b --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/ExampleMain.java @@ -0,0 +1,40 @@ +package top.offsetmonkey538.meshlib.common.api.example; + +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouter; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.rules.DomainHttpRule; + +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; + +/** + * Initializer for the example handlers + */ +public final class ExampleMain { + private ExampleMain() { + + } + + /** + * Initializer for example handlers + *
+ * Checks if the {@code meshEnableExamples} system property is enabled and registers the example handlers if so + */ + public static void onInitialize() { + // Ignore if "meshEnableExamples" isn't set + if (!Boolean.getBoolean("meshEnableExamples")) return; + + + LOGGER.warn("MESH examples enabled!"); + + // Register + HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.listen(ExampleHttpHandler::register); + + HttpRouterRegistry.HTTP_ROUTER_REGISTRATION_EVENT.listen(registry -> { + registry.register("example/example-http-handler", new HttpRouter( + new DomainHttpRule("site.example.com"), + new ExampleHttpHandler("Requested path: ") + )); + }); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java new file mode 100644 index 0000000..d266916 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/example/package-info.java @@ -0,0 +1,7 @@ +/** + * This contains examples for how to implement {@link top.offsetmonkey538.meshlib.common.api.handler.HttpHandler HttpHandler}s + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.example; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java new file mode 100644 index 0000000..27c916f --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandler.java @@ -0,0 +1,29 @@ +package top.offsetmonkey538.meshlib.common.api.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.example.ExampleHttpHandler; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; + +/** + * An http handler for you to implement :D + *
+ * Look at {@link ExampleHttpHandler ExampleHttpHandler} for an example + * + * @see HttpRouterRegistry + */ +public interface HttpHandler { + + /** + * This is called when an HTTP request is received for this handler. + *
+ * + * + * @param ctx the current channel handler context + * @param request the received request + * @param rule the rule used to match this handler + * @throws Exception when anything goes wrong + */ + void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception; +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java new file mode 100644 index 0000000..123f713 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/HttpHandlerTypeRegistry.java @@ -0,0 +1,72 @@ +package top.offsetmonkey538.meshlib.common.api.handler; + +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.impl.router.HttpHandlerTypeRegistryImpl; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; +import top.offsetmonkey538.offsetutils538.api.event.Event; + +import java.util.function.Function; + +/** + * Registry for {@link HttpHandler}s, use the {@link #HTTP_HANDLER_REGISTRATION_EVENT} event for registering your handlers. + */ +public interface HttpHandlerTypeRegistry { + + /** + * Instance + */ + @Internal + HttpHandlerTypeRegistry INSTANCE = new HttpHandlerTypeRegistryImpl(); + + /** + * Internal method for clearing the registry, no touch! + */ + @Internal + static void clear() { + INSTANCE.clearImpl(); + } + + void clearImpl(); + void register(final String type, final Class dataType, final Class handlerType, final Function handlerToData, final Function dataToHandler); + HttpHandlerTypeRegistryImpl.HttpHandlerDefinition get(final String type) throws IllegalArgumentException; + HttpHandlerTypeRegistryImpl.HttpHandlerDefinition get(final Class type) throws IllegalArgumentException; + + + /** + * Event for registering http handlers. + *

+ * The registry is cleared upon reloading, so to make your handlers persist, you need to register them in this event. + *

+ *

+ * Initially called while the server is starting, so make sure to register your handler before that! + *

+ *

+ * Called before the {@link HttpRouterRegistry#HTTP_ROUTER_REGISTRATION_EVENT HTTP_ROUTER_REGISTRATION_EVENT} event. + *

+ */ + Event HTTP_HANDLER_REGISTRATION_EVENT = Event.createEvent(HttpHandlerRegistrationEvent.class, handlers -> registry -> { + for (HttpHandlerRegistrationEvent handler : handlers) handler.register(registry); + }); + + /** + * Handler for {@link #HTTP_HANDLER_REGISTRATION_EVENT}. + */ + @FunctionalInterface + interface HttpHandlerRegistrationEvent { + + /** + * Internal method for invoking the event without providing the registry, no touch! + */ + @Internal + default void invoke() { + register(INSTANCE); + } + + /** + * Registers {@link HttpHandler}s to the provided registry + * + * @param registry the registry to register to + */ + void register(final HttpHandlerTypeRegistry registry); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java new file mode 100644 index 0000000..5bd64ab --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticContentHandler.java @@ -0,0 +1,37 @@ +package top.offsetmonkey538.meshlib.common.api.handler.handlers; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +public record StaticContentHandler(String content) implements HttpHandler { + + @Override + public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception { + HttpResponseUtil.sendString(ctx, request, content); + } + + @Internal + public static void register(final HttpHandlerTypeRegistry registry) { + registry.register("static-content", Data.class, StaticContentHandler.class, handler -> new Data(handler.content), data -> new StaticContentHandler(data.content)); + } + + @Internal + private static final class Data { + private String content = ""; + + @SuppressWarnings("unused") + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + public Data() { + + } + + public Data(final String content) { + this.content = content; + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java new file mode 100644 index 0000000..dd09757 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticDirectoryHandler.java @@ -0,0 +1,200 @@ +package top.offsetmonkey538.meshlib.common.api.handler.handlers; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.CharsetUtil; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendError; +import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendFile; +import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendPermanentRedirect; + +public record StaticDirectoryHandler(Path baseDir, boolean allowDirectoryList) implements HttpHandler { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"); + + public StaticDirectoryHandler(final Path baseDir, final boolean allowDirectoryList) { + this.baseDir = baseDir.normalize().toAbsolutePath(); + this.allowDirectoryList = allowDirectoryList; + } + + @Override + public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception { + final String rawPath = new URI(rule.normalizeUri(request.uri())).getPath(); + final Path requestedPath; + try { + requestedPath = baseDir.resolve(rawPath.startsWith("/") ? rawPath.substring(1) : rawPath).normalize(); + } catch (InvalidPathException e) { + sendError(ctx, request, HttpResponseStatus.BAD_REQUEST, e); + return; + } + + if (!requestedPath.startsWith(baseDir)) { + sendError(ctx, request, HttpResponseStatus.FORBIDDEN); + return; + } + + if (!Files.exists(requestedPath)) { + sendError(ctx, request, HttpResponseStatus.NOT_FOUND); + return; + } + + if (!Files.isDirectory(requestedPath)) { + sendFile(ctx, request, requestedPath); + return; + } + + // At this point we know it's a directory and that it exists + if (!request.uri().endsWith("/")) { + sendPermanentRedirect(ctx, request, request.uri() + "/"); + return; + } + + if (allowDirectoryList) { + sendDirectoryListing(ctx, request, rawPath, requestedPath); + return; + } + + // Try serving an index.html file + sendFile(ctx, request, requestedPath.resolve("index.html")); + } + + private static void sendDirectoryListing(ChannelHandlerContext ctx, FullHttpRequest request, String uriPath, Path directory) throws IOException { + final boolean isKeepAlive = HttpUtil.isKeepAlive(request); + + final ByteBuf byteBuf = Unpooled.copiedBuffer(renderDirectoryListing(uriPath, directory), CharsetUtil.UTF_8); + final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, byteBuf); + + response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); + response.headers().set(CONTENT_LENGTH, byteBuf.readableBytes()); + response.headers().set(CONNECTION, isKeepAlive ? KEEP_ALIVE : CLOSE); + + + ctx.write(response).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + + final ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + if (!isKeepAlive) lastContentFuture.addListener(ChannelFutureListener.CLOSE); + } + + private static StringBuilder renderDirectoryListing(String uriPath, Path directory) throws IOException { + final StringBuilder result = new StringBuilder("Index of ").append(uriPath).append(""); + + if (!"/".equals(uriPath)) { + result.append(""); + } + + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + stream.forEach(path -> { + if (!Files.exists(path) || (!Files.isDirectory(path) && !Files.isRegularFile(path))) { + return; + } + + String name = path.getFileName().toString(); + String icon = "file-icon"; + if (Files.isDirectory(path)) { + name += "/"; + icon = "dir-icon"; + } + String modifiedTime = "-"; + try { + modifiedTime = DATE_FORMATTER.format(Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("UTC"))) + " UTC"; + } catch (IOException e) { + LOGGER.error("Failed to get modification time for file '%s'!", e, path); + } + + result.append(""); + }); + } + + result.append("
Index of "); + result.append(uriPath); + result.append("
NameLast ModifiedSize
Parent Directory--
").append(name).append("").append(modifiedTime).append(""); + formatFileSize(result, path); + result.append("
Provided by MESH Lib
"); + + return result; + } + + private static void formatFileSize(StringBuilder builder, Path path) { + if (Files.isDirectory(path)) { + builder.append("-"); + return; + } + + final long sizeBytes; + try { + sizeBytes = Files.size(path); + } catch (IOException e) { + LOGGER.error("Failed to get size for file '%s'!", e, path); + builder.append("-"); + return; + } + + // https://stackoverflow.com/a/3758880 + long absB = sizeBytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(sizeBytes); + if (absB < 1024) { + builder.append(sizeBytes).append(" B"); + return; + } + + long value = absB; + final CharacterIterator charIterator = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + charIterator.next(); + } + value *= Long.signum(sizeBytes); + builder.append("%.1f %ciB".formatted(value / 1024.0, charIterator.current())); + } + + @Internal + public static void register(final HttpHandlerTypeRegistry registry) { + registry.register("static-directory", Data.class, StaticDirectoryHandler.class, handler -> new Data(handler.baseDir, handler.allowDirectoryList), data -> new StaticDirectoryHandler(Path.of(data.baseDir), data.allowDirectoryList)); + } + + @Internal + private static final class Data { + private String baseDir = ""; + private boolean allowDirectoryList; + + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + @SuppressWarnings("unused") + public Data() { + + } + + public Data(final Path baseDir, final boolean allowDirectoryList) { + this.baseDir = baseDir.toString(); + this.allowDirectoryList = allowDirectoryList; + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java new file mode 100644 index 0000000..94ae267 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/StaticFileHandler.java @@ -0,0 +1,42 @@ +package top.offsetmonkey538.meshlib.common.api.handler.handlers; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +import java.nio.file.Path; + +public record StaticFileHandler(Path fileToServe) implements HttpHandler { + public StaticFileHandler(final Path fileToServe) { + this.fileToServe = fileToServe.normalize().toAbsolutePath(); + } + + @Override + public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest request, HttpRule rule) throws Exception { + HttpResponseUtil.sendFile(ctx, request, fileToServe); + } + + @Internal + public static void register(final HttpHandlerTypeRegistry registry) { + registry.register("static-file", Data.class, StaticFileHandler.class, handler -> new Data(handler.fileToServe), data -> new StaticFileHandler(Path.of(data.fileToServe))); + } + + @Internal + private static final class Data { + private String fileToServe; + + @SuppressWarnings("unused") + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + public Data() { + + } + + public Data(final Path fileToServe) { + this.fileToServe = fileToServe.toString(); + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java new file mode 100644 index 0000000..bc73d92 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/handlers/package-info.java @@ -0,0 +1,7 @@ +/** + * Built-in {@link top.offsetmonkey538.meshlib.common.api.handler.HttpHandler HttpHandler}s + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.handler.handlers; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java new file mode 100644 index 0000000..faa0955 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/handler/package-info.java @@ -0,0 +1,7 @@ +/** + * Http handler + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.handler; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java similarity index 74% rename from common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java rename to common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java index 9e511da..f34abe6 100644 --- a/common/src/main/java/top/offsetmonkey538/meshlib/api/package-info.java +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/package-info.java @@ -3,4 +3,4 @@ *
* There should be no need to interact with anything outside of this package */ -package top.offsetmonkey538.meshlib.api; \ No newline at end of file +package top.offsetmonkey538.meshlib.common.api; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java new file mode 100644 index 0000000..8b94029 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouter.java @@ -0,0 +1,82 @@ +package top.offsetmonkey538.meshlib.common.api.router; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.JsonPrimitive; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; +import top.offsetmonkey538.meshlib.common.impl.router.HttpHandlerTypeRegistryImpl; +import top.offsetmonkey538.meshlib.common.impl.router.rule.HttpRuleTypeRegistryImpl; + +public record HttpRouter(HttpRule rule, HttpHandler handler) { + + /** + * Configures the provided {@link Jankson.Builder} with serializers and deserializers for {@link HttpRule}s and {@link HttpHandler}s. + *

+ * When using my own config library, OffsetUtils538, its {@link top.offsetmonkey538.offsetutils538.api.config.event.JanksonConfigurationEvent#JANKSON_CONFIGURATION_EVENT JANKSON_CONFIGURATION_EVENT} will have this configurator registered already and there's no need to call this method. + *
+ * For other config libraries... idk try to understand whatever the fuck I'm doing in this method I guess..... + *

+ * + * @param janksonBuilder the builder to configure + * @return the builder instance + */ + public static Jankson.Builder configureJankson(final Jankson.Builder janksonBuilder) { + janksonBuilder.registerSerializer(HttpRule.class, (httpRule, marshaller) -> { + @SuppressWarnings({"unchecked"}) + // rule definition of ?,? extends HttpRule should match ?,HttpHandler, no? + final HttpRuleTypeRegistryImpl.HttpRuleDefinition ruleDefinition = (HttpRuleTypeRegistryImpl.HttpRuleDefinition) ((HttpRuleTypeRegistryImpl) HttpRuleTypeRegistry.INSTANCE).get(httpRule.getClass()); + + final JsonObject result = (JsonObject) marshaller.serialize(ruleDefinition.ruleToData().apply(httpRule)); + result.put("type", JsonPrimitive.of(ruleDefinition.type())); + return result; + }); + + janksonBuilder.registerDeserializer(JsonObject.class, HttpRule.class, (jsonObject, marshaller) -> { + final String type = jsonObject.get(String.class, "type"); + if (type == null) throw new RuntimeException("HttpRule doesn't contain 'type' field!"); + + @SuppressWarnings({"unchecked"}) // It's proooobably a subclass of Object... + final HttpRuleTypeRegistryImpl.HttpRuleDefinition ruleDefinition = (HttpRuleTypeRegistryImpl.HttpRuleDefinition) ((HttpRuleTypeRegistryImpl) HttpRuleTypeRegistry.INSTANCE).get(type); + + final JsonObject dummyParent = new JsonObject(); + jsonObject.remove("type"); + dummyParent.put("dataHolder", jsonObject); + final Object dataHolder = dummyParent.get(ruleDefinition.dataType(), "dataHolder"); + + assert dataHolder != null; + return ruleDefinition.dataToRule().apply(dataHolder); + }); + + + janksonBuilder.registerSerializer(HttpHandler.class, (httpHandler, marshaller) -> { + @SuppressWarnings({"unchecked"}) + // handler definition of ?,? extends HttpHandler should match ?,HttpHandler, no? + final HttpHandlerTypeRegistryImpl.HttpHandlerDefinition handlerDefinition = (HttpHandlerTypeRegistryImpl.HttpHandlerDefinition) HttpHandlerTypeRegistry.INSTANCE.get(httpHandler.getClass()); + + final JsonObject result = (JsonObject) marshaller.serialize(handlerDefinition.handlerToData().apply(httpHandler)); + result.put("type", JsonPrimitive.of(handlerDefinition.type())); + return result; + }); + + janksonBuilder.registerDeserializer(JsonObject.class, HttpHandler.class, (jsonObject, marshaller) -> { + final String type = jsonObject.get(String.class, "type"); + if (type == null) throw new RuntimeException("HttpRule doesn't contain 'type' field!"); + + @SuppressWarnings({"unchecked"}) // It's proooobably a subclass of Object... + final HttpHandlerTypeRegistryImpl.HttpHandlerDefinition handlerDefinition = (HttpHandlerTypeRegistryImpl.HttpHandlerDefinition) HttpHandlerTypeRegistry.INSTANCE.get(type); + + final JsonObject dummyParent = new JsonObject(); + jsonObject.remove("type"); + dummyParent.put("dataHolder", jsonObject); + final Object dataHolder = dummyParent.get(handlerDefinition.dataType(), "dataHolder"); + + assert dataHolder != null; + return handlerDefinition.dataToHandler().apply(dataHolder); + }); + + return janksonBuilder; + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouterRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouterRegistry.java new file mode 100644 index 0000000..7de0344 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/HttpRouterRegistry.java @@ -0,0 +1,63 @@ +package top.offsetmonkey538.meshlib.common.api.router; + +import top.offsetmonkey538.meshlib.common.impl.HttpRouterRegistryImpl; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; +import top.offsetmonkey538.offsetutils538.api.event.Event; + +/** + * Registry for {@link HttpRouter}s, use the {@link #HTTP_ROUTER_REGISTRATION_EVENT} event for registering your routers. + */ +public interface HttpRouterRegistry { + /** + * Instance + */ + @Internal + HttpRouterRegistry INSTANCE = new HttpRouterRegistryImpl(); + + /** + * Internal method for clearing the registry, no touch! + */ + @Internal + static void clear() { + INSTANCE.clearImpl(); + } + + @Internal void clearImpl(); + void register(final String id, final HttpRouter router); + + + /** + * Event for registering http routers. + *

+ * The registry is cleared upon reloading, so to make your routers persist, you need to register them in this event. + *

+ *

+ * Initially called while the server is starting, so make sure to register your handler before that! + *

+ */ + Event HTTP_ROUTER_REGISTRATION_EVENT = Event.createEvent(HttpRouterRegistrationEvent.class, handlers -> registry -> { + for (HttpRouterRegistrationEvent handler : handlers) handler.register(registry); + }); + + /** + * Handler for {@link #HTTP_ROUTER_REGISTRATION_EVENT}. + */ + @FunctionalInterface + interface HttpRouterRegistrationEvent { + + /** + * Internal method for invoking the event without providing the registry, no touch! + */ + @Internal + default void invoke() { + register(INSTANCE); + } + + /** + * Registers {@link HttpRouter}s to the provided registry + * + * @param registry the registry to register to + */ + void register(final HttpRouterRegistry registry); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/package-info.java new file mode 100644 index 0000000..0389685 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/router/package-info.java @@ -0,0 +1,7 @@ +/** + * Http router + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.router; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRule.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRule.java new file mode 100644 index 0000000..0150c81 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRule.java @@ -0,0 +1,22 @@ +package top.offsetmonkey538.meshlib.common.api.rule; + +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticDirectoryHandler; +import top.offsetmonkey538.meshlib.common.api.rule.rules.PathHttpRule; + +public interface HttpRule { + boolean matches(final FullHttpRequest request); + + /** + * Should return the uri as if this rule's first match is the root + *

+ * {@link PathHttpRule PathHttpRule} removes the matched path so {@link StaticDirectoryHandler StaticDirectoryHandler} can correctly find the files based on the uri + *

+ * + * @param uri the uri to modify + */ + default String normalizeUri(final String uri) { + // no-op + return uri; + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRuleTypeRegistry.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRuleTypeRegistry.java new file mode 100644 index 0000000..c01308a --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/HttpRuleTypeRegistry.java @@ -0,0 +1,69 @@ +package top.offsetmonkey538.meshlib.common.api.rule; + +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.impl.router.rule.HttpRuleTypeRegistryImpl; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; +import top.offsetmonkey538.offsetutils538.api.event.Event; + +import java.util.function.Function; + +/** + * Registry for {@link HttpRule}s, use the {@link #HTTP_RULE_REGISTRATION_EVENT} event for registering your rules. + */ +public interface HttpRuleTypeRegistry { + /** + * Instance + */ + @Internal + HttpRuleTypeRegistry INSTANCE = new HttpRuleTypeRegistryImpl(); + + /** + * Internal method for clearing the registry, no touch! + */ + @Internal + static void clear() { + INSTANCE.clearImpl(); + } + + @Internal void clearImpl(); + void register(final String type, final Class dataType, final Class ruleType, final Function ruleToData, final Function dataToRule); + + + /** + * Event for registering http routing rules. + *

+ * The registry is cleared upon reloading, so to make your rules persist, you need to register them in this event. + *

+ *

+ * Initially called while the server is starting, so make sure to register your handler before that! + *

+ *

+ * Called before the {@link HttpRouterRegistry#HTTP_ROUTER_REGISTRATION_EVENT HTTP_ROUTER_REGISTRATION_EVENT} event. + *

+ */ + Event HTTP_RULE_REGISTRATION_EVENT = Event.createEvent(HttpRuleRegistrationEvent.class, handlers -> registry -> { + for (HttpRuleRegistrationEvent handler : handlers) handler.register(registry); + }); + + /** + * Handler for {@link #HTTP_RULE_REGISTRATION_EVENT}. + */ + @FunctionalInterface + interface HttpRuleRegistrationEvent { + + /** + * Internal method for invoking the event without providing the registry, no touch! + */ + @Internal + default void invoke() { + register(INSTANCE); + } + + /** + * Registers {@link HttpRule}s to the provided registry + * + * @param registry the registry to register to + */ + void register(final HttpRuleTypeRegistry registry); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/package-info.java new file mode 100644 index 0000000..93808ab --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/package-info.java @@ -0,0 +1,7 @@ +/** + * Http rule + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.rule; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/DomainHttpRule.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/DomainHttpRule.java new file mode 100644 index 0000000..d8caca4 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/DomainHttpRule.java @@ -0,0 +1,44 @@ +package top.offsetmonkey538.meshlib.common.api.rule.rules; + +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +/** + * @param domain i.e. map.example.com + */ +public record DomainHttpRule(String domain) implements HttpRule { + + @Override + public boolean matches(FullHttpRequest request) { + String host = request.headers().get(HttpHeaderNames.HOST); + if (host == null) return false; + + final int portIndex = host.indexOf(':'); + if (portIndex != -1) host = host.substring(0, portIndex); + + return host.equals(domain); + } + + @Internal + public static void register(final HttpRuleTypeRegistry registry) { + registry.register("domain", Data.class, DomainHttpRule.class, rule -> new Data(rule.domain), data -> new DomainHttpRule(data.domain)); + } + + @Internal + private static final class Data { + private String domain = ""; + + @SuppressWarnings("unused") + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + public Data() { + + } + + public Data(final String domain) { + this.domain = domain; + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/PathHttpRule.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/PathHttpRule.java new file mode 100644 index 0000000..4e3310c --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/PathHttpRule.java @@ -0,0 +1,46 @@ +package top.offsetmonkey538.meshlib.common.api.rule.rules; + +import io.netty.handler.codec.http.FullHttpRequest; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +/** + * @param path i.e. /map -> example.com/map + */ +public record PathHttpRule(String path) implements HttpRule { + + public PathHttpRule(final String path) { + this.path = path.startsWith("/") ? path : "/" + path; + } + + @Override + public boolean matches(FullHttpRequest request) { + return request.uri().startsWith(path); + } + + @Override + public String normalizeUri(String uri) { + return uri.replaceFirst(path, ""); + } + + @Internal + public static void register(final HttpRuleTypeRegistry registry) { + registry.register("path", Data.class, PathHttpRule.class, rule -> new Data(rule.path), data -> new PathHttpRule(data.path)); + } + + @Internal + private static final class Data { + private String path = ""; + + @SuppressWarnings("unused") + // Pretty sure this public no-args needs to exist cause jankson wants to create instances + public Data() { + + } + + public Data(final String path) { + this.path = path; + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/package-info.java new file mode 100644 index 0000000..63f7c30 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/rule/rules/package-info.java @@ -0,0 +1,7 @@ +/** + * Built-in {@link top.offsetmonkey538.meshlib.common.api.rule.HttpRule HttpRule}s + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.rule.rules; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/HttpResponseUtil.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/HttpResponseUtil.java new file mode 100644 index 0000000..50eb324 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/HttpResponseUtil.java @@ -0,0 +1,130 @@ +package top.offsetmonkey538.meshlib.common.api.util; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.meshlib.common.impl.util.HttpResponseUtilImpl; +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Provides utils for responding to http requests + */ +public interface HttpResponseUtil { + /** + * Instance + */ + @Internal + HttpResponseUtil INSTANCE = new HttpResponseUtilImpl(); + + + /** + * Sends the requester the file at the provided file + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param fileToSend the path to the file to send + * @throws IOException when io go wrong :( + */ + static void sendFile(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, Path fileToSend) throws IOException { + INSTANCE.sendFileImpl(ctx, request, fileToSend); + } + + /** + * Sends the requester a Permanent Redirect (308) response containing the new location. + *

+ * Clients may cache this and automatically redirect when the url is requested. If that is not desired, use {@link #sendTemporaryRedirect(ChannelHandlerContext, FullHttpRequest, String)} + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param newLocation the new location. + */ + static void sendPermanentRedirect(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, String newLocation) { + INSTANCE.sendRedirectImpl(ctx, request, HttpResponseStatus.PERMANENT_REDIRECT, newLocation); + } + + /** + * Sends the requester a Temporary Redirect (307) response containing the new location. + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param newLocation the new location. + */ + static void sendTemporaryRedirect(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, String newLocation) { + INSTANCE.sendRedirectImpl(ctx, request, HttpResponseStatus.TEMPORARY_REDIRECT, newLocation); + } + + /** + * Sends the requester a plain text 200 response + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param content the plain text response to send + */ + static void sendString(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, String content) { + INSTANCE.sendStringImpl(ctx, request, content); + } + + /** + * Logs and sends the requester an error code. + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param status the status to send + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus, Throwable) + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus, String) + */ + static void sendError(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status) { + sendError(ctx, request, status, (String) null); + } + + /** + * Logs and sends the requester an error code and a reason with it. + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param status the status to send + * @param reason reason to display for the error, MUST NOT be null + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus) + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus, String) + */ + static void sendError(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, Throwable reason) { + sendError(ctx, request, status, reason.getMessage()); + } + + /** + * Logs and sends the requester an error code and optionally a reason with it. + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param status the status to send + * @param reason reason to display for the error, MAY be null or empty + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus) + * @see #sendError(ChannelHandlerContext, FullHttpRequest, HttpResponseStatus, Throwable) + */ + static void sendError(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, @Nullable String reason) { + INSTANCE.sendErrorImpl(ctx, request, status, reason); + } + + /** + * Sends the requester the provided {@link FullHttpResponse}. If request is keep-alive, sets {@link io.netty.handler.codec.http.HttpHeaderNames#CONNECTION CONNECTION} "keep-alive" and returns. Otherwise sets {@link io.netty.handler.codec.http.HttpHeaderNames#CONNECTION CONNECTION} to "close" and closes the connection. + * + * @param ctx the current channel handler context + * @param request the client request. Used to determine if keep-alive is to be used. Setting to null implies a non-keep-alive connection. + * @param response the response to send + */ + static void sendResponse(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, FullHttpResponse response) { + INSTANCE.sendResponseImpl(ctx, request, response); + } + + + @Internal void sendFileImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, Path fileToSend) throws IOException; + @Internal void sendRedirectImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, String newLocation); + @Internal void sendStringImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, String content); + @Internal void sendErrorImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, @Nullable String reason); + @Internal void sendResponseImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, FullHttpResponse response); +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/package-info.java new file mode 100644 index 0000000..ba37ad7 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/api/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Utilities + */ +@NullMarked +package top.offsetmonkey538.meshlib.common.api.util; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/config/MESHLibConfig.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/MESHLibConfig.java new file mode 100644 index 0000000..6a2a2e2 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/MESHLibConfig.java @@ -0,0 +1,28 @@ +package top.offsetmonkey538.meshlib.common.config; + +import blue.endless.jankson.Comment; +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.monkeylib538.common.api.platform.LoaderUtil; +import top.offsetmonkey538.offsetutils538.api.config.Config; + +import java.nio.file.Path; + +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; + +public final class MESHLibConfig implements Config { + @Comment("Port the http server will bind to") + public @Nullable Integer httpPort = null; + @Comment("Port the http server will be accessed from externally. Used by for example Git Pack Manager when generating the download url. The HTTP server will still be hosted on the 'httpPort'. Useful when running the server behind some sort of proxy like docker, nginx, traefik, cloudflare tunnel, etc.") + public @Nullable Integer exposedPort = null; + + + @Override + public Path getConfigDirPath() { + return LoaderUtil.getConfigDir(); + } + + @Override + public String getId() { + return MOD_ID + "/main"; + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/config/RouterConfigHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/RouterConfigHandler.java new file mode 100644 index 0000000..012b039 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/RouterConfigHandler.java @@ -0,0 +1,228 @@ +package top.offsetmonkey538.meshlib.common.config; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.api.DeserializationException; +import blue.endless.jankson.api.SyntaxError; +import com.google.common.collect.ImmutableMap; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.TextDecoration; +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticContentHandler; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticDirectoryHandler; +import top.offsetmonkey538.meshlib.common.api.handler.handlers.StaticFileHandler; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouter; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.rules.DomainHttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.rules.PathHttpRule; +import top.offsetmonkey538.monkeylib538.common.api.command.CommandAbstractionApi; +import top.offsetmonkey538.monkeylib538.common.api.platform.LoaderUtil; +import top.offsetmonkey538.offsetutils538.api.annotation.Unmodifiable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; +import static top.offsetmonkey538.monkeylib538.common.api.command.CommandAbstractionApi.literal; + +public final class RouterConfigHandler { + private RouterConfigHandler() { + + } + + private static final Path ROUTERS_DIR = LoaderUtil.getConfigDir().resolve(MOD_ID).resolve("routers").normalize().toAbsolutePath(); + + public static LiteralArgumentBuilder createExampleConfigCommand() { + final LiteralArgumentBuilder exampleCommand = literal("example").requires(CommandAbstractionApi::isOp); + Consumer> allHandler = context -> {}; + + for (final Map.Entry exampleRouter : Map.of( + "server-properties.json", new HttpRouter(new PathHttpRule("example/server-properties"), new StaticFileHandler(Path.of("server.properties"))), + "ops.json", new HttpRouter(new PathHttpRule("example/ops"), new StaticFileHandler(Path.of("ops.json"))), + "directory.json", new HttpRouter(new DomainHttpRule("directory.example.com"), new StaticDirectoryHandler(Path.of("."), true)), + "index.json", new HttpRouter(new DomainHttpRule("docs.example.com"), new StaticDirectoryHandler(Path.of("/home/dave/Dev/Java/Minecraft/Mods/Loot-Table-Modifier/docs/dist/"), false)), + "hello.json", new HttpRouter(new PathHttpRule("/example/hello"), new StaticContentHandler(""" + Hello World! + ... + ... + Goodbye! :P + """)) + ).entrySet()) { + allHandler = allHandler.andThen(context -> runCommand(exampleRouter, context)); + + final LiteralArgumentBuilder routerCommand = literal(exampleRouter.getKey()); + routerCommand.executes(context -> runCommand(exampleRouter, context)); + exampleCommand.then(routerCommand); + } + + final Consumer> finalAllHandler = allHandler; + return literal(MOD_ID).then(exampleCommand.then(literal("all").executes(context -> { + finalAllHandler.accept(context); + return 1; + }))); + } + + private static int runCommand(final Map.Entry exampleRouter, final CommandContext context) { + final Path routerPath = ROUTERS_DIR.resolve("example").resolve(exampleRouter.getKey()).normalize().toAbsolutePath(); + + boolean success; + try { + success = save(routerPath, exampleRouter.getValue()); + } catch (Exception e) { + LOGGER.error("Failed to create example config at '%s'!", e); + success = false; + } + + if (!success) { + CommandAbstractionApi.sendError(context, "Failed to create example config at '%s'! See log for more details", routerPath); + return 0; + } + + CommandAbstractionApi.sendText(context, Component + .text("Created example config at '") + .append(Component + .text(routerPath.toString()) + .style(style -> style + .hoverEvent(HoverEvent.showText(Component.text("Click to copy"))) + .clickEvent(ClickEvent.copyToClipboard(routerPath.toString())) + .decorate(TextDecoration.UNDERLINED) + ) + ) + .append(Component.text("'!")) + ); + return 1; + } + + private static boolean save(final Path path, final HttpRouter router) { + final String routerId = ROUTERS_DIR.relativize(path).toString(); + final Jankson jankson = configureJankson(); + + // Convert to json + final JsonElement jsonAsElement = jankson.toJson(router); + if (!(jsonAsElement instanceof final JsonObject json)) { + LOGGER.error("Router '%s' could not be serialized to a 'JsonObject', got '%s' instead! Router will not be saved.", routerId, jsonAsElement.getClass().getName()); + return false; + } + + // Convert to string + final String result = json.toJson(false, true); + + // Save + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, result); + } catch (IOException e) { + LOGGER.error("Config file '%s' could not be saved!", e, routerId); + return false; + } + + return true; + } + + public static void init(final HttpRouterRegistry registry) { + if (!Files.exists(ROUTERS_DIR)) try { + Files.createDirectories(ROUTERS_DIR); + } catch (IOException e) { + LOGGER.error("Failed to create routers directory at '%s'!", e, ROUTERS_DIR); + return; + } + + if (!Files.isDirectory(ROUTERS_DIR)) { + LOGGER.error("'%s' is not a directory!", ROUTERS_DIR); + return; + } + + final List configFiles; + try { + configFiles = gatherConfigFiles(); + } catch (IOException e) { + LOGGER.error("Failed to gather config files from '%s'!", e, ROUTERS_DIR); + return; + } + + // Load and register + loadRouters(configFiles, configureJankson()).forEach(registry::register); + } + + private static List gatherConfigFiles() throws IOException { + try (final Stream files = Files.walk(ROUTERS_DIR)) { + return files + .filter(path -> !Files.isDirectory(path)) + .filter(path -> path.getFileName().toString().endsWith(".json")) + .toList(); + } + } + + private static Jankson configureJankson() { + return HttpRouter.configureJankson(Jankson.builder()).build(); + } + + private static @Unmodifiable Map loadRouters(final List configFiles, final Jankson jankson) throws IllegalArgumentException { + final ImmutableMap.Builder resultBuilder = ImmutableMap.builder(); + + for (final Path path : configFiles) { + final String id = ROUTERS_DIR.relativize(path).toString(); + + try { + resultBuilder.put(id, loadRouter(id, path, jankson)); + } catch (IOException e) { + LOGGER.error("Router configuration file '%s' could not be read!!", e, id); + } catch (SyntaxError e) { + LOGGER.error("Router configuration file '%s' is malformed!", e, id); + LOGGER.error(e.getMessage()); + LOGGER.error(e.getLineMessage()); + } catch (Exception e) { + LOGGER.error("Failed to turn deserialized router configuration file '%s' into an HttpRouter!", e, id); + } + } + + try { + return resultBuilder.buildOrThrow(); + } catch (IllegalArgumentException e) { + LOGGER.error("Failed to build map of id to HttpRouter, no routers will be loaded from config dir!", e); + return Map.of(); + } + } + + @SuppressWarnings("DuplicateThrows") + private static HttpRouter loadRouter(final String id, final Path path, final Jankson jankson) throws IOException, SyntaxError, Exception { + final JsonObject json = jankson.load(Files.newInputStream(path)); + + try { + return jankson.fromJsonCarefully(json, JanksonHttpRouter.class).toRouter(); + } catch (DeserializationException e) { + LOGGER.error("Failed to deserialize router configuration file '%s'!", e, id); + return jankson.fromJson(json, JanksonHttpRouter.class).toRouter(); + } + } + + // Exists because jankson requires a no-arg constructor to create an instance and then modify its fields, which wouldn't be possible with the record {@link HttpRouter} + @SuppressWarnings({"unused", "FieldMayBeFinal"}) + private static class JanksonHttpRouter { + private @Nullable HttpRule rule = null; + private @Nullable HttpHandler handler = null; + + public JanksonHttpRouter() { + + } + + public HttpRouter toRouter() throws Exception { + if (rule == null) throw new Exception("rule is null"); + if (handler == null) throw new Exception("handler is null"); + return new HttpRouter(rule, handler); + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/config/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/package-info.java new file mode 100644 index 0000000..316d9c6 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/config/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.config; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/HttpRouterRegistryImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/HttpRouterRegistryImpl.java new file mode 100644 index 0000000..0ebf8be --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/HttpRouterRegistryImpl.java @@ -0,0 +1,31 @@ +package top.offsetmonkey538.meshlib.common.impl; + +import top.offsetmonkey538.meshlib.common.api.router.HttpRouter; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of {@link HttpRouterRegistry} + */ +public final class HttpRouterRegistryImpl implements HttpRouterRegistry { + private final Map routers = new HashMap<>(); + + @Override + public void register(String id, HttpRouter router) { + if (id.isEmpty()) throw new IllegalArgumentException("Id may not be empty!"); + if (routers.containsKey(id)) throw new IllegalArgumentException("Handler with id '" + id + "' already registered!"); + + routers.put(id, router); + } + + @Override + public void clearImpl() { + routers.clear(); + } + + public Iterable> iterable() { + return this.routers.entrySet(); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MESHLibApiImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MESHLibApiImpl.java new file mode 100644 index 0000000..e682c93 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MESHLibApiImpl.java @@ -0,0 +1,81 @@ +package top.offsetmonkey538.meshlib.common.impl; + +import com.google.common.base.Stopwatch; +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.meshlib.common.api.MESHLibApi; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; +import top.offsetmonkey538.meshlib.common.netty.NettyServer; +import top.offsetmonkey538.meshlib.common.platform.PlatformUtil; +import top.offsetmonkey538.monkeylib538.common.api.platform.LoaderUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static top.offsetmonkey538.meshlib.common.MESHLib.CONFIG; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.MESHLib.disableAllHandlers; + +public final class MESHLibApiImpl implements MESHLibApi { + private static boolean enabled = true; + + public void reloadImpl() { + LOGGER.info("Reloading MESH Lib..."); + final Stopwatch fullStopwatch = Stopwatch.createStarted(); + final Stopwatch stageStopwatch = Stopwatch.createStarted(); + HttpHandlerTypeRegistry.clear(); + HttpRuleTypeRegistry.clear(); + HttpRouterRegistry.clear(); + LOGGER.info("Registries cleared in %s", stageStopwatch.stop()); + stageStopwatch.reset().start(); + + + HttpHandlerTypeRegistry.HTTP_HANDLER_REGISTRATION_EVENT.getInvoker().invoke(); + HttpRuleTypeRegistry.HTTP_RULE_REGISTRATION_EVENT.getInvoker().invoke(); + HttpRouterRegistry.HTTP_ROUTER_REGISTRATION_EVENT.getInvoker().invoke(); + LOGGER.info("Registries repopulated in %s", stageStopwatch.stop()); + stageStopwatch.reset().start(); + + // Check if config is OK + final List errors = new ArrayList<>(); + if (CONFIG.get().httpPort == null) errors.add("Field 'httpPort' not set!"); + if (!errors.isEmpty()) { + LOGGER.error("There were problems with the config for MESH Lib, mod will be disabled, see below for more details!"); + errors.stream().map(string -> " " + string).forEach(LOGGER::error); + enabled = false; + } + + LOGGER.info("MESH Lib reloaded in %s!", fullStopwatch.stop()); + } + + @Override + public void initializeImpl() { + if (!enabled) { + LOGGER.info("MESH Lib not enabled, not initializing!"); + return; + } + + LOGGER.info("Initializing MESH Lib..."); + final Stopwatch stopwatch = Stopwatch.createStarted(); + disableAllHandlers(); + + // Initialize + if (Objects.equals(LoaderUtil.getVanillaServerPort(), CONFIG.get().httpPort)) { + LOGGER.info("Initializing MESH Lib on vanilla port %s...", CONFIG.get().httpPort); + PlatformUtil.enableVanillaHandler(); + LOGGER.info("MESH Lib initialized on vanilla port %s in %s!", CONFIG.get().httpPort, stopwatch.stop()); + } else { + LOGGER.info("Initializing MESH Lib on custom port %s...", CONFIG.get().httpPort); + NettyServer.start(); + LOGGER.info("MESH Lib initialized on custom port %s in %s!", CONFIG.get().httpPort, stopwatch.stop()); + } + } + + @Override + public @Nullable Integer getExternalPortImpl() { + if (CONFIG.get().exposedPort != null) return CONFIG.get().exposedPort; + return CONFIG.get().httpPort; + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MainHttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MainHttpHandler.java new file mode 100644 index 0000000..fc10c2a --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/MainHttpHandler.java @@ -0,0 +1,81 @@ +package top.offsetmonkey538.meshlib.common.impl; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouter; +import top.offsetmonkey538.meshlib.common.api.router.HttpRouterRegistry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; +import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendError; + +/* + * Forwards the requests to HttpHandler's registered in HttpRouterRegistry + */ +public final class MainHttpHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + if (!request.decoderResult().isSuccess()) { + sendError(ctx, request, BAD_REQUEST); + return; + } + + HttpRouter router = null; + List matchedRouterIDs = new ArrayList<>(0); + for (Map.Entry possibleRouter : ((HttpRouterRegistryImpl) HttpRouterRegistry.INSTANCE).iterable()) { + if (!possibleRouter.getValue().rule().matches(request)) continue; + + matchedRouterIDs.add(possibleRouter.getKey()); + if (router == null) router = possibleRouter.getValue(); + } + if (router == null) { + LOGGER.warn("No routers matched request for '%s%s'! Ignoring...", request.headers().get(HttpHeaderNames.HOST), request.uri()); + forward(ctx, request); + return; + } + + if (matchedRouterIDs.size() > 1) { + LOGGER.error("More than one router matched request! Ignoring..."); + final StringBuilder builder = new StringBuilder("Matched routers: ").append(matchedRouterIDs.getFirst()); + for (int i = 1; i < matchedRouterIDs.size(); i++) { + builder.append(", "); + builder.append(matchedRouterIDs.get(i)); + } + LOGGER.error(builder.toString()); + forward(ctx, request); + return; + } + + router.handler().handleRequest(ctx, request, router.rule()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + exceptionCaughtImpl(ctx, cause); + } + + private static void exceptionCaughtImpl(ChannelHandlerContext ctx, Throwable cause) { + LOGGER.error("Failed to handle request", cause); + + if (!ctx.channel().isActive()) return; + sendError(ctx, null, HttpResponseStatus.INTERNAL_SERVER_ERROR, cause); + } + + private static void forward(ChannelHandlerContext ctx, FullHttpRequest request) { + // These handlers can be removed from this context now + ctx.pipeline().remove(MOD_ID + "/handler"); + + // Forward to the next handler. + // TODO: I don't think I need to retain anymore? ctx.fireChannelRead(ReferenceCountUtil.retain(request)); + ctx.fireChannelRead(request); + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/impl/ProtocolHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/ProtocolHandler.java similarity index 56% rename from common/src/main/java/top/offsetmonkey538/meshlib/impl/ProtocolHandler.java rename to common/src/main/java/top/offsetmonkey538/meshlib/common/impl/ProtocolHandler.java index 3fa6286..bdbd0ed 100644 --- a/common/src/main/java/top/offsetmonkey538/meshlib/impl/ProtocolHandler.java +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/ProtocolHandler.java @@ -1,4 +1,4 @@ -package top.offsetmonkey538.meshlib.impl; +package top.offsetmonkey538.meshlib.common.impl; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -6,24 +6,25 @@ import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; -import top.offsetmonkey538.meshlib.api.HttpHandlerRegistry; +import io.netty.handler.stream.ChunkedWriteHandler; -import static top.offsetmonkey538.meshlib.MESHLib.LOGGER; -import static top.offsetmonkey538.meshlib.MESHLib.MOD_ID; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; -/** +/* * Checks if a request is HTTP and either forwards it to {@link MainHttpHandler} if it is an HTTP request * and to the Minecraft handler otherwise. */ -public class ProtocolHandler extends ChannelInboundHandlerAdapter { +public final class ProtocolHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object request) throws Exception { if (!(request instanceof ByteBuf buf)) { - LOGGER.warn("Received request '{}' that wasn't a ByteBuf", request); + LOGGER.warn("Received request '%s' that wasn't a ByteBuf", request); return; } + // TODO: pretty sure I will not be able to use this same method for https? there does seem to be a SslHandler.isEncrypted(buf) method which I could use in addition to this? // Read the first line to check if it's an http request // todo: maybe there's a better way to check? final StringBuilder firstLine = new StringBuilder(); @@ -35,28 +36,18 @@ public void channelRead(ChannelHandlerContext ctx, Object request) throws Except final boolean isHttp = firstLine.toString().contains("HTTP"); - // If it's an http request, add the correct handlers - if (isHttp) { - final String uri = firstLine.toString().split(" ")[1]; - if (uri.equals("/")) { - LOGGER.debug("Request was made to root domain! Passing on..."); - forward(ctx, request); - return; - } - - final String handlerId = uri.split("/")[1]; - if (!HttpHandlerRegistry.INSTANCE.has(handlerId)) { - LOGGER.debug("Handler with id '{}' not registered! Passing on...", handlerId); - forward(ctx, request); - return; - } - - final ChannelPipeline pipeline = ctx.pipeline(); - pipeline.addAfter(MOD_ID, MOD_ID + "/codec", new HttpServerCodec()); - pipeline.addAfter(MOD_ID + "/codec", MOD_ID + "/aggregator", new HttpObjectAggregator(65536)); - pipeline.addAfter(MOD_ID + "/aggregator", MOD_ID + "/handler", new MainHttpHandler()); + if (!isHttp) { + forward(ctx, request); + return; } + // If it's an http request, add the correct handlers + final ChannelPipeline pipeline = ctx.pipeline(); + pipeline.addAfter(MOD_ID, MOD_ID + "/codec", new HttpServerCodec()); + pipeline.addAfter(MOD_ID + "/codec", MOD_ID + "/aggregator", new HttpObjectAggregator(65536)); + pipeline.addAfter(MOD_ID + "/aggregator", MOD_ID + "/chunked", new ChunkedWriteHandler()); + pipeline.addAfter(MOD_ID + "/chunked", MOD_ID + "/handler", new MainHttpHandler()); + forward(ctx, request); } diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/package-info.java new file mode 100644 index 0000000..9f72d1d --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.impl; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/HttpHandlerTypeRegistryImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/HttpHandlerTypeRegistryImpl.java new file mode 100644 index 0000000..1e2f0f4 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/HttpHandlerTypeRegistryImpl.java @@ -0,0 +1,48 @@ +package top.offsetmonkey538.meshlib.common.impl.router; + +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandler; +import top.offsetmonkey538.meshlib.common.api.handler.HttpHandlerTypeRegistry; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Implementation of {@link HttpRuleTypeRegistry} + */ +public final class HttpHandlerTypeRegistryImpl implements HttpHandlerTypeRegistry { + private final Map> handlersById = new HashMap<>(); + private final Map, HttpHandlerDefinition> handlersByType = new HashMap<>(); + + @Override + public void clearImpl() { + handlersById.clear(); + handlersByType.clear(); + } + + @Override + public void register(final String type, final Class dataType, final Class handlerType, final Function handlerToData, final Function dataToHandler) { + if (type.isEmpty()) throw new IllegalArgumentException("Id may not be empty!"); + if (handlersById.containsKey(type)) throw new IllegalArgumentException("Handler type with id '" + type + "' already registered!"); + if (handlersByType.containsKey(handlerType)) throw new IllegalArgumentException("Handler type for type '" + handlerType + "' already registered!"); + + final HttpHandlerDefinition handler = new HttpHandlerDefinition<>(type, dataType, handlerType, handlerToData, dataToHandler); + handlersById.put(type, handler); + handlersByType.put(handlerType, handler); + } + + public HttpHandlerDefinition get(final String type) throws IllegalArgumentException { + if (handlersById.containsKey(type)) return handlersById.get(type); + throw new IllegalArgumentException("Http handler with type '" + type + "' not registered!"); + } + + public HttpHandlerDefinition get(final Class type) throws IllegalArgumentException { + if (handlersByType.containsKey(type)) return handlersByType.get(type); + throw new IllegalArgumentException("Http handler with type '" + type + "' not registered!"); + } + + public record HttpHandlerDefinition(String type, Class dataType, Class handlerType, Function handlerToData, Function dataToHandler) { + + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/package-info.java new file mode 100644 index 0000000..c03f8e2 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.impl.router; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/HttpRuleTypeRegistryImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/HttpRuleTypeRegistryImpl.java new file mode 100644 index 0000000..2df9dac --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/HttpRuleTypeRegistryImpl.java @@ -0,0 +1,44 @@ +package top.offsetmonkey538.meshlib.common.impl.router.rule; + +import top.offsetmonkey538.meshlib.common.api.rule.HttpRule; +import top.offsetmonkey538.meshlib.common.api.rule.HttpRuleTypeRegistry; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public final class HttpRuleTypeRegistryImpl implements HttpRuleTypeRegistry { + private final Map> rulesById = new HashMap<>(); + private final Map, HttpRuleDefinition> rulesByType = new HashMap<>(); + + @Override + public void clearImpl() { + rulesById.clear(); + rulesByType.clear(); + } + + @Override + public void register(final String type, final Class dataType, final Class ruleType, final Function ruleToData, final Function dataToRule) { + if (type.isEmpty()) throw new IllegalArgumentException("Id may not be empty!"); + if (rulesById.containsKey(type)) throw new IllegalArgumentException("Handler type with id '" + type + "' already registered!"); + if (rulesByType.containsKey(ruleType)) throw new IllegalArgumentException("Handler type for type '" + ruleType + "' already registered!"); + + final HttpRuleDefinition rule = new HttpRuleDefinition<>(type, dataType, ruleType, ruleToData, dataToRule); + rulesById.put(type, rule); + rulesByType.put(ruleType, rule); + } + + public HttpRuleDefinition get(final String type) throws IllegalArgumentException { + if (rulesById.containsKey(type)) return rulesById.get(type); + throw new IllegalArgumentException("Http rule with type '" + type + "' not registered!"); + } + + public HttpRuleDefinition get(final Class type) throws IllegalArgumentException { + if (rulesByType.containsKey(type)) return rulesByType.get(type); + throw new IllegalArgumentException("Http rule with type '" + type + "' not registered!"); + } + + public record HttpRuleDefinition(String type, Class dataType, Class ruleType, Function ruleToData, Function dataToRule) { + + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/package-info.java new file mode 100644 index 0000000..c8265b4 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/router/rule/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.impl.router.rule; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/HttpResponseUtilImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/HttpResponseUtilImpl.java new file mode 100644 index 0000000..6167190 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/HttpResponseUtilImpl.java @@ -0,0 +1,126 @@ +package top.offsetmonkey538.meshlib.common.impl.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.stream.ChunkedNioFile; +import io.netty.util.CharsetUtil; +import org.jspecify.annotations.Nullable; +import top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION; +import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; +import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.api.util.HttpResponseUtil.sendError; + +public final class HttpResponseUtilImpl implements HttpResponseUtil { + @Override + public void sendFileImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, Path fileToSend) throws IOException { + if (!Files.exists(fileToSend) || !Files.isRegularFile(fileToSend)) { + sendError(ctx, request, HttpResponseStatus.NOT_FOUND); + return; + } + + final boolean isKeepAlive = request != null && HttpUtil.isKeepAlive(request); + final long fileLength = Files.size(fileToSend); + + + final HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); + response.headers().set(CONTENT_LENGTH, fileLength); + response.headers().set(CONTENT_TYPE, getContentType(fileToSend)); + response.headers().set(CONNECTION, isKeepAlive ? KEEP_ALIVE : CLOSE); + ctx.write(response); + + ctx.write( + new ChunkedNioFile(fileToSend.toFile()), + ctx.newProgressivePromise() + ).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + + final ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + if (isKeepAlive) return; + lastContentFuture.addListener(ChannelFutureListener.CLOSE); + } + + @Override + public void sendRedirectImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, String newLocation) { + final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.EMPTY_BUFFER); + + response.headers().set(LOCATION, newLocation); + + sendResponseImpl(ctx, request, response); + } + + @Override + public void sendStringImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, String content) { + final ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8); + final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, byteBuf); + + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + + sendResponseImpl(ctx, request, response); + } + + // TODO: Should the client be sent the reason? "Security through obscurity" and all that? A client can't really do much good with a "File /home/ubuntu/stupidServer/website/file.txt not found!" and they don't need this kind of info + // Then again in some cases it'd probably be good to send some more specific info to the client? hmmmmmmmmmmmmmmmmmmmmmmmm + @Override + public void sendErrorImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, HttpResponseStatus status, @Nullable String reason) { + final StringBuilder messageBuilder = new StringBuilder("Failure: ").append(status); + if (reason != null && !reason.isBlank()) messageBuilder.append("\nReason: ").append(reason); + + final String message = messageBuilder.toString(); + + + LOGGER.error(message); + + final ByteBuf byteBuf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8); + final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, byteBuf); + + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + + sendResponseImpl(ctx, request, response); + } + + @Override + public void sendResponseImpl(ChannelHandlerContext ctx, @Nullable FullHttpRequest request, FullHttpResponse response) { + final boolean isKeepAlive = request != null && HttpUtil.isKeepAlive(request); + + response.headers().set(CONNECTION, isKeepAlive ? KEEP_ALIVE : CLOSE); + ctx.write(response); + final ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + + if (!isKeepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static String getContentType(final Path file) { + final String result; + + try { + result = Files.probeContentType(file); + } catch (IOException e) { + return "text/plain"; + } + + if (result == null) return "text/plain"; + return result; + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/package-info.java new file mode 100644 index 0000000..cecba7f --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/impl/util/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.impl.util; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/NettyServer.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/NettyServer.java new file mode 100644 index 0000000..4d7b872 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/NettyServer.java @@ -0,0 +1,65 @@ +package top.offsetmonkey538.meshlib.common.netty; + +import blue.endless.jankson.annotation.Nullable; +import com.google.common.base.Suppliers; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import top.offsetmonkey538.meshlib.common.impl.ProtocolHandler; +import top.offsetmonkey538.monkeylib538.common.api.platform.LoaderUtil; + +import java.net.InetAddress; +import java.util.function.Supplier; + +import static top.offsetmonkey538.meshlib.common.MESHLib.CONFIG; +import static top.offsetmonkey538.meshlib.common.MESHLib.LOGGER; +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; + +public final class NettyServer { + private NettyServer() {} + + public static final Supplier SERVER_EVENT_GROUP = Suppliers.memoize(() -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Server IO #%d").setDaemon(true).setUncaughtExceptionHandler((thread, cause) -> LOGGER.error("Caught unhandled exception in thread %s:", cause, thread.getName())).build())); + public static final Supplier SERVER_EPOLL_EVENT_GROUP = Suppliers.memoize(() -> new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).setUncaughtExceptionHandler((thread, cause) -> LOGGER.error("Caught unhandled exception in thread %s:", cause, thread.getName())).build())); + + public static @Nullable ChannelFuture channelFuture; + + public static void start() { + final Class chennelClass; + final EventLoopGroup eventLoopGroup; + if (Epoll.isAvailable() && LoaderUtil.isEpollEnabled()) { + chennelClass = EpollServerSocketChannel.class; + eventLoopGroup = SERVER_EPOLL_EVENT_GROUP.get(); + LOGGER.info("Using epoll channel type"); + } else { + chennelClass = NioServerSocketChannel.class; + eventLoopGroup = SERVER_EVENT_GROUP.get(); + LOGGER.info("Using nio channel type"); + } + + final ServerBootstrap bootstrap = new ServerBootstrap(); + channelFuture = bootstrap.channel(chennelClass).group(eventLoopGroup).localAddress((InetAddress) null, CONFIG.get().httpPort).childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addFirst(MOD_ID, new ProtocolHandler()); + } + }).bind().syncUninterruptibly(); + } + + public static void stop() { + if (channelFuture == null) return; + try { + channelFuture.channel().close().sync(); + } catch (InterruptedException e) { + LOGGER.error("Interrupted while stopping Netty server"); + } + } +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/package-info.java new file mode 100644 index 0000000..c457ce4 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/netty/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.netty; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/package-info.java new file mode 100644 index 0000000..a5efa31 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/PlatformUtil.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/PlatformUtil.java new file mode 100644 index 0000000..ceedd80 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/PlatformUtil.java @@ -0,0 +1,21 @@ +package top.offsetmonkey538.meshlib.common.platform; + +import top.offsetmonkey538.offsetutils538.api.annotation.Internal; + +import static top.offsetmonkey538.meshlib.common.MESHLib.load; + +public interface PlatformUtil { + @Internal + PlatformUtil INSTANCE = load(PlatformUtil.class); + + static void enableVanillaHandler() { + INSTANCE.enableVanillaHandlerImpl(); + } + + static void disableVanillaHandler() { + INSTANCE.disableVanillaHandlerImpl(); + } + + void enableVanillaHandlerImpl(); + void disableVanillaHandlerImpl(); +} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/package-info.java new file mode 100644 index 0000000..45b8d25 --- /dev/null +++ b/common/src/main/java/top/offsetmonkey538/meshlib/common/platform/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.common.platform; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/example/ExampleMain.java b/common/src/main/java/top/offsetmonkey538/meshlib/example/ExampleMain.java deleted file mode 100644 index b4e4316..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/example/ExampleMain.java +++ /dev/null @@ -1,32 +0,0 @@ -package top.offsetmonkey538.meshlib.example; - -import top.offsetmonkey538.meshlib.api.HttpHandlerRegistry; - -import static top.offsetmonkey538.meshlib.MESHLib.LOGGER; - -/** - * Initializer for the example handlers - *

- * Called from either the plugin initializer {@code MeshLibPlugin} or defined as an entrypoint in the {@code fabric.mod.json} file - */ -public final class ExampleMain { - private ExampleMain() { - - } - - /** - * Initializer for example handlers - *
- * Checks if the {@code meshEnableExamples} system property is enabled and registers the example handlers if so - */ - public static void onInitialize() { - // Ignore if "meshEnableExamples" isn't set - if (System.getProperty("meshEnableExamples", "").isEmpty()) return; - - - LOGGER.warn("MESH examples enabled!"); - - // Register - HttpHandlerRegistry.INSTANCE.register("simple-server", new SimpleHttpHandler()); - } -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/example/SimpleHttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/example/SimpleHttpHandler.java deleted file mode 100644 index 1e25980..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/example/SimpleHttpHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package top.offsetmonkey538.meshlib.example; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import org.jetbrains.annotations.NotNull; -import top.offsetmonkey538.meshlib.api.HttpHandler; - -import java.nio.charset.StandardCharsets; - -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpResponseStatus.OK; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; - -/** - * An example {@link HttpHandler} implementation to learn from - */ -public class SimpleHttpHandler implements HttpHandler { - - @Override - public void handleRequest(@NotNull ChannelHandlerContext ctx, @NotNull FullHttpRequest request) throws Exception { - // Write "Hello, World!" to a buffer, encoded in UTF-8 - final ByteBuf content = Unpooled.copiedBuffer("Hello, World!", StandardCharsets.UTF_8); - // Create a response with said buffer - final FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content); - - // Set the "CONTENT_TYPE" header to tell the browser that this is plain text encoded in UTF-8 - response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); - - - // Send the response and close the connection - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); - } -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/example/package-info.java b/common/src/main/java/top/offsetmonkey538/meshlib/example/package-info.java deleted file mode 100644 index 86e3d0e..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/example/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * This contains examples for how to implement {@link top.offsetmonkey538.meshlib.api.HttpHandler HttpHandler}s - */ -package top.offsetmonkey538.meshlib.example; diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/impl/HttpHandlerRegistryImpl.java b/common/src/main/java/top/offsetmonkey538/meshlib/impl/HttpHandlerRegistryImpl.java deleted file mode 100644 index 14670cc..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/impl/HttpHandlerRegistryImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package top.offsetmonkey538.meshlib.impl; - -import org.jetbrains.annotations.NotNull; -import top.offsetmonkey538.meshlib.api.HttpHandler; -import top.offsetmonkey538.meshlib.api.HttpHandlerRegistry; - -import java.util.HashMap; -import java.util.Map; - -/** - * Implementation of {@link HttpHandlerRegistry} - */ -public class HttpHandlerRegistryImpl implements HttpHandlerRegistry { - private final Map handlers = new HashMap<>(); - - @Override - public void register(@NotNull String id, @NotNull HttpHandler handler) throws IllegalArgumentException { - if (id.isEmpty()) throw new IllegalArgumentException("Id may not be empty!"); - if (handlers.containsKey(id)) throw new IllegalArgumentException("Handler with id '" + id + "' already registered!"); - - handlers.put(id, handler); - } - - @Override - public @NotNull HttpHandler get(@NotNull String id) throws IllegalStateException { - if (handlers.containsKey(id)) return handlers.get(id); - throw new IllegalStateException("Handler with id '" + id + "' not registered!"); - } - - @Override - public boolean has(@NotNull String id) { - return handlers.containsKey(id); - } -} diff --git a/common/src/main/java/top/offsetmonkey538/meshlib/impl/MainHttpHandler.java b/common/src/main/java/top/offsetmonkey538/meshlib/impl/MainHttpHandler.java deleted file mode 100644 index d82da67..0000000 --- a/common/src/main/java/top/offsetmonkey538/meshlib/impl/MainHttpHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package top.offsetmonkey538.meshlib.impl; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpResponseStatus; -import top.offsetmonkey538.meshlib.api.HttpHandler; -import top.offsetmonkey538.meshlib.api.HttpHandlerRegistry; - -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; -import static top.offsetmonkey538.meshlib.MESHLib.LOGGER; -import static top.offsetmonkey538.meshlib.api.HttpHandler.sendError; - -/** - * Main HTTP handler for MESH. - *

- * Forwards the requests to {@link HttpHandler HttpHandler}s registered in {@link HttpHandlerRegistry} - */ -public class MainHttpHandler extends SimpleChannelInboundHandler { - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { - if (!request.decoderResult().isSuccess()) { - sendError(ctx, BAD_REQUEST); - return; - } - - final String handlerId = request.uri().split("/")[1]; - - HttpHandlerRegistry.INSTANCE.get(handlerId).handleRequest(ctx, request); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - LOGGER.error("Failed to handle request", cause); - - if (!ctx.channel().isActive()) return; - sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, cause.getMessage()); - } -} diff --git a/common/src/main/resources/assets/meshlib/icon.png b/common/src/main/resources/assets/meshlib/icon.png new file mode 100644 index 0000000..4c9d120 Binary files /dev/null and b/common/src/main/resources/assets/meshlib/icon.png differ diff --git a/fabric/build.gradle b/fabric/build.gradle deleted file mode 100644 index 50d687a..0000000 --- a/fabric/build.gradle +++ /dev/null @@ -1,93 +0,0 @@ -import dex.plugins.outlet.v2.util.ReleaseType - -plugins { - id 'fabric-loom' version '1.9-SNAPSHOT' - id 'io.github.dexman545.outlet' version '1.6.1' - id 'maven-publish' -} - -outlet { - maintainPropertiesFile = System.getenv("DISABLE_PROPERTIES_UPDATE") == null - mcVersionRange = rootProject.supported_minecraft_versions - allowedReleaseTypes = Set.of(ReleaseType.RELEASE) - propertiesData = [ - 'yarn_version': outlet.yarnVersion(project.minecraft_version), - 'loader_version': outlet.loaderVersion() - ] -} - - -loom { - runs { - client { - runDir "run/client" - } - server { - runDir "run/server" - } - } -} - -// https://gist.github.com/maityyy/3dbcd558d58a6412c3a2a38c72706e8e -afterEvaluate { - loom.runs.configureEach { - vmArg "-javaagent:${project.configurations.compileClasspath.find{ it.name.contains("sponge-mixin") }}" - if (System.getenv("DISABLE_PROPERTIES_UPDATE") == null) vmArg "-Ddevauth.enabled=true" - } -} - -repositories { - mavenCentral() - mavenLocal() - maven { - name = "DevAuth" - url = "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1" - content { - includeGroup "me.djtheredstoner" - } - } -} - -configurations { - common { - canBeResolved = true - canBeConsumed = false - } - api.extendsFrom common -} - -dependencies { - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_version}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - // DevAuth - modLocalRuntime "me.djtheredstoner:DevAuth-fabric:${devauth_version}" - - include project(path: ":common", configuration: "shadow") - common project(":common") - - // Uncomment for including a module of fabric api - // includeModImplementation fabricApi.module("fabric-api-base", project.fapi_version) -} - -processResources { - final Map properties = Map.of( - "modVersion", rootProject.mod_version, - "supportedMinecraftVersions", rootProject.supported_minecraft_versions - ) - - inputs.properties(properties) - - filesMatching("fabric.mod.json") { - expand(properties) - } - - exclude ".cache/**" -} - - -modrinth { - loaders = ["fabric"] - uploadFile = remapJar.archiveFile -} diff --git a/fabric/gradle.properties b/fabric/gradle.properties deleted file mode 100644 index 11075b5..0000000 --- a/fabric/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -# Fabric -# Check at https://fabricmc.net/develop -# These should be automatically updated, unless the environment -# variable "DISABLE_PROPERTIES_UPDATE" is set. -yarn_version = 1.21.4+build.4 -loader_version = 0.16.9 - -# Dependencies -## DevAuth, check at https://github.com/DJtheRedstoner/DevAuth -devauth_version = 1.2.1 - - -nameSuffix = fabric diff --git a/fabric/src/main/java/top/offsetmonkey538/meshlib/mixin/ServerNetworkIoMixin.java b/fabric/src/main/java/top/offsetmonkey538/meshlib/mixin/ServerNetworkIoMixin.java deleted file mode 100644 index 688bed2..0000000 --- a/fabric/src/main/java/top/offsetmonkey538/meshlib/mixin/ServerNetworkIoMixin.java +++ /dev/null @@ -1,25 +0,0 @@ -package top.offsetmonkey538.meshlib.mixin; - -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; -import top.offsetmonkey538.meshlib.impl.ProtocolHandler; - -import static top.offsetmonkey538.meshlib.MESHLib.MOD_ID; - -/** - * Mixin adding the {@link ProtocolHandler} to the minecraft netty pipeline - */ -@Mixin(targets = "net/minecraft/server/ServerNetworkIo$1") -public abstract class ServerNetworkIoMixin { - - @Inject( - method = "initChannel", - at = @At("TAIL") - ) - private void meshlib$addHttpHandler(Channel channel, CallbackInfo ci) { - channel.pipeline().addFirst(MOD_ID, new ProtocolHandler()); - } -} diff --git a/fabric/src/main/resources/assets/mesh-lib/icon.png b/fabric/src/main/resources/assets/mesh-lib/icon.png deleted file mode 100644 index 5363c79..0000000 Binary files a/fabric/src/main/resources/assets/mesh-lib/icon.png and /dev/null differ diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json deleted file mode 100644 index 4d3ac56..0000000 --- a/fabric/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "schemaVersion": 1, - "id": "mesh-lib", - "version": "${modVersion}", - "name": "MESH Lib", - "description": "Easy to use library for hosting an HTTP server on the Minecraft server's port", - "authors": [ - "OffsetMonkey538" - ], - "contact": { - "sources": "https://github.com/OffsetMods538/MESH-Lib", - "issues": "https://github.com/OffsetMods538/MESH-Lib/issues", - "homepage": "https://github.com/OffsetMods538/MESH-Lib" - }, - "license": "MIT", - "icon": "assets/mesh-lib/icon.png", - "environment": "server", - "entrypoints": { - "server": [ - "top.offsetmonkey538.meshlib.example.ExampleMain::onInitialize" - ] - }, - "mixins": [ - "mesh-lib.mixins.json" - ], - "depends": { - "minecraft": "${supportedMinecraftVersions}" - } -} diff --git a/fabric/src/main/resources/mesh-lib.mixins.json b/fabric/src/main/resources/mesh-lib.mixins.json deleted file mode 100644 index c577538..0000000 --- a/fabric/src/main/resources/mesh-lib.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "top.offsetmonkey538.meshlib.mixin", - "compatibilityLevel": "JAVA_17", - "server": [ - "ServerNetworkIoMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/gradle.properties b/gradle.properties index de17bd0..433b6e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,11 +2,34 @@ org.gradle.jvmargs = -Xmx6G org.gradle.parallel = true -minecraft_version = 1.21.4 - +# MonkeyLib538, check at https://github.com/OffsetMods538/MonkeyLib538 +monkeylib538_version = 3.0.0-beta.2.1769945990982+9b5ebb0 ## Netty, only used for netty-codec-http. Hopefully this version won't conflict with other versions of Netty -netty_version = 4.1.82.Final +netty_version = 4.1.97.Final +## Bundeled with Minecraft, common needs to use +guava_version = 33.4.0-jre +## Bundeled with Minecraft, common needs to use +brigadier_version = 1.3.10 +## JSpecify, check at https://mvnrepository.com/artifact/org.jspecify/jspecify +jspecify_version = 1.0.0 +## DevAuth, check at https://github.com/DJtheRedstoner/DevAuth +devauth_version = 1.2.2 + +## Gradle plugins +neoforged_moddev = 2.0.+ +fabric_loom = 1.15-SNAPSHOT +papermc_paperweight_userdev = 2.0.0-beta.19 +jpenilla_run_task = 3.0.2 +gradleup_shadow = 9.3.1 +jpenilla_resource_factory = 1.3.1 +dexman_outlet = 1.6.1 +modrinth_minotaur = 2.+ # Mod Properties -mod_version = 1.0.5 -supported_minecraft_versions = >=1.19 +mod_version = 2.0.0-alpha.0 +mod_name = MESH Lib +mod_id = meshlib +mod_description = Allows easily hosting an HTTP server on Minecraft. Works on 1.21.1+ fabric, neoforge and paper. +mod_website = https://modrinth.com/mod/mesh-lib +mod_issues = https://github.com/OffsetMods538/MESH-Lib/issues +mod_sources = https://github.com/OffsetMods538/MESH-Lib diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..61285a6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e1b837a..44ae953 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionSha256Sum=0d585f69da091fc5b2beced877feab55a3064d43b8a1d46aeb07996b0915e0e0 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -205,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index 9c87873..0000000 --- a/jitpack.yml +++ /dev/null @@ -1,4 +0,0 @@ -jdk: - - openjdk21 -env: - DISABLE_PROPERTIES_UPDATE: true \ No newline at end of file diff --git a/loader/fabric/1.21.1/gradle.properties b/loader/fabric/1.21.1/gradle.properties new file mode 100644 index 0000000..4506086 --- /dev/null +++ b/loader/fabric/1.21.1/gradle.properties @@ -0,0 +1,18 @@ +project_name = fabric-1.21.1 +commonModdedVersion = 1.21.1 + +# Dependencies +## MonkeyLib538 +monkeylib538_suffix = 1.21.1 +## Adventure +adventure_version = 5.14.2 + +# Minecraft version +minecraft_version = 1.21.1 +# TODO: max version? +supported_minecraft_versions = >=1.21.1 + +# These should be automatically updated, unless the environment +# variable "DISABLE_PROPERTIES_UPDATE" is set. +loader_version = 0.18.4 +fapi_version = 0.116.8+1.21.1 diff --git a/loader/fabric/build.gradle b/loader/fabric/build.gradle new file mode 100644 index 0000000..0e6d457 --- /dev/null +++ b/loader/fabric/build.gradle @@ -0,0 +1,57 @@ +import dex.plugins.outlet.v2.util.ReleaseType +import xyz.jpenilla.resourcefactory.fabric.Environment + +plugins { + id 'multiloader-fabric' +} + +allprojects { + fabricModJson { + mitLicense() + author("OffsetMonkey538") + contact { + extra = ["discord": "https://discord.offsetmonkey538.top"] + } + environment = Environment.SERVER + serverEntrypoint("top.offsetmonkey538.meshlib.fabric.MESHLibInitializer") + mixin("meshlib.modded.mixins.json") + + depends("fabric-api", "*") + depends("adventure-platform-fabric", "*") + depends("monkeylib538", ">=${rootProject.monkeylib538_version}") + } + + // Gotta have this here cause I can't import it in buildSrc + outlet.allowedReleaseTypes = Set.of(ReleaseType.RELEASE) + + dependencies { + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fapi_version}" + + modCompileOnly("top.offsetmonkey538.monkeylib538:monkeylib538-fabric:${rootProject.monkeylib538_version}+fabric") { + exclude(group: "net.fabricmc.fabric-api") + exclude(group: "net.kyori") + } + modImplementation("net.kyori:adventure-platform-fabric:${project.adventure_version}") { + exclude(group: "net.fabricmc.fabric-api") + } + } +} + +subprojects { + dependencies { + modImplementation("top.offsetmonkey538.monkeylib538:monkeylib538-fabric-${project.monkeylib538_suffix}:${rootProject.monkeylib538_version}+fabric+${project.monkeylib538_suffix}") { + exclude(group: "net.fabricmc.fabric-api") + exclude(group: "net.kyori") + } + + includeRuntime "io.netty:netty-codec-http:${rootProject.netty_version}" + } + + modrinth { + dependencies { + required.project "fabric-api" + required.project "adventure-platform-mod" + required.project "monkeylib538" + } + } +} diff --git a/loader/fabric/gradle.properties b/loader/fabric/gradle.properties new file mode 100644 index 0000000..6cc5e81 --- /dev/null +++ b/loader/fabric/gradle.properties @@ -0,0 +1,14 @@ +project_name = fabric + +# Dependencies +## Adventure +adventure_version = 6.8.0 + +# Minecraft version +minecraft_version = 1.21.11 +supported_minecraft_versions = * + +# These should be automatically updated, unless the environment +# variable "DISABLE_PROPERTIES_UPDATE" is set. +loader_version = 0.18.4 +fapi_version = 0.141.2+1.21.11 diff --git a/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/MESHLibInitializer.java b/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/MESHLibInitializer.java new file mode 100644 index 0000000..7e86d66 --- /dev/null +++ b/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/MESHLibInitializer.java @@ -0,0 +1,11 @@ +package top.offsetmonkey538.meshlib.fabric; + +import net.fabricmc.api.DedicatedServerModInitializer; +import top.offsetmonkey538.meshlib.common.MESHLib; + +public final class MESHLibInitializer implements DedicatedServerModInitializer { + @Override + public void onInitializeServer() { + MESHLib.initialize(); + } +} diff --git a/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/package-info.java b/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/package-info.java new file mode 100644 index 0000000..3482422 --- /dev/null +++ b/loader/fabric/src/main/java/top/offsetmonkey538/meshlib/fabric/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.fabric; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/loader/neoforge/1.21.1/gradle.properties b/loader/neoforge/1.21.1/gradle.properties new file mode 100644 index 0000000..7957189 --- /dev/null +++ b/loader/neoforge/1.21.1/gradle.properties @@ -0,0 +1,19 @@ +project_name = neoforge-1.21.1 +commonModdedVersion = 1.21.1 + +# Dependencies +## MonkeyLib538 +monkeylib538_suffix = 1.21.1 +## Adventure +adventure_version = 6.0.1 + +# Minecraft version +minecraft_version = 1.21.1 +# TODO: max version? +supported_minecraft_versions = >=1.21.1 +# TODO: max version? +minecraft_version_range = [1.21.1,) + +neoforge_version = 21.1.218 +parchment_minecraft_version = 1.21.1 +parchment_mappings_version = 2024.11.17 diff --git a/loader/neoforge/build.gradle b/loader/neoforge/build.gradle new file mode 100644 index 0000000..02d6d39 --- /dev/null +++ b/loader/neoforge/build.gradle @@ -0,0 +1,46 @@ +import dex.plugins.outlet.v2.util.ReleaseType + +plugins { + id 'multiloader-neoforge' +} + +allprojects { + neoForgeModsToml { + mitLicense() + mod(rootProject.neo_mod_id) { + authors = "OffsetMonkey538" + dependencies { + required("adventure_platform_neoforge") + required("monkeylib538", "[${rootProject.monkeylib538_version},)") + } + } + mixin("meshlib.modded.mixins.json") + } + + dependencies { + compileOnly("top.offsetmonkey538.monkeylib538:monkeylib538-neoforge:${rootProject.monkeylib538_version}+neoforge") { + exclude(group: "net.kyori") + } + implementation "net.kyori:adventure-platform-neoforge:${project.adventure_version}" + } +} + +subprojects { + // Gotta have this here cause I can't import it in buildSrc + outlet.allowedReleaseTypes = Set.of(ReleaseType.RELEASE) + + dependencies { + implementation("top.offsetmonkey538.monkeylib538:monkeylib538-neoforge-${project.monkeylib538_suffix}:${rootProject.monkeylib538_version}+neoforge+${project.monkeylib538_suffix}") { + exclude(group: "net.kyori") + } + + includeRuntime "io.netty:netty-codec-http:${rootProject.netty_version}" + } + + modrinth { + dependencies { + required.project "adventure-platform-mod" + required.project "monkeylib538" + } + } +} diff --git a/loader/neoforge/gradle.properties b/loader/neoforge/gradle.properties new file mode 100644 index 0000000..6a8b468 --- /dev/null +++ b/loader/neoforge/gradle.properties @@ -0,0 +1,12 @@ +project_name = neoforge + +# Dependencies +## Adventure +adventure_version = 6.8.0 + +# Minecraft version +minecraft_version = 1.21.11 + +neoforge_version = 21.11.37-beta +parchment_minecraft_version = 1.21.11 +parchment_mappings_version = 2025.12.20 diff --git a/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/MESHLibInitializer.java b/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/MESHLibInitializer.java new file mode 100644 index 0000000..2f55ffd --- /dev/null +++ b/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/MESHLibInitializer.java @@ -0,0 +1,13 @@ +package top.offsetmonkey538.meshlib.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import top.offsetmonkey538.meshlib.common.MESHLib; + +@Mod("meshlib") +public final class MESHLibInitializer { + public MESHLibInitializer(IEventBus modEventBus, ModContainer modContainer) { + MESHLib.initialize(); + } +} diff --git a/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/package-info.java b/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/package-info.java new file mode 100644 index 0000000..2cf37cb --- /dev/null +++ b/loader/neoforge/src/main/java/top/offsetmonkey538/meshlib/neoforge/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.neoforge; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/loader/paper/1.21.1/gradle.properties b/loader/paper/1.21.1/gradle.properties new file mode 100644 index 0000000..05dc6f2 --- /dev/null +++ b/loader/paper/1.21.1/gradle.properties @@ -0,0 +1,10 @@ +project_name = paper-1.21.1 + +monkeylib538_suffix = 1.21.1 + +# Minecraft version +minecraft_version = 1.21.1 +# TODO: max version? +supported_minecraft_versions = >=1.21.1 + +paper_version = 1.21.1-R0.1-SNAPSHOT diff --git a/loader/paper/build.gradle b/loader/paper/build.gradle new file mode 100644 index 0000000..096fa67 --- /dev/null +++ b/loader/paper/build.gradle @@ -0,0 +1,45 @@ +import dex.plugins.outlet.v2.util.ReleaseType +import xyz.jpenilla.resourcefactory.paper.PaperPluginYaml + +plugins { + id 'multiloader-paper' +} + +allprojects { + // Gotta have this here cause I can't import it in buildSrc + outlet.allowedReleaseTypes = Set.of(ReleaseType.RELEASE) + + paperPluginYaml { + main = "top.offsetmonkey538.meshlib.paper.platform.PlatformUtilImpl\$MESHLibInitializer" + authors.add("OffsetMonkey538") + load = "STARTUP" + dependencies { + server("MonkeyLib538", PaperPluginYaml.Load.BEFORE, true, true) + } + } + + dependencies { + compileOnly("top.offsetmonkey538.monkeylib538:monkeylib538-paper:${rootProject.monkeylib538_version}+paper") + } +} + +subprojects { + dependencies { + include runtimeOnly("io.netty:netty-codec-http:${rootProject.netty_version}") { + transitive = false + } + } + + tasks.runServer { + jvmArgs "-DmeshEnableExamples=true" + downloadPlugins { + url("https://maven.offsetmonkey538.top/releases/top/offsetmonkey538/monkeylib538/monkeylib538-paper-${project.monkeylib538_suffix}/${rootProject.monkeylib538_version}+paper+${project.monkeylib538_suffix}/monkeylib538-paper-${project.monkeylib538_suffix}-${rootProject.monkeylib538_version}+paper+${project.monkeylib538_suffix}-all.jar") + } + } + + modrinth { + dependencies { + required.project "monkeylib538" + } + } +} diff --git a/loader/paper/gradle.properties b/loader/paper/gradle.properties new file mode 100644 index 0000000..2f5d4ca --- /dev/null +++ b/loader/paper/gradle.properties @@ -0,0 +1,7 @@ +project_name = paper + +# Minecraft version +minecraft_version = 1.21.11 +supported_minecraft_versions = * + +paper_version = 1.21.11-R0.1-SNAPSHOT diff --git a/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/PlatformUtilImpl.java b/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/PlatformUtilImpl.java new file mode 100644 index 0000000..bfa256e --- /dev/null +++ b/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/PlatformUtilImpl.java @@ -0,0 +1,31 @@ +package top.offsetmonkey538.meshlib.paper.platform; + +import io.papermc.paper.network.ChannelInitializeListenerHolder; +import net.kyori.adventure.key.Key; +import org.bukkit.plugin.java.JavaPlugin; +import top.offsetmonkey538.meshlib.common.MESHLib; +import top.offsetmonkey538.meshlib.common.impl.ProtocolHandler; +import top.offsetmonkey538.meshlib.common.platform.PlatformUtil; + +public final class PlatformUtilImpl implements PlatformUtil { + private static final Key HANDLER_KEY = Key.key("meshlib", "meshlib_vanilla_handler"); + + @Override + public void enableVanillaHandlerImpl() { + if (ChannelInitializeListenerHolder.hasListener(HANDLER_KEY)) return; + ChannelInitializeListenerHolder.addListener(HANDLER_KEY, channel -> channel.pipeline().addFirst(MESHLib.MOD_ID, new ProtocolHandler())); + } + + @Override + public void disableVanillaHandlerImpl() { + ChannelInitializeListenerHolder.removeListener(HANDLER_KEY); + } + + + public static final class MESHLibInitializer extends JavaPlugin { + @Override + public void onEnable() { + MESHLib.initialize(); + } + } +} diff --git a/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/package-info.java b/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/package-info.java new file mode 100644 index 0000000..975721b --- /dev/null +++ b/loader/paper/src/main/java/top/offsetmonkey538/meshlib/paper/platform/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.paper.platform; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/loader/paper/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil b/loader/paper/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil new file mode 100644 index 0000000..2725f4f --- /dev/null +++ b/loader/paper/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil @@ -0,0 +1 @@ +top.offsetmonkey538.meshlib.paper.platform.PlatformUtilImpl \ No newline at end of file diff --git a/modded/1.21.1/gradle.properties b/modded/1.21.1/gradle.properties new file mode 100644 index 0000000..ae562f3 --- /dev/null +++ b/modded/1.21.1/gradle.properties @@ -0,0 +1,10 @@ +project_name = modded-1.21.1 + +# Dependencies +## MonkeyLib538 +monkeylib538_suffix = 1.21.1 +## Adventure +adventure_version = 6.0.1 + +# Minecraft version +minecraft_version = 1.21.1 diff --git a/modded/build.gradle b/modded/build.gradle new file mode 100644 index 0000000..1419f47 --- /dev/null +++ b/modded/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'multiloader-modded' +} + +allprojects { + dependencies { + modCompileOnly("top.offsetmonkey538.monkeylib538:monkeylib538-modded:${rootProject.monkeylib538_version}+modded") { + exclude(group: "net.kyori") + } + + modCompileOnly "net.kyori:adventure-platform-mod-shared-fabric-repack:${project.adventure_version}" + } +} + +subprojects { + dependencies { + modCompileOnly("top.offsetmonkey538.monkeylib538:monkeylib538-modded-${project.monkeylib538_suffix}:${rootProject.monkeylib538_version}+modded+${project.monkeylib538_suffix}") { + exclude(group: "net.kyori") + } + } +} diff --git a/modded/gradle.properties b/modded/gradle.properties new file mode 100644 index 0000000..e6ea150 --- /dev/null +++ b/modded/gradle.properties @@ -0,0 +1,8 @@ +project_name = modded + +# Dependencies +## Adventure +adventure_version = 6.8.0 + +# Minecraft version +minecraft_version = 1.21.11 diff --git a/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/ServerConnectionListenerMixin.java b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/ServerConnectionListenerMixin.java new file mode 100644 index 0000000..5c15527 --- /dev/null +++ b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/ServerConnectionListenerMixin.java @@ -0,0 +1,28 @@ +package top.offsetmonkey538.meshlib.modded.mixin; + +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; +import top.offsetmonkey538.meshlib.common.impl.ProtocolHandler; +import top.offsetmonkey538.meshlib.modded.platform.PlatformUtilImpl; + +import static top.offsetmonkey538.meshlib.common.MESHLib.MOD_ID; + +/** + * Mixin adding the {@link ProtocolHandler} to the minecraft netty pipeline + */ +@Mixin(targets = "net/minecraft/server/network/ServerConnectionListener$1") +public abstract class ServerConnectionListenerMixin { + + @Inject( + method = "initChannel", + at = @At("TAIL") + ) + private void meshlib$addHttpHandler(Channel channel, CallbackInfo ci) { + // This method is executed every time a new connection is started. Thus, I can just not add to the vanilla server when that's disabled + if (!PlatformUtilImpl.isVanillaHandlerEnabled) return; + channel.pipeline().addFirst(MOD_ID, new ProtocolHandler()); + } +} diff --git a/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/package-info.java b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/package-info.java new file mode 100644 index 0000000..6e3f2f0 --- /dev/null +++ b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/mixin/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.modded.mixin; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/PlatformUtilImpl.java b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/PlatformUtilImpl.java new file mode 100644 index 0000000..6d2bf06 --- /dev/null +++ b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/PlatformUtilImpl.java @@ -0,0 +1,17 @@ +package top.offsetmonkey538.meshlib.modded.platform; + +import top.offsetmonkey538.meshlib.common.platform.PlatformUtil; + +public final class PlatformUtilImpl implements PlatformUtil { + public static boolean isVanillaHandlerEnabled = false; + + @Override + public void enableVanillaHandlerImpl() { + isVanillaHandlerEnabled = true; + } + + @Override + public void disableVanillaHandlerImpl() { + isVanillaHandlerEnabled = false; + } +} diff --git a/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/package-info.java b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/package-info.java new file mode 100644 index 0000000..ccdeeb3 --- /dev/null +++ b/modded/src/main/java/top/offsetmonkey538/meshlib/modded/platform/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package top.offsetmonkey538.meshlib.modded.platform; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/modded/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil b/modded/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil new file mode 100644 index 0000000..94d750c --- /dev/null +++ b/modded/src/main/resources/META-INF/services/top.offsetmonkey538.meshlib.common.platform.PlatformUtil @@ -0,0 +1 @@ +top.offsetmonkey538.meshlib.modded.platform.PlatformUtilImpl \ No newline at end of file diff --git a/modded/src/main/resources/meshlib.modded.mixins.json b/modded/src/main/resources/meshlib.modded.mixins.json new file mode 100644 index 0000000..7e3874d --- /dev/null +++ b/modded/src/main/resources/meshlib.modded.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "top.offsetmonkey538.meshlib.modded.mixin", + "compatibilityLevel": "JAVA_21", + "server": [ + "ServerConnectionListenerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/paper/build.gradle b/paper/build.gradle deleted file mode 100644 index 30958f2..0000000 --- a/paper/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -import io.papermc.paperweight.userdev.ReobfArtifactConfiguration - -plugins { - id 'com.gradleup.shadow' version '9.0.0-beta4' - id 'io.papermc.paperweight.userdev' version '2.0.0-beta.8' - id 'xyz.jpenilla.run-paper' version '2.3.1' - id 'maven-publish' -} - -paperweight.reobfArtifactConfiguration = ReobfArtifactConfiguration.getMOJANG_PRODUCTION() - -repositories { - mavenCentral() - maven { - name = 'papermc' - url = 'https://repo.papermc.io/repository/maven-public/' - } -} - -configurations { - common { - canBeResolved = true - canBeConsumed = false - } - api.extendsFrom common -} - -dependencies { - // Paper - compileOnly "io.papermc.paper:paper-api:${project.paper_version}" - - // Userdev - paperweight.paperDevBundle(project.paper_version) - - // Common - common project(path: ":common", configuration: "shadow") -} -tasks.build.dependsOn(shadowJar) - -processResources { - final Map properties = Map.of( - "modVersion", rootProject.mod_version, - "lowestMinecraftVersion", outlet.mcVersions().first() // Hopefully outlet always does stuff in this order - ) - - inputs.properties(properties) - - filesMatching("plugin.yml") { - expand(properties) - } -} - -tasks.runServer { - minecraftVersion("1.21.4") - jvmArgs "-DmeshEnableExamples=true" -} - -modrinth { - loaders = ["paper"] - uploadFile = shadowJar -} diff --git a/paper/gradle.properties b/paper/gradle.properties deleted file mode 100644 index ea66afc..0000000 --- a/paper/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ -## Netty, only used for netty-codec-http. Hopefully this version won't conflict with other versions of Netty -netty_version = 4.1.82.Final - -## no idea where to get this -paper_version = 1.21.4-R0.1-SNAPSHOT - - -nameSuffix = paper \ No newline at end of file diff --git a/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLib.java b/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLib.java deleted file mode 100644 index 7821767..0000000 --- a/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLib.java +++ /dev/null @@ -1,15 +0,0 @@ -package top.offsetmonkey538.meshlib; - -import io.papermc.paper.network.ChannelInitializeListenerHolder; -import net.kyori.adventure.key.Key; -import top.offsetmonkey538.meshlib.impl.ProtocolHandler; - -public final class MeshLib { - private MeshLib() { - - } - - public static void initialize() { - ChannelInitializeListenerHolder.addListener(Key.key("meshlib", "meshlib"), channel -> channel.pipeline().addFirst(MESHLib.MOD_ID, new ProtocolHandler())); - } -} diff --git a/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLibPlugin.java b/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLibPlugin.java deleted file mode 100644 index 19000fd..0000000 --- a/paper/src/main/java/top/offsetmonkey538/meshlib/MeshLibPlugin.java +++ /dev/null @@ -1,14 +0,0 @@ -package top.offsetmonkey538.meshlib; - -import org.bukkit.plugin.java.JavaPlugin; -import top.offsetmonkey538.meshlib.example.ExampleMain; - -public class MeshLibPlugin extends JavaPlugin { - - @Override - public void onEnable() { - MeshLib.initialize(); - - ExampleMain.onInitialize(); - } -} diff --git a/paper/src/main/resources/plugin.yml b/paper/src/main/resources/plugin.yml deleted file mode 100644 index 227404d..0000000 --- a/paper/src/main/resources/plugin.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: MESH-Lib -version: '${modVersion}' -main: top.offsetmonkey538.meshlib.MeshLibPlugin -description: Easy to use library for hosting an HTTP server on the Minecraft server's port -author: OffsetMonkey538 -website: https://github.com/OffsetMods538/MESH-Lib -api-version: '${lowestMinecraftVersion}' -load: STARTUP diff --git a/settings.gradle b/settings.gradle index f44ca15..2e77994 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,5 +14,14 @@ rootProject.name = "mesh-lib" include "common" -include "fabric" -include "paper" +include "modded" +include "modded:1.21.1" + +include "loader:fabric" +include "loader:fabric:1.21.1" + +include "loader:neoforge" +include "loader:neoforge:1.21.1" + +include "loader:paper" +include "loader:paper:1.21.1"