diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java new file mode 100644 index 000000000..f537b281d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java @@ -0,0 +1,127 @@ +/* + * 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.multiplayer; + +import org.jackhuang.hmcl.util.Lang; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.jackhuang.hmcl.util.Logging.LOG; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class LocalServerBroadcaster implements AutoCloseable { + private final String address; + private final ThreadGroup threadGroup = new ThreadGroup("JoinSession"); + + private boolean running = true; + + public LocalServerBroadcaster(String address) { + this.address = address; + this.threadGroup.setDaemon(true); + } + + @Override + public void close() { + running = false; + threadGroup.interrupt(); + } + + public static final Pattern ADDRESS_PATTERN = Pattern.compile("^\\s*(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d{1,5})\\s*$"); + + public void start() { + Thread forwardPortThread = new Thread(threadGroup, this::forwardPort, "ForwardPort"); + forwardPortThread.start(); + } + + private void forwardPort() { + try { + Matcher matcher = ADDRESS_PATTERN.matcher(address); + if (!matcher.find()) { + throw new MalformedURLException(); + } + try (SocketChannel forwardingSocket = SocketChannel.open(new InetSocketAddress(matcher.group(0), Lang.parseInt(matcher.group(1), 0))); + ServerSocketChannel serverSocket = ServerSocketChannel.open()) { + serverSocket.socket().bind(null); + + Thread broadcastMOTDThread = new Thread(threadGroup, () -> broadcastMOTD(serverSocket.socket().getLocalPort()), "BroadcastMOTD"); + broadcastMOTDThread.start(); + + while (running) { + SocketChannel forwardedSocket = serverSocket.accept(); + new Thread(threadGroup, () -> forwardTraffic(forwardingSocket, forwardedSocket), "Forward S->D").start(); + new Thread(threadGroup, () -> forwardTraffic(forwardedSocket, forwardingSocket), "Forward D->S").start(); + } + } + } catch (IOException e) { + LOG.log(Level.WARNING, "Error in forwarding port", e); + threadGroup.interrupt(); + } + } + + private void forwardTraffic(SocketChannel src, SocketChannel dest) { + try { + ByteBuffer buf = ByteBuffer.allocate(1024); + while (true) { + int len = src.read(buf); + if (len < 0) break; + dest.write(buf); + } + } catch (IOException e) { + LOG.log(Level.WARNING, "Disconnected", e); + } + } + + private void broadcastMOTD(int port) { + DatagramSocket socket; + InetAddress broadcastAddress; + try { + socket = new DatagramSocket(); + broadcastAddress = InetAddress.getByName("224.0.2.60"); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to create datagram socket", e); + return; + } + + while (running) { + try { + byte[] data = String.format("[MOTD]%s[/MOTD][AD]%d[/AD]", i18n("multiplayer.session.name.motd"), port).getBytes(StandardCharsets.UTF_8); + DatagramPacket packet = new DatagramPacket(data, 0, data.length, broadcastAddress, 4445); + socket.send(packet); + LOG.finest("Broadcast server 0.0.0.0:" + port); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to send motd packet", e); + } + + try { + Thread.sleep(1500); + } catch (InterruptedException ignored) { + return; + } + } + + socket.close(); + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 836aeff31..8e70a5a87 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -852,6 +852,7 @@ multiplayer.exit=HiPer exited unexpectedly with exit code %d multiplayer.hint=The multiplayer online function is in the experimental stage, if you have any questions, please go to mcer.cn to give feedback multiplayer.powered_by=This service is provided under the license of (Matrix Lab)License Agreement multiplayer.report=Illegal and Violation Report +multiplayer.session.name.motd=HMCL Multiplayer Session multiplayer.token=Token multiplayer.token.apply=Apply for a token multiplayer.token.expired=HiPer certificate expired, please restart HMCL and try again.