diff --git a/README.md b/README.md index 3f542c1..2281995 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ PlayerCoordsAPI provides real-time access to your Minecraft player coordinates t - Lightweight HTTP server running only on localhost providing your coordinates - Client-side only - no server-side components needed - Works in singleplayer and multiplayer -- Mod menu integration allowing you to enable/disable the API +- Mod menu integration allowing you to enable/disable the API and configure CORS ## 🚀 Installation @@ -42,8 +42,8 @@ PlayerCoordsAPI provides real-time access to your Minecraft player coordinates t "z": -789.12, "yaw": 180.00, "pitch": 12.50, - "world": "overworld", - "biome": "plains", + "world": "minecraft:overworld", + "biome": "minecraft:plains", "uuid": "550e8400-e29b-41d4-a716-446655440000", "username": "PlayerName" } @@ -58,8 +58,8 @@ PlayerCoordsAPI provides real-time access to your Minecraft player coordinates t | `z` | `number` | North-South | | `yaw` | `number` | Horizontal rotation (degrees) | | `pitch` | `number` | Vertical rotation (degrees) | -| `world` | `string` | Minecraft world | -| `biome` | `string` | Minecraft biome | +| `world` | `string` | Minecraft world registry ID | +| `biome` | `string` | Minecraft biome registry ID | | `uuid` | `string` | Player UUID | | `username` | `string` | Player username | @@ -67,16 +67,17 @@ PlayerCoordsAPI provides real-time access to your Minecraft player coordinates t | Status | Message | |--------|---------------------| -| `403` | Access denied | +| `403` | Access denied / Origin not allowed | | `404` | Player not in world | +| `405` | Method not allowed | ## 🔒 Security For security reasons, the API server: -- Only accepts connections from localhost `127.0.0.1` +- Only accepts connections from loopback addresses such as `127.0.0.1` and `::1` - Runs on port `25565` by default - Provides read-only access to player position data -- Allows requests from any origin (CORS `Access-Control-Allow-Origin: *`) for easy integration with web applications +- Uses a configurable CORS policy. By default it allows all origins for backward compatibility, but you can restrict it in the config screen ## 🛠️ Examples diff --git a/src/client/java/fr/sukikui/playercoordsapi/PlayerCoordsAPIClient.java b/src/client/java/fr/sukikui/playercoordsapi/PlayerCoordsAPIClient.java index 548a08c..56e11c2 100644 --- a/src/client/java/fr/sukikui/playercoordsapi/PlayerCoordsAPIClient.java +++ b/src/client/java/fr/sukikui/playercoordsapi/PlayerCoordsAPIClient.java @@ -2,6 +2,8 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; +import fr.sukikui.playercoordsapi.config.CorsUtils; +import fr.sukikui.playercoordsapi.config.ModConfig; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; @@ -25,6 +27,13 @@ public class PlayerCoordsAPIClient implements ClientModInitializer { private static final int PORT = 25565; private static final long START_RETRY_DELAY_MS = 5_000L; + private static final String ALLOWED_METHODS = "GET, OPTIONS"; + private static final String DEFAULT_ALLOWED_HEADERS = "Content-Type, Authorization"; + private static final String ACCESS_DENIED_RESPONSE = "{\"error\": \"Access denied\"}"; + private static final String ORIGIN_NOT_ALLOWED_RESPONSE = "{\"error\": \"Origin not allowed\"}"; + private static final String NON_BROWSER_CLIENTS_NOT_ALLOWED_RESPONSE = "{\"error\": \"Non-browser local clients not allowed\"}"; + private static final String METHOD_NOT_ALLOWED_RESPONSE = "{\"error\": \"Method not allowed\"}"; + private static final String PLAYER_NOT_IN_WORLD_RESPONSE = "{\"error\": \"Player not in world\"}"; private HttpServer server; private ExecutorService serverExecutor; @@ -164,39 +173,82 @@ private void cleanupServerResources() { } private void handleCoordsRequest(HttpExchange exchange) throws IOException { - String method = exchange.getRequestMethod(); + InetAddress remoteAddress = exchange.getRemoteAddress().getAddress(); + if (remoteAddress == null || !remoteAddress.isLoopbackAddress()) { + sendResponse(exchange, 403, ACCESS_DENIED_RESPONSE, CorsDecision.noCors()); + return; + } - if (method.equalsIgnoreCase("OPTIONS")) { - sendResponse(exchange, 204, null); + CorsDecision corsDecision = evaluateCorsDecision(exchange); + if (!corsDecision.allowed()) { + sendResponse(exchange, 403, corsDecision.errorResponse(), CorsDecision.noCors()); return; } - if (!method.equalsIgnoreCase("GET")) { - exchange.getResponseHeaders().set("Allow", "GET, OPTIONS"); - sendResponse(exchange, 405, "{\"error\": \"Method not allowed\"}"); + String method = exchange.getRequestMethod(); + + if (method.equalsIgnoreCase("OPTIONS")) { + sendResponse(exchange, 204, null, corsDecision); return; } - // Check if the client is allowed to access (only localhost) - InetAddress remoteAddress = exchange.getRemoteAddress().getAddress(); - if (remoteAddress == null || !remoteAddress.isLoopbackAddress()) { - sendResponse(exchange, 403, "{\"error\": \"Access denied\"}"); + if (!method.equalsIgnoreCase("GET")) { + exchange.getResponseHeaders().set("Allow", ALLOWED_METHODS); + sendResponse(exchange, 405, METHOD_NOT_ALLOWED_RESPONSE, corsDecision); return; } PlayerSnapshot snapshot = latestSnapshot; if (snapshot != null) { - sendResponse(exchange, 200, snapshot.toJson()); + sendResponse(exchange, 200, snapshot.toJson(), corsDecision); } else { - sendResponse(exchange, 404, "{\"error\": \"Player not in world\"}"); + sendResponse(exchange, 404, PLAYER_NOT_IN_WORLD_RESPONSE, corsDecision); } } - private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { - // Add CORS headers - exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); - exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, OPTIONS"); - exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization"); + private CorsDecision evaluateCorsDecision(HttpExchange exchange) { + ModConfig config = PlayerCoordsAPI.getConfig(); + String requestOrigin = exchange.getRequestHeaders().getFirst("Origin"); + + if (requestOrigin == null || requestOrigin.isBlank()) { + return config.allowNonBrowserLocalClients + ? CorsDecision.noCors() + : CorsDecision.denied(NON_BROWSER_CLIENTS_NOT_ALLOWED_RESPONSE); + } + + if (config.corsPolicy == ModConfig.CorsPolicy.ALLOW_ALL) { + return CorsDecision.allowed("*", resolveAllowedHeaders(exchange), false); + } + + if (!CorsUtils.isOriginAllowed(config, requestOrigin)) { + return CorsDecision.denied(ORIGIN_NOT_ALLOWED_RESPONSE); + } + + return CorsUtils.normalizeOrigin(requestOrigin) + .map(origin -> CorsDecision.allowed(origin, resolveAllowedHeaders(exchange), true)) + .orElseGet(() -> CorsDecision.denied(ORIGIN_NOT_ALLOWED_RESPONSE)); + } + + private String resolveAllowedHeaders(HttpExchange exchange) { + String requestedHeaders = exchange.getRequestHeaders().getFirst("Access-Control-Request-Headers"); + + if (requestedHeaders == null || requestedHeaders.isBlank()) { + return DEFAULT_ALLOWED_HEADERS; + } + + return requestedHeaders; + } + + private void sendResponse(HttpExchange exchange, int statusCode, String response, CorsDecision corsDecision) throws IOException { + if (corsDecision.allowOrigin() != null) { + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", corsDecision.allowOrigin()); + exchange.getResponseHeaders().set("Access-Control-Allow-Methods", ALLOWED_METHODS); + exchange.getResponseHeaders().set("Access-Control-Allow-Headers", corsDecision.allowHeaders()); + + if (corsDecision.varyByOrigin()) { + exchange.getResponseHeaders().set("Vary", "Origin, Access-Control-Request-Headers"); + } + } if (response != null) { byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); @@ -237,6 +289,20 @@ private static String escapeJson(String value) { return escaped.toString(); } + private record CorsDecision(boolean allowed, String allowOrigin, String allowHeaders, boolean varyByOrigin, String errorResponse) { + private static CorsDecision allowed(String allowOrigin, String allowHeaders, boolean varyByOrigin) { + return new CorsDecision(true, allowOrigin, allowHeaders, varyByOrigin, null); + } + + private static CorsDecision denied(String errorResponse) { + return new CorsDecision(false, null, null, false, errorResponse); + } + + private static CorsDecision noCors() { + return new CorsDecision(true, null, null, false, null); + } + } + private record PlayerSnapshot( double x, double y, diff --git a/src/client/java/fr/sukikui/playercoordsapi/config/ModMenuIntegration.java b/src/client/java/fr/sukikui/playercoordsapi/config/ModMenuIntegration.java index 1e13195..5f85109 100644 --- a/src/client/java/fr/sukikui/playercoordsapi/config/ModMenuIntegration.java +++ b/src/client/java/fr/sukikui/playercoordsapi/config/ModMenuIntegration.java @@ -2,7 +2,6 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; -import me.shedaniel.autoconfig.AutoConfigClient; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; @@ -10,6 +9,6 @@ public class ModMenuIntegration implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return parent -> AutoConfigClient.getConfigScreen(ModConfig.class, parent).get(); + return PlayerCoordsConfigScreen::new; } } diff --git a/src/client/java/fr/sukikui/playercoordsapi/config/PlayerCoordsConfigScreen.java b/src/client/java/fr/sukikui/playercoordsapi/config/PlayerCoordsConfigScreen.java new file mode 100644 index 0000000..98ac3e5 --- /dev/null +++ b/src/client/java/fr/sukikui/playercoordsapi/config/PlayerCoordsConfigScreen.java @@ -0,0 +1,724 @@ +package fr.sukikui.playercoordsapi.config; + +import me.shedaniel.autoconfig.AutoConfig; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.TooltipPositioner; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.CyclingButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.gui.widget.TextWidget; +import net.minecraft.client.input.CharInput; +import net.minecraft.client.input.KeyInput; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.MathHelper; +import org.joml.Vector2i; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +@Environment(EnvType.CLIENT) +final class PlayerCoordsConfigScreen extends Screen { + private static final TooltipPositioner TOP_TOOLTIP_POSITIONER = (screenWidth, screenHeight, x, y, width, height) -> + new Vector2i( + MathHelper.clamp(x + 12, 6, Math.max(6, screenWidth - width - 6)), + MathHelper.clamp(y - height - 12, 6, Math.max(6, screenHeight - height - 6)) + ); + private static final int CONTENT_WIDTH = 340; + private static final int ROW_HEIGHT = 20; + private static final int ROW_SPACING = 28; + private static final int BUTTON_HEIGHT = 20; + private static final int CONTROL_WIDTH = 170; + private static final int ORIGIN_ROW_HEIGHT = 28; + private static final int ORIGIN_ROW_SPACING = 6; + private static final int ORIGIN_SCHEME_WIDTH = 64; + private static final int ORIGIN_PORT_WIDTH = 52; + private static final int ORIGIN_REMOVE_WIDTH = 20; + + private final Screen parent; + private final ModConfig workingConfig = new ModConfig(); + private final List originDrafts; + private final List originRows = new ArrayList<>(); + + private TextWidget enabledLabel; + private TextWidget corsPolicyLabel; + private TextWidget nonBrowserClientsLabel; + private CyclingButtonWidget enabledButton; + private CyclingButtonWidget corsPolicyButton; + private CyclingButtonWidget nonBrowserClientsButton; + private ButtonWidget addOriginButton; + private ButtonWidget doneButton; + + private Optional whitelistError = Optional.empty(); + private int scrollOffset; + private int listTop; + private int listBottom; + private int listLeft; + private int listWidth; + private int corsPolicyRowY; + private int nonBrowserClientsRowY; + + PlayerCoordsConfigScreen(Screen parent) { + super(Text.translatable("text.autoconfig.playercoordsapi.title")); + this.parent = parent; + + ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).getConfig(); + workingConfig.enabled = config.enabled; + workingConfig.corsPolicy = config.corsPolicy == null ? ModConfig.CorsPolicy.ALLOW_ALL : config.corsPolicy; + workingConfig.allowNonBrowserLocalClients = config.allowNonBrowserLocalClients; + workingConfig.allowedOrigins = new ArrayList<>(config.allowedOrigins == null ? List.of() : config.allowedOrigins); + workingConfig.originEntries = new ArrayList<>(config.originEntries == null ? List.of() : config.originEntries); + + this.originDrafts = createDrafts(workingConfig.originEntries, workingConfig.allowedOrigins); + syncOriginDraftsWithCorsPolicy(); + } + + @Override + protected void init() { + int left = this.width / 2 - CONTENT_WIDTH / 2; + int controlX = left + CONTENT_WIDTH - CONTROL_WIDTH; + int y = 32; + + enabledLabel = this.addDrawableChild(new TextWidget(left, y + 6, CONTROL_WIDTH - 12, ROW_HEIGHT, Text.translatable("config.playercoordsapi.option.enabled"), this.textRenderer)); + enabledLabel.active = false; + enabledButton = this.addDrawableChild(CyclingButtonWidget.onOffBuilder( + ScreenTexts.ON.copy().formatted(Formatting.GREEN), + ScreenTexts.OFF.copy().formatted(Formatting.RED), + workingConfig.enabled + ) + .omitKeyText() + .build(controlX, y, CONTROL_WIDTH, ROW_HEIGHT, Text.translatable("config.playercoordsapi.option.enabled"), (button, value) -> workingConfig.enabled = value)); + + y += ROW_SPACING; + + corsPolicyRowY = y; + corsPolicyLabel = this.addDrawableChild(new TextWidget(left, y + 6, CONTROL_WIDTH - 12, ROW_HEIGHT, Text.translatable("config.playercoordsapi.option.cors_policy"), this.textRenderer)); + corsPolicyLabel.active = false; + corsPolicyButton = this.addDrawableChild(CyclingButtonWidget.builder(PlayerCoordsConfigScreen::getCorsPolicyLabel, workingConfig.corsPolicy) + .values(ModConfig.CorsPolicy.values()) + .omitKeyText() + .build(controlX, y, CONTROL_WIDTH, ROW_HEIGHT, Text.translatable("config.playercoordsapi.option.cors_policy"), (button, value) -> { + workingConfig.corsPolicy = value; + syncOriginDraftsWithCorsPolicy(); + scrollOffset = 0; + clearAndInit(); + })); + + y += ROW_SPACING; + nonBrowserClientsRowY = y; + + nonBrowserClientsLabel = this.addDrawableChild(new TextWidget( + left, + y + 6, + CONTROL_WIDTH - 12, + ROW_HEIGHT, + Text.translatable("config.playercoordsapi.option.allow_non_browser_local_clients"), + this.textRenderer + )); + nonBrowserClientsLabel.active = false; + nonBrowserClientsButton = this.addDrawableChild(CyclingButtonWidget.onOffBuilder( + ScreenTexts.ON.copy().formatted(Formatting.GREEN), + ScreenTexts.OFF.copy().formatted(Formatting.RED), + workingConfig.allowNonBrowserLocalClients + ) + .omitKeyText() + .build( + controlX, + y, + CONTROL_WIDTH, + ROW_HEIGHT, + Text.translatable("config.playercoordsapi.option.allow_non_browser_local_clients"), + (button, value) -> workingConfig.allowNonBrowserLocalClients = value + )); + + y += ROW_SPACING; + + int bottomButtonsY = this.height - 28; + int addButtonY = bottomButtonsY - BUTTON_HEIGHT - 8; + listLeft = left; + listWidth = CONTENT_WIDTH; + listTop = y + 40; + listBottom = addButtonY - 8; + + buildOriginRows(); + + addOriginButton = this.addDrawableChild(ButtonWidget.builder( + Text.translatable("config.playercoordsapi.option.add_origin"), + button -> { + if (hasEmptyOriginDraft()) { + return; + } + + originDrafts.add(new OriginDraft()); + scrollOffset = Integer.MAX_VALUE; + clearAndInit(); + }) + .dimensions(left, addButtonY, CONTENT_WIDTH, BUTTON_HEIGHT) + .build()); + + this.addDrawableChild(ButtonWidget.builder(ScreenTexts.CANCEL, button -> close()) + .dimensions(left, bottomButtonsY, 150, BUTTON_HEIGHT) + .build()); + + doneButton = this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, button -> saveAndClose()) + .dimensions(left + CONTENT_WIDTH - 150, bottomButtonsY, 150, BUTTON_HEIGHT) + .build()); + + updateValidation(); + clampScroll(); + } + + @Override + public void close() { + if (this.client != null) { + this.client.setScreen(parent); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (mouseX >= listLeft + && mouseX <= listLeft + listWidth + && mouseY >= listTop + && mouseY <= listBottom + && getMaxScroll() > 0) { + scrollOffset = MathHelper.clamp(scrollOffset - (int) (verticalAmount * 18.0), 0, getMaxScroll()); + layoutOriginRows(); + return true; + } + + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + @Override + public boolean mouseClicked(Click click, boolean doubleClick) { + if (isWhitelistEditable() && click.button() == 0) { + for (OriginRow row : originRows) { + if (row.tryFocusField(click, doubleClick)) { + return true; + } + } + } + + return super.mouseClicked(click, doubleClick); + } + + @Override + public boolean keyPressed(KeyInput keyInput) { + TextFieldWidget focusedTextField = getFocusedTextField(); + + if (focusedTextField != null && focusedTextField.keyPressed(keyInput)) { + return true; + } + + return super.keyPressed(keyInput); + } + + @Override + public boolean charTyped(CharInput charInput) { + TextFieldWidget focusedTextField = getFocusedTextField(); + + if (focusedTextField != null && focusedTextField.charTyped(charInput)) { + return true; + } + + return super.charTyped(charInput); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + layoutOriginRows(); + renderOriginBlocks(context); + super.render(context, mouseX, mouseY, delta); + + int left = this.width / 2 - CONTENT_WIDTH / 2; + int controlX = left + CONTENT_WIDTH - CONTROL_WIDTH; + + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 12, 0xFFFFFF); + int headerY = listTop - 20; + int schemeX = left; + int hostX = schemeX + ORIGIN_SCHEME_WIDTH + 8; + int portX = left + CONTENT_WIDTH - ORIGIN_REMOVE_WIDTH - 8 - ORIGIN_PORT_WIDTH; + boolean whitelistEditable = isWhitelistEditable(); + int headerColor = whitelistEditable ? 0xA0A0A0 : 0x707070; + + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.playercoordsapi.option.allowed_origins"), left, headerY - 12, headerColor); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.playercoordsapi.option.origin_scheme"), schemeX, headerY, headerColor); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.playercoordsapi.option.origin_host"), hostX, headerY, headerColor); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.playercoordsapi.option.origin_port"), portX, headerY, headerColor); + + if (getMaxScroll() > 0) { + int indicatorX = controlX + CONTROL_WIDTH - 8; + context.drawTextWithShadow(this.textRenderer, Text.literal(scrollOffset > 0 ? "^" : ""), indicatorX, headerY - 12, 0x808080); + context.drawTextWithShadow(this.textRenderer, Text.literal(scrollOffset < getMaxScroll() ? "v" : ""), indicatorX, listBottom - 8, 0x808080); + } + + whitelistError.ifPresent(error -> context.drawCenteredTextWithShadow( + this.textRenderer, + error, + this.width / 2, + this.height - 44, + 0xFF5555 + )); + + renderHoveredTooltip(context, mouseX, mouseY); + } + + private void saveAndClose() { + workingConfig.originEntries = collectOriginEntries(); + workingConfig.allowedOrigins = CorsUtils.normalizeConfiguredOriginsFromEntries(workingConfig.originEntries); + + ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).getConfig(); + config.enabled = workingConfig.enabled; + config.corsPolicy = workingConfig.corsPolicy; + config.allowNonBrowserLocalClients = workingConfig.allowNonBrowserLocalClients; + config.originEntries = new ArrayList<>(workingConfig.originEntries); + config.allowedOrigins = new ArrayList<>(workingConfig.allowedOrigins); + AutoConfig.getConfigHolder(ModConfig.class).save(); + + close(); + } + + private void updateValidation() { + whitelistError = validateWhitelist(); + + if (addOriginButton != null) { + addOriginButton.active = isWhitelistEditable() && !hasEmptyOriginDraft(); + } + + if (doneButton != null) { + doneButton.active = whitelistError.isEmpty(); + } + } + + private Optional validateWhitelist() { + if (workingConfig.corsPolicy != ModConfig.CorsPolicy.CUSTOM_WHITELIST) { + return Optional.empty(); + } + + if (originDrafts.isEmpty()) { + return Optional.of(Text.translatable("config.playercoordsapi.option.allowed_origins.error.empty")); + } + + List normalizedOrigins = new ArrayList<>(); + + for (OriginDraft draft : originDrafts) { + Optional normalizedOrigin = draft.toNormalizedOrigin(); + + if (normalizedOrigin.isEmpty()) { + return Optional.of(Text.translatable("config.playercoordsapi.option.allowed_origins.error.invalid")); + } + + normalizedOrigins.add(normalizedOrigin.get()); + } + + if (CorsUtils.hasDuplicateOrigins(normalizedOrigins)) { + return Optional.of(Text.translatable("config.playercoordsapi.option.allowed_origins.error.duplicate")); + } + + return Optional.empty(); + } + + private void buildOriginRows() { + originRows.clear(); + + for (OriginDraft draft : originDrafts) { + OriginRow row = new OriginRow(draft); + originRows.add(row); + this.addDrawableChild(row.schemeButton); + this.addDrawableChild(row.hostField); + this.addDrawableChild(row.portField); + this.addDrawableChild(row.removeButton); + } + } + + private void layoutOriginRows() { + clampScroll(); + boolean whitelistEditable = isWhitelistEditable(); + + int rowWidth = CONTENT_WIDTH; + int hostWidth = rowWidth - ORIGIN_SCHEME_WIDTH - ORIGIN_PORT_WIDTH - ORIGIN_REMOVE_WIDTH - 24; + + for (int i = 0; i < originRows.size(); i++) { + OriginRow row = originRows.get(i); + int rowY = listTop + i * (ORIGIN_ROW_HEIGHT + ORIGIN_ROW_SPACING) - scrollOffset; + boolean visible = rowY + ORIGIN_ROW_HEIGHT >= listTop && rowY <= listBottom; + + row.setState(visible, whitelistEditable); + + if (!visible) { + continue; + } + + row.schemeButton.setPosition(listLeft + 4, rowY + 4); + row.hostField.setPosition(listLeft + ORIGIN_SCHEME_WIDTH + 12, rowY + 4); + row.portField.setPosition(listLeft + CONTENT_WIDTH - ORIGIN_REMOVE_WIDTH - ORIGIN_PORT_WIDTH - 12, rowY + 4); + row.removeButton.setPosition(listLeft + CONTENT_WIDTH - ORIGIN_REMOVE_WIDTH - 4, rowY + 4); + + row.hostField.setWidth(hostWidth); + } + } + + private void renderOriginBlocks(DrawContext context) { + int panelTop = listTop - 4; + int panelBottom = listBottom + 2; + boolean whitelistEditable = isWhitelistEditable(); + + drawBorder(context, listLeft, panelTop, CONTENT_WIDTH, panelBottom - panelTop, 0xFF4A4A4A); + + for (int i = 0; i < originRows.size(); i++) { + int rowY = listTop + i * (ORIGIN_ROW_HEIGHT + ORIGIN_ROW_SPACING) - scrollOffset; + + if (rowY + ORIGIN_ROW_HEIGHT < listTop || rowY > listBottom) { + continue; + } + + int fillColor = whitelistEditable ? 0x44202020 : 0x44303030; + context.fill(listLeft + 1, rowY, listLeft + CONTENT_WIDTH - 1, rowY + ORIGIN_ROW_HEIGHT, fillColor); + drawBorder(context, listLeft, rowY, CONTENT_WIDTH, ORIGIN_ROW_HEIGHT, 0xFF5A5A5A); + } + + if (!whitelistEditable) { + context.fill(listLeft + 1, panelTop + 1, listLeft + CONTENT_WIDTH - 1, panelBottom - 1, 0x55202020); + } + } + + private static void drawBorder(DrawContext context, int x, int y, int width, int height, int color) { + context.fill(x, y, x + width, y + 1, color); + context.fill(x, y + height - 1, x + width, y + height, color); + context.fill(x, y, x + 1, y + height, color); + context.fill(x + width - 1, y, x + width, y + height, color); + } + + private List collectOriginEntries() { + List originEntries = new ArrayList<>(); + + for (OriginDraft draft : originDrafts) { + if (draft.isEmpty()) { + continue; + } + + originEntries.add(draft.toConfigEntry()); + } + + return originEntries; + } + + private int getMaxScroll() { + int contentHeight = originRows.size() * (ORIGIN_ROW_HEIGHT + ORIGIN_ROW_SPACING) - ORIGIN_ROW_SPACING; + int visibleHeight = Math.max(0, listBottom - listTop); + return Math.max(0, contentHeight - visibleHeight); + } + + private void clampScroll() { + scrollOffset = MathHelper.clamp(scrollOffset, 0, getMaxScroll()); + } + + private void removeDraft(OriginDraft draft) { + originDrafts.remove(draft); + clearAndInit(); + } + + private void syncOriginDraftsWithCorsPolicy() { + if (workingConfig.corsPolicy == ModConfig.CorsPolicy.CUSTOM_WHITELIST) { + if (originDrafts.isEmpty()) { + originDrafts.add(new OriginDraft()); + } + + return; + } + + originDrafts.removeIf(OriginDraft::isEmpty); + } + + private boolean hasEmptyOriginDraft() { + for (OriginDraft draft : originDrafts) { + if (draft.host == null || draft.host.trim().isEmpty()) { + return true; + } + } + + return false; + } + + private void renderHoveredTooltip(DrawContext context, int mouseX, int mouseY) { + List tooltip = getHoveredTooltip(mouseX, mouseY); + + if (!tooltip.isEmpty()) { + List orderedTooltip = new ArrayList<>(tooltip.size()); + for (Text line : tooltip) { + orderedTooltip.add(line.asOrderedText()); + } + context.drawTooltip(this.textRenderer, orderedTooltip, TOP_TOOLTIP_POSITIONER, mouseX, mouseY, false); + } + } + + private List getHoveredTooltip(int mouseX, int mouseY) { + int headerY = listTop - 20; + int hostX = listLeft + ORIGIN_SCHEME_WIDTH + 8; + int hostWidth = CONTENT_WIDTH - ORIGIN_SCHEME_WIDTH - ORIGIN_PORT_WIDTH - ORIGIN_REMOVE_WIDTH - 24; + int portX = listLeft + CONTENT_WIDTH - ORIGIN_REMOVE_WIDTH - 8 - ORIGIN_PORT_WIDTH; + + if (isHoveringLabel(corsPolicyLabel, mouseX, mouseY)) { + return buildTooltipLines("config.playercoordsapi.option.cors_policy.tooltip", 6, false); + } + + if (isHoveringLabel(nonBrowserClientsLabel, mouseX, mouseY)) { + return buildTooltipLines("config.playercoordsapi.option.allow_non_browser_local_clients.tooltip", 4, false); + } + + if (isWithin(mouseX, mouseY, hostX, headerY, hostWidth, ROW_HEIGHT)) { + return buildHostTooltip(); + } + + if (isWithin(mouseX, mouseY, portX, headerY, ORIGIN_PORT_WIDTH, ROW_HEIGHT)) { + return buildTooltip("config.playercoordsapi.option.origin_port.tooltip", true); + } + + for (OriginRow row : originRows) { + if (row.hostField.visible && row.hostField.isMouseOver(mouseX, mouseY)) { + return buildHostTooltip(); + } + + if (row.portField.visible && row.portField.isMouseOver(mouseX, mouseY)) { + return buildTooltip("config.playercoordsapi.option.origin_port.tooltip", true); + } + } + + return List.of(); + } + + private TextFieldWidget getFocusedTextField() { + for (OriginRow row : originRows) { + if (row.hostField.visible && row.hostField.isFocused()) { + return row.hostField; + } + + if (row.portField.visible && row.portField.isFocused()) { + return row.portField; + } + } + + return null; + } + + private List buildHostTooltip() { + List tooltip = new ArrayList<>(); + tooltip.add(Text.translatable("config.playercoordsapi.option.origin_host.tooltip")); + tooltip.add(Text.translatable("config.playercoordsapi.option.origin_host.tooltip.localhost") + .append(Text.literal(" localhost").formatted(Formatting.GREEN))); + tooltip.add(Text.translatable("config.playercoordsapi.option.origin_host.tooltip.domain") + .append(Text.literal(" example.com").formatted(Formatting.GREEN))); + tooltip.add(Text.translatable("config.playercoordsapi.option.origin_host.tooltip.ipv4") + .append(Text.literal(" 127.0.0.1").formatted(Formatting.GREEN))); + tooltip.add(Text.translatable("config.playercoordsapi.option.origin_host.tooltip.ipv6") + .append(Text.literal(" 2001:db8::1").formatted(Formatting.GREEN))); + + if (!isWhitelistEditable()) { + tooltip.add(Text.translatable("config.playercoordsapi.option.allowed_origins.disabled").formatted(Formatting.GRAY)); + } + + return tooltip; + } + + private List buildTooltip(String key, boolean includeWhitelistDisabledHint) { + List tooltip = new ArrayList<>(); + tooltip.add(Text.translatable(key)); + + if (includeWhitelistDisabledHint && !isWhitelistEditable()) { + tooltip.add(Text.translatable("config.playercoordsapi.option.allowed_origins.disabled").formatted(Formatting.GRAY)); + } + + return tooltip; + } + + private List buildTooltipLines(String keyPrefix, int lineCount, boolean includeWhitelistDisabledHint) { + List tooltip = new ArrayList<>(lineCount + 1); + + for (int i = 1; i <= lineCount; i++) { + tooltip.add(Text.translatable(keyPrefix + "." + i)); + } + + if (includeWhitelistDisabledHint && !isWhitelistEditable()) { + tooltip.add(Text.translatable("config.playercoordsapi.option.allowed_origins.disabled").formatted(Formatting.GRAY)); + } + + return tooltip; + } + + private static boolean isWithin(double mouseX, double mouseY, int x, int y, int width, int height) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + + private static boolean isHoveringLabel(TextWidget label, int mouseX, int mouseY) { + return isWithin(mouseX, mouseY, label.getX(), label.getY(), label.getWidth(), ROW_HEIGHT); + } + + private boolean isWhitelistEditable() { + return workingConfig.corsPolicy == ModConfig.CorsPolicy.CUSTOM_WHITELIST; + } + + private static Text getCorsPolicyLabel(ModConfig.CorsPolicy policy) { + return Text.translatable("config.playercoordsapi.option.cors_policy." + policy.name().toLowerCase(Locale.ROOT)); + } + + private static Text getOriginSchemeModeLabel(ModConfig.OriginSchemeMode mode) { + return Text.translatable("config.playercoordsapi.option.origin_scheme." + mode.name().toLowerCase(Locale.ROOT)); + } + + private static List createDrafts(List originEntries, List origins) { + List drafts = new ArrayList<>(); + + if (originEntries != null && !originEntries.isEmpty()) { + for (ModConfig.OriginEntry originEntry : originEntries) { + if (originEntry != null) { + drafts.add(OriginDraft.fromConfigEntry(originEntry)); + } + } + + return drafts; + } + + for (String origin : origins) { + CorsUtils.createConfiguredOriginEntry(origin) + .map(OriginDraft::fromConfigEntry) + .ifPresent(drafts::add); + } + + return drafts; + } + + private final class OriginRow { + private final CyclingButtonWidget schemeButton; + private final TextFieldWidget hostField; + private final TextFieldWidget portField; + private final ButtonWidget removeButton; + + private OriginRow(OriginDraft draft) { + this.schemeButton = CyclingButtonWidget.builder(PlayerCoordsConfigScreen::getOriginSchemeModeLabel, draft.mode) + .values(ModConfig.OriginSchemeMode.values()) + .omitKeyText() + .build(0, 0, ORIGIN_SCHEME_WIDTH, ROW_HEIGHT, ScreenTexts.EMPTY, (button, value) -> { + draft.mode = value; + updateValidation(); + }); + + this.hostField = new TextFieldWidget(PlayerCoordsConfigScreen.this.textRenderer, 0, 0, 160, ROW_HEIGHT, ScreenTexts.EMPTY); + this.hostField.setMaxLength(255); + this.hostField.setPlaceholder(Text.translatable("config.playercoordsapi.option.origin_host")); + this.hostField.setText(draft.host); + this.hostField.setChangedListener(value -> { + draft.host = value; + updateValidation(); + }); + + this.portField = new TextFieldWidget(PlayerCoordsConfigScreen.this.textRenderer, 0, 0, ORIGIN_PORT_WIDTH, ROW_HEIGHT, ScreenTexts.EMPTY); + this.portField.setMaxLength(5); + this.portField.setTextPredicate(value -> value.isEmpty() || value.chars().allMatch(Character::isDigit)); + this.portField.setPlaceholder(Text.translatable("config.playercoordsapi.option.origin_port")); + this.portField.setText(draft.port); + this.portField.setChangedListener(value -> { + draft.port = value; + updateValidation(); + }); + + this.removeButton = ButtonWidget.builder(Text.literal("X"), button -> removeDraft(draft)) + .dimensions(0, 0, ORIGIN_REMOVE_WIDTH, ROW_HEIGHT) + .build(); + } + + private void setState(boolean visible, boolean editable) { + schemeButton.visible = visible; + schemeButton.active = visible && editable; + hostField.visible = visible; + hostField.active = visible && editable; + hostField.setEditable(visible && editable); + hostField.setEditableColor(0xFFE0E0E0); + hostField.setUneditableColor(0xFF808080); + hostField.setFocusUnlocked(editable); + if (!editable) { + hostField.setFocused(false); + } + portField.visible = visible; + portField.active = visible && editable; + portField.setEditable(visible && editable); + portField.setEditableColor(0xFFE0E0E0); + portField.setUneditableColor(0xFF808080); + portField.setFocusUnlocked(editable); + if (!editable) { + portField.setFocused(false); + } + removeButton.visible = visible; + removeButton.active = visible && editable; + } + + private boolean tryFocusField(Click click, boolean doubleClick) { + if (!hostField.visible || !hostField.active) { + return false; + } + + if (hostField.isMouseOver(click.x(), click.y()) && hostField.mouseClicked(click, doubleClick)) { + hostField.setFocused(true); + portField.setFocused(false); + PlayerCoordsConfigScreen.this.setFocused(hostField); + return true; + } + + if (portField.isMouseOver(click.x(), click.y()) && portField.mouseClicked(click, doubleClick)) { + portField.setFocused(true); + hostField.setFocused(false); + PlayerCoordsConfigScreen.this.setFocused(portField); + return true; + } + + return false; + } + } + + private static final class OriginDraft { + private ModConfig.OriginSchemeMode mode; + private String host; + private String port; + + private OriginDraft() { + this(ModConfig.OriginSchemeMode.AUTO, "", ""); + } + + private OriginDraft(ModConfig.OriginSchemeMode mode, String host, String port) { + this.mode = mode; + this.host = host; + this.port = port; + } + + private static OriginDraft fromConfigEntry(ModConfig.OriginEntry originEntry) { + return new OriginDraft( + originEntry.schemeMode == null ? ModConfig.OriginSchemeMode.AUTO : originEntry.schemeMode, + originEntry.host == null ? "" : originEntry.host, + originEntry.port == null ? "" : originEntry.port + ); + } + + private ModConfig.OriginEntry toConfigEntry() { + ModConfig.OriginEntry originEntry = new ModConfig.OriginEntry(); + originEntry.schemeMode = mode; + originEntry.host = host; + originEntry.port = port; + return originEntry; + } + + private Optional toNormalizedOrigin() { + return CorsUtils.normalizeConfiguredOrigin(host, port, mode); + } + + private boolean isEmpty() { + return host == null || host.trim().isEmpty(); + } + } +} diff --git a/src/main/java/fr/sukikui/playercoordsapi/PlayerCoordsAPI.java b/src/main/java/fr/sukikui/playercoordsapi/PlayerCoordsAPI.java index 0699aa2..c2d8d75 100644 --- a/src/main/java/fr/sukikui/playercoordsapi/PlayerCoordsAPI.java +++ b/src/main/java/fr/sukikui/playercoordsapi/PlayerCoordsAPI.java @@ -1,5 +1,6 @@ package fr.sukikui.playercoordsapi; +import fr.sukikui.playercoordsapi.config.CorsUtils; import fr.sukikui.playercoordsapi.config.ModConfig; import me.shedaniel.autoconfig.AutoConfig; import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer; @@ -24,6 +25,26 @@ public void onInitialize() { AutoConfig.register(ModConfig.class, JanksonConfigSerializer::new); config = AutoConfig.getConfigHolder(ModConfig.class).getConfig(); + if (config.corsPolicy == null) { + config.corsPolicy = ModConfig.CorsPolicy.ALLOW_ALL; + } + + if (config.allowedOrigins == null) { + config.allowedOrigins = new java.util.ArrayList<>(ModConfig.DEFAULT_ALLOWED_ORIGINS); + } + + if (config.originEntries == null) { + config.originEntries = new java.util.ArrayList<>(); + } + + if (config.originEntries.isEmpty() && !config.allowedOrigins.isEmpty()) { + config.originEntries = CorsUtils.createConfiguredOriginEntries(config.allowedOrigins); + } + + config.allowedOrigins = config.originEntries.isEmpty() + ? CorsUtils.normalizeConfiguredOrigins(config.allowedOrigins) + : CorsUtils.normalizeConfiguredOriginsFromEntries(config.originEntries); + // This code runs as soon as Minecraft is in a mod-load-ready state. // However, some things (like resources) may still be uninitialized. // Proceed with mild caution. diff --git a/src/main/java/fr/sukikui/playercoordsapi/config/CorsUtils.java b/src/main/java/fr/sukikui/playercoordsapi/config/CorsUtils.java new file mode 100644 index 0000000..4225e4e --- /dev/null +++ b/src/main/java/fr/sukikui/playercoordsapi/config/CorsUtils.java @@ -0,0 +1,335 @@ +package fr.sukikui.playercoordsapi.config; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +public final class CorsUtils { + private static final String HTTP_SCHEME = "http"; + private static final String HTTPS_SCHEME = "https"; + + private CorsUtils() { + } + + public static Optional normalizeOrigin(String rawOrigin) { + return normalizeOrigin(rawOrigin, false); + } + + public static Optional normalizeConfiguredOrigin(String rawOrigin) { + return normalizeOrigin(rawOrigin, true); + } + + public static Optional normalizeConfiguredOrigin(String host, String port, ModConfig.OriginSchemeMode schemeMode) { + String authority = buildAuthority(host, port); + + if (authority.isEmpty()) { + return Optional.empty(); + } + + return switch (schemeMode) { + case AUTO -> normalizeConfiguredOrigin(authority); + case HTTP -> normalizeOrigin(HTTP_SCHEME + "://" + authority); + case HTTPS -> normalizeOrigin(HTTPS_SCHEME + "://" + authority); + }; + } + + private static Optional normalizeOrigin(String rawOrigin, boolean allowImplicitScheme) { + if (rawOrigin == null) { + return Optional.empty(); + } + + String trimmedOrigin = rawOrigin.trim(); + + if (trimmedOrigin.isEmpty() || trimmedOrigin.equalsIgnoreCase("null")) { + return Optional.empty(); + } + + if (allowImplicitScheme && !trimmedOrigin.contains("://")) { + Optional inferredOrigin = inferOriginWithScheme(trimmedOrigin); + + if (inferredOrigin.isEmpty()) { + return Optional.empty(); + } + + trimmedOrigin = inferredOrigin.get(); + } + + URI originUri; + + try { + originUri = new URI(trimmedOrigin); + } catch (URISyntaxException e) { + return Optional.empty(); + } + + String scheme = originUri.getScheme(); + String host = originUri.getHost(); + + if (scheme == null || host == null) { + return Optional.empty(); + } + + String normalizedScheme = scheme.toLowerCase(Locale.ROOT); + + if (!normalizedScheme.equals(HTTP_SCHEME) && !normalizedScheme.equals(HTTPS_SCHEME)) { + return Optional.empty(); + } + + if (originUri.getRawUserInfo() != null || originUri.getRawQuery() != null || originUri.getRawFragment() != null) { + return Optional.empty(); + } + + String path = originUri.getRawPath(); + + if (path != null && !path.isEmpty() && !path.equals("/")) { + return Optional.empty(); + } + + int port = originUri.getPort(); + + if (port < -1 || port > 65535) { + return Optional.empty(); + } + + boolean isDefaultPort = port == -1 + || (normalizedScheme.equals(HTTP_SCHEME) && port == 80) + || (normalizedScheme.equals(HTTPS_SCHEME) && port == 443); + String normalizedHost = stripIpv6Brackets(host).toLowerCase(Locale.ROOT); + String hostForOutput = normalizedHost.contains(":") ? "[" + normalizedHost + "]" : normalizedHost; + + if (isDefaultPort) { + return Optional.of(normalizedScheme + "://" + hostForOutput); + } + + return Optional.of(normalizedScheme + "://" + hostForOutput + ":" + port); + } + + private static Optional inferOriginWithScheme(String rawOrigin) { + String authority = prepareAuthority(rawOrigin); + URI authorityUri; + + try { + authorityUri = new URI("//" + authority); + } catch (URISyntaxException e) { + return Optional.empty(); + } + + String host = authorityUri.getHost(); + + if (host == null) { + return Optional.empty(); + } + + String inferredScheme = inferScheme(host, authorityUri.getPort()); + return Optional.of(inferredScheme + "://" + authority); + } + + private static String prepareAuthority(String rawOrigin) { + String authority = rawOrigin.startsWith("//") ? rawOrigin.substring(2) : rawOrigin; + + if (!authority.contains("[") && !authority.contains("]") && authority.indexOf(':') != authority.lastIndexOf(':') && !authority.contains("/")) { + return "[" + authority + "]"; + } + + return authority; + } + + private static boolean isLoopbackHost(String host) { + String normalizedHost = stripIpv6Brackets(host).toLowerCase(Locale.ROOT); + + return normalizedHost.equals("localhost") + || normalizedHost.equals("::1") + || normalizedHost.equals("0:0:0:0:0:0:0:1") + || normalizedHost.startsWith("127."); + } + + private static String inferScheme(String host, int port) { + if (port == 80) { + return HTTP_SCHEME; + } + + if (port == 443) { + return HTTPS_SCHEME; + } + + return isLoopbackHost(host) ? HTTP_SCHEME : HTTPS_SCHEME; + } + + private static String buildAuthority(String host, String port) { + String trimmedHost = host == null ? "" : host.trim(); + + if (trimmedHost.isEmpty() || trimmedHost.contains("://") || trimmedHost.contains("/") || trimmedHost.contains("?") || trimmedHost.contains("#")) { + return ""; + } + + String authorityHost = trimmedHost; + + if (!authorityHost.contains("[") && !authorityHost.contains("]") && authorityHost.indexOf(':') != authorityHost.lastIndexOf(':')) { + authorityHost = "[" + authorityHost + "]"; + } + + String trimmedPort = port == null ? "" : port.trim(); + + if (trimmedPort.isEmpty()) { + return authorityHost; + } + + try { + int parsedPort = Integer.parseInt(trimmedPort); + + if (parsedPort < 1 || parsedPort > 65535) { + return ""; + } + } catch (NumberFormatException e) { + return ""; + } + + return authorityHost + ":" + trimmedPort; + } + + private static String stripIpv6Brackets(String host) { + if (host == null) { + return null; + } + + if (host.startsWith("[") && host.endsWith("]")) { + return host.substring(1, host.length() - 1); + } + + return host; + } + + public static boolean isOriginAllowed(ModConfig config, String origin) { + Optional normalizedOrigin = normalizeOrigin(origin); + ModConfig.CorsPolicy corsPolicy = config.corsPolicy == null + ? ModConfig.CorsPolicy.ALLOW_ALL + : config.corsPolicy; + List allowedOrigins = config.allowedOrigins == null + ? List.of() + : config.allowedOrigins; + + if (normalizedOrigin.isEmpty()) { + return false; + } + + return switch (corsPolicy) { + case ALLOW_ALL -> true; + case LOCAL_WEB_APPS_ONLY -> isLoopbackOrigin(normalizedOrigin.get()); + case CUSTOM_WHITELIST -> allowedOrigins.contains(normalizedOrigin.get()); + }; + } + + public static boolean isLoopbackOrigin(String origin) { + Optional normalizedOrigin = normalizeOrigin(origin); + + if (normalizedOrigin.isEmpty()) { + return false; + } + + try { + URI originUri = new URI(normalizedOrigin.get()); + String host = stripIpv6Brackets(originUri.getHost()); + + if (host == null) { + return false; + } + + if (host.equalsIgnoreCase("localhost")) { + return true; + } + + return InetAddress.getByName(host).isLoopbackAddress(); + } catch (URISyntaxException | UnknownHostException e) { + return false; + } + } + + public static List normalizeConfiguredOrigins(List origins) { + Set normalizedOrigins = new LinkedHashSet<>(); + + for (String origin : origins) { + normalizeConfiguredOrigin(origin).ifPresent(normalizedOrigins::add); + } + + return new ArrayList<>(normalizedOrigins); + } + + public static List normalizeConfiguredOriginsFromEntries(List originEntries) { + Set normalizedOrigins = new LinkedHashSet<>(); + + for (ModConfig.OriginEntry originEntry : originEntries) { + if (originEntry == null || originEntry.schemeMode == null) { + continue; + } + + normalizeConfiguredOrigin(originEntry.host, originEntry.port, originEntry.schemeMode) + .ifPresent(normalizedOrigins::add); + } + + return new ArrayList<>(normalizedOrigins); + } + + public static List createConfiguredOriginEntries(List origins) { + List originEntries = new ArrayList<>(); + + for (String origin : origins) { + createConfiguredOriginEntry(origin).ifPresent(originEntries::add); + } + + return originEntries; + } + + public static Optional createConfiguredOriginEntry(String origin) { + Optional normalizedOrigin = normalizeOrigin(origin); + + if (normalizedOrigin.isEmpty()) { + return Optional.empty(); + } + + try { + URI originUri = new URI(normalizedOrigin.get()); + String scheme = originUri.getScheme(); + String host = stripIpv6Brackets(originUri.getHost()); + + if (scheme == null || host == null) { + return Optional.empty(); + } + + String port = originUri.getPort() == -1 ? "" : Integer.toString(originUri.getPort()); + String authority = buildAuthority(host, port); + Optional autoOrigin = normalizeConfiguredOrigin(authority); + + ModConfig.OriginEntry originEntry = new ModConfig.OriginEntry(); + originEntry.host = host; + originEntry.port = port; + originEntry.schemeMode = autoOrigin.isPresent() && autoOrigin.get().equals(normalizedOrigin.get()) + ? ModConfig.OriginSchemeMode.AUTO + : ("http".equalsIgnoreCase(scheme) ? ModConfig.OriginSchemeMode.HTTP : ModConfig.OriginSchemeMode.HTTPS); + + return Optional.of(originEntry); + } catch (URISyntaxException e) { + return Optional.empty(); + } + } + + public static boolean hasDuplicateOrigins(List origins) { + Set normalizedOrigins = new LinkedHashSet<>(); + + for (String origin : origins) { + Optional normalizedOrigin = normalizeConfiguredOrigin(origin); + + if (normalizedOrigin.isPresent() && !normalizedOrigins.add(normalizedOrigin.get())) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/fr/sukikui/playercoordsapi/config/ModConfig.java b/src/main/java/fr/sukikui/playercoordsapi/config/ModConfig.java index 694b252..310172f 100644 --- a/src/main/java/fr/sukikui/playercoordsapi/config/ModConfig.java +++ b/src/main/java/fr/sukikui/playercoordsapi/config/ModConfig.java @@ -2,12 +2,36 @@ import me.shedaniel.autoconfig.ConfigData; import me.shedaniel.autoconfig.annotation.Config; -import me.shedaniel.autoconfig.annotation.ConfigEntry; import fr.sukikui.playercoordsapi.PlayerCoordsAPI; +import java.util.ArrayList; +import java.util.List; + @Config(name = PlayerCoordsAPI.MOD_ID) public class ModConfig implements ConfigData { - - @ConfigEntry.Gui.Tooltip(count = 0) // This tells autoconfig to use the tooltip from lang files + public static final List DEFAULT_ALLOWED_ORIGINS = List.of(); + + public enum CorsPolicy { + ALLOW_ALL, + LOCAL_WEB_APPS_ONLY, + CUSTOM_WHITELIST + } + + public enum OriginSchemeMode { + AUTO, + HTTP, + HTTPS + } + + public static class OriginEntry { + public OriginSchemeMode schemeMode = OriginSchemeMode.AUTO; + public String host = ""; + public String port = ""; + } + public boolean enabled = true; -} \ No newline at end of file + public CorsPolicy corsPolicy = CorsPolicy.ALLOW_ALL; + public boolean allowNonBrowserLocalClients = true; + public List allowedOrigins = new ArrayList<>(DEFAULT_ALLOWED_ORIGINS); + public List originEntries = new ArrayList<>(); +} diff --git a/src/main/resources/assets/playercoordsapi/lang/de_de.json b/src/main/resources/assets/playercoordsapi/lang/de_de.json index 33c5a4f..b147332 100644 --- a/src/main/resources/assets/playercoordsapi/lang/de_de.json +++ b/src/main/resources/assets/playercoordsapi/lang/de_de.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Mod aktivieren", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "PlayerCoordsAPI aktivieren oder deaktivieren" + "config.playercoordsapi.option.enabled": "Mod aktivieren", + "config.playercoordsapi.option.cors_policy": "CORS-Richtlinie", + "config.playercoordsapi.option.cors_policy.allow_all": "Alles erlauben", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Lokale Web-Apps", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Benutzerdefinierte Whitelist", + "config.playercoordsapi.option.cors_policy.tooltip.1": "Die API bleibt nur von diesem Rechner aus erreichbar.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Diese Option legt fest, welche Websites sie im Browser lesen dürfen.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Alles erlauben: akzeptiert alle Web-Origins.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Lokale Web-Apps: akzeptiert nur localhost, 127.0.0.1 und ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Benutzerdefinierte Whitelist: akzeptiert nur die unten hinzugefügten Origins.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Gilt nicht für Clients ohne Origin-Header.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Clients ohne Browser", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Erlaubt oder blockiert lokale Clients ohne Origin-Header.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Beispiele: curl, Skripte, Desktop-Apps und native Tools.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "An: Diese lokalen Clients dürfen die API aufrufen.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Aus: Nur Browser oder Web-Apps, die durch die CORS-Richtlinie erlaubt sind, dürfen sie nutzen.", + "config.playercoordsapi.option.allowed_origins": "Erlaubte Origins", + "config.playercoordsapi.option.add_origin": "Erlaubte Origin hinzufügen", + "config.playercoordsapi.option.origin_scheme": "Schema", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Host / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Hostname oder IP-Adresse.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Domain:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Port", + "config.playercoordsapi.option.origin_port.tooltip": "Optionaler Port, zum Beispiel 3000. Leer lassen, um den Standardport zu verwenden.", + "config.playercoordsapi.option.allowed_origins.disabled": "Stelle die CORS-Richtlinie auf Benutzerdefinierte Whitelist, um diese Liste zu bearbeiten.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Gib eine gültige Origin ein.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Füge mindestens eine Origin hinzu.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Entferne doppelte Origins." } diff --git a/src/main/resources/assets/playercoordsapi/lang/en_us.json b/src/main/resources/assets/playercoordsapi/lang/en_us.json index d969f44..42df378 100644 --- a/src/main/resources/assets/playercoordsapi/lang/en_us.json +++ b/src/main/resources/assets/playercoordsapi/lang/en_us.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Enable Mod", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Enable or disable PlayerCoordsAPI" -} \ No newline at end of file + "config.playercoordsapi.option.enabled": "Enable Mod", + "config.playercoordsapi.option.cors_policy": "CORS Policy", + "config.playercoordsapi.option.cors_policy.allow_all": "Allow all", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Local web apps", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Custom whitelist", + "config.playercoordsapi.option.cors_policy.tooltip.1": "The API is still only reachable from this machine.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "This setting controls which websites can read it from a browser.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Allow all: accepts every web origin.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Local web apps: only accepts localhost, 127.0.0.1, and ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Custom whitelist: only accepts the origins listed below.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Does not apply to clients without an Origin header.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Non-browser clients", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Allows or blocks local clients without an Origin header.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Examples: curl, scripts, desktop apps, and native tools.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "On: those local clients can call the API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Off: only browsers or web apps allowed by the CORS policy can use it.", + "config.playercoordsapi.option.allowed_origins": "Allowed origins", + "config.playercoordsapi.option.add_origin": "Add allowed origin", + "config.playercoordsapi.option.origin_scheme": "Scheme", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Host / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Host name or IP address.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Domain:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Port", + "config.playercoordsapi.option.origin_port.tooltip": "Optional port, for example 3000. Leave empty to use the default port.", + "config.playercoordsapi.option.allowed_origins.disabled": "Switch CORS Policy to Custom whitelist to edit this list.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Enter a valid origin.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Add at least one origin.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Remove duplicate origins." +} diff --git a/src/main/resources/assets/playercoordsapi/lang/es_es.json b/src/main/resources/assets/playercoordsapi/lang/es_es.json index be3f4e9..78d9c72 100644 --- a/src/main/resources/assets/playercoordsapi/lang/es_es.json +++ b/src/main/resources/assets/playercoordsapi/lang/es_es.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Activar el mod", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Activar o desactivar PlayerCoordsAPI" + "config.playercoordsapi.option.enabled": "Activar mod", + "config.playercoordsapi.option.cors_policy": "Política CORS", + "config.playercoordsapi.option.cors_policy.allow_all": "Permitir todo", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Apps web locales", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Lista blanca personalizada", + "config.playercoordsapi.option.cors_policy.tooltip.1": "La API sigue siendo accesible solo desde esta máquina.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Este ajuste define qué sitios web pueden leerla desde un navegador.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Permitir todo: acepta cualquier origen web.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Apps web locales: solo acepta localhost, 127.0.0.1 y ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Lista blanca: solo acepta los orígenes añadidos abajo.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "No se aplica a clientes sin cabecera Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Clientes sin navegador", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Permite o bloquea clientes locales sin cabecera Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Ejemplos: curl, scripts, aplicaciones de escritorio y herramientas nativas.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "Activado: esos clientes locales pueden llamar a la API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Desactivado: solo pueden usarla navegadores o apps web permitidos por la política CORS.", + "config.playercoordsapi.option.allowed_origins": "Orígenes permitidos", + "config.playercoordsapi.option.add_origin": "Añadir origen permitido", + "config.playercoordsapi.option.origin_scheme": "Esquema", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Host / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Nombre de host o dirección IP.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Dominio:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Puerto", + "config.playercoordsapi.option.origin_port.tooltip": "Puerto opcional, por ejemplo 3000. Déjalo vacío para usar el puerto predeterminado.", + "config.playercoordsapi.option.allowed_origins.disabled": "Cambia la política CORS a Lista blanca personalizada para editar esta lista.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Introduce un origen válido.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Añade al menos un origen.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Elimina los orígenes duplicados." } diff --git a/src/main/resources/assets/playercoordsapi/lang/fr_fr.json b/src/main/resources/assets/playercoordsapi/lang/fr_fr.json index 72a8963..6680d37 100644 --- a/src/main/resources/assets/playercoordsapi/lang/fr_fr.json +++ b/src/main/resources/assets/playercoordsapi/lang/fr_fr.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Activer le Mod", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Activer ou désactiver PlayerCoordsAPI" -} \ No newline at end of file + "config.playercoordsapi.option.enabled": "Activer le mod", + "config.playercoordsapi.option.cors_policy": "Politique CORS", + "config.playercoordsapi.option.cors_policy.allow_all": "Autoriser tout", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Apps web locales", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Liste blanche personnalisée", + "config.playercoordsapi.option.cors_policy.tooltip.1": "L'API reste accessible uniquement depuis cette machine.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Ce réglage définit quels sites web peuvent la lire depuis un navigateur.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Autoriser tout : accepte toutes les origines web.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Apps web locales : accepte seulement localhost, 127.0.0.1 et ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Liste blanche : accepte seulement les origines ajoutées ci-dessous.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Ne s'applique pas aux clients sans en-tête Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Clients hors navigateur", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Autorise ou bloque les clients locaux sans en-tête Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Exemples : curl, scripts, applications desktop, outils natifs.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "Activé : ces clients locaux peuvent appeler l'API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Désactivé : seuls les navigateurs ou apps web autorisés peuvent l'utiliser.", + "config.playercoordsapi.option.allowed_origins": "Origines autorisées", + "config.playercoordsapi.option.add_origin": "Ajouter une origine autorisée", + "config.playercoordsapi.option.origin_scheme": "Schéma", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Hôte / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Nom d’hôte ou adresse IP.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost :", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Domaine :", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4 :", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6 :", + "config.playercoordsapi.option.origin_port": "Port", + "config.playercoordsapi.option.origin_port.tooltip": "Port facultatif, par exemple 3000. Laissez vide pour utiliser le port par défaut.", + "config.playercoordsapi.option.allowed_origins.disabled": "Passez la politique CORS sur Liste blanche personnalisée pour modifier cette liste.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Saisissez une origine valide.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Ajoutez au moins une origine.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Supprimez les origines en double." +} diff --git a/src/main/resources/assets/playercoordsapi/lang/it_it.json b/src/main/resources/assets/playercoordsapi/lang/it_it.json index 061b234..c05ac1c 100644 --- a/src/main/resources/assets/playercoordsapi/lang/it_it.json +++ b/src/main/resources/assets/playercoordsapi/lang/it_it.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Abilita mod", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Abilita o disabilita PlayerCoordsAPI" + "config.playercoordsapi.option.enabled": "Attiva mod", + "config.playercoordsapi.option.cors_policy": "Criterio CORS", + "config.playercoordsapi.option.cors_policy.allow_all": "Consenti tutto", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "App web locali", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Whitelist personalizzata", + "config.playercoordsapi.option.cors_policy.tooltip.1": "L'API resta raggiungibile solo da questa macchina.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Questa opzione definisce quali siti web possono leggerla da un browser.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Consenti tutto: accetta qualsiasi origine web.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "App web locali: accetta solo localhost, 127.0.0.1 e ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Whitelist personalizzata: accetta solo le origini aggiunte qui sotto.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Non si applica ai client senza header Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Client senza browser", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Consente o blocca i client locali senza header Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Esempi: curl, script, applicazioni desktop e strumenti nativi.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "Attivo: questi client locali possono chiamare l'API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Disattivo: possono usarla solo browser o app web consentiti dalla politica CORS.", + "config.playercoordsapi.option.allowed_origins": "Origini consentite", + "config.playercoordsapi.option.add_origin": "Aggiungi origine consentita", + "config.playercoordsapi.option.origin_scheme": "Schema", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Host / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Nome host o indirizzo IP.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Dominio:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Porta", + "config.playercoordsapi.option.origin_port.tooltip": "Porta facoltativa, per esempio 3000. Lasciala vuota per usare la porta predefinita.", + "config.playercoordsapi.option.allowed_origins.disabled": "Imposta la politica CORS su Whitelist personalizzata per modificare questa lista.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Inserisci un'origine valida.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Aggiungi almeno un'origine.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Rimuovi le origini duplicate." } diff --git a/src/main/resources/assets/playercoordsapi/lang/ja_jp.json b/src/main/resources/assets/playercoordsapi/lang/ja_jp.json index f8dca9c..2cedef8 100644 --- a/src/main/resources/assets/playercoordsapi/lang/ja_jp.json +++ b/src/main/resources/assets/playercoordsapi/lang/ja_jp.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Modを有効にする", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "PlayerCoordsAPIを有効または無効にします" + "config.playercoordsapi.option.enabled": "Modを有効化", + "config.playercoordsapi.option.cors_policy": "CORSポリシー", + "config.playercoordsapi.option.cors_policy.allow_all": "すべて許可", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "ローカル Web アプリ", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "カスタム許可リスト", + "config.playercoordsapi.option.cors_policy.tooltip.1": "API はこのマシンからのみ引き続き利用できます。", + "config.playercoordsapi.option.cors_policy.tooltip.2": "この設定は、ブラウザーからどの Web サイトが読み取れるかを決めます。", + "config.playercoordsapi.option.cors_policy.tooltip.3": "すべて許可: すべての Web Origin を受け入れます。", + "config.playercoordsapi.option.cors_policy.tooltip.4": "ローカル Web アプリ: localhost、127.0.0.1、::1 のみ受け入れます。", + "config.playercoordsapi.option.cors_policy.tooltip.5": "カスタム許可リスト: 下に追加した Origin のみ受け入れます。", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Origin ヘッダーのないクライアントには適用されません。", + "config.playercoordsapi.option.allow_non_browser_local_clients": "ブラウザー外クライアント", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Origin ヘッダーのないローカルクライアントを許可または拒否します。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "例: curl、スクリプト、デスクトップアプリ、ネイティブツール。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "オン: それらのローカルクライアントは API を呼び出せます。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "オフ: CORS ポリシーで許可されたブラウザーまたは Web アプリだけが利用できます。", + "config.playercoordsapi.option.allowed_origins": "許可された Origin", + "config.playercoordsapi.option.add_origin": "許可する Origin を追加", + "config.playercoordsapi.option.origin_scheme": "スキーム", + "config.playercoordsapi.option.origin_scheme.auto": "自動", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "ホスト / IP", + "config.playercoordsapi.option.origin_host.tooltip": "ホスト名または IP アドレスです。", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "ローカルホスト:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "ドメイン:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "ポート", + "config.playercoordsapi.option.origin_port.tooltip": "任意のポートです。例: 3000。空欄の場合は既定のポートを使います。", + "config.playercoordsapi.option.allowed_origins.disabled": "この一覧を編集するには、CORS ポリシーをカスタム許可リストに切り替えてください。", + "config.playercoordsapi.option.allowed_origins.error.invalid": "有効な Origin を入力してください。", + "config.playercoordsapi.option.allowed_origins.error.empty": "少なくとも 1 つの Origin を追加してください。", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "重複した Origin を削除してください。" } diff --git a/src/main/resources/assets/playercoordsapi/lang/pt_br.json b/src/main/resources/assets/playercoordsapi/lang/pt_br.json index 94ec953..f3ab13b 100644 --- a/src/main/resources/assets/playercoordsapi/lang/pt_br.json +++ b/src/main/resources/assets/playercoordsapi/lang/pt_br.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Ativar o mod", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Ativar ou desativar o PlayerCoordsAPI" + "config.playercoordsapi.option.enabled": "Ativar mod", + "config.playercoordsapi.option.cors_policy": "Política CORS", + "config.playercoordsapi.option.cors_policy.allow_all": "Permitir tudo", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Apps web locais", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Lista personalizada", + "config.playercoordsapi.option.cors_policy.tooltip.1": "A API continua acessível apenas nesta máquina.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Esta opção define quais sites podem lê-la em um navegador.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Permitir tudo: aceita qualquer origem web.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Apps web locais: aceita apenas localhost, 127.0.0.1 e ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Lista personalizada: aceita apenas as origens adicionadas abaixo.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Não se aplica a clientes sem o cabeçalho Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Clientes sem navegador", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Permite ou bloqueia clientes locais sem o cabeçalho Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Exemplos: curl, scripts, aplicativos desktop e ferramentas nativas.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "Ativado: esses clientes locais podem chamar a API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Desativado: só navegadores ou apps web permitidos pela política CORS podem usá-la.", + "config.playercoordsapi.option.allowed_origins": "Origens permitidas", + "config.playercoordsapi.option.add_origin": "Adicionar origem permitida", + "config.playercoordsapi.option.origin_scheme": "Esquema", + "config.playercoordsapi.option.origin_scheme.auto": "Auto", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Host / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Nome do host ou endereço IP.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Domínio:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Porta", + "config.playercoordsapi.option.origin_port.tooltip": "Porta opcional, por exemplo 3000. Deixe em branco para usar a porta padrão.", + "config.playercoordsapi.option.allowed_origins.disabled": "Altere a política CORS para Lista personalizada para editar esta lista.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Insira uma origem válida.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Adicione pelo menos uma origem.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Remova origens duplicadas." } diff --git a/src/main/resources/assets/playercoordsapi/lang/ru_ru.json b/src/main/resources/assets/playercoordsapi/lang/ru_ru.json index af62526..b2bab56 100644 --- a/src/main/resources/assets/playercoordsapi/lang/ru_ru.json +++ b/src/main/resources/assets/playercoordsapi/lang/ru_ru.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "Включить мод", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "Включить или отключить PlayerCoordsAPI" + "config.playercoordsapi.option.enabled": "Включить мод", + "config.playercoordsapi.option.cors_policy": "Политика CORS", + "config.playercoordsapi.option.cors_policy.allow_all": "Разрешить всё", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "Локальные веб-приложения", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "Пользовательский белый список", + "config.playercoordsapi.option.cors_policy.tooltip.1": "API по-прежнему доступно только с этого компьютера.", + "config.playercoordsapi.option.cors_policy.tooltip.2": "Этот параметр определяет, какие сайты могут читать его в браузере.", + "config.playercoordsapi.option.cors_policy.tooltip.3": "Разрешить всё: принимает любые web-origin.", + "config.playercoordsapi.option.cors_policy.tooltip.4": "Локальные веб-приложения: принимает только localhost, 127.0.0.1 и ::1.", + "config.playercoordsapi.option.cors_policy.tooltip.5": "Пользовательский белый список: принимает только origins, добавленные ниже.", + "config.playercoordsapi.option.cors_policy.tooltip.6": "Не применяется к клиентам без заголовка Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients": "Клиенты вне браузера", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "Разрешает или блокирует локальных клиентов без заголовка Origin.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "Примеры: curl, скрипты, desktop-приложения и нативные инструменты.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "Вкл.: эти локальные клиенты могут вызывать API.", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "Выкл.: пользоваться API смогут только браузеры или web-приложения, разрешённые политикой CORS.", + "config.playercoordsapi.option.allowed_origins": "Разрешённые origins", + "config.playercoordsapi.option.add_origin": "Добавить origin", + "config.playercoordsapi.option.origin_scheme": "Схема", + "config.playercoordsapi.option.origin_scheme.auto": "Авто", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "Хост / IP", + "config.playercoordsapi.option.origin_host.tooltip": "Имя хоста или IP-адрес.", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "Localhost:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "Домен:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "Порт", + "config.playercoordsapi.option.origin_port.tooltip": "Необязательный порт, например 3000. Оставьте пустым, чтобы использовать порт по умолчанию.", + "config.playercoordsapi.option.allowed_origins.disabled": "Переключите политику CORS на Пользовательский белый список, чтобы редактировать этот список.", + "config.playercoordsapi.option.allowed_origins.error.invalid": "Введите корректный origin.", + "config.playercoordsapi.option.allowed_origins.error.empty": "Добавьте хотя бы один origin.", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "Удалите повторяющиеся origins." } diff --git a/src/main/resources/assets/playercoordsapi/lang/zh_cn.json b/src/main/resources/assets/playercoordsapi/lang/zh_cn.json index ae503c8..c0ad2b8 100644 --- a/src/main/resources/assets/playercoordsapi/lang/zh_cn.json +++ b/src/main/resources/assets/playercoordsapi/lang/zh_cn.json @@ -1,5 +1,37 @@ { "text.autoconfig.playercoordsapi.title": "PlayerCoordsAPI", - "text.autoconfig.playercoordsapi.option.enabled": "启用模组", - "text.autoconfig.playercoordsapi.option.enabled.@Tooltip": "启用或禁用 PlayerCoordsAPI" + "config.playercoordsapi.option.enabled": "启用模组", + "config.playercoordsapi.option.cors_policy": "CORS 策略", + "config.playercoordsapi.option.cors_policy.allow_all": "允许全部", + "config.playercoordsapi.option.cors_policy.local_web_apps_only": "本地 Web 应用", + "config.playercoordsapi.option.cors_policy.custom_whitelist": "自定义白名单", + "config.playercoordsapi.option.cors_policy.tooltip.1": "该 API 仍然只能从这台机器访问。", + "config.playercoordsapi.option.cors_policy.tooltip.2": "此设置决定哪些网站可以在浏览器中读取它。", + "config.playercoordsapi.option.cors_policy.tooltip.3": "允许全部:接受所有 Web Origin。", + "config.playercoordsapi.option.cors_policy.tooltip.4": "本地 Web 应用:仅接受 localhost、127.0.0.1 和 ::1。", + "config.playercoordsapi.option.cors_policy.tooltip.5": "自定义白名单:仅接受下方添加的来源。", + "config.playercoordsapi.option.cors_policy.tooltip.6": "不适用于没有 Origin 标头的客户端。", + "config.playercoordsapi.option.allow_non_browser_local_clients": "非浏览器客户端", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.1": "允许或阻止没有 Origin 标头的本地客户端。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.2": "例如:curl、脚本、桌面应用和原生工具。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.3": "开启:这些本地客户端可以调用该 API。", + "config.playercoordsapi.option.allow_non_browser_local_clients.tooltip.4": "关闭:只有被 CORS 策略允许的浏览器或 Web 应用才能使用它。", + "config.playercoordsapi.option.allowed_origins": "允许的来源", + "config.playercoordsapi.option.add_origin": "添加允许的来源", + "config.playercoordsapi.option.origin_scheme": "协议", + "config.playercoordsapi.option.origin_scheme.auto": "自动", + "config.playercoordsapi.option.origin_scheme.http": "HTTP", + "config.playercoordsapi.option.origin_scheme.https": "HTTPS", + "config.playercoordsapi.option.origin_host": "主机 / IP", + "config.playercoordsapi.option.origin_host.tooltip": "主机名或 IP 地址。", + "config.playercoordsapi.option.origin_host.tooltip.localhost": "本地主机:", + "config.playercoordsapi.option.origin_host.tooltip.domain": "域名:", + "config.playercoordsapi.option.origin_host.tooltip.ipv4": "IPv4:", + "config.playercoordsapi.option.origin_host.tooltip.ipv6": "IPv6:", + "config.playercoordsapi.option.origin_port": "端口", + "config.playercoordsapi.option.origin_port.tooltip": "可选端口,例如 3000。留空时使用默认端口。", + "config.playercoordsapi.option.allowed_origins.disabled": "将 CORS 策略切换为自定义白名单后才能编辑此列表。", + "config.playercoordsapi.option.allowed_origins.error.invalid": "请输入有效的来源。", + "config.playercoordsapi.option.allowed_origins.error.empty": "至少添加一个来源。", + "config.playercoordsapi.option.allowed_origins.error.duplicate": "请移除重复的来源。" }