From a17832d06dd3b68d7a44471a0d579869d1fc98d7 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Wed, 21 Feb 2024 11:26:17 +0800 Subject: [PATCH 1/9] fix(gui): change jfx focus color --- urban-spork-client-gui/resource/console.css | 142 ++++++++++---------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/urban-spork-client-gui/resource/console.css b/urban-spork-client-gui/resource/console.css index 4208ca56..9aac40db 100644 --- a/urban-spork-client-gui/resource/console.css +++ b/urban-spork-client-gui/resource/console.css @@ -1,146 +1,148 @@ * { - -fx-primary-color: #f8f8ff; - -fx-primary-text: #000; - -fx-secondary-color: derive(#000, 90%); + -fx-primary-color: #f8f8ff; + -fx-primary-text: #000; + -fx-secondary-color: derive(#000, 90%); + -jfx-focus-color: -fx-secondary-color; } .root { - -fx-font-family: 'Microsoft YaHei UI'; - -fx-font-size: 12px; + -fx-font-family: 'Microsoft YaHei UI'; + -fx-font-size: 12px; } .jfx-tab-pane { - -fx-pref-height: 500; - /* -fx-padding: 1px; */ - /* -fx-background-color: -fx-secondary-color, -fx-primary-color; */ - /* -fx-background-insets: 0, 1; */ - -fx-pref-width: 500; + -fx-pref-height: 500; + /* -fx-padding: 1px; */ + /* -fx-background-color: -fx-secondary-color, -fx-primary-color; */ + /* -fx-background-insets: 0, 1; */ + -fx-pref-width: 500; } .jfx-tab-pane .headers-region { - -fx-background-color: -fx-secondary-color; + -fx-background-color: -fx-secondary-color; } .jfx-tab-pane .tab-header-background { - -fx-background-color: -fx-secondary-color; + -fx-background-color: -fx-secondary-color; } .jfx-tab-pane .tab-header-area .jfx-rippler { - -jfx-rippler-fill: #000; + -jfx-rippler-fill: #000; } .jfx-tab-pane .tab-header-area .tab-selected-line { - -fx-background-color: #000; + -fx-background-color: #000; } GridPane { - /* debug */ - /* -fx-grid-lines-visible: true;*/ + /* debug */ + /* -fx-grid-lines-visible: true;*/ } .label { - -fx-alignment: center-right; - /* -fx-border-color: -fx-secondary-color; */ - -fx-min-width: 65; + -fx-alignment: center-right; + /* -fx-border-color: -fx-secondary-color; */ + -fx-min-width: 65; } .jfx-button { - -fx-background-color: -fx-secondary-color; - -fx-background-radius: 5px; - -fx-border-radius: 5px; - -jfx-button-type: RAISED; - -fx-content-display: center; - -fx-graphic-text-gap: 20; - -fx-pref-height: 30; - -fx-pref-width: 70; - /* -fx-border-color: -fx-secondary-color; */ - -fx-text-alignment: center; - -fx-text-fill: -fx-primary-text; + -fx-background-color: -fx-secondary-color; + -fx-background-radius: 5px; + -fx-border-radius: 5px; + -jfx-button-type: RAISED; + -fx-content-display: center; + -fx-graphic-text-gap: 20; + -fx-pref-height: 30; + -fx-pref-width: 70; + /* -fx-border-color: -fx-secondary-color; */ + -fx-text-alignment: center; + -fx-text-fill: -fx-primary-text; } .hide-show.toggle-button { - -fx-background-image: url('image/eye.png'); - -fx-background-position: 100% 50%; - -fx-background-repeat: no-repeat; + -fx-background-image: url('image/eye.png'); + -fx-background-position: 100% 50%; + -fx-background-repeat: no-repeat; } .hide-show.toggle-button:selected { - -fx-background-image: url('image/eye-close.png'); + -fx-background-image: url('image/eye-close.png'); } .choice-box .label { - -fx-alignment: center-left; - /* -fx-font-family: 'Georgia'; */ - -fx-font-size: 13px; - -fx-font-weight: bold; - /* -fx-background-color: #000000; */ - /* -fx-mark-color: orange; */ + -fx-alignment: center-left; + /* -fx-font-family: 'Georgia'; */ + -fx-font-size: 13px; + -fx-font-weight: bold; + /* -fx-background-color: #000000; */ + /* -fx-mark-color: orange; */ } .list-view { - -fx-background-color: transparent, #fff, transparent, #fff; - -fx-background-insets: 0; - -fx-border-color: #d3d3d3; + -fx-background-color: transparent, #fff, transparent, #fff; + -fx-background-insets: 0; + -fx-border-color: #d3d3d3; } .list-view:focused { - -fx-background-color: transparent, #fff, transparent, #fff; + -fx-background-color: transparent, #fff, transparent, #fff; } .list-view .list-cell:selected { - -fx-font-weight: 700; + -fx-font-weight: 700; + -fx-background-color: -fx-secondary-color; } .jfx-list-view .scroll-bar:horizontal .track, .jfx-list-view .scroll-bar:vertical .track { - -fx-background-color: transparent; - -fx-background-radius: 0; - -fx-border-color: transparent; - -fx-border-radius: 2em; + -fx-background-color: transparent; + -fx-background-radius: 0; + -fx-border-color: transparent; + -fx-border-radius: 2em; } .jfx-list-view .scroll-bar:horizontal .increment-button, .jfx-list-view .scroll-bar:horizontal .decrement-button { - -fx-background-color: transparent; - -fx-background-radius: 0; - -fx-padding: 0 0 10; + -fx-background-color: transparent; + -fx-background-radius: 0; + -fx-padding: 0 0 10; } .jfx-list-view .scroll-bar:vertical .increment-button, .jfx-list-view .scroll-bar:vertical .decrement-button { - -fx-background-color: transparent; - -fx-background-radius: 0; - -fx-padding: 0 10 0 0; + -fx-background-color: transparent; + -fx-background-radius: 0; + -fx-padding: 0 10 0 0; } .jfx-list-view .scroll-bar .increment-arrow, .jfx-list-view .scroll-bar .decrement-arrow { - -fx-padding: 0; - -fx-shape: ' '; + -fx-padding: 0; + -fx-shape: ' '; } .jfx-list-view .scroll-bar:horizontal .thumb, .jfx-list-view .scroll-bar:vertical .thumb { - -fx-background-color: derive(#000, 90%); - -fx-background-insets: 2, 0, 0; - -fx-background-radius: 2em; + -fx-background-color: derive(#000, 90%); + -fx-background-insets: 2, 0, 0; + -fx-background-radius: 2em; } .text-area { - -fx-background-color: transparent, #fff, transparent, #fff; - -fx-background-insets: 0; - -fx-font-family: 'Consolas'; - -fx-font-size: 13px; - -fx-font-weight: 800; - -fx-highlight-fill: #0f0; - -fx-highlight-text-fill: #000; - -fx-text-fill: #0f0; + -fx-background-color: transparent, #fff, transparent, #fff; + -fx-background-insets: 0; + -fx-font-family: 'Consolas'; + -fx-font-size: 13px; + -fx-font-weight: 800; + -fx-highlight-fill: #0f0; + -fx-highlight-text-fill: #000; + -fx-text-fill: #0f0; } .text-area:focused { - -fx-background-color: transparent, #fff, transparent, #fff; + -fx-background-color: transparent, #fff, transparent, #fff; } .text-area .content { - -fx-background-color: #000; + -fx-background-color: #000; } From 55b512388a64958d2c4c7203858794c56d7fce5f Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Fri, 23 Feb 2024 12:00:50 +0800 Subject: [PATCH 2/9] feat(ss): handle tcp relay codec exception --- README.md | 2 -- .../shadowsocks/ClientUdpRelayHandler.java | 2 +- .../common/channel/ExceptionHandler.java | 15 +++------------ .../codec/shadowsocks/tcp/TcpRelayCodec.java | 18 ++++++++++++++++++ .../src/com/urbanspork/server/Server.java | 3 ++- .../urbanspork/server/ServerInitializer.java | 2 +- .../channel/ExceptionHandlerTestCase.java | 3 +-- .../shadowsocks/EmbeddedChannelTestCase.java | 11 ++++++++--- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ef51eda7..7a0d9c97 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ A sock5 proxy -**WARNING**: Be aware of the risk when using this software because neither **Detection Prevention** nor **Replay Protection** is implemented currently - ## Quick start put *config.json* file into the unpacked folder before running diff --git a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUdpRelayHandler.java b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUdpRelayHandler.java index 4cba26d5..ed156ea8 100644 --- a/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUdpRelayHandler.java +++ b/urban-spork-client/src/com/urbanspork/client/shadowsocks/ClientUdpRelayHandler.java @@ -51,7 +51,7 @@ protected void initChannel(Channel ch) { ch.pipeline().addLast( new UdpRelayCodec(config, Mode.Client), new InboundHandler(inboundChannel, sender),// server->client->sender - new ExceptionHandler(config, Mode.Client) + new ExceptionHandler(config) ); } }).bind(0) // automatically assigned port now, may have security implications diff --git a/urban-spork-common/src/com/urbanspork/common/channel/ExceptionHandler.java b/urban-spork-common/src/com/urbanspork/common/channel/ExceptionHandler.java index 0ee98059..edcf7c42 100644 --- a/urban-spork-common/src/com/urbanspork/common/channel/ExceptionHandler.java +++ b/urban-spork-common/src/com/urbanspork/common/channel/ExceptionHandler.java @@ -1,6 +1,5 @@ package com.urbanspork.common.channel; -import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.Protocol; import com.urbanspork.common.transport.Transport; @@ -8,7 +7,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.socket.DatagramChannel; -import io.netty.channel.socket.SocketChannel; import org.bouncycastle.crypto.InvalidCipherTextException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,11 +16,9 @@ public class ExceptionHandler extends ChannelInboundHandlerAdapter { private static final Logger logger = LoggerFactory.getLogger(ExceptionHandler.class); private final ServerConfig config; - private final Mode mode; - public ExceptionHandler(ServerConfig config, Mode mode) { + public ExceptionHandler(ServerConfig config) { this.config = config; - this.mode = mode; } @Override @@ -37,15 +33,10 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { String msg = String.format("[%s][%s][%s] Caught exception", transport, protocol, transLog); logger.error(msg, cause); } - if (channel instanceof SocketChannel socketChannel && Mode.Server == mode) { - socketChannel.config().setSoLinger(0); - ctx.deregister(); - } else { - ctx.close(); - } + ctx.close(); } - private static String transLog(Channel channel) { + public static String transLog(Channel channel) { return channel.localAddress() + (channel.isActive() ? "-" : "!") + channel.remoteAddress(); } } diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TcpRelayCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TcpRelayCodec.java index ba10a146..fdde2c40 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TcpRelayCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/TcpRelayCodec.java @@ -1,17 +1,22 @@ package com.urbanspork.common.codec.shadowsocks.tcp; +import com.urbanspork.common.channel.ExceptionHandler; import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.shadowsocks.Identity; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.ByteToMessageCodec; import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; public class TcpRelayCodec extends ByteToMessageCodec { + private static final Logger logger = LoggerFactory.getLogger(TcpRelayCodec.class); private final Session session; private final AeadCipherCodec cipher; @@ -35,4 +40,17 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throw protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { cipher.decode(session, in, out); } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (Mode.Server == session.mode() && ctx.channel() instanceof SocketChannel socketChannel) { + String transLog = ExceptionHandler.transLog(ctx.channel()); + logger.error("[tcp][{}] {}", transLog, cause.getMessage()); + ctx.deregister(); + socketChannel.config().setSoLinger(0); + socketChannel.shutdownOutput().addListener(future -> socketChannel.unsafe().beginRead()); + } else { + ctx.fireExceptionCaught(cause); + } + } } \ No newline at end of file diff --git a/urban-spork-server/src/com/urbanspork/server/Server.java b/urban-spork-server/src/com/urbanspork/server/Server.java index 4bede1a9..7a052704 100644 --- a/urban-spork-server/src/com/urbanspork/server/Server.java +++ b/urban-spork-server/src/com/urbanspork/server/Server.java @@ -94,6 +94,7 @@ private static Instance startup(EventLoopGroup bossGroup, EventLoopGroup workerG tcp = (ServerSocketChannel) new ServerBootstrap().group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ServerInitializer(config, context)) + .childOption(ChannelOption.ALLOW_HALF_CLOSURE, true) .bind(config.getPort()).sync().addListener(future -> logger.info("Startup tcp server => {}", config)).channel() .closeFuture().addListener(future -> context.release()).channel(); } catch (Exception e) { @@ -115,7 +116,7 @@ protected void initChannel(Channel ch) { ch.pipeline().addLast( new UdpRelayCodec(config, Mode.Server), new ServerUDPRelayHandler(config.getPacketEncoding(), workerGroup), - new ExceptionHandler(config, Mode.Server) + new ExceptionHandler(config) ); } }) diff --git a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java index d50471e6..70a29c35 100644 --- a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java @@ -29,6 +29,6 @@ protected void initChannel(Channel c) { } else { pipeline.addLast(new TcpRelayCodec(context, config, Mode.Server)); } - pipeline.addLast(new RemoteConnectHandler(config), new ExceptionHandler(config, Mode.Server)); + pipeline.addLast(new RemoteConnectHandler(config), new ExceptionHandler(config)); } } diff --git a/urban-spork-test/test/com/urbanspork/common/channel/ExceptionHandlerTestCase.java b/urban-spork-test/test/com/urbanspork/common/channel/ExceptionHandlerTestCase.java index 65b53614..9b9b79ce 100644 --- a/urban-spork-test/test/com/urbanspork/common/channel/ExceptionHandlerTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/channel/ExceptionHandlerTestCase.java @@ -1,6 +1,5 @@ package com.urbanspork.common.channel; -import com.urbanspork.common.codec.shadowsocks.Mode; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.config.ServerConfigTestCase; import com.urbanspork.test.TestDice; @@ -22,7 +21,7 @@ void testCaughtException() { public void channelRead(ChannelHandlerContext ctx, Object msg) { throw new UnsupportedOperationException(msg.toString()); } - }, new ExceptionHandler(config, Mode.Client)); + }, new ExceptionHandler(config)); Assertions.assertTrue(channel.isActive()); channel.writeInbound("Testcase"); Assertions.assertFalse(channel.isActive()); diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java index 3685a5e8..2b74c03e 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java @@ -126,9 +126,14 @@ void testAead2022TcpAntiReplay() { DefaultSocks5CommandRequest request = new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, Socks5AddressType.DOMAIN, "localhost", 16800); client.pipeline().addLast(new TcpRelayCodec(context, config, request, Mode.Client)); client.writeOutbound(Unpooled.wrappedBuffer(Dice.rollBytes(10))); - ByteBuf msg = client.readOutbound(); - server1.writeInbound(msg.copy()); - Assertions.assertThrows(DecoderException.class, () -> server2.writeInbound(msg)); + ByteBuf msg1 = client.readOutbound(); + ByteBuf msg2 = msg1.copy(); + Assertions.assertTrue(msg1.isReadable()); + Assertions.assertTrue(msg2.isReadable()); + server1.writeInbound(msg1); + ByteBuf tooShortMsg = Unpooled.wrappedBuffer(Dice.rollBytes(33)); + Assertions.assertThrows(DecoderException.class, () -> server2.writeInbound(tooShortMsg)); + Assertions.assertThrows(DecoderException.class, () -> server2.writeInbound(msg2)); client.close(); server1.close(); server2.close(); From 306dd80cd0b1e0f68147909fe260233f6fad9d5b Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Mon, 26 Feb 2024 17:14:02 +0800 Subject: [PATCH 3/9] test: add missing test --- .../common/codec/shadowsocks/tcp/Context.java | 9 +++++---- .../codec/shadowsocks/tcp/ContextTestCase.java | 13 +++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/ContextTestCase.java diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java index ed2cbe9c..f9e82215 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/Context.java @@ -38,10 +38,11 @@ public void release() { record Key(byte[] nonce) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Key nonce1 = (Key) o; - return Arrays.equals(nonce, nonce1.nonce); + if (o instanceof Key other) { + return Arrays.equals(nonce, other.nonce); + } else { + return false; + } } @Override diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/ContextTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/ContextTestCase.java new file mode 100644 index 00000000..9ceedab7 --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/ContextTestCase.java @@ -0,0 +1,13 @@ +package com.urbanspork.common.codec.shadowsocks.tcp; + +import com.urbanspork.test.TestUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Shadowsocks - TCP Context") +class ContextTestCase { + @Test + void testEqualsAndHashcode() { + TestUtil.testEqualsAndHashcode(new Context.Key(new byte[]{1, 2, 3}), new Context.Key(new byte[]{1, 2, 4})); + } +} From 459b50e4dadb08138aaf734ca2bb6070882cfeb8 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Mon, 26 Feb 2024 17:40:02 +0800 Subject: [PATCH 4/9] refactor: make code of server relay more understandable --- .../common/channel/AttributeKeys.java | 2 - .../shadowsocks/tcp/AeadCipherCodec.java | 12 ++-- .../udp/Aead2022CipherCodecImpl.java | 20 +++--- .../shadowsocks/udp/AeadCipherCodec.java | 5 +- .../shadowsocks/udp/AeadCipherCodecImpl.java | 8 +-- .../codec/shadowsocks/udp/UdpRelayCodec.java | 7 +- .../common/protocol/socks/Address.java | 5 -- .../common/transport/tcp/RelayingPayload.java | 5 ++ .../common/transport/udp/RelayingPacket.java | 5 ++ .../com/urbanspork/common/util/LruCache.java | 2 +- .../src/com/urbanspork/server/Server.java | 2 +- .../urbanspork/server/ServerInitializer.java | 6 +- ...ctHandler.java => ServerRelayHandler.java} | 65 +++++++++---------- ...PCodec.java => ServerUdpOverTcpCodec.java} | 4 +- ...andler.java => ServerUdpRelayHandler.java} | 10 ++- ...verAEADCodec.java => ServerAeadCodec.java} | 40 ++++++++---- .../client/vmess/ClientAEADCodecTestCase.java | 4 +- .../shadowsocks/EmbeddedChannelTestCase.java | 7 +- .../tcp/AeadCipherCodecsTestCase.java | 9 ++- .../udp/AeadCipherCodecTestCase.java | 19 ++---- .../udp/AeadCipherCodecsTestCase.java | 22 +++---- ...e.java => ServerRelayHandlerTestCase.java} | 10 +-- .../server/ServerUDPRelayHandlerTestCase.java | 6 +- .../server/vmess/ServerAEADCodecTestCase.java | 6 +- 24 files changed, 147 insertions(+), 134 deletions(-) create mode 100644 urban-spork-common/src/com/urbanspork/common/transport/tcp/RelayingPayload.java create mode 100644 urban-spork-common/src/com/urbanspork/common/transport/udp/RelayingPacket.java rename urban-spork-server/src/com/urbanspork/server/{RemoteConnectHandler.java => ServerRelayHandler.java} (54%) rename urban-spork-server/src/com/urbanspork/server/{ServerUDPOverTCPCodec.java => ServerUdpOverTcpCodec.java} (88%) rename urban-spork-server/src/com/urbanspork/server/{ServerUDPRelayHandler.java => ServerUdpRelayHandler.java} (94%) rename urban-spork-server/src/com/urbanspork/server/vmess/{ServerAEADCodec.java => ServerAeadCodec.java} (84%) rename urban-spork-test/test/com/urbanspork/server/{RemoteConnectHandlerTestCase.java => ServerRelayHandlerTestCase.java} (62%) diff --git a/urban-spork-common/src/com/urbanspork/common/channel/AttributeKeys.java b/urban-spork-common/src/com/urbanspork/common/channel/AttributeKeys.java index 661f63b0..37016369 100644 --- a/urban-spork-common/src/com/urbanspork/common/channel/AttributeKeys.java +++ b/urban-spork-common/src/com/urbanspork/common/channel/AttributeKeys.java @@ -1,14 +1,12 @@ package com.urbanspork.common.channel; import com.urbanspork.common.config.ServerConfig; -import com.urbanspork.common.transport.Transport; import io.netty.util.AttributeKey; public class AttributeKeys { public static final AttributeKey SERVER_CONFIG = AttributeKey.newInstance("SERVER_CONFIG"); public static final AttributeKey SOCKS_PORT = AttributeKey.newInstance("SOCKS5_PORT"); - public static final AttributeKey TRANSPORT = AttributeKey.newInstance("TRANSPORT"); private AttributeKeys() {} } diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java index ef9c1478..eae88792 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodec.java @@ -11,6 +11,7 @@ import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; import com.urbanspork.common.protocol.socks.Address; +import com.urbanspork.common.transport.tcp.RelayingPayload; import com.urbanspork.common.util.ByteString; import com.urbanspork.common.util.Dice; import io.netty.buffer.ByteBuf; @@ -20,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; @@ -179,10 +181,10 @@ private void initAEAD2022PayloadDecoder(Session session, ByteBuf in, List(address, first)); } else { out.add(first); } @@ -193,14 +195,16 @@ private void initPayloadDecoder(Session session, ByteBuf in, List out) t byte[] salt = session.identity().salt(); in.readBytes(salt); PayloadDecoder newPayloadDecoder = AEAD.TCP.newPayloadDecoder(cipherMethod, keys.encKey(), salt); - List list = new ArrayList<>(1); + List list = new ArrayList<>(); newPayloadDecoder.decodePayload(in, list); if (list.isEmpty()) { in.resetReaderIndex(); return; } if (Mode.Server == session.mode()) { - Address.decode((ByteBuf) list.getFirst(), out); + ByteBuf first = (ByteBuf) list.getFirst(); + InetSocketAddress address = Address.decode(first); + list.set(0, new RelayingPayload<>(address, first)); } out.addAll(list); this.payloadDecoder = newPayloadDecoder; diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java index e118ebf4..8c12215e 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/Aead2022CipherCodecImpl.java @@ -9,6 +9,7 @@ import com.urbanspork.common.protocol.shadowsocks.aead2022.AEAD2022; import com.urbanspork.common.protocol.shadowsocks.aead2022.UdpCipher; import com.urbanspork.common.protocol.socks.Address; +import com.urbanspork.common.transport.udp.RelayingPacket; import com.urbanspork.common.util.Dice; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -18,7 +19,6 @@ import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; -import java.util.List; class Aead2022CipherCodecImpl implements AeadCipherCodec { private static final Logger logger = LoggerFactory.getLogger(Aead2022CipherCodecImpl.class); @@ -101,16 +101,16 @@ private void encodeServerPacketAead2022(Context context, ByteBuf msg, ByteBuf ou } @Override - public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + public RelayingPacket decode(Context context, ByteBuf in) throws InvalidCipherTextException { if (Mode.Client == context.mode()) { - decodeServerPocketAead2022(context, in, out); + return decodeServerPocketAead2022(context, in); } else { - decodeClientPocketAead2022(context, in, out); + return decodeClientPocketAead2022(context, in); } } // Client -> Server - private void decodeClientPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + private RelayingPacket decodeClientPocketAead2022(Context context, ByteBuf in) throws InvalidCipherTextException { int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); int tagSize = cipherMethod.tagSize(); boolean requireEih = cipherKind.supportEih() && context.userManager().userCount() > 0; @@ -140,12 +140,12 @@ private void decodeClientPocketAead2022(Context context, ByteBuf in, List(address, packet); } // Server -> Client - private void decodeServerPocketAead2022(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + private RelayingPacket decodeServerPocketAead2022(Context context, ByteBuf in) throws InvalidCipherTextException { int nonceLength = AEAD2022.UDP.getNonceLength(cipherKind); int tagSize = cipherMethod.tagSize(); int headerLength = nonceLength + tagSize + 8 + 8 + 1 + 8 + 2; @@ -171,7 +171,7 @@ private void decodeServerPocketAead2022(Context context, ByteBuf in, List(address, packet); } } \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java index ae314b1c..4950bd10 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodec.java @@ -1,13 +1,12 @@ package com.urbanspork.common.codec.shadowsocks.udp; +import com.urbanspork.common.transport.udp.RelayingPacket; import io.netty.buffer.ByteBuf; import org.bouncycastle.crypto.InvalidCipherTextException; -import java.util.List; - interface AeadCipherCodec { void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCipherTextException; - void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException; + RelayingPacket decode(Context context, ByteBuf in) throws InvalidCipherTextException; } diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java index 785d7408..3163fa96 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecImpl.java @@ -4,12 +4,12 @@ import com.urbanspork.common.codec.shadowsocks.Keys; import com.urbanspork.common.protocol.shadowsocks.aead.AEAD; import com.urbanspork.common.protocol.socks.Address; +import com.urbanspork.common.transport.udp.RelayingPacket; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.bouncycastle.crypto.InvalidCipherTextException; import java.net.InetSocketAddress; -import java.util.List; /** * AEAD Cipher Codec @@ -39,11 +39,11 @@ public void encode(Context context, ByteBuf msg, ByteBuf out) throws InvalidCiph } @Override - public void decode(Context context, ByteBuf in, List out) throws InvalidCipherTextException { + public RelayingPacket decode(Context context, ByteBuf in) throws InvalidCipherTextException { byte[] salt = context.control().salt(); in.readBytes(salt); ByteBuf packet = AEAD.UDP.newPayloadDecoder(cipherMethod, keys.encKey(), salt).decodePacket(in); - Address.decode(packet, out); - out.add(packet.slice()); + InetSocketAddress address = Address.decode(packet); + return new RelayingPacket<>(address, packet); } } \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UdpRelayCodec.java b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UdpRelayCodec.java index 595c60c1..7f77e010 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UdpRelayCodec.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/shadowsocks/udp/UdpRelayCodec.java @@ -5,6 +5,7 @@ import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.shadowsocks.Control; import com.urbanspork.common.transport.udp.DatagramPacketWrapper; +import com.urbanspork.common.transport.udp.RelayingPacket; import com.urbanspork.common.util.LruCache; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -17,7 +18,6 @@ import java.net.InetSocketAddress; import java.time.Duration; -import java.util.ArrayList; import java.util.List; public class UdpRelayCodec extends MessageToMessageCodec { @@ -55,16 +55,15 @@ protected void encode(ChannelHandlerContext ctx, DatagramPacketWrapper msg, List @Override protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List out) throws Exception { - List list = new ArrayList<>(2); Control control = getControl(msg.sender()); Context context = new Context(mode, control, null, userManager); - cipher.decode(context, msg.content(), list); + RelayingPacket packet = cipher.decode(context, msg.content()); logger.trace("[udp][{}][decode]{}|{}", mode, msg.sender(), control); if (cipher instanceof Aead2022CipherCodecImpl && !control.validatePacketId()) { logger.error("[udp][{}→]{} packet_id {} out of window", mode, msg.sender(), control.getPacketId()); return; } - out.add(new DatagramPacket((ByteBuf) list.get(1), (InetSocketAddress) list.get(0), msg.sender())); + out.add(new DatagramPacket(packet.content(), packet.address(), msg.sender())); } @Override diff --git a/urban-spork-common/src/com/urbanspork/common/protocol/socks/Address.java b/urban-spork-common/src/com/urbanspork/common/protocol/socks/Address.java index b5d5db25..ac164457 100644 --- a/urban-spork-common/src/com/urbanspork/common/protocol/socks/Address.java +++ b/urban-spork-common/src/com/urbanspork/common/protocol/socks/Address.java @@ -12,7 +12,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.util.List; public interface Address { @@ -45,10 +44,6 @@ static void encode(Socks5CommandRequest request, ByteBuf out) { out.writeShort(request.dstPort()); } - static void decode(ByteBuf in, List out) { - out.add(decode(in)); - } - static InetSocketAddress decode(ByteBuf in) { Socks5AddressType addressType = Socks5AddressType.valueOf(in.readByte()); if (addressType == Socks5AddressType.IPv4) { diff --git a/urban-spork-common/src/com/urbanspork/common/transport/tcp/RelayingPayload.java b/urban-spork-common/src/com/urbanspork/common/transport/tcp/RelayingPayload.java new file mode 100644 index 00000000..53de72e3 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/transport/tcp/RelayingPayload.java @@ -0,0 +1,5 @@ +package com.urbanspork.common.transport.tcp; + +import java.net.InetSocketAddress; + +public record RelayingPayload(InetSocketAddress address, T content) {} diff --git a/urban-spork-common/src/com/urbanspork/common/transport/udp/RelayingPacket.java b/urban-spork-common/src/com/urbanspork/common/transport/udp/RelayingPacket.java new file mode 100644 index 00000000..e097b5c9 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/transport/udp/RelayingPacket.java @@ -0,0 +1,5 @@ +package com.urbanspork.common.transport.udp; + +import java.net.InetSocketAddress; + +public record RelayingPacket(InetSocketAddress address, T content) {} diff --git a/urban-spork-common/src/com/urbanspork/common/util/LruCache.java b/urban-spork-common/src/com/urbanspork/common/util/LruCache.java index 0db26af3..27223a48 100644 --- a/urban-spork-common/src/com/urbanspork/common/util/LruCache.java +++ b/urban-spork-common/src/com/urbanspork/common/util/LruCache.java @@ -85,7 +85,7 @@ private void expire(K key, V value) { } static class Pair { - V value; + final V value; Timeout timeout; public Pair(V value, Timeout timeout) { diff --git a/urban-spork-server/src/com/urbanspork/server/Server.java b/urban-spork-server/src/com/urbanspork/server/Server.java index 7a052704..0f47b66f 100644 --- a/urban-spork-server/src/com/urbanspork/server/Server.java +++ b/urban-spork-server/src/com/urbanspork/server/Server.java @@ -115,7 +115,7 @@ private static Optional startupUdp(EventLoopGroup bossGroup, Ev protected void initChannel(Channel ch) { ch.pipeline().addLast( new UdpRelayCodec(config, Mode.Server), - new ServerUDPRelayHandler(config.getPacketEncoding(), workerGroup), + new ServerUdpRelayHandler(config.getPacketEncoding(), workerGroup), new ExceptionHandler(config) ); } diff --git a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java index 70a29c35..7a89abc0 100644 --- a/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerInitializer.java @@ -6,7 +6,7 @@ import com.urbanspork.common.codec.shadowsocks.tcp.TcpRelayCodec; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.Protocol; -import com.urbanspork.server.vmess.ServerAEADCodec; +import com.urbanspork.server.vmess.ServerAeadCodec; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; @@ -25,10 +25,10 @@ public ServerInitializer(ServerConfig config, Context context) { protected void initChannel(Channel c) { ChannelPipeline pipeline = c.pipeline(); if (Protocol.vmess == config.getProtocol()) { - pipeline.addLast(new ServerAEADCodec(config)); + pipeline.addLast(new ServerAeadCodec(config)); } else { pipeline.addLast(new TcpRelayCodec(context, config, Mode.Server)); } - pipeline.addLast(new RemoteConnectHandler(config), new ExceptionHandler(config)); + pipeline.addLast(new ServerRelayHandler(config), new ExceptionHandler(config)); } } diff --git a/urban-spork-server/src/com/urbanspork/server/RemoteConnectHandler.java b/urban-spork-server/src/com/urbanspork/server/ServerRelayHandler.java similarity index 54% rename from urban-spork-server/src/com/urbanspork/server/RemoteConnectHandler.java rename to urban-spork-server/src/com/urbanspork/server/ServerRelayHandler.java index e3a021bf..d7871fdc 100644 --- a/urban-spork-server/src/com/urbanspork/server/RemoteConnectHandler.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerRelayHandler.java @@ -1,9 +1,9 @@ package com.urbanspork.server; -import com.urbanspork.common.channel.AttributeKeys; import com.urbanspork.common.channel.DefaultChannelInboundHandler; import com.urbanspork.common.config.ServerConfig; -import com.urbanspork.common.transport.Transport; +import com.urbanspork.common.transport.tcp.RelayingPayload; +import com.urbanspork.common.transport.udp.RelayingPacket; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; @@ -18,55 +18,48 @@ import java.net.InetSocketAddress; -class RemoteConnectHandler extends ChannelInboundHandlerAdapter { +class ServerRelayHandler extends ChannelInboundHandlerAdapter { - private static final Logger logger = LoggerFactory.getLogger(RemoteConnectHandler.class); + private static final Logger logger = LoggerFactory.getLogger(ServerRelayHandler.class); private final ServerConfig config; private Promise p; - public RemoteConnectHandler(ServerConfig config) { + public ServerRelayHandler(ServerConfig config) { this.config = config; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (Transport.UDP.equals(ctx.channel().attr(AttributeKeys.TRANSPORT).get())) { - udp(ctx, msg); - } else { - tcp(ctx, msg); + if (msg instanceof RelayingPayload payload) { + relayTcp(ctx, payload); + } else if (msg instanceof RelayingPacket pocket) { + relayUdp(ctx, pocket); + } else { // should always relay tcp + p.addListener(future -> ctx.fireChannelRead(msg)); } } - private void udp(ChannelHandlerContext ctx, Object msg) { - if (msg instanceof InetSocketAddress address) { - ctx.pipeline().addLast( - new ServerUDPOverTCPCodec(address), - new ServerUDPRelayHandler(config.getPacketEncoding(), ctx.channel().eventLoop().parent().next()) - ); - } else { - ctx.fireChannelRead(msg); - } + private void relayUdp(ChannelHandlerContext ctx, RelayingPacket relayingPayload) { + ctx.pipeline().remove(this).addLast( + new ServerUdpOverTcpCodec(relayingPayload.address()), + new ServerUdpRelayHandler(config.getPacketEncoding(), ctx.channel().eventLoop().parent().next()) + ); + ctx.fireChannelRead(relayingPayload.content()); } - private void tcp(ChannelHandlerContext ctx, Object msg) { + private void relayTcp(ChannelHandlerContext ctx, RelayingPayload relayingPayload) { Channel localChannel = ctx.channel(); - if (msg instanceof InetSocketAddress address) { - p = ctx.executor().newPromise(); - connect(localChannel, address, p); - } else { - p.addListener((FutureListener) future -> { - if (future.isSuccess()) { - Channel remoteChannel = future.get(); - localChannel.pipeline().addLast(new DefaultChannelInboundHandler(remoteChannel)); - if (!ctx.isRemoved()) { - localChannel.pipeline().remove(RemoteConnectHandler.this); - } - remoteChannel.writeAndFlush(msg); - } else { - ctx.close(); - } - }); - } + p = ctx.executor().newPromise(); + connect(localChannel, relayingPayload.address(), p); + p.addListener((FutureListener) future -> { + if (future.isSuccess()) { + Channel remoteChannel = future.get(); + localChannel.pipeline().remove(this).addLast(new DefaultChannelInboundHandler(remoteChannel)); + remoteChannel.writeAndFlush(relayingPayload.content()); + } else { + ctx.close(); + } + }); } private void connect(Channel localChannel, InetSocketAddress remoteAddress, Promise promise) { diff --git a/urban-spork-server/src/com/urbanspork/server/ServerUDPOverTCPCodec.java b/urban-spork-server/src/com/urbanspork/server/ServerUdpOverTcpCodec.java similarity index 88% rename from urban-spork-server/src/com/urbanspork/server/ServerUDPOverTCPCodec.java rename to urban-spork-server/src/com/urbanspork/server/ServerUdpOverTcpCodec.java index 8a40d355..dc0d3f6c 100644 --- a/urban-spork-server/src/com/urbanspork/server/ServerUDPOverTCPCodec.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerUdpOverTcpCodec.java @@ -9,11 +9,11 @@ import java.net.InetSocketAddress; import java.util.List; -class ServerUDPOverTCPCodec extends MessageToMessageCodec { +class ServerUdpOverTcpCodec extends MessageToMessageCodec { private final InetSocketAddress address; - ServerUDPOverTCPCodec(InetSocketAddress address) { + ServerUdpOverTcpCodec(InetSocketAddress address) { this.address = address; } diff --git a/urban-spork-server/src/com/urbanspork/server/ServerUDPRelayHandler.java b/urban-spork-server/src/com/urbanspork/server/ServerUdpRelayHandler.java similarity index 94% rename from urban-spork-server/src/com/urbanspork/server/ServerUDPRelayHandler.java rename to urban-spork-server/src/com/urbanspork/server/ServerUdpRelayHandler.java index 0bea3610..8117b211 100644 --- a/urban-spork-server/src/com/urbanspork/server/ServerUDPRelayHandler.java +++ b/urban-spork-server/src/com/urbanspork/server/ServerUdpRelayHandler.java @@ -21,14 +21,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -public class ServerUDPRelayHandler extends SimpleChannelInboundHandler { +public class ServerUdpRelayHandler extends SimpleChannelInboundHandler { - private static final Logger logger = LoggerFactory.getLogger(ServerUDPRelayHandler.class); + private static final Logger logger = LoggerFactory.getLogger(ServerUdpRelayHandler.class); private final Map workerChannels = new ConcurrentHashMap<>(); private final EventLoopGroup workerGroup; private final PacketEncoding packetEncoding; - public ServerUDPRelayHandler(PacketEncoding packetEncoding, EventLoopGroup workerGroup) { + public ServerUdpRelayHandler(PacketEncoding packetEncoding, EventLoopGroup workerGroup) { super(false); this.packetEncoding = packetEncoding; this.workerGroup = workerGroup; @@ -73,9 +73,7 @@ protected void initChannel(Channel ch) { .channel(); workerChannel.closeFuture().addListener(future -> { Channel removed = workerChannels.remove(callback); - if (removed != null) { - logger.info("[udp][binding]{} != {}", callback, removed); - } + logger.info("[udp][binding]{} != {}", callback, removed); }); logger.info("[udp][binding]{} == {}", callback, workerChannel); return workerChannel; diff --git a/urban-spork-server/src/com/urbanspork/server/vmess/ServerAEADCodec.java b/urban-spork-server/src/com/urbanspork/server/vmess/ServerAeadCodec.java similarity index 84% rename from urban-spork-server/src/com/urbanspork/server/vmess/ServerAEADCodec.java rename to urban-spork-server/src/com/urbanspork/server/vmess/ServerAeadCodec.java index 668a076a..f393490d 100644 --- a/urban-spork-server/src/com/urbanspork/server/vmess/ServerAEADCodec.java +++ b/urban-spork-server/src/com/urbanspork/server/vmess/ServerAeadCodec.java @@ -1,6 +1,5 @@ package com.urbanspork.server.vmess; -import com.urbanspork.common.channel.AttributeKeys; import com.urbanspork.common.codec.aead.CipherMethod; import com.urbanspork.common.codec.aead.CipherMethods; import com.urbanspork.common.codec.aead.PayloadDecoder; @@ -18,7 +17,8 @@ import com.urbanspork.common.protocol.vmess.header.RequestHeader; import com.urbanspork.common.protocol.vmess.header.RequestOption; import com.urbanspork.common.protocol.vmess.header.SecurityType; -import com.urbanspork.common.transport.Transport; +import com.urbanspork.common.transport.tcp.RelayingPayload; +import com.urbanspork.common.transport.udp.RelayingPacket; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -27,6 +27,7 @@ import org.bouncycastle.crypto.InvalidCipherTextException; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -36,7 +37,7 @@ import static com.urbanspork.common.protocol.vmess.aead.Const.KDF_SALT_AEAD_RESP_HEADER_PAYLOAD_KEY; import static com.urbanspork.common.protocol.vmess.aead.Encrypt.openVMessAEADHeader; -public class ServerAEADCodec extends ByteToMessageCodec { +public class ServerAeadCodec extends ByteToMessageCodec { private final byte[][] keys; private RequestHeader header; @@ -44,11 +45,11 @@ public class ServerAEADCodec extends ByteToMessageCodec { private PayloadEncoder payloadEncoder; private PayloadDecoder payloadDecoder; - public ServerAEADCodec(ServerConfig config) { + public ServerAeadCodec(ServerConfig config) { this(ID.newID(new String[]{config.getPassword()})); } - ServerAEADCodec(byte[][] keys) { + ServerAeadCodec(byte[][] keys) { this.keys = keys; } @@ -78,6 +79,7 @@ public void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws I @Override public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + InetSocketAddress address = null; if (payloadDecoder == null) { byte[] authID = new byte[16]; in.getBytes(0, authID); @@ -103,13 +105,8 @@ public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) thro SecurityType security = SecurityType.valueOf((byte) (b35 & 0x0F)); decrypted.skipBytes(1); // fixed 0 RequestCommand command = new RequestCommand(decrypted.readByte()); // command - InetSocketAddress address = null; if (RequestCommand.TCP.equals(command) || RequestCommand.UDP.equals(command)) { address = Address.readAddressPort(decrypted); - if (RequestCommand.UDP.equals(command)) { - ctx.channel().attr(AttributeKeys.TRANSPORT).set(Transport.UDP); - } - out.add(address); } decrypted.skipBytes(paddingLen); byte[] actual = new byte[Integer.BYTES]; @@ -121,10 +118,27 @@ public void decode(ChannelHandlerContext ctx, ByteBuf in, List out) thro session = new ServerSession(requestBodyIV, requestBodyKey, responseHeader); payloadDecoder = AEADBodyCodec.getBodyDecoder(header, session); } - if (RequestCommand.UDP.equals(header.command())) { - payloadDecoder.decodePacket(in, out); + decode(address, in, out); + } + + private void decode(InetSocketAddress address, ByteBuf in, List out) throws InvalidCipherTextException { + List list = new ArrayList<>(); + boolean isUdp = RequestCommand.UDP.equals(header.command()); + if (isUdp) { + payloadDecoder.decodePacket(in, list); } else { - payloadDecoder.decodePayload(in, out); + payloadDecoder.decodePayload(in, list); + } + if (list.isEmpty()) { + return; + } + if (address != null) { + if (isUdp) { + list.set(0, new RelayingPacket<>(address, list.getFirst())); + } else { + list.set(0, new RelayingPayload<>(address, list.getFirst())); + } } + out.addAll(list); } } diff --git a/urban-spork-test/test/com/urbanspork/client/vmess/ClientAEADCodecTestCase.java b/urban-spork-test/test/com/urbanspork/client/vmess/ClientAEADCodecTestCase.java index f7d59433..98a035a8 100644 --- a/urban-spork-test/test/com/urbanspork/client/vmess/ClientAEADCodecTestCase.java +++ b/urban-spork-test/test/com/urbanspork/client/vmess/ClientAEADCodecTestCase.java @@ -7,7 +7,7 @@ import com.urbanspork.common.protocol.vmess.header.RequestHeader; import com.urbanspork.common.protocol.vmess.header.SecurityType; import com.urbanspork.common.util.Dice; -import com.urbanspork.server.vmess.ServerAEADCodec; +import com.urbanspork.server.vmess.ServerAeadCodec; import com.urbanspork.test.TestDice; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -36,7 +36,7 @@ void testDecodeDifferentSession() throws Exception { ClientAEADCodec clientCodec2 = new ClientAEADCodec(header, session2); ServerConfig config = new ServerConfig(); config.setPassword(uuid); - ServerAEADCodec serverCodec = new ServerAEADCodec(config); + ServerAeadCodec serverCodec = new ServerAeadCodec(config); ByteBuf msg = Unpooled.wrappedBuffer(Dice.rollBytes(1024)); clientCodec1.encode(null, msg, Unpooled.buffer()); ByteBuf buf = Unpooled.buffer(); diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java index 2b74c03e..25713ac0 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/EmbeddedChannelTestCase.java @@ -6,6 +6,7 @@ import com.urbanspork.common.codec.shadowsocks.udp.UdpRelayCodec; import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.protocol.Protocol; +import com.urbanspork.common.transport.tcp.RelayingPayload; import com.urbanspork.common.transport.udp.DatagramPacketWrapper; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; @@ -47,9 +48,9 @@ void testTcpRelayChannel() { client.writeOutbound(Unpooled.wrappedBuffer(message.getBytes())); ByteBuf msg = client.readOutbound(); server.writeInbound(msg); - InetSocketAddress address = server.readInbound(); - Assertions.assertEquals(address.getPort(), port); - msg = server.readInbound(); + RelayingPayload payload = server.readInbound(); + Assertions.assertEquals(payload.address().getPort(), port); + msg = payload.content(); server.writeOutbound(msg); msg = server.readOutbound(); client.writeInbound(msg); diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecsTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecsTestCase.java index 0f702038..5e3eb6e9 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecsTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/tcp/AeadCipherCodecsTestCase.java @@ -9,6 +9,7 @@ import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocol; import com.urbanspork.common.protocol.shadowsocks.Identity; +import com.urbanspork.common.transport.tcp.RelayingPayload; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; import io.netty.buffer.ByteBuf; @@ -76,6 +77,13 @@ private void cipherTest(Session request, Session response) throws Exception { outBuf.release(); len += readableBytes; } + if (obj instanceof RelayingPayload payload) { + ByteBuf outBuf = (ByteBuf) payload.content(); + int readableBytes = outBuf.readableBytes(); + outBuf.readBytes(out, len, readableBytes); + outBuf.release(); + len += readableBytes; + } } Assertions.assertEquals(in.length, len); Assertions.assertArrayEquals(in, out); @@ -126,5 +134,4 @@ private ByteBuf merge(List list) { Session newContext(Mode mode, CipherKind kind, Socks5CommandRequest request) { return new Session(mode, new Identity(kind), request, ServerUserManager.EMPTY, new Context()); } - } \ No newline at end of file diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecTestCase.java index 86942bc6..7ca3986d 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecTestCase.java @@ -6,6 +6,7 @@ import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocol; import com.urbanspork.common.protocol.shadowsocks.Control; +import com.urbanspork.common.transport.udp.RelayingPacket; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; import com.urbanspork.test.template.TraceLevelLoggerTestTemplate; @@ -18,9 +19,7 @@ import org.junit.jupiter.api.Test; import java.net.InetSocketAddress; -import java.util.ArrayList; import java.util.Base64; -import java.util.List; @DisplayName("Shadowsocks - AEAD Cipher UDP Codec") class AeadCipherCodecTestCase extends TraceLevelLoggerTestTemplate { @@ -37,35 +36,32 @@ void testIncorrectPassword() { @Test void testTooShortHeader() { AeadCipherCodec codec = newAEADCipherCodec(); - List out = new ArrayList<>(); ByteBuf in = Unpooled.wrappedBuffer(Dice.rollBytes(3)); Context context = new Context(Mode.Client, new Control(TestDice.rollCipher()), null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, in, out)); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(context, in)); } @Test void testEmptyMsg() throws InvalidCipherTextException { AeadCipherCodec codec = newAEADCipherCodec(); InetSocketAddress address = InetSocketAddress.createUnresolved(TestDice.rollHost(), TestDice.rollPort()); - List out = new ArrayList<>(); ByteBuf in = Unpooled.buffer(); CipherKind kind = TestDice.rollCipher(); codec.encode(new Context(Mode.Client, new Control(kind), address, ServerUserManager.EMPTY), Unpooled.EMPTY_BUFFER, in); Assertions.assertTrue(in.isReadable()); - codec.decode(new Context(Mode.Server, new Control(kind), address, ServerUserManager.EMPTY), in, out); + RelayingPacket pocket = codec.decode(new Context(Mode.Server, new Control(kind), address, ServerUserManager.EMPTY), in); Assertions.assertFalse(in.isReadable()); - Assertions.assertFalse(out.isEmpty()); + Assertions.assertNotNull(pocket); } @Test void testTooShortPacket() { AeadCipherCodec codec = newAEADCipherCodec(); ByteBuf in = Unpooled.buffer(); - List out = new ArrayList<>(); Context c1 = new Context(Mode.Client, new Control(CipherKind.aead2022_blake3_aes_128_gcm), null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c1, in, out)); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c1, in)); Context c2 = new Context(Mode.Server, new Control(CipherKind.aead2022_blake3_aes_128_gcm), null, ServerUserManager.EMPTY); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c2, in, out)); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c2, in)); } @Test @@ -84,8 +80,7 @@ private static void testInvalidSocketType(Context c) throws InvalidCipherTextExc in.writeBytes(msg); ByteBuf out = Unpooled.buffer(); codec.encode(c, in, out); - ArrayList list = new ArrayList<>(); - Assertions.assertThrows(DecoderException.class, () -> codec.decode(c, out, list)); + Assertions.assertThrows(DecoderException.class, () -> codec.decode(c, out)); } static AeadCipherCodec newAEADCipherCodec() { diff --git a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecsTestCase.java b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecsTestCase.java index 8204409c..d20a8a01 100644 --- a/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecsTestCase.java +++ b/urban-spork-test/test/com/urbanspork/common/codec/shadowsocks/udp/AeadCipherCodecsTestCase.java @@ -9,6 +9,7 @@ import com.urbanspork.common.manage.shadowsocks.ServerUserManager; import com.urbanspork.common.protocol.Protocol; import com.urbanspork.common.protocol.shadowsocks.Control; +import com.urbanspork.common.transport.udp.RelayingPacket; import com.urbanspork.common.util.Dice; import com.urbanspork.test.TestDice; import io.netty.buffer.ByteBuf; @@ -63,22 +64,21 @@ void parameterizedTest(CipherKind kind) throws Exception { } private void cipherTest(Context request, Context response) throws Exception { - List list = cipherTest(request, response, Unpooled.copiedBuffer(in)); + List> list = cipherTest(request, response, Unpooled.copiedBuffer(in)); byte[] out = new byte[in.length]; int len = 0; - for (Object obj : list) { - if (obj instanceof ByteBuf outBuf) { - int readableBytes = outBuf.readableBytes(); - outBuf.readBytes(out, len, readableBytes); - outBuf.release(); - len += readableBytes; - } + for (RelayingPacket packet : list) { + ByteBuf msg = packet.content(); + int readableBytes = msg.readableBytes(); + msg.readBytes(out, len, readableBytes); + msg.release(); + len += readableBytes; } Assertions.assertEquals(in.length, len); Assertions.assertArrayEquals(in, out); } - private List cipherTest(Context request, Context response, ByteBuf in) + private List> cipherTest(Context request, Context response, ByteBuf in) throws Exception { ServerConfig config = new ServerConfig(); config.setCipher(kind); @@ -91,9 +91,9 @@ private List cipherTest(Context request, Context response, ByteBuf in) client.encode(request, slice, buf); encodeSlices.add(buf); } - List out = new ArrayList<>(); + List> out = new ArrayList<>(); for (ByteBuf buf : encodeSlices) { - server.decode(response, buf, out); + out.add(server.decode(response, buf)); } return out; } diff --git a/urban-spork-test/test/com/urbanspork/server/RemoteConnectHandlerTestCase.java b/urban-spork-test/test/com/urbanspork/server/ServerRelayHandlerTestCase.java similarity index 62% rename from urban-spork-test/test/com/urbanspork/server/RemoteConnectHandlerTestCase.java rename to urban-spork-test/test/com/urbanspork/server/ServerRelayHandlerTestCase.java index 0fb2d998..7d1e1b00 100644 --- a/urban-spork-test/test/com/urbanspork/server/RemoteConnectHandlerTestCase.java +++ b/urban-spork-test/test/com/urbanspork/server/ServerRelayHandlerTestCase.java @@ -2,6 +2,7 @@ import com.urbanspork.common.config.ServerConfig; import com.urbanspork.common.config.ServerConfigTestCase; +import com.urbanspork.common.transport.tcp.RelayingPayload; import com.urbanspork.common.util.Dice; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; @@ -11,14 +12,13 @@ import java.net.InetSocketAddress; -@DisplayName("Server - Remote Connect Handler") -class RemoteConnectHandlerTestCase { +@DisplayName("Server - Server Relay Handler") +class ServerRelayHandlerTestCase { @Test void testConnectFailed() { ServerConfig config = ServerConfigTestCase.testConfig(0); - EmbeddedChannel channel = new EmbeddedChannel(new RemoteConnectHandler(config)); - channel.writeInbound(new InetSocketAddress(0)); - channel.writeInbound(Unpooled.wrappedBuffer(Dice.rollBytes(10))); + EmbeddedChannel channel = new EmbeddedChannel(new ServerRelayHandler(config)); + channel.writeInbound(new RelayingPayload<>(new InetSocketAddress(0), Unpooled.wrappedBuffer(Dice.rollBytes(10)))); Assertions.assertFalse(channel.isActive()); } } diff --git a/urban-spork-test/test/com/urbanspork/server/ServerUDPRelayHandlerTestCase.java b/urban-spork-test/test/com/urbanspork/server/ServerUDPRelayHandlerTestCase.java index 21e68a13..eb93e752 100644 --- a/urban-spork-test/test/com/urbanspork/server/ServerUDPRelayHandlerTestCase.java +++ b/urban-spork-test/test/com/urbanspork/server/ServerUDPRelayHandlerTestCase.java @@ -18,16 +18,16 @@ import java.net.InetSocketAddress; -@DisplayName("Shadowsocks - Server UDP Relay Handler") +@DisplayName("Server - Server UDP Relay Handler") class ServerUDPRelayHandlerTestCase { @ParameterizedTest @EnumSource(PacketEncoding.class) void testWorkAndIdle(PacketEncoding packetEncoding) throws Exception { NioEventLoopGroup group = new NioEventLoopGroup(); - ServerUDPRelayHandler handler = new ServerUDPRelayHandler(packetEncoding, group); + ServerUdpRelayHandler handler = new ServerUdpRelayHandler(packetEncoding, group); handler.workerChannel(new InetSocketAddress(TestDice.rollPort()), new EmbeddedChannel(handler)); InetSocketAddress recipient = new InetSocketAddress(TestDice.rollPort()); - Channel outboundChannel = handler.workerChannel(recipient, new EmbeddedChannel(new ServerUDPRelayHandler(packetEncoding, group))); + Channel outboundChannel = handler.workerChannel(recipient, new EmbeddedChannel(new ServerUdpRelayHandler(packetEncoding, group))); Assertions.assertNotNull(outboundChannel); ChannelPipeline workerPipeline = outboundChannel.pipeline(); ChannelInboundHandlerAdapter last = (ChannelInboundHandlerAdapter) workerPipeline.last(); diff --git a/urban-spork-test/test/com/urbanspork/server/vmess/ServerAEADCodecTestCase.java b/urban-spork-test/test/com/urbanspork/server/vmess/ServerAEADCodecTestCase.java index c8edf9b4..144b8acc 100644 --- a/urban-spork-test/test/com/urbanspork/server/vmess/ServerAEADCodecTestCase.java +++ b/urban-spork-test/test/com/urbanspork/server/vmess/ServerAEADCodecTestCase.java @@ -42,7 +42,7 @@ void testDecodeNoMatchedAuthID() { ByteBuf buf = readOutbound(clientCodec); ServerConfig config = new ServerConfig(); config.setPassword(java.util.UUID.randomUUID().toString()); - ServerAEADCodec serverCodec = new ServerAEADCodec(config); + ServerAeadCodec serverCodec = new ServerAeadCodec(config); EmbeddedChannel serverChannel = new EmbeddedChannel(); serverChannel.pipeline().addLast(serverCodec); Assertions.assertThrows(DecoderException.class, () -> serverChannel.writeInbound(buf)); @@ -55,7 +55,7 @@ void testInvalidRequest() { ByteBuf buf = readOutbound(clientCodec); ServerConfig config = new ServerConfig(); config.setPassword(UUID); - ServerAEADCodec serverCodec = new ServerAEADCodec(config); + ServerAeadCodec serverCodec = new ServerAeadCodec(config); EmbeddedChannel serverChannel = new EmbeddedChannel(); serverChannel.pipeline().addLast(serverCodec); Assertions.assertThrows(DecoderException.class, () -> serverChannel.writeInbound(buf)); @@ -71,6 +71,6 @@ private static ByteBuf readOutbound(ClientAEADCodec clientCodec) { private static EmbeddedChannel serverChannel() { ServerConfig config = new ServerConfig(); config.setPassword(UUID); - return (EmbeddedChannel) new EmbeddedChannel().pipeline().addLast(new ServerAEADCodec(config)).channel(); + return (EmbeddedChannel) new EmbeddedChannel().pipeline().addLast(new ServerAeadCodec(config)).channel(); } } From ff753d0c5447beace12c37908f3a41bc7036f517 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Thu, 29 Feb 2024 17:53:46 +0800 Subject: [PATCH 5/9] fix(ss): detected memory leak --- .../client/AbstractClientUdpRelayHandler.java | 1 + .../codec/socks/DatagramPacketDecoder.java | 2 +- .../transport/udp/DatagramPacketWrapper.java | 42 ++++++++++++++++++- .../server/ServerUdpOverTcpCodec.java | 3 +- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/urban-spork-client/src/com/urbanspork/client/AbstractClientUdpRelayHandler.java b/urban-spork-client/src/com/urbanspork/client/AbstractClientUdpRelayHandler.java index b637d24f..3799b1fe 100644 --- a/urban-spork-client/src/com/urbanspork/client/AbstractClientUdpRelayHandler.java +++ b/urban-spork-client/src/com/urbanspork/client/AbstractClientUdpRelayHandler.java @@ -19,6 +19,7 @@ public abstract class AbstractClientUdpRelayHandler extends SimpleChannelInbo private final LruCache binding; protected AbstractClientUdpRelayHandler(ServerConfig config, Duration keepAlive) { + super(false); this.config = config; this.binding = new LruCache<>(1024, keepAlive, (k, channel) -> { logger.info("[udp][binding][expire]{} != {}", k, channel.localAddress()); diff --git a/urban-spork-common/src/com/urbanspork/common/codec/socks/DatagramPacketDecoder.java b/urban-spork-common/src/com/urbanspork/common/codec/socks/DatagramPacketDecoder.java index d5db18ac..1e405f65 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/socks/DatagramPacketDecoder.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/socks/DatagramPacketDecoder.java @@ -23,6 +23,6 @@ protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List out) { + msg.retain(); out.add(msg.packet().content()); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) { InetSocketAddress sender = (InetSocketAddress) ctx.channel().remoteAddress(); - out.add(new DatagramPacket(msg.retainedDuplicate(), address, sender)); + out.add(new DatagramPacket(msg.retain(), address, sender)); } } From 9fe0e7bba9777778c91e941009980e5af71536af Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Sat, 2 Mar 2024 12:30:50 +0800 Subject: [PATCH 6/9] chore: bump version to 2.4.3 --- pom.xml | 4 ++-- urban-spork-client-gui/pom.xml | 2 +- urban-spork-client/pom.xml | 2 +- urban-spork-common/pom.xml | 2 +- urban-spork-server/pom.xml | 2 +- urban-spork-test/pom.xml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 8296b788..ac9bbe8c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 urban-spork urban-spork - 2.4.2 + 2.4.3 pom urban-spork-common @@ -14,7 +14,7 @@ UTF-8 - 2.4.2 + 2.4.3 3.2.3 3.3.0 3.12.1 diff --git a/urban-spork-client-gui/pom.xml b/urban-spork-client-gui/pom.xml index 76c767ef..44e46ffa 100644 --- a/urban-spork-client-gui/pom.xml +++ b/urban-spork-client-gui/pom.xml @@ -3,7 +3,7 @@ urban-spork urban-spork - 2.4.2 + 2.4.3 urban-spork-client-gui urban-spork-client-gui diff --git a/urban-spork-client/pom.xml b/urban-spork-client/pom.xml index be85df50..be504cb2 100644 --- a/urban-spork-client/pom.xml +++ b/urban-spork-client/pom.xml @@ -3,7 +3,7 @@ urban-spork urban-spork - 2.4.2 + 2.4.3 urban-spork-client diff --git a/urban-spork-common/pom.xml b/urban-spork-common/pom.xml index 8bb9a7c5..f093b1e7 100644 --- a/urban-spork-common/pom.xml +++ b/urban-spork-common/pom.xml @@ -4,7 +4,7 @@ urban-spork urban-spork - 2.4.2 + 2.4.3 urban-spork-common diff --git a/urban-spork-server/pom.xml b/urban-spork-server/pom.xml index a6aff415..e812ab4a 100644 --- a/urban-spork-server/pom.xml +++ b/urban-spork-server/pom.xml @@ -3,7 +3,7 @@ urban-spork urban-spork - 2.4.2 + 2.4.3 urban-spork-server diff --git a/urban-spork-test/pom.xml b/urban-spork-test/pom.xml index 16312b36..d670635c 100644 --- a/urban-spork-test/pom.xml +++ b/urban-spork-test/pom.xml @@ -6,7 +6,7 @@ urban-spork urban-spork - 2.4.2 + 2.4.3 urban-spork-test From 5f2c8e74e0489b7e28a6a8350f91117cc6fbba68 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Mon, 22 Apr 2024 19:22:24 +0800 Subject: [PATCH 7/9] feat(ss): shadowsocks server configuration url --- urban-spork-client-gui/resource/console.css | 8 +- .../client/gui/console/component/Console.java | 180 ++++++++++++------ .../gui/console/widget/ConsoleButton.java | 4 +- .../gui/console/widget/ConsoleLiteButton.java | 14 ++ .../com/urbanspork/client/gui/i18n/I18n.java | 4 +- .../client/gui/i18n/language_en.properties | 4 +- .../client/gui/i18n/language_zh.properties | 8 +- .../urbanspork/common/codec/CipherKind.java | 8 +- .../shadowsocks/ShareableServerConfig.java | 52 +++++ .../ShareableServerConfigTestCase.java | 47 +++++ 10 files changed, 257 insertions(+), 72 deletions(-) create mode 100644 urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleLiteButton.java create mode 100644 urban-spork-common/src/com/urbanspork/common/config/shadowsocks/ShareableServerConfig.java create mode 100644 urban-spork-test/test/com/urbanspork/common/config/shadowsocks/ShareableServerConfigTestCase.java diff --git a/urban-spork-client-gui/resource/console.css b/urban-spork-client-gui/resource/console.css index 9aac40db..929ed67c 100644 --- a/urban-spork-client-gui/resource/console.css +++ b/urban-spork-client-gui/resource/console.css @@ -11,7 +11,7 @@ } .jfx-tab-pane { - -fx-pref-height: 500; + -fx-pref-height: 525; /* -fx-padding: 1px; */ /* -fx-background-color: -fx-secondary-color, -fx-primary-color; */ /* -fx-background-insets: 0, 1; */ @@ -45,7 +45,7 @@ GridPane { -fx-min-width: 65; } -.jfx-button { +.jfx-button, .lite-button { -fx-background-color: -fx-secondary-color; -fx-background-radius: 5px; -fx-border-radius: 5px; @@ -59,6 +59,10 @@ GridPane { -fx-text-fill: -fx-primary-text; } +.lite-button { + -fx-background-color: -fx-primary-color; +} + .hide-show.toggle-button { -fx-background-image: url('image/eye.png'); -fx-background-position: 100% 50%; diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java index e093abcb..1336bcb2 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java @@ -7,12 +7,25 @@ import com.jfoenix.controls.JFXTextField; import com.jfoenix.validation.RequiredFieldValidator; import com.urbanspork.client.gui.Resource; -import com.urbanspork.client.gui.console.widget.*; +import com.urbanspork.client.gui.console.widget.ConsoleButton; +import com.urbanspork.client.gui.console.widget.ConsoleColumnConstraints; +import com.urbanspork.client.gui.console.widget.ConsoleLabel; +import com.urbanspork.client.gui.console.widget.ConsoleLiteButton; +import com.urbanspork.client.gui.console.widget.ConsoleLogTextArea; +import com.urbanspork.client.gui.console.widget.ConsolePasswordTextField; +import com.urbanspork.client.gui.console.widget.ConsoleRowConstraints; +import com.urbanspork.client.gui.console.widget.ConsoleTextField; +import com.urbanspork.client.gui.console.widget.CurrentConfigCipherChoiceBox; +import com.urbanspork.client.gui.console.widget.CurrentConfigPasswordToggleButton; +import com.urbanspork.client.gui.console.widget.CurrentConfigProtocolChoiceBox; +import com.urbanspork.client.gui.console.widget.NumericTextField; +import com.urbanspork.client.gui.console.widget.ServerConfigListView; import com.urbanspork.client.gui.i18n.I18n; import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.config.ClientConfig; import com.urbanspork.common.config.ConfigHandler; import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.config.shadowsocks.ShareableServerConfig; import com.urbanspork.common.protocol.Protocol; import javafx.application.Platform; import javafx.application.Preloader; @@ -20,18 +33,24 @@ import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; import javafx.scene.control.ChoiceBox; +import javafx.scene.control.DialogPane; import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; +import javafx.scene.control.TextInputDialog; import javafx.scene.control.ToggleButton; import javafx.scene.image.Image; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.RowConstraints; import javafx.stage.Stage; @@ -39,6 +58,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.URI; import java.util.Arrays; import java.util.List; @@ -60,12 +80,16 @@ public class Console extends Preloader { private ObservableList serverConfigObservableList; - private Button addServerConfigButton; + private Button newServerConfigButton; private Button delServerConfigButton; private Button copyServerConfigButton; + private Button importServerConfigButton; + + private Button shareServerConfigButton; + private Button moveUpServerConfigButton; private Button moveDownServerConfigButton; @@ -122,6 +146,7 @@ public void start(Stage primaryStage) { primaryStage.hide(); }); primaryStage.hide(); + afterStart(); } public void hide() { @@ -151,7 +176,7 @@ public JFXListView getServerConfigJFXListView() { return serverConfigJFXListView; } - public void addServerConfig() { + public void newServerConfig() { if (validate()) { ServerConfig newValue = new ServerConfig(); newValue.setCipher(CipherKind.aes_256_gcm); @@ -255,11 +280,13 @@ public void cancelServerConfig() { private void initElement() { serverConfigJFXListView = new ServerConfigListView(); logTextarea = new ConsoleLogTextArea(); - addServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_ADD, event -> addServerConfig()); + newServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_NEW, event -> newServerConfig()); delServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_DEL, event -> deleteServerConfig()); copyServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_COPY, event -> copyServerConfig()); - moveUpServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_UP, event -> moveUpServerConfig()); - moveDownServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_DOWN, event -> moveDownServerConfig()); + importServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_IMPORT); + shareServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_SHARE); + moveUpServerConfigButton = new ConsoleLiteButton(I18n.CONSOLE_BUTTON_UP, event -> moveUpServerConfig()); + moveDownServerConfigButton = new ConsoleLiteButton(I18n.CONSOLE_BUTTON_DOWN, event -> moveDownServerConfig()); confirmServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_CONFIRM, event -> confirmServerConfig()); cancelServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_CANCEL, event -> cancelServerConfig()); currentConfigHostTextField = new ConsoleTextField(); @@ -301,7 +328,7 @@ private JFXTabPane initTabPane() { RowConstraints rGap1 = new ConsoleRowConstraints(10); // container grid RowConstraints rContainer0 = new ConsoleRowConstraints(35); - RowConstraints rContainer1 = new RowConstraints(); + RowConstraints rContainer1 = new ConsoleRowConstraints(30); RowConstraints rContainer2 = new RowConstraints(); rContainer2.setVgrow(Priority.ALWAYS); ObservableList rConstraints0 = gridPane0.getRowConstraints(); @@ -311,7 +338,6 @@ private JFXTabPane initTabPane() { rConstraints0.add(rGap0); } rConstraints0.add(rContainer0); - rConstraints0.add(rGap1); for (int i = 0; i < 2; i++) { rConstraints0.add(rGap1); rConstraints0.add(rContainer1); @@ -356,13 +382,15 @@ private JFXTabPane initTabPane() { private void addGridPane0Children(GridPane gridPane0) { // ---------- Grid Children ---------- - gridPane0.add(addServerConfigButton, 1, 13); - gridPane0.add(copyServerConfigButton, 3, 13); - gridPane0.add(moveUpServerConfigButton, 1, 15); - gridPane0.add(moveDownServerConfigButton, 5, 15); - gridPane0.add(delServerConfigButton, 5, 13); - gridPane0.add(confirmServerConfigButton, 9, 15); - gridPane0.add(cancelServerConfigButton, 11, 15); + gridPane0.add(wrap(moveUpServerConfigButton), 1, 13); + gridPane0.add(wrap(moveDownServerConfigButton), 5, 13); + gridPane0.add(newServerConfigButton, 1, 15); + gridPane0.add(copyServerConfigButton, 3, 15); + gridPane0.add(delServerConfigButton, 5, 15); + gridPane0.add(importServerConfigButton, 1, 17); + gridPane0.add(shareServerConfigButton, 3, 17); + gridPane0.add(confirmServerConfigButton, 9, 17); + gridPane0.add(cancelServerConfigButton, 11, 17); gridPane0.add(serverConfigJFXListView, 1, 1, 5, 11); gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_HOST), 7, 1); gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_PORT), 7, 3); @@ -382,6 +410,13 @@ private void addGridPane0Children(GridPane gridPane0) { gridPane0.add(clientConfigPortTextField, 9, 13, 3, 1); } + private static HBox wrap(Node node) { + HBox hbox = new HBox(); + hbox.setAlignment(Pos.BOTTOM_CENTER); + hbox.getChildren().add(node); + return hbox; + } + private void initModule() { initElement(); root = initTabPane(); @@ -400,37 +435,34 @@ private void initController() { private void initClientConfigPortTextField() { clientConfigPortTextField.getValidators().add(requiredFieldValidator); - clientConfigPortTextField.textProperty().addListener( - (o, oldValue, newValue) -> { - clientConfigPortTextField.validate(); - int port = clientConfigPortTextField.getIntValue(); - if (clientConfig.getPort() != port) { - clientConfig.setPort(port); - Proxy.launch(); - saveConfig(); - } - }); + clientConfigPortTextField.textProperty().addListener((o, oldValue, newValue) -> { + clientConfigPortTextField.validate(); + int port = clientConfigPortTextField.getIntValue(); + if (clientConfig.getPort() != port) { + clientConfig.setPort(port); + Proxy.launch(); + saveConfig(); + } + }); } private void initCurrentConfigPasswordPasswordField() { currentConfigPasswordPasswordField.getValidators().add(requiredFieldValidator); - currentConfigPasswordPasswordField.focusedProperty().addListener( - (o, oldValue, newValue) -> { - if (Boolean.TRUE.equals(oldValue) && Boolean.FALSE.equals(newValue)) { - currentConfigPasswordPasswordField.validate(); - } - }); + currentConfigPasswordPasswordField.focusedProperty().addListener((o, oldValue, newValue) -> { + if (Boolean.TRUE.equals(oldValue) && Boolean.FALSE.equals(newValue)) { + currentConfigPasswordPasswordField.validate(); + } + }); initCurrentConfigPasswordCommonEvent(currentConfigPasswordPasswordField); } private void initCurrentConfigPasswordTextField() { currentConfigPasswordTextField.getValidators().add(requiredFieldValidator); - currentConfigPasswordTextField.focusedProperty().addListener( - (o, oldValue, newValue) -> { - if (Boolean.TRUE.equals(oldValue) && Boolean.FALSE.equals(newValue)) { - currentConfigPasswordTextField.validate(); - } - }); + currentConfigPasswordTextField.focusedProperty().addListener((o, oldValue, newValue) -> { + if (Boolean.TRUE.equals(oldValue) && Boolean.FALSE.equals(newValue)) { + currentConfigPasswordTextField.validate(); + } + }); initCurrentConfigPasswordCommonEvent(currentConfigPasswordTextField); } @@ -450,8 +482,7 @@ private void initCurrentConfigPasswordCommonEvent(TextField field) { private void initCurrentConfigPortTextField() { currentConfigPortTextField.getValidators().add(requiredFieldValidator); - currentConfigPortTextField.textProperty().addListener( - (o, oldValue, newValue) -> currentConfigPortTextField.validate()); + currentConfigPortTextField.textProperty().addListener((o, oldValue, newValue) -> currentConfigPortTextField.validate()); } private void initCurrentConfigCipherChoiceBox() { @@ -460,8 +491,7 @@ private void initCurrentConfigCipherChoiceBox() { currentConfigCipherChoiceBox.setValue(CipherKind.aes_128_gcm); // currentConfigHostTextField currentConfigHostTextField.getValidators().add(requiredFieldValidator); - currentConfigHostTextField.textProperty().addListener( - (o, oldValue, newValue) -> currentConfigHostTextField.validate()); + currentConfigHostTextField.textProperty().addListener((o, oldValue, newValue) -> currentConfigHostTextField.validate()); } private void initCurrentConfigProtocolChoiceBox() { @@ -476,32 +506,56 @@ private void initServerConfigListView() { serverConfigJFXListView.setItems(serverConfigObservableList); MultipleSelectionModel selectionModel = serverConfigJFXListView.getSelectionModel(); selectionModel.select(clientConfig.getIndex()); - selectionModel.selectedItemProperty().addListener( - new ChangeListener<>() { - - private boolean changing = false; - - @Override - public void changed(ObservableValue observable, ServerConfig oldValue, ServerConfig newValue) { - if (serverConfigObservableList.contains(oldValue)) { - if (validate()) { - resetValidation(); - pack(oldValue); - serverConfigJFXListView.refresh(); - display(newValue); - } else if (!changing) { - changing = true; - Platform.runLater(() -> { - selectionModel.select(oldValue); - changing = false; - }); - } - } else { + selectionModel.selectedItemProperty().addListener(new ChangeListener<>() { + + private boolean changing = false; + + @Override + public void changed(ObservableValue observable, ServerConfig oldValue, ServerConfig newValue) { + if (serverConfigObservableList.contains(oldValue)) { + if (validate()) { resetValidation(); + pack(oldValue); + serverConfigJFXListView.refresh(); display(newValue); + } else if (!changing) { + changing = true; + Platform.runLater(() -> { + selectionModel.select(oldValue); + changing = false; + }); } + } else { + resetValidation(); + display(newValue); } - }); + } + }); + } + + private void afterStart() { + importServerConfigButton.setOnAction(actionEvent -> { + TextInputDialog dialog = new TextInputDialog(); + dialog.setGraphic(null); + dialog.setTitle(I18n.CONSOLE_BUTTON_IMPORT); + dialog.setHeaderText(null); + dialog.showAndWait().map(URI::create).flatMap(ShareableServerConfig::fromUri).ifPresent(serverConfigObservableList::add); + }); + shareServerConfigButton.setOnAction(actionEvent -> ShareableServerConfig.produceUri(serverConfigJFXListView.getSelectionModel().getSelectedItem()).ifPresent(uri -> { + String string = uri.toString(); + TextInputDialog dialog = new TextInputDialog(); + dialog.setGraphic(null); + dialog.setTitle(I18n.CONSOLE_BUTTON_SHARE); + dialog.setHeaderText(null); + DialogPane dialogPane = dialog.getDialogPane(); + dialogPane.lookupButton(ButtonType.OK).setVisible(false); + dialogPane.lookupButton(ButtonType.CANCEL).setVisible(false); + dialogPane.setPrefWidth((string.length() * 8)); + TextField editor = dialog.getEditor(); + editor.setText(string); + editor.setEditable(false); + dialog.show(); + })); } private boolean validate() { diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleButton.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleButton.java index 221da977..cfb0fbaf 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleButton.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleButton.java @@ -5,10 +5,12 @@ import javafx.event.EventHandler; public class ConsoleButton extends JFXButton { + public ConsoleButton(String text) { + setText(text); + } public ConsoleButton(String text, EventHandler handler) { setText(text); setOnAction(handler); } - } diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleLiteButton.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleLiteButton.java new file mode 100644 index 00000000..eafb22cc --- /dev/null +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/widget/ConsoleLiteButton.java @@ -0,0 +1,14 @@ +package com.urbanspork.client.gui.console.widget; + +import com.jfoenix.controls.JFXButton; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; + + +public class ConsoleLiteButton extends JFXButton { + public ConsoleLiteButton(String text, EventHandler handler) { + getStyleClass().add("lite-button"); + setText(text); + setOnAction(handler); + } +} diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java index d8636fa7..1351fb24 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java @@ -19,7 +19,9 @@ public final class I18n { public static final String TRAY_MENU_SERVERS = BUNDLE.getString("tray.menu.servers"); public static final String CONSOLE_TAB0_TEXT = BUNDLE.getString("console.tab0.text"); public static final String CONSOLE_TAB1_TEXT = BUNDLE.getString("console.tab1.text"); - public static final String CONSOLE_BUTTON_ADD = BUNDLE.getString("console.button.add"); + public static final String CONSOLE_BUTTON_NEW = BUNDLE.getString("console.button.new"); + public static final String CONSOLE_BUTTON_IMPORT = BUNDLE.getString("console.button.import"); + public static final String CONSOLE_BUTTON_SHARE = BUNDLE.getString("console.button.share"); public static final String CONSOLE_BUTTON_DEL = BUNDLE.getString("console.button.del"); public static final String CONSOLE_BUTTON_COPY = BUNDLE.getString("console.button.copy"); public static final String CONSOLE_BUTTON_UP = BUNDLE.getString("console.button.up"); diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties index f9dd321a..2a05d0ef 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties @@ -6,7 +6,9 @@ tray.menu.language=Language tray.menu.servers=Servers console.tab0.text=Server Configuration console.tab1.text=Log -console.button.add=Add... +console.button.new=New +console.button.import=Import... +console.button.share=Share... console.button.del=Delete console.button.copy=Copy console.button.up=Up diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties index e667f196..4dbfd946 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties @@ -3,10 +3,12 @@ tray.tooltip=Urban Spork tray.exit=\u9000\u51FA tray.menu.console=\u63A7\u5236\u53F0 tray.menu.language=\u8BED\u8A00 -tray.menu.servers=\u670d\u52a1\u5668 +tray.menu.servers=\u670D\u52A1\u5668 console.tab0.text=\u914D\u7F6E\u670D\u52A1\u5668 console.tab1.text=\u67E5\u770B\u65E5\u5FD7 -console.button.add=\u65B0\u589E +console.button.new=\u65B0\u5EFA +console.button.import=\u5BFC\u5165 +console.button.share=\u5206\u4EAB console.button.del=\u5220\u9664 console.button.copy=\u590D\u5236 console.button.up=\u4E0A\u79FB @@ -17,7 +19,7 @@ console.label.host=\u5730\u5740 console.label.port=\u7AEF\u53E3 console.label.password=\u5BC6\u7801 console.label.cipher=\u52A0\u5BC6 -console.label.protocol=\u534f\u8bae +console.label.protocol=\u534F\u8BAE console.label.remark=\u5907\u6CE8 console.label.proxy.port=\u4EE3\u7406\u7AEF\u53E3 console.validator.required.field.message=\u5FC5\u8F93\u9879 \ No newline at end of file diff --git a/urban-spork-common/src/com/urbanspork/common/codec/CipherKind.java b/urban-spork-common/src/com/urbanspork/common/codec/CipherKind.java index 98bef633..f731e117 100644 --- a/urban-spork-common/src/com/urbanspork/common/codec/CipherKind.java +++ b/urban-spork-common/src/com/urbanspork/common/codec/CipherKind.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Arrays; +import java.util.Optional; + public enum CipherKind { aes_128_gcm(16), @@ -9,7 +12,6 @@ public enum CipherKind { chacha20_poly1305(32), aead2022_blake3_aes_128_gcm("2022-blake3-aes-128-gcm", 16), aead2022_blake3_aes_256_gcm("2022-blake3-aes-256-gcm", 32), -// aead2022_blake3_chacha20_poly1305("2022-blake3-chacha20-poly1305", true), ; private final String value; @@ -37,6 +39,10 @@ public String toString() { return value; } + public static Optional from(String method) { + return Arrays.stream(CipherKind.values()).filter(kind -> kind.toString().equals(method)).findAny(); + } + public int keySize() { return keySize; } diff --git a/urban-spork-common/src/com/urbanspork/common/config/shadowsocks/ShareableServerConfig.java b/urban-spork-common/src/com/urbanspork/common/config/shadowsocks/ShareableServerConfig.java new file mode 100644 index 00000000..3cf20310 --- /dev/null +++ b/urban-spork-common/src/com/urbanspork/common/config/shadowsocks/ShareableServerConfig.java @@ -0,0 +1,52 @@ +package com.urbanspork.common.config.shadowsocks; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.config.ServerConfig; +import com.urbanspork.common.protocol.Protocol; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +public class ShareableServerConfig { + private static final String SS = "ss"; + + private ShareableServerConfig() {} + + public static Optional fromUri(URI uri) { + String scheme = uri.getScheme(); + if (!SS.equals(scheme)) { + return Optional.empty(); + } + String userInfo = uri.getUserInfo(); + if (userInfo == null) { + return Optional.empty(); + } + int index = userInfo.indexOf(":"); + if (index == -1) { + return Optional.empty(); + } + String method = userInfo.substring(0, index); + String password = userInfo.substring(index + 1); + Optional kind = CipherKind.from(method); + if (kind.isEmpty()) { + return Optional.empty(); + } + ServerConfig config = new ServerConfig(); + config.setCipher(kind.get()); + config.setHost(uri.getHost()); + config.setPort(uri.getPort()); + config.setPassword(password); + config.setRemark(uri.getFragment()); + config.setProtocol(Protocol.shadowsocks); + return Optional.of(config); + } + + public static Optional produceUri(ServerConfig config) { + try { + return Optional.of(new URI(SS, config.getCipher().toString() + ":" + config.getPassword(), config.getHost(), config.getPort(), null, null, config.getRemark())); + } catch (URISyntaxException e) { + return Optional.empty(); + } + } +} diff --git a/urban-spork-test/test/com/urbanspork/common/config/shadowsocks/ShareableServerConfigTestCase.java b/urban-spork-test/test/com/urbanspork/common/config/shadowsocks/ShareableServerConfigTestCase.java new file mode 100644 index 00000000..680e9c8f --- /dev/null +++ b/urban-spork-test/test/com/urbanspork/common/config/shadowsocks/ShareableServerConfigTestCase.java @@ -0,0 +1,47 @@ +package com.urbanspork.common.config.shadowsocks; + +import com.urbanspork.common.codec.CipherKind; +import com.urbanspork.common.config.ServerConfig; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.net.URI; +import java.util.Optional; +import java.util.stream.Stream; + +class ShareableServerConfigTestCase { + @Test + void testFromAndProduce() { + URI uri = URI.create("ss://2022-blake3-aes-128-gcm:zCjw%2FijIimRXjECgtogjd7IsgvPL7cdyqU5J8BIfobM=:Y57CGhfZ%2Fmln4tBzJ4J78AMNLbJoakz8T+5v0SaEkfI=@example.com:443#name"); + Optional op = ShareableServerConfig.fromUri(uri); + Assertions.assertTrue(op.isPresent()); + Optional op2 = ShareableServerConfig.produceUri(op.get()); + Assertions.assertTrue(op2.isPresent()); + Assertions.assertEquals(op2.get(), uri); + } + + @Test + void testProduceIllegalUri() { + ServerConfig config = new ServerConfig(); + config.setCipher(CipherKind.aes_128_gcm); + Assertions.assertFalse(ShareableServerConfig.produceUri(config).isPresent()); + } + + @ParameterizedTest + @ArgumentsSource(IllegalUriProvider.class) + void testFromIllegalUri(URI uri) { + Assertions.assertFalse(ShareableServerConfig.fromUri(uri).isPresent()); + } + + static class IllegalUriProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of("file://2022-blake3-aes-128-gcm:5mOQSa20Kt6ay2LXruBoHQ%3D%3D@example.com:443/#name", "ss://unknown:5mOQSa20Kt6ay2LXruBoHQ%3D%3D@example.com:443/#name", "ss://unknown@example.com:443/#name", "ss://example.com:443/#name").map(URI::create).map(Arguments::of); + } + } +} \ No newline at end of file From 8f64ed19919dfa4e01f8c9b6968c987cfc31183e Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Tue, 23 Apr 2024 11:35:24 +0800 Subject: [PATCH 8/9] refactor(gui): i18n resource bundle --- urban-spork-client-gui/pom.xml | 7 +-- .../i18n => resource}/language_en.properties | 0 .../i18n => resource}/language_zh.properties | 0 .../com/urbanspork/client/gui/Resource.java | 2 +- .../client/gui/console/component/Console.java | 46 +++++++++---------- .../client/gui/console/component/Tray.java | 6 +-- .../com/urbanspork/client/gui/i18n/I18N.java | 41 +++++++++++++++++ .../com/urbanspork/client/gui/i18n/I18n.java | 46 ------------------- .../gui/tray/menu/item/ConsoleMenuItem.java | 4 +- .../gui/tray/menu/item/ExitMenuItem.java | 4 +- .../gui/tray/menu/item/LanguageMenuItem.java | 9 ++-- .../gui/tray/menu/item/ServersMenuItem.java | 4 +- 12 files changed, 80 insertions(+), 89 deletions(-) rename urban-spork-client-gui/{src/com/urbanspork/client/gui/i18n => resource}/language_en.properties (100%) rename urban-spork-client-gui/{src/com/urbanspork/client/gui/i18n => resource}/language_zh.properties (100%) create mode 100644 urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18N.java delete mode 100644 urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java diff --git a/urban-spork-client-gui/pom.xml b/urban-spork-client-gui/pom.xml index 44e46ffa..78756882 100644 --- a/urban-spork-client-gui/pom.xml +++ b/urban-spork-client-gui/pom.xml @@ -23,16 +23,11 @@ - - src - - **/*.properties - - resource **/*.xml + **/*.properties diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties b/urban-spork-client-gui/resource/language_en.properties similarity index 100% rename from urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_en.properties rename to urban-spork-client-gui/resource/language_en.properties diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties b/urban-spork-client-gui/resource/language_zh.properties similarity index 100% rename from urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/language_zh.properties rename to urban-spork-client-gui/resource/language_zh.properties diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/Resource.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/Resource.java index 3795254f..f05806cb 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/Resource.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/Resource.java @@ -35,7 +35,7 @@ public class Resource { } String language = config.getLanguage(); ResourceBundle bundle; - String baseName = "com.urbanspork.client.gui.i18n.language"; + String baseName = "language"; try { if (language == null) { Locale locale = Locale.getDefault(); diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java index 1336bcb2..dab88c56 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Console.java @@ -20,7 +20,7 @@ import com.urbanspork.client.gui.console.widget.CurrentConfigProtocolChoiceBox; import com.urbanspork.client.gui.console.widget.NumericTextField; import com.urbanspork.client.gui.console.widget.ServerConfigListView; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import com.urbanspork.common.codec.CipherKind; import com.urbanspork.common.config.ClientConfig; import com.urbanspork.common.config.ConfigHandler; @@ -68,7 +68,7 @@ public class Console extends Preloader { private final ClientConfig clientConfig = Resource.config(); - private final RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator(I18n.CONSOLE_VALIDATOR_REQUIRED_FIELD_MESSAGE); + private final RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator(I18N.getString(I18N.CONSOLE_VALIDATOR_REQUIRED_FIELD_MESSAGE)); private Stage primaryStage; @@ -140,7 +140,7 @@ public void start(Stage primaryStage) { primaryStage.setScene(scene); primaryStage.setResizable(false); primaryStage.getIcons().add(new Image(Resource.PROGRAM_ICON.toString())); - primaryStage.setTitle(I18n.PROGRAM_TITLE); + primaryStage.setTitle(I18N.getString(I18N.PROGRAM_TITLE)); primaryStage.setOnCloseRequest(event -> { event.consume(); primaryStage.hide(); @@ -280,15 +280,15 @@ public void cancelServerConfig() { private void initElement() { serverConfigJFXListView = new ServerConfigListView(); logTextarea = new ConsoleLogTextArea(); - newServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_NEW, event -> newServerConfig()); - delServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_DEL, event -> deleteServerConfig()); - copyServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_COPY, event -> copyServerConfig()); - importServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_IMPORT); - shareServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_SHARE); - moveUpServerConfigButton = new ConsoleLiteButton(I18n.CONSOLE_BUTTON_UP, event -> moveUpServerConfig()); - moveDownServerConfigButton = new ConsoleLiteButton(I18n.CONSOLE_BUTTON_DOWN, event -> moveDownServerConfig()); - confirmServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_CONFIRM, event -> confirmServerConfig()); - cancelServerConfigButton = new ConsoleButton(I18n.CONSOLE_BUTTON_CANCEL, event -> cancelServerConfig()); + newServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_NEW), event -> newServerConfig()); + delServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_DEL), event -> deleteServerConfig()); + copyServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_COPY), event -> copyServerConfig()); + importServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_IMPORT)); + shareServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_SHARE)); + moveUpServerConfigButton = new ConsoleLiteButton(I18N.getString(I18N.CONSOLE_BUTTON_UP), event -> moveUpServerConfig()); + moveDownServerConfigButton = new ConsoleLiteButton(I18N.getString(I18N.CONSOLE_BUTTON_DOWN), event -> moveDownServerConfig()); + confirmServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_CONFIRM), event -> confirmServerConfig()); + cancelServerConfigButton = new ConsoleButton(I18N.getString(I18N.CONSOLE_BUTTON_CANCEL), event -> cancelServerConfig()); currentConfigHostTextField = new ConsoleTextField(); currentConfigPortTextField = new NumericTextField(); currentConfigPasswordPasswordField = new JFXPasswordField(); @@ -346,7 +346,7 @@ private JFXTabPane initTabPane() { // grid children addGridPane0Children(gridPane0); // tab0 - Tab tab0 = new Tab(I18n.CONSOLE_TAB0_TEXT); + Tab tab0 = new Tab(I18N.getString(I18N.CONSOLE_TAB0_TEXT)); tab0.setContent(gridPane0); tab0.setClosable(false); // ==================== @@ -368,7 +368,7 @@ private JFXTabPane initTabPane() { // grid children gridPane1.add(logTextarea, 1, 1); // tab1 - Tab tab1 = new Tab(I18n.CONSOLE_TAB1_TEXT); + Tab tab1 = new Tab(I18N.getString(I18N.CONSOLE_TAB1_TEXT)); tab1.setContent(gridPane1); tab1.setClosable(false); // ==================== @@ -392,13 +392,13 @@ private void addGridPane0Children(GridPane gridPane0) { gridPane0.add(confirmServerConfigButton, 9, 17); gridPane0.add(cancelServerConfigButton, 11, 17); gridPane0.add(serverConfigJFXListView, 1, 1, 5, 11); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_HOST), 7, 1); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_PORT), 7, 3); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_PASSWORD), 7, 5); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_CIPHER), 7, 7); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_PROTOCOL), 7, 9); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_REMARK), 7, 11); - gridPane0.add(new ConsoleLabel(I18n.CONSOLE_LABEL_PROXY_PORT), 7, 13); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_HOST)), 7, 1); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_PORT)), 7, 3); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_PASSWORD)), 7, 5); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_CIPHER)), 7, 7); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_PROTOCOL)), 7, 9); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_REMARK)), 7, 11); + gridPane0.add(new ConsoleLabel(I18N.getString(I18N.CONSOLE_LABEL_PROXY_PORT)), 7, 13); gridPane0.add(currentConfigHostTextField, 9, 1, 3, 1); gridPane0.add(currentConfigPortTextField, 9, 3, 3, 1); gridPane0.add(currentConfigPasswordTextField, 9, 5, 3, 1); @@ -537,7 +537,7 @@ private void afterStart() { importServerConfigButton.setOnAction(actionEvent -> { TextInputDialog dialog = new TextInputDialog(); dialog.setGraphic(null); - dialog.setTitle(I18n.CONSOLE_BUTTON_IMPORT); + dialog.setTitle(I18N.getString(I18N.CONSOLE_BUTTON_IMPORT)); dialog.setHeaderText(null); dialog.showAndWait().map(URI::create).flatMap(ShareableServerConfig::fromUri).ifPresent(serverConfigObservableList::add); }); @@ -545,7 +545,7 @@ private void afterStart() { String string = uri.toString(); TextInputDialog dialog = new TextInputDialog(); dialog.setGraphic(null); - dialog.setTitle(I18n.CONSOLE_BUTTON_SHARE); + dialog.setTitle(I18N.getString(I18N.CONSOLE_BUTTON_SHARE)); dialog.setHeaderText(null); DialogPane dialogPane = dialog.getDialogPane(); dialogPane.lookupButton(ButtonType.OK).setVisible(false); diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java index dea843ba..315cc1f6 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java @@ -1,7 +1,7 @@ package com.urbanspork.client.gui.console.component; import com.urbanspork.client.gui.Resource; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import com.urbanspork.client.gui.tray.menu.item.ConsoleMenuItem; import com.urbanspork.client.gui.tray.menu.item.ExitMenuItem; import com.urbanspork.client.gui.tray.menu.item.LanguageMenuItem; @@ -19,7 +19,7 @@ public class Tray { private static final ImageIcon icon = new ImageIcon(Resource.TRAY_ICON); - private static final TrayIcon trayIcon = IS_SUPPORTED ? new TrayIcon(icon.getImage(), I18n.PROGRAM_TITLE, menu) : null; + private static final TrayIcon trayIcon = IS_SUPPORTED ? new TrayIcon(icon.getImage(), I18N.getString(I18N.PROGRAM_TITLE), menu) : null; private static Console console; @@ -38,7 +38,7 @@ public static void displayMessage(String caption, String text, MessageType messa public static void setToolTip(String tooltip) { if (IS_SUPPORTED) { - trayIcon.setToolTip(I18n.TRAY_TOOLTIP + System.lineSeparator() + tooltip); + trayIcon.setToolTip(I18N.getString(I18N.TRAY_TOOLTIP) + System.lineSeparator() + tooltip); } } diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18N.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18N.java new file mode 100644 index 00000000..8dc595e6 --- /dev/null +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18N.java @@ -0,0 +1,41 @@ +package com.urbanspork.client.gui.i18n; + +import com.urbanspork.client.gui.Resource; + +import java.util.Locale; + +public interface I18N { + String PROGRAM_TITLE = "program.title"; + String TRAY_TOOLTIP = "tray.tooltip"; + String TRAY_EXIT = "tray.exit"; + String TRAY_MENU_CONSOLE = "tray.menu.console"; + String TRAY_MENU_LANGUAGE = "tray.menu.language"; + String TRAY_MENU_SERVERS = "tray.menu.servers"; + String CONSOLE_TAB0_TEXT = "console.tab0.text"; + String CONSOLE_TAB1_TEXT = "console.tab1.text"; + String CONSOLE_BUTTON_NEW = "console.button.new"; + String CONSOLE_BUTTON_IMPORT = "console.button.import"; + String CONSOLE_BUTTON_SHARE = "console.button.share"; + String CONSOLE_BUTTON_DEL = "console.button.del"; + String CONSOLE_BUTTON_COPY = "console.button.copy"; + String CONSOLE_BUTTON_UP = "console.button.up"; + String CONSOLE_BUTTON_DOWN = "console.button.down"; + String CONSOLE_BUTTON_CONFIRM = "console.button.confirm"; + String CONSOLE_BUTTON_CANCEL = "console.button.cancel"; + String CONSOLE_LABEL_HOST = "console.label.host"; + String CONSOLE_LABEL_PORT = "console.label.port"; + String CONSOLE_LABEL_PASSWORD = "console.label.password"; + String CONSOLE_LABEL_CIPHER = "console.label.cipher"; + String CONSOLE_LABEL_PROTOCOL = "console.label.protocol"; + String CONSOLE_LABEL_REMARK = "console.label.remark"; + String CONSOLE_LABEL_PROXY_PORT = "console.label.proxy.port"; + String CONSOLE_VALIDATOR_REQUIRED_FIELD_MESSAGE = "console.validator.required.field.message"; + + static Locale[] languages() { + return new Locale[]{Locale.CHINESE, Locale.ENGLISH}; + } + + static String getString(String key) { + return Resource.bundle().getString(key); + } +} diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java deleted file mode 100644 index 1351fb24..00000000 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/i18n/I18n.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.urbanspork.client.gui.i18n; - -import com.urbanspork.client.gui.Resource; - -import java.util.Locale; -import java.util.ResourceBundle; - -public final class I18n { - - private static final ResourceBundle BUNDLE = Resource.bundle(); - - private static final Locale[] LANGUAGES = new Locale[]{Locale.CHINESE, Locale.ENGLISH}; - - public static final String PROGRAM_TITLE = BUNDLE.getString("program.title"); - public static final String TRAY_TOOLTIP = BUNDLE.getString("tray.tooltip"); - public static final String TRAY_EXIT = BUNDLE.getString("tray.exit"); - public static final String TRAY_MENU_CONSOLE = BUNDLE.getString("tray.menu.console"); - public static final String TRAY_MENU_LANGUAGE = BUNDLE.getString("tray.menu.language"); - public static final String TRAY_MENU_SERVERS = BUNDLE.getString("tray.menu.servers"); - public static final String CONSOLE_TAB0_TEXT = BUNDLE.getString("console.tab0.text"); - public static final String CONSOLE_TAB1_TEXT = BUNDLE.getString("console.tab1.text"); - public static final String CONSOLE_BUTTON_NEW = BUNDLE.getString("console.button.new"); - public static final String CONSOLE_BUTTON_IMPORT = BUNDLE.getString("console.button.import"); - public static final String CONSOLE_BUTTON_SHARE = BUNDLE.getString("console.button.share"); - public static final String CONSOLE_BUTTON_DEL = BUNDLE.getString("console.button.del"); - public static final String CONSOLE_BUTTON_COPY = BUNDLE.getString("console.button.copy"); - public static final String CONSOLE_BUTTON_UP = BUNDLE.getString("console.button.up"); - public static final String CONSOLE_BUTTON_DOWN = BUNDLE.getString("console.button.down"); - public static final String CONSOLE_BUTTON_CONFIRM = BUNDLE.getString("console.button.confirm"); - public static final String CONSOLE_BUTTON_CANCEL = BUNDLE.getString("console.button.cancel"); - public static final String CONSOLE_LABEL_HOST = BUNDLE.getString("console.label.host"); - public static final String CONSOLE_LABEL_PORT = BUNDLE.getString("console.label.port"); - public static final String CONSOLE_LABEL_PASSWORD = BUNDLE.getString("console.label.password"); - public static final String CONSOLE_LABEL_CIPHER = BUNDLE.getString("console.label.cipher"); - public static final String CONSOLE_LABEL_PROTOCOL = BUNDLE.getString("console.label.protocol"); - public static final String CONSOLE_LABEL_REMARK = BUNDLE.getString("console.label.remark"); - public static final String CONSOLE_LABEL_PROXY_PORT = BUNDLE.getString("console.label.proxy.port"); - public static final String CONSOLE_VALIDATOR_REQUIRED_FIELD_MESSAGE = BUNDLE.getString("console.validator.required.field.message"); - - private I18n() {} - - public static Locale[] languages() { - return LANGUAGES; - } - -} diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java index 095c22af..8a574e10 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java @@ -1,7 +1,7 @@ package com.urbanspork.client.gui.tray.menu.item; import com.urbanspork.client.gui.console.component.Console; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import javafx.application.Platform; import java.awt.*; @@ -22,7 +22,7 @@ public Menu getMenuItem() { @Override public String getLabel() { - return I18n.TRAY_MENU_CONSOLE; + return I18N.getString(I18N.TRAY_MENU_CONSOLE); } @Override diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java index a2fb1ea2..1b8566fe 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java @@ -2,7 +2,7 @@ import com.urbanspork.client.gui.console.component.Proxy; import com.urbanspork.client.gui.console.component.Tray; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import javafx.application.Platform; import java.awt.*; @@ -17,7 +17,7 @@ public MenuItem getMenuItem() { @Override public String getLabel() { - return I18n.TRAY_EXIT; + return I18N.getString(I18N.TRAY_EXIT); } @Override diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java index b0b775c5..6c62e1ac 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java @@ -2,7 +2,7 @@ import com.urbanspork.client.gui.Resource; import com.urbanspork.client.gui.console.component.Tray; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import com.urbanspork.common.config.ClientConfig; import com.urbanspork.common.config.ConfigHandler; @@ -21,8 +21,9 @@ public MenuItem getMenuItem() { ClientConfig config = Resource.config(); String language = config.getLanguage(); final Locale configLanguage = Locale.of(language); - List items = new ArrayList<>(I18n.languages().length); - for (Locale locale : I18n.languages()) { + Locale[] languages = I18N.languages(); + List items = new ArrayList<>(languages.length); + for (Locale locale : languages) { CheckboxMenuItem item = new CheckboxMenuItem(); item.setName(locale.getLanguage()); item.setLabel(locale.getDisplayLanguage(configLanguage)); @@ -58,7 +59,7 @@ private void itemStateChanged(ClientConfig config, List items, @Override public String getLabel() { - return I18n.TRAY_MENU_LANGUAGE; + return I18N.getString(I18N.TRAY_MENU_LANGUAGE); } @Override diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java index d56e25c5..c85f7156 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java @@ -4,7 +4,7 @@ import com.urbanspork.client.gui.console.component.Console; import com.urbanspork.client.gui.console.component.Proxy; import com.urbanspork.client.gui.console.component.Tray; -import com.urbanspork.client.gui.i18n.I18n; +import com.urbanspork.client.gui.i18n.I18N; import com.urbanspork.common.config.ClientConfig; import com.urbanspork.common.config.ConfigHandler; import com.urbanspork.common.config.ServerConfig; @@ -76,7 +76,7 @@ public ActionListener getActionListener() { @Override public String getLabel() { - return I18n.TRAY_MENU_SERVERS; + return I18N.getString(I18N.TRAY_MENU_SERVERS); } private String getLabel(ServerConfig config) { From aaa204dbd9610e3c86e71e2bdcb05f48771d8c12 Mon Sep 17 00:00:00 2001 From: Zmax0 Date: Thu, 25 Apr 2024 14:44:05 +0800 Subject: [PATCH 9/9] feat(gui): use Swing to create the tray menu --- .../client/gui/console/component/Tray.java | 63 ++++++++------- .../urbanspork/client/gui/tray/TrayIcon.java | 81 +++++++++++++++++++ .../gui/tray/menu/item/ConsoleMenuItem.java | 6 -- .../gui/tray/menu/item/ExitMenuItem.java | 8 -- .../gui/tray/menu/item/LanguageMenuItem.java | 65 +++++---------- .../gui/tray/menu/item/ServersMenuItem.java | 71 +++++++--------- .../tray/menu/item/TrayMenuItemBuilder.java | 18 ++--- 7 files changed, 169 insertions(+), 143 deletions(-) create mode 100644 urban-spork-client-gui/src/com/urbanspork/client/gui/tray/TrayIcon.java diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java index 315cc1f6..1cda8f12 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/component/Tray.java @@ -11,23 +11,25 @@ import java.awt.*; import java.awt.TrayIcon.MessageType; +import com.urbanspork.client.gui.tray.TrayIcon; +import javafx.application.Platform; + public class Tray { private static final boolean IS_SUPPORTED = SystemTray.isSupported(); - - private static final PopupMenu menu = new PopupMenu(); - + private static final JPopupMenu menu = new JPopupMenu(); private static final ImageIcon icon = new ImageIcon(Resource.TRAY_ICON); - - private static final TrayIcon trayIcon = IS_SUPPORTED ? new TrayIcon(icon.getImage(), I18N.getString(I18N.PROGRAM_TITLE), menu) : null; - + private static TrayIcon trayIcon; private static Console console; private Tray() {} public static void init(Console console) { Tray.console = console; - start(); + if (IS_SUPPORTED) { + trayIcon = new TrayIcon(icon.getImage(), I18N.getString(I18N.PROGRAM_TITLE), () -> Platform.runLater(console::show), menu); + start(); + } } public static void displayMessage(String caption, String text, MessageType messageType) { @@ -54,28 +56,31 @@ public static void exit() { } private static void start() { - if (IS_SUPPORTED) { - // ============================== - // tray icon - // ============================== - SystemTray tray = SystemTray.getSystemTray(); - trayIcon.setImageAutoSize(true); - try { - tray.add(trayIcon); - } catch (AWTException e) { - displayMessage("Error", e.getMessage(), MessageType.ERROR); - } - // ============================== - // tray menu - // ============================== - menu.add(new ServersMenuItem(console).build()); - menu.addSeparator(); - menu.add(new ConsoleMenuItem(console).build()); - menu.addSeparator(); - menu.add(new LanguageMenuItem().build()); - menu.addSeparator(); - menu.add(new ExitMenuItem().build()); + // ============================== + // tray icon + // ============================== + SystemTray tray = SystemTray.getSystemTray(); + trayIcon.setImageAutoSize(true); + try { + tray.add(trayIcon); + } catch (AWTException e) { + displayMessage("Error", e.getMessage(), MessageType.ERROR); } + // ============================== + // tray menu + // ============================== + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignore) { + // ignore + } + SwingUtilities.updateComponentTreeUI(menu); + menu.add(new ServersMenuItem(console).build()); + menu.addSeparator(); + menu.add(new ConsoleMenuItem(console).build()); + menu.addSeparator(); + menu.add(new LanguageMenuItem().build()); + menu.addSeparator(); + menu.add(new ExitMenuItem().build()); } - } \ No newline at end of file diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/TrayIcon.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/TrayIcon.java new file mode 100644 index 00000000..5bcf1928 --- /dev/null +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/TrayIcon.java @@ -0,0 +1,81 @@ +package com.urbanspork.client.gui.tray; + +import javax.swing.*; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; + +public class TrayIcon extends java.awt.TrayIcon { + private JDialog dialog; + + public TrayIcon(Image image, String tooltip, Runnable onLeftClick, JPopupMenu popup) { + super(image, tooltip); + setImageAutoSize(true); + // add mouse listener + addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 && onLeftClick != null) { + onLeftClick.run(); + } + if (e.getButton() == MouseEvent.BUTTON3 && e.isPopupTrigger()) { + dialog.setLocation(adjustLocation(e.getPoint(), popup.getPreferredSize().getHeight())); + dialog.setVisible(true); + popup.show(dialog, 0, 0); + } + } + }); + // add property change listener + SystemTray.getSystemTray().addPropertyChangeListener("trayIcons", e -> { + java.awt.TrayIcon[] oldArray = (java.awt.TrayIcon[]) e.getOldValue(); + java.awt.TrayIcon[] newArray = (java.awt.TrayIcon[]) e.getNewValue(); + if (contains(oldArray, this) && !contains(newArray, this)) { + dialog.dispose(); + } + if (!contains(oldArray, this) && contains(newArray, this)) { + dialog = new JDialog(); + dialog.setUndecorated(true); + } + }); + // add popup menu listener + popup.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + // should do nothing + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + dialog.setVisible(false); + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + dialog.setVisible(false); + } + }); + } + + private static Point adjustLocation(Point p, double menuHeight) { + GraphicsDevice screenDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0]; + Rectangle bounds = screenDevice.getDefaultConfiguration().getBounds(); + if (bounds.contains(p)) { + return p; + } else { + double scale = screenDevice.getDisplayMode().getWidth() / bounds.getWidth(); + int x = (int) (p.getX() / scale); + int y = (int) (p.getY() / scale - menuHeight); + return new Point(x, y); + } + } + + private boolean contains(Object[] arr, Object obj) { + if (arr == null || arr.length == 0) { + return false; + } + return Arrays.asList(arr).contains(obj); + } +} diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java index 8a574e10..3749968d 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ConsoleMenuItem.java @@ -4,7 +4,6 @@ import com.urbanspork.client.gui.i18n.I18N; import javafx.application.Platform; -import java.awt.*; import java.awt.event.ActionListener; public class ConsoleMenuItem implements TrayMenuItemBuilder { @@ -15,11 +14,6 @@ public ConsoleMenuItem(Console console) { this.console = console; } - @Override - public Menu getMenuItem() { - return null; - } - @Override public String getLabel() { return I18N.getString(I18N.TRAY_MENU_CONSOLE); diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java index 1b8566fe..bb376eee 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ExitMenuItem.java @@ -5,16 +5,9 @@ import com.urbanspork.client.gui.i18n.I18N; import javafx.application.Platform; -import java.awt.*; import java.awt.event.ActionListener; public class ExitMenuItem implements TrayMenuItemBuilder { - - @Override - public MenuItem getMenuItem() { - return null; - } - @Override public String getLabel() { return I18N.getString(I18N.TRAY_EXIT); @@ -28,5 +21,4 @@ public ActionListener getActionListener() { Proxy.exit(); }; } - } \ No newline at end of file diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java index 6c62e1ac..60304ef7 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/LanguageMenuItem.java @@ -6,65 +6,44 @@ import com.urbanspork.common.config.ClientConfig; import com.urbanspork.common.config.ConfigHandler; -import java.awt.*; +import javax.swing.*; import java.awt.TrayIcon.MessageType; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; -public class LanguageMenuItem implements TrayMenuItemBuilder { - - @Override - public MenuItem getMenuItem() { - Menu menu = new Menu(getLabel()); +public class LanguageMenuItem { + public JMenuItem build() { + JMenu menu = new JMenu(getLabel()); ClientConfig config = Resource.config(); String language = config.getLanguage(); final Locale configLanguage = Locale.of(language); Locale[] languages = I18N.languages(); - List items = new ArrayList<>(languages.length); + ButtonGroup group = new ButtonGroup(); for (Locale locale : languages) { - CheckboxMenuItem item = new CheckboxMenuItem(); + JRadioButtonMenuItem item = new JRadioButtonMenuItem(); item.setName(locale.getLanguage()); - item.setLabel(locale.getDisplayLanguage(configLanguage)); + item.setText(locale.getDisplayLanguage(configLanguage)); if (locale.equals(configLanguage)) { - item.setState(true); + item.setSelected(true); } - item.addItemListener(l -> itemStateChanged(config, items, item)); - items.add(item); + item.addActionListener(evt -> { + if (item.isSelected()) { + config.setLanguage(item.getName()); + try { + ConfigHandler.DEFAULT.save(config); + } catch (Exception e) { + Tray.displayMessage("Error", "Save file error, cause: " + e.getMessage(), MessageType.ERROR); + return; + } + Tray.displayMessage("Config is saved", "Take effect after restart", MessageType.INFO); + } + }); + group.add(item); menu.add(item); } return menu; } - private void itemStateChanged(ClientConfig config, List items, CheckboxMenuItem item) { - if (item.getState()) { - config.setLanguage(item.getName()); - try { - ConfigHandler.DEFAULT.save(config); - } catch (Exception e) { - Tray.displayMessage("Error", "Save file error, cause: " + e.getMessage(), MessageType.ERROR); - return; - } - Tray.displayMessage("Config is saved", "Take effect after restart", MessageType.INFO); - for (CheckboxMenuItem i : items) { - if (!i.equals(item) && i.getState()) { - i.setState(false); - } - } - } else { - item.setState(true); - } - } - - @Override - public String getLabel() { + private String getLabel() { return I18N.getString(I18N.TRAY_MENU_LANGUAGE); } - - @Override - public ActionListener getActionListener() { - return null; - } - } \ No newline at end of file diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java index c85f7156..5785f3bc 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/ServersMenuItem.java @@ -9,13 +9,12 @@ import com.urbanspork.common.config.ConfigHandler; import com.urbanspork.common.config.ServerConfig; -import java.awt.*; +import javax.swing.*; import java.awt.TrayIcon.MessageType; -import java.awt.event.ActionListener; -import java.util.ArrayList; +import java.awt.event.ItemListener; import java.util.List; -public class ServersMenuItem implements TrayMenuItemBuilder { +public class ServersMenuItem { private final Console console; @@ -23,59 +22,44 @@ public ServersMenuItem(Console console) { this.console = console; } - @Override - public MenuItem getMenuItem() { - Menu menu = new Menu(getLabel()); + public JMenuItem build() { + JMenu menu = new JMenu(getLabel()); ClientConfig config = Resource.config(); List servers = config.getServers(); + ButtonGroup group = new ButtonGroup(); if (servers != null && !servers.isEmpty()) { - final List items = new ArrayList<>(); - for (int j = 0; j < servers.size(); j++) { - ServerConfig server = servers.get(j); - CheckboxMenuItem item = new CheckboxMenuItem(); - item.setLabel(getLabel(server)); - if (config.getIndex() == j) { - item.setState(true); + for (int i = 0; i < servers.size(); i++) { + ServerConfig server = servers.get(i); + JRadioButtonMenuItem item = new JRadioButtonMenuItem(); + item.setText(getLabel(server)); + if (config.getIndex() == i) { + item.setSelected(true); } - item.addItemListener(listener -> itemStateChanged(config, items, item)); - items.add(item); + item.addItemListener(createItemListener(item, config, i)); + group.add(item); menu.add(item); } } return menu; } - private void itemStateChanged(ClientConfig config, List items, CheckboxMenuItem item) { - if (item.getState()) { - for (int k = 0; k < items.size(); k++) { - CheckboxMenuItem i = items.get(k); - if (i.equals(item)) { - config.setIndex(k); - console.getServerConfigJFXListView().getSelectionModel().select(k); + private ItemListener createItemListener(JRadioButtonMenuItem item, ClientConfig config, int index) { + return event -> { + if (item.isSelected()) { + config.setIndex(index); + console.getServerConfigJFXListView().getSelectionModel().select(index); + try { + ConfigHandler.DEFAULT.save(config); + } catch (Exception e) { + Tray.displayMessage("Error", "Save file error, cause: " + e.getMessage(), MessageType.ERROR); + return; } - if (!i.equals(item) && i.getState()) { - i.setState(false); - } - } - try { - ConfigHandler.DEFAULT.save(config); - } catch (Exception e) { - Tray.displayMessage("Error", "Save file error, cause: " + e.getMessage(), MessageType.ERROR); - return; + Proxy.launch(); } - Proxy.launch(); - } else { - item.setState(true); - } + }; } - @Override - public ActionListener getActionListener() { - return null; - } - - @Override - public String getLabel() { + private String getLabel() { return I18N.getString(I18N.TRAY_MENU_SERVERS); } @@ -87,5 +71,4 @@ private String getLabel(ServerConfig config) { } return builder.toString(); } - } \ No newline at end of file diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/TrayMenuItemBuilder.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/TrayMenuItemBuilder.java index 95762f77..198a1f43 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/TrayMenuItemBuilder.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/tray/menu/item/TrayMenuItemBuilder.java @@ -1,26 +1,18 @@ package com.urbanspork.client.gui.tray.menu.item; -import java.awt.*; +import javax.swing.*; import java.awt.event.ActionListener; -import java.util.Optional; public interface TrayMenuItemBuilder { - MenuItem getMenuItem(); - String getLabel(); ActionListener getActionListener(); - default MenuItem build() { - return Optional.ofNullable(getMenuItem()).orElse(build(getLabel(), getActionListener())); - } - - default MenuItem build(String label, ActionListener listener) { - MenuItem item = new MenuItem(); - item.setLabel(label); - item.addActionListener(listener); + default JMenuItem build() { + JMenuItem item = new JMenuItem(); + item.setText(getLabel()); + item.addActionListener(getActionListener()); return item; } - }