diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ServerAddressValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ServerAddressValidator.java deleted file mode 100644 index 8a73c8ef4..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ServerAddressValidator.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2022 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.construct; - -import com.jfoenix.validation.base.ValidatorBase; -import javafx.beans.NamedArg; -import javafx.scene.control.TextInputControl; -import org.jackhuang.hmcl.util.StringUtils; - -import java.util.regex.Pattern; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class ServerAddressValidator extends ValidatorBase { - private final boolean nullable; - - public ServerAddressValidator() { - this(false); - } - - public ServerAddressValidator(@NamedArg("nullable") boolean nullable) { - this(i18n("input.url"), nullable); - } - - public ServerAddressValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) { - super(message); - this.nullable = nullable; - } - - @Override - protected void eval() { - if (srcControl.get() instanceof TextInputControl) { - evalTextInputField(); - } - } - - private static final Pattern PATTERN = Pattern.compile("[-a-zA-Z0-9@:%._+~#=]{1,256}(:\\d+)?"); - - private void evalTextInputField() { - TextInputControl textField = ((TextInputControl) srcControl.get()); - - if (StringUtils.isBlank(textField.getText())) - hasErrors.set(!nullable); - else - hasErrors.set(!PATTERN.matcher(textField.getText()).matches()); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index c2675a7df..235abc096 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -45,6 +45,8 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.ServerAddress; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; @@ -451,6 +453,16 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag txtServerIP = new JFXTextField(); txtServerIP.setPromptText(i18n("settings.advanced.server_ip.prompt")); + Validator.addTo(txtServerIP).accept(str -> { + if (StringUtils.isBlank(str)) + return true; + try { + ServerAddress.parse(str); + return true; + } catch (Exception ignored) { + return false; + } + }); FXUtils.setLimitWidth(txtServerIP, 300); serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index a54a8409e..4cf983085 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.ServerAddress; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.io.FileUtils; @@ -295,15 +296,21 @@ public class DefaultLauncher extends Launcher { res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features)); if (StringUtils.isNotBlank(options.getServerIp())) { - String[] args = options.getServerIp().split(":"); - if (GameVersionNumber.asGameVersion(gameVersion).compareTo("1.20") < 0) { - res.add("--server"); - res.add(args[0]); - res.add("--port"); - res.add(args.length > 1 ? args[1] : "25565"); - } else { - res.add("--quickPlayMultiplayer"); - res.add(args[0] + ":" + (args.length > 1 ? args[1] : "25565")); + String address = options.getServerIp(); + + try { + ServerAddress parsed = ServerAddress.parse(address); + if (GameVersionNumber.asGameVersion(gameVersion).compareTo("1.20") < 0) { + res.add("--server"); + res.add(parsed.getHost()); + res.add("--port"); + res.add(parsed.getPort() >= 0 ? String.valueOf(parsed.getPort()) : "25565"); + } else { + res.add("--quickPlayMultiplayer"); + res.add(parsed.getPort() < 0 ? address + ":25565" : address); + } + } catch (IllegalArgumentException e) { + LOG.warning("Invalid server address: " + address, e); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java new file mode 100644 index 000000000..abf017438 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java @@ -0,0 +1,125 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * @author Glavo + */ +public final class ServerAddress { + + private static final int UNKNOWN_PORT = -1; + + private static IllegalArgumentException illegalAddress(String address) { + return new IllegalArgumentException("Invalid server address: " + address); + } + + /** + * @throws IllegalArgumentException if the address is not a valid server address + */ + public static @NotNull ServerAddress parse(@NotNull String address) { + Objects.requireNonNull(address); + + if (!address.startsWith("[")) { + int colonPos = address.indexOf(':'); + if (colonPos >= 0) { + if (colonPos == address.length() - 1) + throw illegalAddress(address); + + String host = address.substring(0, colonPos); + int port; + try { + port = Integer.parseInt(address.substring(colonPos + 1)); + } catch (NumberFormatException e) { + throw illegalAddress(address); + } + if (port < 0 || port > 0xFFFF) + throw illegalAddress(address); + return new ServerAddress(host, port); + } else { + return new ServerAddress(address); + } + } else { + // Parse IPv6 address + int colonIndex = address.indexOf(':'); + int closeBracketIndex = address.lastIndexOf(']'); + + if (colonIndex < 0 || closeBracketIndex < colonIndex) + throw illegalAddress(address); + + String host = address.substring(1, closeBracketIndex); + if (closeBracketIndex == address.length() - 1) + return new ServerAddress(host); + + if (address.length() < closeBracketIndex + 3 || address.charAt(closeBracketIndex + 1) != ':') + throw illegalAddress(address); + + int port; + try { + port = Integer.parseInt(address.substring(closeBracketIndex + 2)); + } catch (NumberFormatException e) { + throw illegalAddress(address); + } + + if (port < 0 || port > 0xFFFF) + throw illegalAddress(address); + + return new ServerAddress(host, port); + } + } + + private final String host; + private final int port; + + public ServerAddress(@NotNull String host) { + this(host, UNKNOWN_PORT); + } + + public ServerAddress(@NotNull String host, int port) { + this.host = Objects.requireNonNull(host); + this.port = port; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ServerAddress)) return false; + ServerAddress that = (ServerAddress) o; + return port == that.port && Objects.equals(host, that.host); + } + + @Override + public int hashCode() { + return Objects.hash(host, port); + } + + @Override + public String toString() { + return String.format("ServerAddress[host='%s', port=%d]", host, port); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java new file mode 100644 index 000000000..1a036c3e3 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java @@ -0,0 +1,62 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Glavo + */ +public final class ServerAddressTest { + + @Test + public void testParse() { + assertEquals(new ServerAddress("example.com"), ServerAddress.parse("example.com")); + assertEquals(new ServerAddress("example.com", 25565), ServerAddress.parse("example.com:25565")); + + assertEquals(new ServerAddress("127.0.0.0"), ServerAddress.parse("127.0.0.0")); + assertEquals(new ServerAddress("127.0.0.0", 0), ServerAddress.parse("127.0.0.0:0")); + assertEquals(new ServerAddress("127.0.0.0", 12345), ServerAddress.parse("127.0.0.0:12345")); + + assertEquals(new ServerAddress("::1"), ServerAddress.parse("[::1]")); + assertEquals(new ServerAddress("::1", 0), ServerAddress.parse("[::1]:0")); + assertEquals(new ServerAddress("::1", 12345), ServerAddress.parse("[::1]:12345")); + assertEquals(new ServerAddress("2001:db8::1"), ServerAddress.parse("[2001:db8::1]")); + assertEquals(new ServerAddress("2001:db8::1", 0), ServerAddress.parse("[2001:db8::1]:0")); + assertEquals(new ServerAddress("2001:db8::1", 12345), ServerAddress.parse("[2001:db8::1]:12345")); + + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[]]")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[]:0")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]|")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]|0")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:a")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:65536")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:-1")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[ ]:-1")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[-]:-1")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:a")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:65536")); + assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:-1")); + } +}