diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 000000000..dee8040a9 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,26 @@ +name: Build PR + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +on: + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + + build: + strategy: + matrix: + java-version: + - 17 + - 21 + uses: ./.github/workflows/build.yml + with: + javaVersion: "${{ matrix.java-version }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..d426d58cb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build + +on: + workflow_call: + inputs: + javaVersion: + required: true + type: string + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: 'Checkout' + uses: actions/checkout@v4 + - name: 'Set Up Java' + uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.javaVersion }} + distribution: 'temurin' + cache: 'maven' + - name: 'Build Project' + run: | + export MAVEN_OPTS="-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN" + mvn --batch-mode --errors --fail-at-end test diff --git a/.gitignore b/.gitignore index 1e6c9124f..5ddf319f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /target /gnupg +.idea +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 0e0ded2bb..3455e196a 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,38 @@ -#Netty-socketio Overview - +Netty-socketio Overview +=== This project is an open-source Java implementation of [Socket.IO](http://socket.io/) server. Based on [Netty](http://netty.io/) server framework. -Checkout [Demo project](https://github.com/mrniko/netty-socketio-demo) - Licensed under the Apache License 2.0. Features ================================ -* Supports __0.7__...__0.9.16__ (netty-socketio 1.6.6) and __1.0+__ (netty-socketio latest version) version of [Socket.IO-client](https://github.com/LearnBoost/socket.io-client) +* __1.x - 4.x__ version of [Socket.IO-client](https://github.com/socketio/socket.io) is supported * Supports xhr-polling transport * Supports websocket transport * Supports namespaces and rooms * Supports ack (acknowledgment of received data) * Supports SSL -* Supports client store (Memory, [Redisson](https://github.com/mrniko/redisson), [Hazelcast](http://www.hazelcast.com/)) -* Supports distributed broadcast across netty-socketio nodes ([Redisson](https://github.com/mrniko/redisson), [Hazelcast](http://www.hazelcast.com/)) +* Supports client store ([Redisson](https://redisson.pro), Hazelcast, Memory) +* Supports distributed broadcast across netty-socketio nodes ([Redisson](https://redisson.pro), Hazelcast) * Supports OSGi * Supports Spring +* Contains Java module info for JPMS. * Lock-free and thread-safe implementation * Declarative handler configuration via annotations +JAR is compatible with Java 8 but needs Java 11+ for building the module-info. + +### Maven + +Include the following to your dependency list: +```xml + + com.corundumstudio.socketio + netty-socketio + 2.0.13 + +``` Performance ================================ @@ -38,33 +49,220 @@ Customer feedback in __2014__: Projects using netty-socketio ================================ +Multiplayer Orchestra: [multiplayer-orchestra.com](https://multiplayer-orchestra.com/) AVOS Cloud: [avoscloud.com](https://avoscloud.com/) -Bingo Crack: [bingocrack.com](http://bingocrack.com/) Kambi Sports Solutions: [kambi.com](http://kambi.com/) ARSnova: [arsnova.eu](https://arsnova.eu) -Zipwhip: [zipwhip.com](https://zipwhip.com/) Recent Releases ================================ -####Please Note: trunk is current development branch. +#### Please Note: trunk is current development branch. + +#### 19-Mar-2025 - version 2.0.13 released + +Fixed - Socket.IO Client v2 fails to connect (thanks to @penguinlab) +Fixed - Support for v1 / readNamespace. V1 namespace is without special characters , or ? (thanks to @Pacl0) + +#### 01-Nov-2024 - version 2.0.12 released + +Feature - enableCors setting added (thanks to @zd925) +Feature - ability to define http request decoder (thanks to @gurkancakir) + +Fixed - if ack returns after the timeout trigger ,a IllegalStateException will be thrown (thanks to @malinGH) +Fixed - prevents the client from repeatedly connecting to the namespace (thanks to @bigtian99) +Fixed - treat attachments as binary by default (thanks to @nicsor) +Fixed - recoverable handshake failure handling (thanks to @berinhardt) +Fixed - HashedWheelTimeoutScheduler should remove scheduledFutures before task execution (thanks to @berinhardt) + +#### 25-Jul-2024 - version 2.0.11 released + +Fixed - custom namespaces support auth data (thanks to @Addi) +Fixed - attachments frame buffer reading +Fixed - memory leak while WebSocketServerHandshakeException or Channel failed (thanks to @h3mant-1) + +#### 01-Mar-2024 - version 2.0.9 released + +Feature - v3/v4 parsing of multiple messages in one HTTP polling body (thanks to @unverbraucht) + +Fixed - IllegalReferenceCountException error + +#### 24-Jan-2024 - version 2.0.8 released + +Fixed - possible OOM caused by attachments parsing + +#### 23-Jan-2024 - version 2.0.7 released + +Github Actions support made by @liangyuanpeng + +Feature - Java module support (thanks to @unverbraucht) +Feature - AuthTokenListener added (thanks to @unverbraucht) +Feature - multi-packet separator support (thanks to @unverbraucht) + +Improvement - Send Bulk leave event instead of multiple leave events on disconnect (thanks to @ksahu750) + +Fixed - NPE during ack response timeout (thanks to @BlueSodaWater) +Fixed - Resource Leak in readVersion() method (thanks to @JHOANG23) + +#### 24-Oct-2023 - version 2.0.6 released + +Feature - added availability to add key-value pairs to socket store during authorization (thanks to @shutuper) +Feature - getRoomOperations() method with room varargs param (thanks to @shutuper) + +#### 01-Jul-2023 - version 2.0.3 released +Fixed - client can't connect to non-default namespace (thanks to @lyjnew) + +#### 17-May-2023 - version 2.0.2 released +Fixed - NPE in WebSocketTransport and PollingTransport (thanks to @Hunterk95) + +#### 14-May-2023 - version 2.0.1 released +JDK 8 is minimum version required for usage. + +Fixed - message larger than maxFramePayloadLength causes memory leak + +#### 14-May-2023 - version 2.0.0 released +Feature - SocketIO 4.x protocol support (thanks to @HashZhang) -####05-Dec-2014 - version 1.7.5 released (SocketIO 1.0+ protocol) +Fixed - add all the socketio specific classes to bundles (thanks to @rishabhgup) + +#### 17-Apr-2023 - version 1.7.25 released +Fixed - io.netty.channel.ChannelPipelineException error + +#### 14-Apr-2023 - version 1.7.24 released +Feature - SocketIOClient.isWritable() method added (thanks @xuwenqing04) +Feature - Namespace.addListeners(Iterable listeners) method added (thanks @damonxue) + +Fixed - disconnect event packet should be a subtype of MESSAGE (thanks @MasterShi) +Fixed - BaseStoreFactory throws NPE + +#### 02-Feb-2023 - version 1.7.23 released +Feature - Added support for bulk join and leave (thanks @ksahu750) + +Fixed - auto disconnected (regression since 1.7.22) + +#### 22-Sep-2022 - version 1.7.22 released +Feature - ping/pong support for socketio v4 protocol (thanks to @HashZhang) + +#### 06-Aug-2022 - version 1.7.21 released +Fixed - transport name should be checked in uppercase (regression since 1.7.20) + +#### 26-Jul-2022 - version 1.7.20 released +Feature - needClientAuth setting added (thanks to @huws) + +Fixed - ContinuationWebSocketFrame should be used if payload > maxFramePayloadLength (thanks to 俞伟搏) +Fixed - event listener src class and dest bean is no match (thanks to @cifaz) +Fixed - illegal transport parameter (thanks to @mirus36) + +#### 29-Apr-2021 - version 1.7.19 released +Feature - writeBufferWaterMarkLow and writeBufferWaterMarkHigh settings added (thanks to @xuminwlt) +Feature - allowHeaders setting added (thanks to @HuangHuan) +Feature - getCurrentRoomSize() method added (thanks to @sunxiaoyu3) + +Fixed - namespace bug (thanks to @@johntyty912) +Fixed - multiple nodes with redisson receive same message (thanks to 梁嘉祺) +Fixed - multiple nodes receive only one LEAVE msg on client disconnect (thanks to @GaryLeung922) + +#### 17-Jan-2020 - version 1.7.18 released +Feature - support for event interceptors which catch all events (thanks to @yosiat) +Fixed - namespace event broadcasting (thanks to Volodymyr Masliy) + +#### 11-Jan-2019 - version 1.7.17 released +Feature - randomSession setting added to Config object (thanks to @yuanxiangz) +Fixed - NPE in WebSocketTransport +Fixed - NPE & memory leak (thanks to zhaolianwang) +Fixed - namespace parsing (thanks to Redliver) +Fixed - Redisson 3.9+ compatibility + +#### 06-Jul-2018 - version 1.7.16 released +Fixed - non thread-safe ACK handling (thanks to @dawnbreaks) +Fixed - inactive long-polling channels cause memory leak (thanks to @dawnbreaks) +Fixed - websocket CloseFrame processing (thanks to @hangsu.cho) +Fixed - WebSocketTransport NPE + +#### 15-May-2018 - version 1.7.15 released + +Fixed - Session ID is not unique anymore +Fixed - fixed underlying connection not closing on ping timeout +Fixed - the "fin_close" problem + +#### 26-Feb-2018 - version 1.7.14 released +Feature - added local socket address for the connection (thanks to @SergeyGrigorev) +Feature - `addPingListener` method added (thanks to @lovebing) +Feature - add ThreadFactory for HashedWheelTimer (thanks to @hand515) +Fixed - changed SO_LINGER to be handled as child channel (not server channel) option (thanks to @robymus) +Fixed - ByteBuf leak if binary attachments are used +Fixed - restore session from Cookie (thanks to @wuxudong) +Fixed - NumberFormatException when b64 is bool value (thanks to @vonway) +Fixed - data encoding for polling transport + +#### 20-Sep-2017 - version 1.7.13 released +Feature - Added option to change the SSL KeyFactoryAlgorithm using Configuration (thanks to @robymus) +Improvement - Binary ack handling improvements (thanks to Sergey Bushik) +Fixed - Failed to mark a promise as success because it has succeeded already (thanks to @robymus) + +#### 27-Aug-2016 - version 1.7.12 released +Feature - `SocketIOServer.removeAllListeners` method added +Feature - `BroadcastOperations.sendEvent` method with `excludedClient` param added +Improvement - Redisson updated to 2.4.0 +Fixed - memory leak in Namespace object (thanks to @CrazyIvan007) + + +#### 13-Jul-2016 - version 1.7.11 released +Fixed - Throw error if transport not supported +Fixed - Client disconnecting when using Polling - IndexOutOfBoundsException + +#### 4-Mar-2016 - version 1.7.10 released +Fixed - netty updated to 4.1.0.CR3 version +Fixed - binary packet parsing (thanks to Winston Li) + +#### 6-Feb-2016 - version 1.7.9 released +Feature - Compression support +Fixed - DotNET client request handling +Fixed - Packet length format parsing +Fixed - skipping 'd=' in packet +Fixed - Polling clients sporadically get prematurely disconnected (thanks to lpage30) +Fixed - connections stay open forever if server sent `close` packet +Fixed - compatibility with Redisson latest version + +#### 30-Nov-2015 - version 1.7.8 released +Improvement - `WebSocketServerHandshaker.allowExtensions` is `true` now +Improvement - SessionID cookie implementation (thanks to @ryandietrich) +Fixed - clientRooms leak (thanks to @andreaspalm) +Fixed - ExceptionListener not used for errors in JSON parsing +Fixed - "silent channel" attack + +#### 26-Mar-2015 - version 1.6.7 released +Improvement - `useStrictOrdering` param added for websocket packets strict ordering +Improvement - `FAIL_ON_EMPTY_BEANS = false` option setted in json decoder + +#### 18-Feb-2015 - version 1.7.7 released +Improvement - no need to add jackson lib if you use own JsonSupport impl +Fixed - SocketIO client 1.3.x support +Fixed - Charset encoding handling (thanks to alim-akbashev) + +#### 17-Jan-2015 - version 1.7.6 released +Improvement - `SocketIONamespace.getName()` added +Fixed - WebSocket frames aggregation +Fixed - WebSocket buffer release +Fixed - `Unexpected end-of-input in VALUE_STRING` error +Fixed - Access-Control-Allow-Credentials is TRUE for requests with origin header + +#### 05-Dec-2014 - version 1.7.5 released Feature - `Configuration.sslProtocol` param added Fixed - BinaryEvent ack handling Fixed - BinaryEvent non b64 encoding/decoding Fixed - buffer leak during packet encoding -####15-Nov-2014 - version 1.7.4 released (SocketIO 1.0+ protocol) +#### 15-Nov-2014 - version 1.7.4 released Fixed - packet encoding Fixed - BinaryEvent encoding/decoding Fixed - unchallenged connections handling -####29-Sep-2014 - version 1.6.6 released +#### 29-Sep-2014 - version 1.6.6 released Feature - `origin` setting added Feature - `crossDomainPolicy` setting added Feature - `SocketIOServer.startAsync` method added -####24-Sep-2014 - version 1.7.3 released (SocketIO 1.0+ protocol) +#### 24-Sep-2014 - version 1.7.3 released Feature - Epoll support Improvement - BinaryEvent support Fixed - SocketIOClient disconnect handling @@ -73,23 +271,23 @@ Fixed - NPE then no transport defined during auth Fixed - ping timeout for polling transport Fixed - buffer leak in PacketEncoder -####22-Aug-2014 - version 1.7.2 released (SocketIO 1.0+ protocol) +#### 22-Aug-2014 - version 1.7.2 released Fixed - wrong outgoing message encoding using websocket transport Fixed - NPE in websocket transport Fixed - multiple packet decoding in polling transport Fixed - buffer leak -####07-Jul-2014 - version 1.7.1 released (SocketIO 1.0+ protocol) +#### 07-Jul-2014 - version 1.7.1 released Feature - ability to set custom `Access-Control-Allow-Origin` via Configuration.origin Fixed - connection via CLI socket.io-client -####28-Jun-2014 - version 1.7.0 released (SocketIO 1.0+ protocol) +#### 28-Jun-2014 - version 1.7.0 released Feature - Socket.IO 1.0 protocol support. Thanks to the new protocol decoding/encoding has speedup __Dropped__ - `SocketIOClient.sendMessage`, `SocketIOClient.sendJsonObject` methods and corresponding listeners __Dropped__ - Flashsocket transport support __Dropped__ - protocol version 0.7 ... 0.9.16 -####13-May-2014 - version 1.6.5 released (JDK 1.6+ compatible) +#### 13-May-2014 - version 1.6.5 released Improvement - single packet encoding optimized, used mostly in WebSocket transport. Encoding time reduced up to 40% (thanks to Viktor Endersz) Improvement - rooms handling optimized Improvement - ExceptionListener.exceptionCaught method added @@ -99,7 +297,7 @@ Feature - maxFramePayloadLength setting added Feature - getAllClients and getClient methods added to SocketIONamespace Fixed - SocketIOServer.getAllClients returns wrong clients amount -####25-Mar-2014 - version 1.6.4 released (JDK 1.6+ compatible, Netty 4.0.17) +#### 25-Mar-2014 - version 1.6.4 released Fixed - message release problem Fixed - problem with exception listener configuration redefinition __Breaking api change__ - DataListener.onData now throws Exception @@ -107,7 +305,7 @@ Improvement - data parameter added to exception listener Improvement - ability to setup socket configuration Improvement - Configuration.autoAck parameter added -####06-Mar-2014 - version 1.6.3 released (JDK 1.6+ compatible, Netty 4.0.17) +#### 06-Mar-2014 - version 1.6.3 released Fixed - AckCallback handling during client disconnect Fixed - unauthorized handshake HTTP code changed to 401 __Breaking api change__ - Configuration.heartbeatThreadPoolSize setting removed @@ -115,7 +313,7 @@ Feature - annotated Spring beans support via _SpringAnnotationScanner_ Feature - common exception listener Improvement - _ScheduledExecutorService_ replaced with _HashedWheelTimer_ -####08-Feb-2014 - version 1.6.2 released (JDK 1.6+ compatible, Netty 4.0.15) +#### 08-Feb-2014 - version 1.6.2 released Fixed - wrong namespace client disconnect handling Fixed - exception in onConnect/onDisconnect/isAuthorized methods leads to server hang __Breaking api change__ - SocketIOClient.sendEvent methods signature changed @@ -124,11 +322,11 @@ Improvement - multi type events ack support via _MultiTypeAckCallback_ Improvement - SocketIOClient.getHandshakeData method added Improvement - Jedis replaced with [Redisson](https://github.com/mrniko/redisson) -####14-Jan-2014 - version 1.6.1 released (JDK 1.6+ compatible, Netty 4.0.14) +#### 14-Jan-2014 - version 1.6.1 released Fixed - JDK 1.6+ compatibility Feature - authorization support -####19-Dec-2013 - version 1.6.0 released (JDK 1.6+ compatible, Netty 4.0.13) +#### 19-Dec-2013 - version 1.6.0 released Fixed - XHR-pooling transport regression Fixed - Websocket transport regression Fixed - namespace NPE in PacketHandler @@ -139,193 +337,31 @@ Feature - OSGi support (thanks to rdevera) Improvement - XHR-pooling optimization Improvement - SocketIOClient.getAllRooms method added -####07-Dec-2013 - version 1.5.4 released (JDK 1.6+ compatible, Netty 4.0.13) +#### 07-Dec-2013 - version 1.5.4 released Fixed - flash policy "request leak" after page reload (thanks to ntrp) Fixed - websocket swf loading (thanks to ntrp) Fixed - wrong urls causes a potential DDoS Fixed - Event.class package visibility changed to avoid direct usage Improvement - Simplified Jackson modules registration -####24-Oct-2013 - version 1.5.2 released (JDK 1.6+ compatible, Netty 4.0.11) +#### 24-Oct-2013 - version 1.5.2 released Fixed - NPE during shutdown Improvement - isEmpty method added to Namespace -####13-Oct-2013 - version 1.5.1 released (JDK 1.6+ compatible, Netty 4.0.9) +#### 13-Oct-2013 - version 1.5.1 released Fixed - wrong ack timeout callback invocation Fixed - bigdecimal serialization for JSON Fixed - infinity loop during packet handling exception Fixed - 'client not found' handling -####27-Aug-2013 - version 1.5.0 released (JDK 1.6+ compatible, Netty 4.0.7) +#### 27-Aug-2013 - version 1.5.0 released Improvement - encoding buffers allocation optimization. Improvement - encoding buffers now pooled in memory to reduce GC pressure (netty 4.x feature). -####03-Aug-2013 - version 1.0.1 released (JDK 1.5+ compatible) +#### 03-Aug-2013 - version 1.0.1 released Fixed - error on unknown property during deserialization. Fixed - memory leak in long polling transport. Improvement - logging error info with inbound data. -####07-Jun-2013 - version 1.0.0 released (JDK 1.5+ compatible) +#### 07-Jun-2013 - version 1.0.0 released First stable release. - - -### Maven - -Include the following to your dependency list: - - - com.corundumstudio.socketio - netty-socketio - 1.6.5 - - - -Usage example -================================ -##Server - -Base configuration. More details about Configuration object is [here](https://github.com/mrniko/netty-socketio/wiki/Configuration-details). - - Configuration config = new Configuration(); - config.setHostname("localhost"); - config.setPort(81); - - SocketIOServer server = new SocketIOServer(config); - -Programmatic handlers binding: - - server.addMessageListener(new DataListener() { - @Override - public void onData(SocketIOClient client, String message, AckRequest ackRequest) { - ... - } - }); - - server.addEventListener("someevent", SomeClass.class, new DataListener() { - @Override - public void onData(SocketIOClient client, Object data, AckRequest ackRequest) { - ... - } - }); - - server.addConnectListener(new ConnectListener() { - @Override - public void onConnect(SocketIOClient client) { - ... - } - }); - - server.addDisconnectListener(new DisconnectListener() { - @Override - public void onDisconnect(SocketIOClient client) { - ... - } - }); - - - // Don't forget to include type field on javascript side, - // it named '@class' by default and should equals to full class name. - // - // TIP: you can customize type field name via Configuration.jsonTypeFieldName property. - - server.addJsonObjectListener(SomeClass.class, new DataListener() { - @Override - public void onData(SocketIOClient client, SomeClass data, AckRequest ackRequest) { - - ... - - // send object to socket.io client - SampleObject obj = new SampleObject(); - client.sendJsonObject(obj); - } - }); - -Declarative handlers binding. Handlers could be bound via annotations on any object: - - pubic class SomeBusinessService { - - ... - // some stuff code - ... - - // SocketIOClient, AckRequest and Data could be ommited - @OnEvent('someevent') - public void onSomeEventHandler(SocketIOClient client, SomeClass data, AckRequest ackRequest) { - ... - } - - @OnConnect - public void onConnectHandler(SocketIOClient client) { - ... - } - - @OnDisconnect - public void onDisconnectHandler(SocketIOClient client) { - ... - } - - // only data object is required in arguments, - // SocketIOClient and AckRequest could be ommited - @OnJsonObject - public void onSomeEventHandler(SocketIOClient client, SomeClass data, AckRequest ackRequest) { - ... - } - - // only data object is required in arguments, - // SocketIOClient and AckRequest could be ommited - @OnMessage - public void onSomeEventHandler(SocketIOClient client, String data, AckRequest ackRequest) { - ... - } - - } - - SomeBusinessService someService = new SomeBusinessService(); - server.addListeners(someService); - - - server.start(); - - ... - - server.stop(); - -##Client - - - - diff --git a/checkstyle.xml b/checkstyle.xml index 9e2164bb5..0600f4cb7 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -19,7 +19,7 @@ + "https://checkstyle.org/dtds/configuration_1_3.dtd"> + @@ -58,22 +59,19 @@ - - - + - - + - + @@ -114,21 +112,24 @@ + - + - - + + + @@ -148,35 +149,96 @@ - + + + - + + + + + + - - - - + + + + + + - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -184,9 +246,10 @@ - - - + + + + @@ -212,6 +275,14 @@ + - + diff --git a/header.txt b/header.txt index 94fec660e..0d8bc91ed 100644 --- a/header.txt +++ b/header.txt @@ -1,4 +1,4 @@ -Copyright 2012 Nikita Koksharov +Copyright (c) 2012-2023 Nikita Koksharov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pom.xml b/pom.xml index c73e4e295..cffff8d74 100644 --- a/pom.xml +++ b/pom.xml @@ -1,351 +1,473 @@ - 4.0.0 + 4.0.0 - com.corundumstudio.socketio - netty-socketio - 1.7.6-SNAPSHOT - bundle - NettySocketIO - Socket.IO server implemented on Java - 2012 - https://github.com/mrniko/netty-socketio + com.corundumstudio.socketio + netty-socketio + 2.0.14-SNAPSHOT + bundle + NettySocketIO + Socket.IO server implemented on Java + 2012 + https://github.com/mrniko/netty-socketio - + scm:git:git@github.com:mrniko/netty-socketio.git scm:git:git@github.com:mrniko/netty-socketio.git scm:git:git@github.com:mrniko/netty-socketio.git - HEAD + HEAD - - - Apache v2 - http://www.apache.org/licenses/LICENSE-2.0.html - manual - - + + + Apache v2 + http://www.apache.org/licenses/LICENSE-2.0.html + manual + + - - - mrniko - Nikita Koksharov - abracham.mitchell@gmail.com - - Architect - Developer - - +4 - - + + + mrniko + Nikita Koksharov + abracham.mitchell@gmail.com + + Architect + Developer + + +4 + + - - - repo1 - Release - file://C:/123 - - + + + repo1 + Release + file://C:/123 + + - - true - UTF-8 - + + UTF-8 + UTF-8 + true + 4.1.119.Final + - - - exclude-swf-files - - - - src/main/resources - - static/** - - - - - - - release-sign-artifacts - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.4 - - - sign-artifacts - verify - - sign - - - - - - - - + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + true + true + + + ${implementation.build} + ${maven.build.timestamp} + ${javac.src.version} + ${javac.target.version} + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + + + net.ju-n.maven.plugins + checksum-maven-plugin + 1.4 + + + + + + + Linux + + + Linux + unix + + + + + io.netty + netty-transport-native-epoll + linux-x86_64 + ${netty.version} + + + io.netty + netty-transport-native-epoll + linux-aarch_64 + ${netty.version} + + + + + + + + io.netty + netty-buffer + ${netty.version} + + + io.netty + netty-common + ${netty.version} + + + io.netty + netty-transport + ${netty.version} + + + io.netty + netty-handler + ${netty.version} + + + io.netty + netty-codec-http + ${netty.version} + + + io.netty + netty-codec + ${netty.version} + + + io.netty + netty-transport-native-epoll + ${netty.version} + provided + - - - io.netty - netty-buffer - 4.0.24.Final - - - io.netty - netty-common - 4.0.24.Final - - - io.netty - netty-transport - 4.0.24.Final - - - io.netty - netty-handler - 4.0.24.Final - - - io.netty - netty-codec-http - 4.0.24.Final - - - io.netty - netty-codec - 4.0.24.Final - - - io.netty - netty-transport-native-epoll - 4.0.24.Final - - - + org.jmockit jmockit - 1.12 + 1.49 test - - + + junit junit - 4.11 + 4.13.2 test - + - + org.slf4j slf4j-api - 1.7.7 - + 2.0.16 + - - com.fasterxml.jackson.core - jackson-core - 2.4.3 - - - com.fasterxml.jackson.core - jackson-databind - 2.4.3 - + + com.fasterxml.jackson.core + jackson-core + 2.18.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.3 + - + org.springframework spring-beans - [2.5,) + [6.0.16,) provided - - + + org.springframework spring-core - [2.5,) + [6.0.16,) - - commons-logging - commons-logging - + + commons-logging + commons-logging + provided - + - org.redisson - redisson - 1.1.5 - provided + org.redisson + redisson + 3.45.1 + provided - com.hazelcast - hazelcast-client - 3.1.3 - provided + com.hazelcast + hazelcast-client + 3.12.12 + provided - - + - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.1 - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.9 - - true - true - - + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrepo + https://oss.sonatype.org/ + true + 15 + + - - org.apache.maven.plugins - maven-pmd-plugin - 3.0.1 - - - verify - - pmd - cpd - - - - - true - 100 - 1.6 - true - - + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + attach-javadocs + + jar + + + + + public + none + true + + - - org.apache.maven.plugins - maven-checkstyle-plugin - 2.11 - - - verify - - checkstyle - - - - - true - false - /checkstyle.xml - - + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + true + true + release + deploy + + - - maven-compiler-plugin - 3.1 - - 1.6 - 1.6 - true - true - - + + org.apache.maven.plugins + maven-enforcer-plugin + 3.3.0 + + + enforce-maven + + enforce + + + + + 3.0.5 + + + + + + - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar - - - - + + org.apache.maven.plugins + maven-pmd-plugin + 3.11.0 + + + verify + + pmd + cpd + + + + + true + 100 + 1.6 + true + + + - - org.apache.maven.plugins - maven-surefire-plugin - 2.16 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + default-compile - -Dfile.encoding=utf-8 + 11 + - + + + base-compile + + compile + + + 8 + 8 + + + module-info.java + + + + + default-testCompile + process-test-sources + + testCompile + + + + + + 8 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.46/jmockit-1.46.jar + + + - - org.apache.felix - maven-bundle-plugin - 2.3.7 - true - - - ${project.artifactId} - - org.springframework.*;resolution:=optional,com.hazelcast.*;resolution:=optional,org.redisson.*;resolution:=optional,* - - - com.corundumstudio.socketio;version="${project.version}", - com.corundumstudio.socketio.annotation;version="${project.version}", - com.corundumstudio.socketio.listener;version="${project.version}", - com.corundumstudio.socketio.protocol;version="${project.version}", - com.corundumstudio.socketio.store;version="${project.version}", - com.corundumstudio.socketio.store.pubsub;version="${project.version}", - - - - + + org.apache.felix + maven-bundle-plugin + 5.1.9 + true + + + ${project.artifactId} + + org.springframework.*;resolution:=optional,com.hazelcast.*;resolution:=optional,org.redisson.*;resolution:=optional,* + + + com.corundumstudio.socketio.*;version="${project.version}" + + + + - - com.mycila - license-maven-plugin - 2.6 - - ${basedir} -
${basedir}/header.txt
- false - true - false - - src/** - - - target/** - - true - - JAVADOC_STYLE - - true - true - UTF-8 -
- - - - check - - - -
-
-
+ + com.mycila + license-maven-plugin + 2.6 + + ${basedir} +
${basedir}/header.txt
+ false + true + false + + src/** + + + target/** + src/main/java/module-info.java + + true + + JAVADOC_STYLE + + true + true + UTF-8 +
+ + + + check + + + +
+
+
diff --git a/src/main/java/com/corundumstudio/socketio/AckCallback.java b/src/main/java/com/corundumstudio/socketio/AckCallback.java index 65c600504..21b743913 100644 --- a/src/main/java/com/corundumstudio/socketio/AckCallback.java +++ b/src/main/java/com/corundumstudio/socketio/AckCallback.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/AckMode.java b/src/main/java/com/corundumstudio/socketio/AckMode.java index 081cd8152..1eb5057d2 100644 --- a/src/main/java/com/corundumstudio/socketio/AckMode.java +++ b/src/main/java/com/corundumstudio/socketio/AckMode.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/AckRequest.java b/src/main/java/com/corundumstudio/socketio/AckRequest.java index 31e28dddf..5830d55dd 100644 --- a/src/main/java/com/corundumstudio/socketio/AckRequest.java +++ b/src/main/java/com/corundumstudio/socketio/AckRequest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ /** * Ack request received from Socket.IO client. - * You can always check is it true through + * You can always check is it true through * {@link #isAckRequested()} method. * * You can call {@link #sendAckData} methods only during @@ -80,7 +80,7 @@ public void sendAckData(List objs) { if (!isAckRequested() || !sended.compareAndSet(false, true)) { return; } - Packet ackPacket = new Packet(PacketType.MESSAGE); + Packet ackPacket = new Packet(PacketType.MESSAGE, client.getEngineIOVersion()); ackPacket.setSubType(PacketType.ACK); ackPacket.setAckId(originalPacket.getAckId()); ackPacket.setData(objs); diff --git a/src/main/java/com/corundumstudio/socketio/AuthTokenListener.java b/src/main/java/com/corundumstudio/socketio/AuthTokenListener.java new file mode 100644 index 000000000..48c1ff7f7 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/AuthTokenListener.java @@ -0,0 +1,30 @@ +package com.corundumstudio.socketio; + +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +public interface AuthTokenListener { + + /** Socket.IO clients from version 4 can offer an auth token when connecting + * to a namespace. This listener gets invoked if a token is found in the connect + * packet + * @param authToken the token as parsed by the JSON parser + * @param client client that is connecting + * @return authorization result + */ + AuthTokenResult getAuthTokenResult(Object authToken, SocketIOClient client); + +} diff --git a/src/main/java/com/corundumstudio/socketio/AuthTokenResult.java b/src/main/java/com/corundumstudio/socketio/AuthTokenResult.java new file mode 100644 index 000000000..310498e48 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/AuthTokenResult.java @@ -0,0 +1,37 @@ +package com.corundumstudio.socketio; + +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +public class AuthTokenResult { + + public final static AuthTokenResult AuthTokenResultSuccess = new AuthTokenResult(true, null); + private final boolean success; + private final Object errorData; + + public AuthTokenResult(final boolean success, final Object errorData) { + this.success = success; + this.errorData = errorData; + } + + public boolean isSuccess() { + return success; + } + + public Object getErrorData() { + return errorData; + } +} diff --git a/src/main/java/com/corundumstudio/socketio/AuthorizationListener.java b/src/main/java/com/corundumstudio/socketio/AuthorizationListener.java index 886a076ce..42d3cc563 100644 --- a/src/main/java/com/corundumstudio/socketio/AuthorizationListener.java +++ b/src/main/java/com/corundumstudio/socketio/AuthorizationListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,13 @@ public interface AuthorizationListener { /** - * Checks is client with handshake data is authorized + * Checks whether a client with handshake data is authorized on connection + * Optionally returns storeParams that will be added to {@link SocketIOClient} store * - * @param data - handshake data - * @return - true if client is authorized of false otherwise + * @param data handshake data + * @return - {@link AuthorizationResult} */ - boolean isAuthorized(HandshakeData data); + AuthorizationResult getAuthorizationResult(HandshakeData data); + } diff --git a/src/main/java/com/corundumstudio/socketio/AuthorizationResult.java b/src/main/java/com/corundumstudio/socketio/AuthorizationResult.java new file mode 100644 index 000000000..c970ef19f --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/AuthorizationResult.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio; + +import java.util.Collections; +import java.util.Map; + +public class AuthorizationResult { + + public static final AuthorizationResult SUCCESSFUL_AUTHORIZATION = new AuthorizationResult(true); + public static final AuthorizationResult FAILED_AUTHORIZATION = new AuthorizationResult(false); + private final boolean isAuthorized; + private final Map storeParams; + + public AuthorizationResult(boolean isAuthorized) { + this.isAuthorized = isAuthorized; + this.storeParams = Collections.emptyMap(); + } + + public AuthorizationResult(boolean isAuthorized, Map storeParams) { + this.isAuthorized = isAuthorized; + this.storeParams = isAuthorized && storeParams != null ? + Collections.unmodifiableMap(storeParams) : Collections.emptyMap(); + } + + /** + * @return true if a client is authorized, otherwise - false + * */ + public boolean isAuthorized() { + return isAuthorized; + } + + /** + * @return key-value pairs (unmodifiable) that will be added to {@link SocketIOClient } store. + * If a client is not authorized, storeParams will always be ignored (empty map) + * */ + public Map getStoreParams() { + return storeParams; + } +} diff --git a/src/main/java/com/corundumstudio/socketio/BroadcastAckCallback.java b/src/main/java/com/corundumstudio/socketio/BroadcastAckCallback.java index ec388d161..0b135a182 100644 --- a/src/main/java/com/corundumstudio/socketio/BroadcastAckCallback.java +++ b/src/main/java/com/corundumstudio/socketio/BroadcastAckCallback.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/BroadcastOperations.java b/src/main/java/com/corundumstudio/socketio/BroadcastOperations.java index 25293553b..41ee558c0 100644 --- a/src/main/java/com/corundumstudio/socketio/BroadcastOperations.java +++ b/src/main/java/com/corundumstudio/socketio/BroadcastOperations.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,98 +15,29 @@ */ package com.corundumstudio.socketio; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.corundumstudio.socketio.misc.IterableCollection; -import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.protocol.Packet; -import com.corundumstudio.socketio.protocol.PacketType; -import com.corundumstudio.socketio.store.StoreFactory; -import com.corundumstudio.socketio.store.pubsub.DispatchMessage; -import com.corundumstudio.socketio.store.pubsub.PubSubStore; + +import java.util.Collection; +import java.util.function.Predicate; /** - * Fully thread-safe. + * broadcast interface * */ -public class BroadcastOperations implements ClientOperations { - - private final Iterable clients; - private final Map> namespaceRooms = new HashMap>(); - private final StoreFactory storeFactory; - - public BroadcastOperations(Iterable clients, StoreFactory storeFactory) { - super(); - this.clients = clients; - for (SocketIOClient socketIOClient : clients) { - Namespace namespace = (Namespace)socketIOClient.getNamespace(); - Set rooms = namespace.getRooms(socketIOClient); - - List roomsList = namespaceRooms.get(namespace.getName()); - if (roomsList == null) { - roomsList = new ArrayList(); - namespaceRooms.put(namespace.getName(), roomsList); - } - roomsList.addAll(rooms); - } - this.storeFactory = storeFactory; - } +public interface BroadcastOperations extends ClientOperations { - private void dispatch(Packet packet) { - for (Entry> entry : namespaceRooms.entrySet()) { - for (String room : entry.getValue()) { - storeFactory.pubSubStore().publish(PubSubStore.DISPATCH, new DispatchMessage(room, packet, entry.getKey())); - } - } - } + Collection getClients(); - public Collection getClients() { - return new IterableCollection(clients); - } + void send(Packet packet, BroadcastAckCallback ackCallback); - @Override - public void send(Packet packet) { - for (SocketIOClient client : clients) { - client.send(packet); - } - dispatch(packet); - } + void sendEvent(String name, SocketIOClient excludedClient, Object... data); - public void send(Packet packet, BroadcastAckCallback ackCallback) { - for (SocketIOClient client : clients) { - client.send(packet, ackCallback.createClientCallback(client)); - } - ackCallback.loopFinished(); - } + void sendEvent(String name, Predicate excludePredicate, Object... data); - @Override - public void disconnect() { - for (SocketIOClient client : clients) { - client.disconnect(); - } - } + void sendEvent(String name, Object data, BroadcastAckCallback ackCallback); - @Override - public void sendEvent(String name, Object... data) { - Packet packet = new Packet(PacketType.MESSAGE); - packet.setSubType(PacketType.EVENT); - packet.setName(name); - packet.setData(Arrays.asList(data)); - send(packet); - } + void sendEvent(String name, Object data, SocketIOClient excludedClient, BroadcastAckCallback ackCallback); - public void sendEvent(String name, Object data, BroadcastAckCallback ackCallback) { - for (SocketIOClient client : clients) { - client.sendEvent(name, ackCallback.createClientCallback(client), data); - } - ackCallback.loopFinished(); - } + void sendEvent(String name, Object data, Predicate excludePredicate, BroadcastAckCallback ackCallback); } diff --git a/src/main/java/com/corundumstudio/socketio/ClientOperations.java b/src/main/java/com/corundumstudio/socketio/ClientOperations.java index bbd48d46a..207c360c0 100644 --- a/src/main/java/com/corundumstudio/socketio/ClientOperations.java +++ b/src/main/java/com/corundumstudio/socketio/ClientOperations.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/Configuration.java b/src/main/java/com/corundumstudio/socketio/Configuration.java index 63e59bd81..c29539ed0 100644 --- a/src/main/java/com/corundumstudio/socketio/Configuration.java +++ b/src/main/java/com/corundumstudio/socketio/Configuration.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,18 @@ */ package com.corundumstudio.socketio; -import java.io.InputStream; -import java.util.Arrays; -import java.util.List; - import com.corundumstudio.socketio.handler.SuccessAuthorizationListener; import com.corundumstudio.socketio.listener.DefaultExceptionListener; import com.corundumstudio.socketio.listener.ExceptionListener; -import com.corundumstudio.socketio.protocol.JacksonJsonSupport; import com.corundumstudio.socketio.protocol.JsonSupport; import com.corundumstudio.socketio.store.MemoryStoreFactory; import com.corundumstudio.socketio.store.StoreFactory; +import io.netty.handler.codec.http.HttpDecoderConfig; + +import javax.net.ssl.KeyManagerFactory; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; public class Configuration { @@ -44,6 +45,7 @@ public class Configuration { private int upgradeTimeout = 10000; private int pingTimeout = 60000; private int pingInterval = 25000; + private int firstDataTimeout = 5000; private int maxHttpContentLength = 64 * 1024; private int maxFramePayloadLength = 64 * 1024; @@ -58,17 +60,21 @@ public class Configuration { private InputStream keyStore; private String keyStorePassword; + private String allowHeaders; + private String trustStoreFormat = "JKS"; private InputStream trustStore; private String trustStorePassword; + private String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + private boolean preferDirectBuffer = true; private SocketConfig socketConfig = new SocketConfig(); private StoreFactory storeFactory = new MemoryStoreFactory(); - private JsonSupport jsonSupport = new JacksonJsonSupport(); + private JsonSupport jsonSupport; private AuthorizationListener authorizationListener = new SuccessAuthorizationListener(); @@ -78,13 +84,25 @@ public class Configuration { private String origin; + private boolean enableCors = true; + + private boolean httpCompression = true; + + private boolean websocketCompression = true; + + private boolean randomSession = false; + + private boolean needClientAuth = false; + + private HttpRequestDecoderConfiguration httpRequestDecoderConfiguration = new HttpRequestDecoderConfiguration(); + public Configuration() { } /** * Defend from further modifications by cloning * - * @param configuration - Configuration object to clone + * @param conf - Configuration object to clone */ Configuration(Configuration conf) { setBossThreads(conf.getBossThreads()); @@ -93,10 +111,26 @@ public Configuration() { setPingInterval(conf.getPingInterval()); setPingTimeout(conf.getPingTimeout()); + setFirstDataTimeout(conf.getFirstDataTimeout()); setHostname(conf.getHostname()); setPort(conf.getPort()); + if (conf.getJsonSupport() == null) { + try { + getClass().getClassLoader().loadClass("com.fasterxml.jackson.databind.ObjectMapper"); + try { + Class jjs = getClass().getClassLoader().loadClass("com.corundumstudio.socketio.protocol.JacksonJsonSupport"); + JsonSupport js = (JsonSupport) jjs.getConstructor().newInstance(); + conf.setJsonSupport(js); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Can't find jackson lib in classpath", e); + } + } + setJsonSupport(new JsonSupportWrapper(conf.getJsonSupport())); setContext(conf.getContext()); setAllowCustomRequests(conf.isAllowCustomRequests()); @@ -107,8 +141,9 @@ public Configuration() { setTrustStore(conf.getTrustStore()); setTrustStoreFormat(conf.getTrustStoreFormat()); setTrustStorePassword(conf.getTrustStorePassword()); + setKeyManagerFactoryAlgorithm(conf.getKeyManagerFactoryAlgorithm()); - setTransports(conf.getTransports().toArray(new Transport[conf.getTransports().size()])); + setTransports(conf.getTransports().toArray(new Transport[0])); setMaxHttpContentLength(conf.getMaxHttpContentLength()); setPackagePrefix(conf.getPackagePrefix()); @@ -123,7 +158,15 @@ public Configuration() { setAddVersionHeader(conf.isAddVersionHeader()); setOrigin(conf.getOrigin()); + setEnableCors(conf.isEnableCors()); + setAllowHeaders(conf.getAllowHeaders()); setSSLProtocol(conf.getSSLProtocol()); + + setHttpCompression(conf.isHttpCompression()); + setWebsocketCompression(conf.isWebsocketCompression()); + setRandomSession(conf.randomSession); + setNeedClientAuth(conf.isNeedClientAuth()); + setHttpRequestDecoderConfiguration(conf.getHttpRequestDecoderConfiguration()); } public JsonSupport getJsonSupport() { @@ -134,7 +177,7 @@ public JsonSupport getJsonSupport() { * Allows to setup custom implementation of * JSON serialization/deserialization * - * @param jsonSupport + * @param jsonSupport - json mapper * * @see JsonSupport */ @@ -150,7 +193,7 @@ public String getHostname() { * Optional parameter. If not set then bind address * will be 0.0.0.0 or ::0 * - * @param hostname + * @param hostname - name of host */ public void setHostname(String hostname) { this.hostname = hostname; @@ -180,7 +223,7 @@ public void setWorkerThreads(int workerThreads) { /** * Ping interval * - * @param value - time in milliseconds + * @param heartbeatIntervalSecs - time in milliseconds */ public void setPingInterval(int heartbeatIntervalSecs) { this.pingInterval = heartbeatIntervalSecs; @@ -193,7 +236,7 @@ public int getPingInterval() { * Ping timeout * Use 0 to disable it * - * @param value - time in milliseconds + * @param heartbeatTimeoutSecs - time in milliseconds */ public void setPingTimeout(int heartbeatTimeoutSecs) { this.pingTimeout = heartbeatTimeoutSecs; @@ -231,7 +274,7 @@ public void setAllowCustomRequests(boolean allowCustomRequests) { /** * SSL key store password * - * @param keyStorePassword + * @param keyStorePassword - password of key store */ public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; @@ -243,7 +286,7 @@ public String getKeyStorePassword() { /** * SSL key store stream, maybe appointed to any source * - * @param keyStore + * @param keyStore - key store input stream */ public void setKeyStore(InputStream keyStore) { this.keyStore = keyStore; @@ -255,7 +298,7 @@ public InputStream getKeyStore() { /** * Key store format * - * @param keyStoreFormat + * @param keyStoreFormat - key store format */ public void setKeyStoreFormat(String keyStoreFormat) { this.keyStoreFormat = keyStoreFormat; @@ -267,7 +310,7 @@ public String getKeyStoreFormat() { /** * Set maximum http content length limit * - * @param maxContentLength + * @param value * the maximum length of the aggregated http content. */ public void setMaxHttpContentLength(int value) { @@ -329,7 +372,7 @@ public boolean isPreferDirectBuffer() { * Data store - used to store session data and implements distributed pubsub. * Default is {@code MemoryStoreFactory} * - * @param storeFactory - implements StoreFactory + * @param clientStoreFactory - implements StoreFactory * * @see com.corundumstudio.socketio.store.MemoryStoreFactory * @see com.corundumstudio.socketio.store.RedissonStoreFactory @@ -344,7 +387,7 @@ public StoreFactory getStoreFactory() { /** * Authorization listener invoked on every handshake. - * Accepts or denies a client by {@code AuthorizationListener.isAuthorized} method. + * Accepts or denies a client by {@code AuthorizationListener.getAuthorizationResult} method. * Accepts all clients by default. * * @param authorizationListener - authorization listener itself @@ -362,7 +405,7 @@ public AuthorizationListener getAuthorizationListener() { * Exception listener invoked on any exception in * SocketIO listener * - * @param exceptionListener + * @param exceptionListener - listener * * @see com.corundumstudio.socketio.listener.ExceptionListener */ @@ -379,7 +422,7 @@ public SocketConfig getSocketConfig() { /** * TCP socket configuration * - * @param socketConfig + * @param socketConfig - config */ public void setSocketConfig(SocketConfig socketConfig) { this.socketConfig = socketConfig; @@ -391,7 +434,7 @@ public void setSocketConfig(SocketConfig socketConfig) { * * @see AckMode * - * @param ackMode + * @param ackMode - ack mode */ public void setAckMode(AckMode ackMode) { this.ackMode = ackMode; @@ -422,10 +465,18 @@ public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } + public String getKeyManagerFactoryAlgorithm() { + return keyManagerFactoryAlgorithm; + } + public void setKeyManagerFactoryAlgorithm(String keyManagerFactoryAlgorithm) { + this.keyManagerFactoryAlgorithm = keyManagerFactoryAlgorithm; + } + + /** * Set maximum websocket frame content length limit * - * @param maxContentLength + * @param maxFramePayloadLength - length */ public void setMaxFramePayloadLength(int maxFramePayloadLength) { this.maxFramePayloadLength = maxFramePayloadLength; @@ -437,7 +488,7 @@ public int getMaxFramePayloadLength() { /** * Transport upgrade timeout in milliseconds * - * @param upgradeTimeout + * @param upgradeTimeout - upgrade timeout */ public void setUpgradeTimeout(int upgradeTimeout) { this.upgradeTimeout = upgradeTimeout; @@ -448,9 +499,10 @@ public int getUpgradeTimeout() { /** * Adds Server header with lib version to http response. + *

* Default is true * - * @param addVersionHeader + * @param addVersionHeader - true to add header */ public void setAddVersionHeader(boolean addVersionHeader) { this.addVersionHeader = addVersionHeader; @@ -466,18 +518,35 @@ public boolean isAddVersionHeader() { * * If value is null then request ORIGIN header value used. * - * @param origin + * @param origin - origin */ public void setOrigin(String origin) { this.origin = origin; } + public String getOrigin() { return origin; } + /** + * cors dispose + *

+ * Default is true + * + * @param enableCors enableCors + */ + public void setEnableCors(boolean enableCors) { + this.enableCors = enableCors; + } + + public boolean isEnableCors() { + return enableCors; + } + public boolean isUseLinuxNativeEpoll() { return useLinuxNativeEpoll; } + public void setUseLinuxNativeEpoll(boolean useLinuxNativeEpoll) { this.useLinuxNativeEpoll = useLinuxNativeEpoll; } @@ -485,7 +554,7 @@ public void setUseLinuxNativeEpoll(boolean useLinuxNativeEpoll) { /** * Set the name of the requested SSL protocol * - * @param sslProtocol + * @param sslProtocol - name of protocol */ public void setSSLProtocol(String sslProtocol) { this.sslProtocol = sslProtocol; @@ -495,4 +564,96 @@ public String getSSLProtocol() { } + /** + * Set the response Access-Control-Allow-Headers + * @param allowHeaders - allow headers + * */ + public void setAllowHeaders(String allowHeaders) { + this.allowHeaders = allowHeaders; + } + public String getAllowHeaders() { + return allowHeaders; + } + + /** + * Timeout between channel opening and first data transfer + * Helps to avoid 'silent channel' attack and prevents + * 'Too many open files' problem in this case + * + * @param firstDataTimeout - timeout value + */ + public void setFirstDataTimeout(int firstDataTimeout) { + this.firstDataTimeout = firstDataTimeout; + } + public int getFirstDataTimeout() { + return firstDataTimeout; + } + + /** + * Activate http protocol compression. Uses {@code gzip} or + * {@code deflate} encoding choice depends on the {@code "Accept-Encoding"} header value. + *

+ * Default is true + * + * @param httpCompression - true to use http compression + */ + public void setHttpCompression(boolean httpCompression) { + this.httpCompression = httpCompression; + } + public boolean isHttpCompression() { + return httpCompression; + } + + /** + * Activate websocket protocol compression. + * Uses {@code permessage-deflate} encoding only. + *

+ * Default is true + * + * @param websocketCompression - true to use websocket compression + */ + public void setWebsocketCompression(boolean websocketCompression) { + this.websocketCompression = websocketCompression; + } + public boolean isWebsocketCompression() { + return websocketCompression; + } + + public boolean isRandomSession() { + return randomSession; + } + + public void setRandomSession(boolean randomSession) { + this.randomSession = randomSession; + } + + /** + * Enable/disable client authentication. + * Has no effect unless a trust store has been provided. + * + * Default is false + * + * @param needClientAuth - true to use client authentication + */ + public void setNeedClientAuth(boolean needClientAuth) { + this.needClientAuth = needClientAuth; + } + public boolean isNeedClientAuth() { + return needClientAuth; + } + + public HttpRequestDecoderConfiguration getHttpRequestDecoderConfiguration() { + return httpRequestDecoderConfiguration; + } + + public void setHttpRequestDecoderConfiguration(HttpRequestDecoderConfiguration httpRequestDecoderConfiguration) { + this.httpRequestDecoderConfiguration = httpRequestDecoderConfiguration; + } + + public HttpDecoderConfig getHttpDecoderConfig() { + return new HttpDecoderConfig() + .setMaxInitialLineLength(httpRequestDecoderConfiguration.getMaxInitialLineLength()) + .setMaxHeaderSize(httpRequestDecoderConfiguration.getMaxHeaderSize()) + .setMaxChunkSize(httpRequestDecoderConfiguration.getMaxChunkSize()); + } } diff --git a/src/main/java/com/corundumstudio/socketio/Disconnectable.java b/src/main/java/com/corundumstudio/socketio/Disconnectable.java index 39840965e..83111b1fd 100644 --- a/src/main/java/com/corundumstudio/socketio/Disconnectable.java +++ b/src/main/java/com/corundumstudio/socketio/Disconnectable.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/DisconnectableHub.java b/src/main/java/com/corundumstudio/socketio/DisconnectableHub.java index be4e13d09..e7794d56a 100644 --- a/src/main/java/com/corundumstudio/socketio/DisconnectableHub.java +++ b/src/main/java/com/corundumstudio/socketio/DisconnectableHub.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/HandshakeData.java b/src/main/java/com/corundumstudio/socketio/HandshakeData.java index 6b475c503..9ed241efb 100644 --- a/src/main/java/com/corundumstudio/socketio/HandshakeData.java +++ b/src/main/java/com/corundumstudio/socketio/HandshakeData.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,49 +21,80 @@ import java.util.List; import java.util.Map; +import io.netty.handler.codec.http.HttpHeaders; + public class HandshakeData implements Serializable { private static final long serialVersionUID = 1196350300161819978L; - private Map> headers; + private HttpHeaders headers; private InetSocketAddress address; private Date time = new Date(); + private InetSocketAddress local; private String url; private Map> urlParams; private boolean xdomain; + private Object authToken; + // needed for correct deserialization public HandshakeData() { } - public HandshakeData(Map> headers, Map> urlParams, InetSocketAddress address, String url, boolean xdomain) { + public HandshakeData(HttpHeaders headers, Map> urlParams, InetSocketAddress address, String url, boolean xdomain) { + this(headers, urlParams, address, null, url, xdomain); + } + + public HandshakeData(HttpHeaders headers, Map> urlParams, InetSocketAddress address, InetSocketAddress local, String url, boolean xdomain) { super(); this.headers = headers; this.urlParams = urlParams; this.address = address; + this.local = local; this.url = url; this.xdomain = xdomain; } + /** + * Client network address + * + * @return network address + */ public InetSocketAddress getAddress() { return address; } - public Map> getHeaders() { - return headers; + /** + * Connection local address + * + * @return local address + */ + public InetSocketAddress getLocal() { + return local; } - public String getSingleHeader(String name) { - List values = headers.get(name); - if (values != null && values.size() == 1) { - return values.iterator().next(); - } - return null; + /** + * Http headers sent during first client request + * + * @return headers + */ + public HttpHeaders getHttpHeaders() { + return headers; } + /** + * Client connection date + * + * @return date + */ public Date getTime() { return time; } + /** + * Url used by client during first request + * + * @return url + */ public String getUrl() { return url; } @@ -72,6 +103,11 @@ public boolean isXdomain() { return xdomain; } + /** + * Url params stored in url used by client during first request + * + * @return map + */ public Map> getUrlParams() { return urlParams; } @@ -84,4 +120,11 @@ public String getSingleUrlParam(String name) { return null; } + public void setAuthToken(Object token) { + this.authToken = token; + } + + public Object getAuthToken() { + return this.authToken; + } } diff --git a/src/main/java/com/corundumstudio/socketio/HttpRequestDecoderConfiguration.java b/src/main/java/com/corundumstudio/socketio/HttpRequestDecoderConfiguration.java new file mode 100644 index 000000000..2897881f2 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/HttpRequestDecoderConfiguration.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio; + +public class HttpRequestDecoderConfiguration { + + private int maxInitialLineLength; + private int maxHeaderSize; + private int maxChunkSize; + + public HttpRequestDecoderConfiguration(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) { + this.maxInitialLineLength = maxInitialLineLength; + this.maxHeaderSize = maxHeaderSize; + this.maxChunkSize = maxChunkSize; + } + + public HttpRequestDecoderConfiguration() { + this(4096, 8192, 8192); + } + + public int getMaxInitialLineLength() { + return maxInitialLineLength; + } + + public void setMaxInitialLineLength(int maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + } + + public int getMaxHeaderSize() { + return maxHeaderSize; + } + + public void setMaxHeaderSize(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + } + + public int getMaxChunkSize() { + return maxChunkSize; + } + + public void setMaxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + } +} diff --git a/src/main/java/com/corundumstudio/socketio/JsonSupportWrapper.java b/src/main/java/com/corundumstudio/socketio/JsonSupportWrapper.java index 879e4a4cc..a94d5f733 100644 --- a/src/main/java/com/corundumstudio/socketio/JsonSupportWrapper.java +++ b/src/main/java/com/corundumstudio/socketio/JsonSupportWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ class JsonSupportWrapper implements JsonSupport { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(JsonSupportWrapper.class); private final JsonSupport delegate; @@ -37,47 +37,48 @@ class JsonSupportWrapper implements JsonSupport { this.delegate = delegate; } + @Override public AckArgs readAckArgs(ByteBufInputStream src, AckCallback callback) throws IOException { try { return delegate.readAckArgs(src, callback); } catch (Exception e) { src.reset(); log.error("Can't read ack args: " + src.readLine() + " for type: " + callback.getResultClass(), e); - return null; + throw new IOException(e); } } + @Override public T readValue(String namespaceName, ByteBufInputStream src, Class valueType) throws IOException { try { return delegate.readValue(namespaceName, src, valueType); } catch (Exception e) { src.reset(); log.error("Can't read value: " + src.readLine() + " for type: " + valueType, e); - return null; + throw new IOException(e); } } + @Override public void writeValue(ByteBufOutputStream out, Object value) throws IOException { try { delegate.writeValue(out, value); } catch (Exception e) { log.error("Can't write value: " + value, e); + throw new IOException(e); } } + @Override public void addEventMapping(String namespaceName, String eventName, Class ... eventClass) { delegate.addEventMapping(namespaceName, eventName, eventClass); } + @Override public void removeEventMapping(String namespaceName, String eventName) { delegate.removeEventMapping(namespaceName, eventName); } - @Override - public void writeJsonpValue(ByteBufOutputStream out, Object value) throws IOException { - delegate.writeJsonpValue(out, value); - } - @Override public List getArrays() { return delegate.getArrays(); diff --git a/src/main/java/com/corundumstudio/socketio/MultiRoomBroadcastOperations.java b/src/main/java/com/corundumstudio/socketio/MultiRoomBroadcastOperations.java new file mode 100644 index 000000000..59f69dd49 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/MultiRoomBroadcastOperations.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio; + +import com.corundumstudio.socketio.protocol.Packet; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +/** + * author: liangjiaqi + * date: 2020/8/8 6:02 PM + */ +public class MultiRoomBroadcastOperations implements BroadcastOperations { + + private final Collection broadcastOperations; + + public MultiRoomBroadcastOperations(Collection broadcastOperations) { + this.broadcastOperations = broadcastOperations; + } + + @Override + public Collection getClients() { + Set clients = new HashSet(); + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return clients; + } + for( BroadcastOperations b : this.broadcastOperations ) { + clients.addAll( b.getClients() ); + } + return clients; + } + + @Override + public void send(Packet packet, BroadcastAckCallback ackCallback) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.send( packet, ackCallback ); + } + } + + @Override + public void sendEvent(String name, SocketIOClient excludedClient, Object... data) { + Predicate excludePredicate = (socketIOClient) -> Objects.equals( + socketIOClient.getSessionId(), excludedClient.getSessionId() + ); + sendEvent(name, excludePredicate, data); + } + + @Override + public void sendEvent(String name, Predicate excludePredicate, Object... data) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.sendEvent( name, excludePredicate, data ); + } + } + + @Override + public void sendEvent(String name, Object data, BroadcastAckCallback ackCallback) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.sendEvent( name, data, ackCallback ); + } + } + + @Override + public void sendEvent(String name, Object data, SocketIOClient excludedClient, BroadcastAckCallback ackCallback) { + Predicate excludePredicate = (socketIOClient) -> Objects.equals( + socketIOClient.getSessionId(), excludedClient.getSessionId() + ); + sendEvent(name, data, excludePredicate, ackCallback); + } + + @Override + public void sendEvent(String name, Object data, Predicate excludePredicate, BroadcastAckCallback ackCallback) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.sendEvent( name, data, excludePredicate, ackCallback ); + } + } + + @Override + public void send(Packet packet) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.send( packet ); + } + } + + @Override + public void disconnect() { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.disconnect(); + } + } + + @Override + public void sendEvent(String name, Object... data) { + if( this.broadcastOperations == null || this.broadcastOperations.size() == 0 ) { + return; + } + for( BroadcastOperations b : this.broadcastOperations ) { + b.sendEvent( name, data ); + } + } +} diff --git a/src/main/java/com/corundumstudio/socketio/MultiTypeAckCallback.java b/src/main/java/com/corundumstudio/socketio/MultiTypeAckCallback.java index b1dd46dd1..c4ae704cb 100644 --- a/src/main/java/com/corundumstudio/socketio/MultiTypeAckCallback.java +++ b/src/main/java/com/corundumstudio/socketio/MultiTypeAckCallback.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/MultiTypeArgs.java b/src/main/java/com/corundumstudio/socketio/MultiTypeArgs.java index 3e8b35bb8..5741acbb4 100644 --- a/src/main/java/com/corundumstudio/socketio/MultiTypeArgs.java +++ b/src/main/java/com/corundumstudio/socketio/MultiTypeArgs.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,8 +50,9 @@ public T second() { /** * "index out of bounds"-safe method for getting elements * - * @param index - * @return + * @param type of argument + * @param index to get + * @return argument */ public T get(int index) { if (size() <= index) { diff --git a/src/main/java/com/corundumstudio/socketio/SingleRoomBroadcastOperations.java b/src/main/java/com/corundumstudio/socketio/SingleRoomBroadcastOperations.java new file mode 100644 index 000000000..7187925ae --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/SingleRoomBroadcastOperations.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio; + +import com.corundumstudio.socketio.misc.IterableCollection; +import com.corundumstudio.socketio.protocol.EngineIOVersion; +import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.protocol.PacketType; +import com.corundumstudio.socketio.store.StoreFactory; +import com.corundumstudio.socketio.store.pubsub.DispatchMessage; +import com.corundumstudio.socketio.store.pubsub.PubSubType; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Author: liangjiaqi + * Date: 2020/8/8 6:08 PM + */ +public class SingleRoomBroadcastOperations implements BroadcastOperations { + private final String namespace; + private final String room; + private final Iterable clients; + private final StoreFactory storeFactory; + + public SingleRoomBroadcastOperations(String namespace, String room, Iterable clients, StoreFactory storeFactory) { + super(); + this.namespace = namespace; + this.room = room; + this.clients = clients; + this.storeFactory = storeFactory; + } + + private void dispatch(Packet packet) { + this.storeFactory.pubSubStore().publish( + PubSubType.DISPATCH, + new DispatchMessage(this.room, packet, this.namespace)); + } + + @Override + public Collection getClients() { + return new IterableCollection(clients); + } + + @Override + public void send(Packet packet) { + for (SocketIOClient client : clients) { + packet.setEngineIOVersion(client.getEngineIOVersion()); + client.send(packet); + } + dispatch(packet); + } + + @Override + public void send(Packet packet, BroadcastAckCallback ackCallback) { + for (SocketIOClient client : clients) { + client.send(packet, ackCallback.createClientCallback(client)); + } + ackCallback.loopFinished(); + } + + @Override + public void disconnect() { + for (SocketIOClient client : clients) { + client.disconnect(); + } + } + + @Override + public void sendEvent(String name, SocketIOClient excludedClient, Object... data) { + Predicate excludePredicate = (socketIOClient) -> Objects.equals( + socketIOClient.getSessionId(), excludedClient.getSessionId() + ); + sendEvent(name, excludePredicate, data); + } + + @Override + public void sendEvent(String name, Predicate excludePredicate, Object... data) { + Packet packet = new Packet(PacketType.MESSAGE, EngineIOVersion.UNKNOWN); + packet.setSubType(PacketType.EVENT); + packet.setName(name); + packet.setData(Arrays.asList(data)); + + for (SocketIOClient client : clients) { + packet.setEngineIOVersion(client.getEngineIOVersion()); + if (excludePredicate.test(client)) { + continue; + } + client.send(packet); + } + dispatch(packet); + } + + @Override + public void sendEvent(String name, Object... data) { + Packet packet = new Packet(PacketType.MESSAGE, EngineIOVersion.UNKNOWN); + packet.setSubType(PacketType.EVENT); + packet.setName(name); + packet.setData(Arrays.asList(data)); + send(packet); + } + + @Override + public void sendEvent(String name, Object data, BroadcastAckCallback ackCallback) { + for (SocketIOClient client : clients) { + client.sendEvent(name, ackCallback.createClientCallback(client), data); + } + ackCallback.loopFinished(); + } + + @Override + public void sendEvent(String name, Object data, SocketIOClient excludedClient, BroadcastAckCallback ackCallback) { + Predicate excludePredicate = (socketIOClient) -> Objects.equals( + socketIOClient.getSessionId(), excludedClient.getSessionId() + ); + sendEvent(name, data, excludePredicate, ackCallback); + } + + @Override + public void sendEvent(String name, Object data, Predicate excludePredicate, BroadcastAckCallback ackCallback) { + for (SocketIOClient client : clients) { + if (excludePredicate.test(client)) { + continue; + } + client.sendEvent(name, ackCallback.createClientCallback(client), data); + } + ackCallback.loopFinished(); + } +} diff --git a/src/main/java/com/corundumstudio/socketio/SocketConfig.java b/src/main/java/com/corundumstudio/socketio/SocketConfig.java index 2c6e9c7fb..03a34d854 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketConfig.java +++ b/src/main/java/com/corundumstudio/socketio/SocketConfig.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,10 @@ public class SocketConfig { private int acceptBackLog = 1024; + private int writeBufferWaterMarkLow = -1; + + private int writeBufferWaterMarkHigh = -1; + public boolean isTcpNoDelay() { return tcpNoDelay; } @@ -86,4 +90,19 @@ public void setAcceptBackLog(int acceptBackLog) { this.acceptBackLog = acceptBackLog; } + public int getWriteBufferWaterMarkLow() { + return writeBufferWaterMarkLow; + } + + public void setWriteBufferWaterMarkLow(int writeBufferWaterMarkLow) { + this.writeBufferWaterMarkLow = writeBufferWaterMarkLow; + } + + public int getWriteBufferWaterMarkHigh() { + return writeBufferWaterMarkHigh; + } + + public void setWriteBufferWaterMarkHigh(int writeBufferWaterMarkHigh) { + this.writeBufferWaterMarkHigh = writeBufferWaterMarkHigh; + } } diff --git a/src/main/java/com/corundumstudio/socketio/SocketIOChannelInitializer.java b/src/main/java/com/corundumstudio/socketio/SocketIOChannelInitializer.java index 9465593b6..099a6ea0e 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketIOChannelInitializer.java +++ b/src/main/java/com/corundumstudio/socketio/SocketIOChannelInitializer.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,6 @@ */ package com.corundumstudio.socketio; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpRequestDecoder; -import io.netty.handler.codec.http.HttpResponseEncoder; -import io.netty.handler.ssl.SslHandler; - import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; @@ -32,6 +23,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,25 +36,39 @@ import com.corundumstudio.socketio.handler.PacketListener; import com.corundumstudio.socketio.handler.WrongUrlHandler; import com.corundumstudio.socketio.namespace.NamespacesHub; +import com.corundumstudio.socketio.protocol.JsonSupport; import com.corundumstudio.socketio.protocol.PacketDecoder; import com.corundumstudio.socketio.protocol.PacketEncoder; -import com.corundumstudio.socketio.protocol.JsonSupport; import com.corundumstudio.socketio.scheduler.CancelableScheduler; -import com.corundumstudio.socketio.scheduler.HashedWheelScheduler; +import com.corundumstudio.socketio.scheduler.HashedWheelTimeoutScheduler; import com.corundumstudio.socketio.store.StoreFactory; import com.corundumstudio.socketio.store.pubsub.DisconnectMessage; -import com.corundumstudio.socketio.store.pubsub.PubSubStore; -import com.corundumstudio.socketio.transport.WebSocketTransport; +import com.corundumstudio.socketio.store.pubsub.PubSubType; import com.corundumstudio.socketio.transport.PollingTransport; +import com.corundumstudio.socketio.transport.WebSocketTransport; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.ssl.SslHandler; public class SocketIOChannelInitializer extends ChannelInitializer implements DisconnectableHub { public static final String SOCKETIO_ENCODER = "socketioEncoder"; + public static final String WEB_SOCKET_TRANSPORT_COMPRESSION = "webSocketTransportCompression"; public static final String WEB_SOCKET_TRANSPORT = "webSocketTransport"; + public static final String WEB_SOCKET_AGGREGATOR = "webSocketAggregator"; public static final String XHR_POLLING_TRANSPORT = "xhrPollingTransport"; public static final String AUTHORIZE_HANDLER = "authorizeHandler"; public static final String PACKET_HANDLER = "packetHandler"; public static final String HTTP_ENCODER = "httpEncoder"; + public static final String HTTP_COMPRESSION = "httpCompression"; public static final String HTTP_AGGREGATOR = "httpAggregator"; public static final String HTTP_REQUEST_DECODER = "httpDecoder"; public static final String SSL_HANDLER = "ssl"; @@ -70,7 +76,7 @@ public class SocketIOChannelInitializer extends ChannelInitializer impl public static final String RESOURCE_HANDLER = "resourceHandler"; public static final String WRONG_URL_HANDLER = "wrongUrlBlocker"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(SocketIOChannelInitializer.class); private AckManager ackManager; @@ -81,7 +87,7 @@ public class SocketIOChannelInitializer extends ChannelInitializer impl private EncoderHandler encoderHandler; private WrongUrlHandler wrongUrlHandler; - private CancelableScheduler scheduler = new HashedWheelScheduler(); + private CancelableScheduler scheduler = new HashedWheelTimeoutScheduler(); private InPacketHandler packetHandler; private SSLContext sslContext; @@ -99,7 +105,7 @@ public void start(Configuration configuration, NamespacesHub namespacesHub) { JsonSupport jsonSupport = configuration.getJsonSupport(); PacketEncoder encoder = new PacketEncoder(configuration, jsonSupport); - PacketDecoder decoder = new PacketDecoder(jsonSupport, namespacesHub, ackManager); + PacketDecoder decoder = new PacketDecoder(jsonSupport, ackManager); String connectPath = configuration.getContext() + "/"; @@ -135,21 +141,54 @@ public void start(Configuration configuration, NamespacesHub namespacesHub) { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); + addSslHandler(pipeline); + addSocketioHandlers(pipeline); + } + /** + * Adds the ssl handler + * + * @param pipeline - channel pipeline + */ + protected void addSslHandler(ChannelPipeline pipeline) { if (sslContext != null) { SSLEngine engine = sslContext.createSSLEngine(); engine.setUseClientMode(false); + if (configuration.isNeedClientAuth() &&(configuration.getTrustStore() != null)) { + engine.setNeedClientAuth(true); + } pipeline.addLast(SSL_HANDLER, new SslHandler(engine)); } + } + + /** + * Adds the socketio channel handlers + * + * @param pipeline - channel pipeline + */ + protected void addSocketioHandlers(ChannelPipeline pipeline) { + pipeline.addLast(HTTP_REQUEST_DECODER, new HttpRequestDecoder(configuration.getHttpDecoderConfig())); + pipeline.addLast(HTTP_AGGREGATOR, new HttpObjectAggregator(configuration.getMaxHttpContentLength()) { + @Override + protected Object newContinueResponse(HttpMessage start, int maxContentLength, + ChannelPipeline pipeline) { + return null; + } - pipeline.addLast(HTTP_REQUEST_DECODER, new HttpRequestDecoder()); - pipeline.addLast(HTTP_AGGREGATOR, new HttpObjectAggregator(configuration.getMaxHttpContentLength())); + }); pipeline.addLast(HTTP_ENCODER, new HttpResponseEncoder()); + if (configuration.isHttpCompression()) { + pipeline.addLast(HTTP_COMPRESSION, new HttpContentCompressor()); + } + pipeline.addLast(PACKET_HANDLER, packetHandler); pipeline.addLast(AUTHORIZE_HANDLER, authorizeHandler); pipeline.addLast(XHR_POLLING_TRANSPORT, xhrPollingTransport); + if (configuration.isWebsocketCompression()) { + pipeline.addLast(WEB_SOCKET_TRANSPORT_COMPRESSION, new WebSocketServerCompressionHandler()); + } pipeline.addLast(WEB_SOCKET_TRANSPORT, webSocketTransport); pipeline.addLast(SOCKETIO_ENCODER, encoderHandler); @@ -170,7 +209,7 @@ private SSLContext createSSLContext(Configuration configuration) throws Exceptio KeyStore ks = KeyStore.getInstance(configuration.getKeyStoreFormat()); ks.load(configuration.getKeyStore(), configuration.getKeyStorePassword().toCharArray()); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(configuration.getKeyManagerFactoryAlgorithm()); kmf.init(ks, configuration.getKeyStorePassword().toCharArray()); SSLContext serverContext = SSLContext.getInstance(configuration.getSSLProtocol()); @@ -178,12 +217,13 @@ private SSLContext createSSLContext(Configuration configuration) throws Exceptio return serverContext; } + @Override public void onDisconnect(ClientHead client) { ackManager.onDisconnect(client); authorizeHandler.onDisconnect(client); configuration.getStoreFactory().onDisconnect(client); - configuration.getStoreFactory().pubSubStore().publish(PubSubStore.DISCONNECT, new DisconnectMessage(client.getSessionId())); + configuration.getStoreFactory().pubSubStore().publish(PubSubType.DISCONNECT, new DisconnectMessage(client.getSessionId())); log.debug("Client with sessionId: {} disconnected", client.getSessionId()); } diff --git a/src/main/java/com/corundumstudio/socketio/SocketIOClient.java b/src/main/java/com/corundumstudio/socketio/SocketIOClient.java index 45453a933..289205df2 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketIOClient.java +++ b/src/main/java/com/corundumstudio/socketio/SocketIOClient.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.Set; import java.util.UUID; +import com.corundumstudio.socketio.protocol.EngineIOVersion; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.store.Store; @@ -43,6 +44,19 @@ public interface SocketIOClient extends ClientOperations, Store { */ Transport getTransport(); + /** + * Engine IO Protocol version + * @return + */ + EngineIOVersion getEngineIOVersion(); + + /** + * Returns true if and only if the I/O thread will perform the requested write operation immediately. + * Any write requests made when this method returns false are queued until the I/O thread is ready to process the queued write requests. + * @return + */ + boolean isWritable(); + /** * Send event with ack callback * @@ -91,22 +105,45 @@ public interface SocketIOClient extends ClientOperations, Store { /** * Join client to room * - * @param room + * @param room - name of room */ void joinRoom(String room); /** - * Join client to room + * Join client to rooms * - * @param room + * @param rooms - names of rooms + */ + void joinRooms(Set rooms); + + /** + * Leave client from room + * + * @param room - name of room */ void leaveRoom(String room); + /** + * Leave client from rooms + * + * @param rooms - names of rooms + */ + void leaveRooms(Set rooms); + /** * Get all rooms a client is joined in. * - * @return + * @return name of rooms */ Set getAllRooms(); + /** + * Get current room Size (contain in cluster) + * + * @param room - name of room + * + * @return int + */ + int getCurrentRoomSize(String room); + } diff --git a/src/main/java/com/corundumstudio/socketio/SocketIONamespace.java b/src/main/java/com/corundumstudio/socketio/SocketIONamespace.java index 13843db7f..7c331444b 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketIONamespace.java +++ b/src/main/java/com/corundumstudio/socketio/SocketIONamespace.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,23 +26,28 @@ */ public interface SocketIONamespace extends ClientListeners { + String getName(); + BroadcastOperations getBroadcastOperations(); BroadcastOperations getRoomOperations(String room); + BroadcastOperations getRoomOperations(String... rooms); + /** * Get all clients connected to namespace * - * @return + * @return collection of clients */ Collection getAllClients(); /** * Get client by uuid connected to namespace * - * @param uuid - * @return + * @param uuid - id of client + * @return client */ SocketIOClient getClient(UUID uuid); + void addAuthTokenListener(AuthTokenListener listener); } diff --git a/src/main/java/com/corundumstudio/socketio/SocketIOServer.java b/src/main/java/com/corundumstudio/socketio/SocketIOServer.java index c20e4ea91..5af4b8bd6 100644 --- a/src/main/java/com/corundumstudio/socketio/SocketIOServer.java +++ b/src/main/java/com/corundumstudio/socketio/SocketIOServer.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,9 @@ */ package com.corundumstudio.socketio; +import com.corundumstudio.socketio.listener.*; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.*; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; @@ -27,17 +26,14 @@ import io.netty.util.concurrent.FutureListener; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.corundumstudio.socketio.listener.ClientListeners; -import com.corundumstudio.socketio.listener.ConnectListener; -import com.corundumstudio.socketio.listener.DataListener; -import com.corundumstudio.socketio.listener.DisconnectListener; -import com.corundumstudio.socketio.listener.MultiTypeEventListener; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; @@ -47,7 +43,7 @@ */ public class SocketIOServer implements ClientListeners { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(SocketIOServer.class); private final Configuration configCopy; private final Configuration configuration; @@ -83,8 +79,8 @@ public Collection getAllClients() { /** * Get client by uuid from default namespace * - * @param uuid - * @return + * @param uuid - id of client + * @return client */ public SocketIOClient getClient(UUID uuid) { return namespacesHub.get(Namespace.DEFAULT_NAME).getClient(uuid); @@ -100,19 +96,38 @@ public Collection getAllNamespaces() { } public BroadcastOperations getBroadcastOperations() { - return new BroadcastOperations(getAllClients(), configCopy.getStoreFactory()); + Collection namespaces = namespacesHub.getAllNamespaces(); + List list = new ArrayList(); + BroadcastOperations broadcast = null; + if( namespaces != null && namespaces.size() > 0 ) { + for( SocketIONamespace n : namespaces ) { + broadcast = n.getBroadcastOperations(); + list.add( broadcast ); + } + } + return new MultiRoomBroadcastOperations( list ); } /** * Get broadcast operations for clients within - * room by room name + * rooms by rooms' names * - * @param room - * @return + * @param rooms rooms' names + * @return broadcast operations */ - public BroadcastOperations getRoomOperations(String room) { - Iterable clients = namespacesHub.getRoomClients(room); - return new BroadcastOperations(clients, configCopy.getStoreFactory()); + public BroadcastOperations getRoomOperations(String... rooms) { + Collection namespaces = namespacesHub.getAllNamespaces(); + List list = new ArrayList(); + BroadcastOperations broadcast = null; + if( namespaces != null && namespaces.size() > 0 ) { + for( SocketIONamespace n : namespaces ) { + for ( String room : rooms ) { + broadcast = n.getRoomOperations( room ); + list.add( broadcast ); + } + } + } + return new MultiRoomBroadcastOperations( list ); } /** @@ -124,6 +139,8 @@ public void start() { /** * Start server asynchronously + * + * @return void */ public Future startAsync() { log.info("Session store / pubsub factory used: {}", configCopy.getStoreFactory()); @@ -131,7 +148,7 @@ public Future startAsync() { pipelineFactory.start(configCopy, namespacesHub); - Class channelClass = NioServerSocketChannel.class; + Class channelClass = NioServerSocketChannel.class; if (configCopy.isUseLinuxNativeEpoll()) { channelClass = EpollServerSocketChannel.class; } @@ -169,9 +186,16 @@ protected void applyConnectionOptions(ServerBootstrap bootstrap) { bootstrap.childOption(ChannelOption.SO_RCVBUF, config.getTcpReceiveBufferSize()); bootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(config.getTcpReceiveBufferSize())); } + //default value @see WriteBufferWaterMark.DEFAULT + if (config.getWriteBufferWaterMarkLow() != -1 && config.getWriteBufferWaterMarkHigh() != -1) { + bootstrap.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark( + config.getWriteBufferWaterMarkLow(), config.getWriteBufferWaterMarkHigh() + )); + } + bootstrap.childOption(ChannelOption.SO_KEEPALIVE, config.isTcpKeepAlive()); + bootstrap.childOption(ChannelOption.SO_LINGER, config.getSoLinger()); - bootstrap.option(ChannelOption.SO_LINGER, config.getSoLinger()); bootstrap.option(ChannelOption.SO_REUSEADDR, config.isReuseAddress()); bootstrap.option(ChannelOption.SO_BACKLOG, config.getAcceptBackLog()); } @@ -230,6 +254,18 @@ public void addEventListener(String eventName, Class eventClass, DataList mainNamespace.addEventListener(eventName, eventClass, listener); } + @Override + public void addEventInterceptor(EventInterceptor eventInterceptor) { + mainNamespace.addEventInterceptor(eventInterceptor); + + } + + + @Override + public void removeAllListeners(String eventName) { + mainNamespace.removeAllListeners(eventName); + } + @Override public void addDisconnectListener(DisconnectListener listener) { mainNamespace.addDisconnectListener(listener); @@ -240,13 +276,27 @@ public void addConnectListener(ConnectListener listener) { mainNamespace.addConnectListener(listener); } + @Override + public void addPingListener(PingListener listener) { + mainNamespace.addPingListener(listener); + } + @Override + public void addPongListener(PongListener listener) { + mainNamespace.addPongListener(listener); + } + @Override public void addListeners(Object listeners) { mainNamespace.addListeners(listeners); } @Override - public void addListeners(Object listeners, Class listenersClass) { + public void addListeners(Iterable listeners) { + mainNamespace.addListeners(listeners); + } + + @Override + public void addListeners(Object listeners, Class listenersClass) { mainNamespace.addListeners(listeners, listenersClass); } diff --git a/src/main/java/com/corundumstudio/socketio/Transport.java b/src/main/java/com/corundumstudio/socketio/Transport.java index 5a3f4b230..b979731f1 100644 --- a/src/main/java/com/corundumstudio/socketio/Transport.java +++ b/src/main/java/com/corundumstudio/socketio/Transport.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,14 +32,4 @@ public enum Transport { public String getValue() { return value; } - - public static Transport byName(String value) { - for (Transport t : Transport.values()) { - if (t.getValue().equals(value)) { - return t; - } - } - throw new IllegalArgumentException("Can't find " + value + " transport"); - } - } diff --git a/src/main/java/com/corundumstudio/socketio/VoidAckCallback.java b/src/main/java/com/corundumstudio/socketio/VoidAckCallback.java index c2452732f..45402dd39 100644 --- a/src/main/java/com/corundumstudio/socketio/VoidAckCallback.java +++ b/src/main/java/com/corundumstudio/socketio/VoidAckCallback.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/ack/AckManager.java b/src/main/java/com/corundumstudio/socketio/ack/AckManager.java index c0bd612f8..48b61f3a0 100644 --- a/src/main/java/com/corundumstudio/socketio/ack/AckManager.java +++ b/src/main/java/com/corundumstudio/socketio/ack/AckManager.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,33 +15,29 @@ */ package com.corundumstudio.socketio.ack; +import com.corundumstudio.socketio.*; +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.scheduler.CancelableScheduler; +import com.corundumstudio.socketio.scheduler.SchedulerKey; +import com.corundumstudio.socketio.scheduler.SchedulerKey.Type; +import io.netty.util.internal.PlatformDependent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.corundumstudio.socketio.AckCallback; -import com.corundumstudio.socketio.Disconnectable; -import com.corundumstudio.socketio.MultiTypeAckCallback; -import com.corundumstudio.socketio.MultiTypeArgs; -import com.corundumstudio.socketio.SocketIOClient; -import com.corundumstudio.socketio.handler.ClientHead; -import com.corundumstudio.socketio.protocol.Packet; -import com.corundumstudio.socketio.scheduler.CancelableScheduler; -import com.corundumstudio.socketio.scheduler.SchedulerKey; -import com.corundumstudio.socketio.scheduler.SchedulerKey.Type; - public class AckManager implements Disconnectable { - class AckEntry { + static class AckEntry { - final Map> ackCallbacks = new ConcurrentHashMap>(); + final Map> ackCallbacks = PlatformDependent.newConcurrentHashMap(); final AtomicLong ackIndex = new AtomicLong(-1); public long addAckCallback(AckCallback callback) { @@ -68,9 +64,9 @@ public void initAckIndex(long index) { } - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(AckManager.class); - private final Map ackEntries = new ConcurrentHashMap(); + private final ConcurrentMap ackEntries = PlatformDependent.newConcurrentHashMap(); private final CancelableScheduler scheduler; @@ -88,7 +84,7 @@ private AckEntry getAckEntry(UUID sessionId) { AckEntry ackEntry = ackEntries.get(sessionId); if (ackEntry == null) { ackEntry = new AckEntry(); - AckEntry oldAckEntry = ackEntries.put(sessionId, ackEntry); + AckEntry oldAckEntry = ackEntries.putIfAbsent(sessionId, ackEntry); if (oldAckEntry != null) { ackEntry = oldAckEntry; } @@ -96,6 +92,7 @@ private AckEntry getAckEntry(UUID sessionId) { return ackEntry; } + @SuppressWarnings("unchecked") public void onAck(SocketIOClient client, Packet packet) { AckSchedulerKey key = new AckSchedulerKey(Type.ACK_TIMEOUT, client.getSessionId(), packet.getAckId()); scheduler.cancel(key); @@ -120,7 +117,7 @@ public void onAck(SocketIOClient client, Packet packet) { } } - private AckCallback removeCallback(UUID sessionId, long index) { + private AckCallback removeCallback(UUID sessionId, long index) { AckEntry ackEntry = ackEntries.get(sessionId); // may be null if client disconnected // before timeout occurs @@ -135,7 +132,7 @@ public AckCallback getCallback(UUID sessionId, long index) { return ackEntry.getAckCallback(index); } - public long registerAck(UUID sessionId, AckCallback callback) { + public long registerAck(UUID sessionId, AckCallback callback) { AckEntry ackEntry = getAckEntry(sessionId); ackEntry.initAckIndex(0); long index = ackEntry.addAckCallback(callback); @@ -149,7 +146,7 @@ public long registerAck(UUID sessionId, AckCallback callback) { return index; } - private void scheduleTimeout(final long index, final UUID sessionId, AckCallback callback) { + private void scheduleTimeout(final long index, final UUID sessionId, AckCallback callback) { if (callback.getTimeout() == -1) { return; } @@ -157,7 +154,7 @@ private void scheduleTimeout(final long index, final UUID sessionId, AckCallback scheduler.scheduleCallback(key, new Runnable() { @Override public void run() { - AckCallback cb = removeCallback(sessionId, index); + AckCallback cb = removeCallback(sessionId, index); if (cb != null) { cb.onTimeout(); } diff --git a/src/main/java/com/corundumstudio/socketio/ack/AckSchedulerKey.java b/src/main/java/com/corundumstudio/socketio/ack/AckSchedulerKey.java index 9a11fc9b1..abf300385 100644 --- a/src/main/java/com/corundumstudio/socketio/ack/AckSchedulerKey.java +++ b/src/main/java/com/corundumstudio/socketio/ack/AckSchedulerKey.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/annotation/AnnotationScanner.java b/src/main/java/com/corundumstudio/socketio/annotation/AnnotationScanner.java index 9756841a7..b2bddb071 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/AnnotationScanner.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/AnnotationScanner.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,6 @@ public interface AnnotationScanner { void addListener(Namespace namespace, Object object, Method method, Annotation annotation); - void validate(Method method, Class clazz); + void validate(Method method, Class clazz); } \ No newline at end of file diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnConnect.java b/src/main/java/com/corundumstudio/socketio/annotation/OnConnect.java index 8c98f0ef6..3cb42284d 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnConnect.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnConnect.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnConnectScanner.java b/src/main/java/com/corundumstudio/socketio/annotation/OnConnectScanner.java index a3cbc7099..2d5cbcc43 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnConnectScanner.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnConnectScanner.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,12 @@ public class OnConnectScanner implements AnnotationScanner { + @Override public Class getScanAnnotation() { return OnConnect.class; } + @Override public void addListener(Namespace namespace, final Object object, final Method method, Annotation annotation) { namespace.addConnectListener(new ConnectListener() { @Override @@ -45,19 +47,19 @@ public void onConnect(SocketIOClient client) { }); } - public void validate(Method method, Class clazz) { + @Override + public void validate(Method method, Class clazz) { if (method.getParameterTypes().length != 1) { throw new IllegalArgumentException("Wrong OnConnect listener signature: " + clazz + "." + method.getName()); } - boolean valid = false; + for (Class eventType : method.getParameterTypes()) { - if (eventType.equals(SocketIOClient.class)) { - valid = true; - } - } - if (!valid) { - throw new IllegalArgumentException("Wrong OnConnect listener signature: " + clazz + "." + method.getName()); + if (SocketIOClient.class.equals(eventType)) { + return; + } } + + throw new IllegalArgumentException("Wrong OnConnect listener signature: " + clazz + "." + method.getName()); } } diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnect.java b/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnect.java index d42c5c3e1..3a42eda37 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnect.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnect.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnectScanner.java b/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnectScanner.java index 4ed71116c..a682fc9d1 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnectScanner.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnDisconnectScanner.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,19 +48,18 @@ public void onDisconnect(SocketIOClient client) { } @Override - public void validate(Method method, Class clazz) { + public void validate(Method method, Class clazz) { if (method.getParameterTypes().length != 1) { throw new IllegalArgumentException("Wrong OnDisconnect listener signature: " + clazz + "." + method.getName()); } - boolean valid = false; + for (Class eventType : method.getParameterTypes()) { - if (eventType.equals(SocketIOClient.class)) { - valid = true; - } - } - if (!valid) { - throw new IllegalArgumentException("Wrong OnDisconnect listener signature: " + clazz + "." + method.getName()); + if (SocketIOClient.class.equals(eventType)) { + return; + } } + + throw new IllegalArgumentException("Wrong OnDisconnect listener signature: " + clazz + "." + method.getName()); } } diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnEvent.java b/src/main/java/com/corundumstudio/socketio/annotation/OnEvent.java index 821f91bbd..d237f9975 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnEvent.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnEvent.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ /** * Event name + * + * @return value */ String value(); diff --git a/src/main/java/com/corundumstudio/socketio/annotation/OnEventScanner.java b/src/main/java/com/corundumstudio/socketio/annotation/OnEventScanner.java index effca7906..754fb4591 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/OnEventScanner.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/OnEventScanner.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public void addListener(Namespace namespace, final Object object, final Method m final List dataIndexes = dataIndexes(method); if (dataIndexes.size() > 1) { - List classes = new ArrayList(); + List> classes = new ArrayList>(); for (int index : dataIndexes) { Class param = method.getParameterTypes()[index]; classes.add(param); @@ -77,7 +77,7 @@ public void onData(SocketIOClient client, MultiTypeArgs data, AckRequest ackSend throw new SocketIOException(e); } } - }, classes.toArray(new Class[classes.size()])); + }, classes.toArray(new Class[0])); } else { Class objectType = Void.class; if (!dataIndexes.isEmpty()) { @@ -113,7 +113,7 @@ public void onData(SocketIOClient client, Object data, AckRequest ackSender) { private List dataIndexes(Method method) { List result = new ArrayList(); int index = 0; - for (Class type : method.getParameterTypes()) { + for (Class type : method.getParameterTypes()) { if (!type.equals(AckRequest.class) && !type.equals(SocketIOClient.class)) { result.add(index); } @@ -122,9 +122,9 @@ private List dataIndexes(Method method) { return result; } - private int paramIndex(Method method, Class clazz) { + private int paramIndex(Method method, Class clazz) { int index = 0; - for (Class type : method.getParameterTypes()) { + for (Class type : method.getParameterTypes()) { if (type.equals(clazz)) { return index; } @@ -134,7 +134,7 @@ private int paramIndex(Method method, Class clazz) { } @Override - public void validate(Method method, Class clazz) { + public void validate(Method method, Class clazz) { int paramsCount = method.getParameterTypes().length; final int socketIOClientIndex = paramIndex(method, SocketIOClient.class); final int ackRequestIndex = paramIndex(method, AckRequest.class); diff --git a/src/main/java/com/corundumstudio/socketio/annotation/ScannerEngine.java b/src/main/java/com/corundumstudio/socketio/annotation/ScannerEngine.java index da66f6f4a..724e04e38 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/ScannerEngine.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/ScannerEngine.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public class ScannerEngine { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(ScannerEngine.class); private static final List annotations = Arrays.asList(new OnConnectScanner(), new OnDisconnectScanner(), new OnEventScanner()); @@ -36,7 +36,7 @@ public class ScannerEngine { private Method findSimilarMethod(Class objectClazz, Method method) { Method[] methods = objectClazz.getDeclaredMethods(); for (Method m : methods) { - if (equals(m, method)) { + if (isEquals(m, method)) { return m; } } @@ -86,7 +86,7 @@ public void scan(Namespace namespace, Object object, Class clazz) } - private boolean equals(Method method1, Method method2) { + private boolean isEquals(Method method1, Method method2) { if (!method1.getName().equals(method2.getName()) || !method1.getReturnType().equals(method2.getReturnType())) { return false; diff --git a/src/main/java/com/corundumstudio/socketio/annotation/SpringAnnotationScanner.java b/src/main/java/com/corundumstudio/socketio/annotation/SpringAnnotationScanner.java index 529bde646..6013f8e8a 100644 --- a/src/main/java/com/corundumstudio/socketio/annotation/SpringAnnotationScanner.java +++ b/src/main/java/com/corundumstudio/socketio/annotation/SpringAnnotationScanner.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public class SpringAnnotationScanner implements BeanPostProcessor { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(SpringAnnotationScanner.class); private final List> annotations = Arrays.asList(OnConnect.class, OnDisconnect.class, OnEvent.class); @@ -42,6 +42,10 @@ public class SpringAnnotationScanner implements BeanPostProcessor { private Class originalBeanClass; + private Object originalBean; + + private String originalBeanName; + public SpringAnnotationScanner(SocketIOServer socketIOServer) { super(); this.socketIOServer = socketIOServer; @@ -50,9 +54,10 @@ public SpringAnnotationScanner(SocketIOServer socketIOServer) { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (originalBeanClass != null) { - socketIOServer.addListeners(bean, originalBeanClass); - log.info("{} bean listeners added", beanName); + socketIOServer.addListeners(originalBean, originalBeanClass); + log.info("{} bean listeners added", originalBeanName); originalBeanClass = null; + originalBeanName = null; } return bean; } @@ -82,6 +87,8 @@ public boolean matches(Method method) { if (add.get()) { originalBeanClass = bean.getClass(); + originalBean = bean; + originalBeanName = beanName; } return bean; } diff --git a/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java b/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java index ec7b7470f..b446a4fe8 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/AuthorizeHandler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,15 @@ package com.corundumstudio.socketio.handler; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.QueryStringDecoder; import java.io.IOException; import java.net.InetSocketAddress; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.TimeUnit; +import com.corundumstudio.socketio.*; +import com.corundumstudio.socketio.protocol.EngineIOVersion; +import com.corundumstudio.socketio.store.Store; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +35,7 @@ import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.Transport; import com.corundumstudio.socketio.ack.AckManager; +import com.corundumstudio.socketio.messages.HttpErrorMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; import com.corundumstudio.socketio.protocol.AuthPacket; @@ -55,14 +46,29 @@ import com.corundumstudio.socketio.scheduler.SchedulerKey.Type; import com.corundumstudio.socketio.store.StoreFactory; import com.corundumstudio.socketio.store.pubsub.ConnectMessage; -import com.corundumstudio.socketio.store.pubsub.PubSubStore; +import com.corundumstudio.socketio.store.pubsub.PubSubType; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; @Sharable public class AuthorizeHandler extends ChannelInboundHandlerAdapter implements Disconnectable { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(AuthorizeHandler.class); - private final CancelableScheduler disconnectScheduler; + private final CancelableScheduler scheduler; private final String connectPath; private final Configuration configuration; @@ -77,7 +83,7 @@ public AuthorizeHandler(String connectPath, CancelableScheduler scheduler, Confi super(); this.connectPath = connectPath; this.configuration = configuration; - this.disconnectScheduler = scheduler; + this.scheduler = scheduler; this.namespacesHub = namespacesHub; this.storeFactory = storeFactory; this.disconnectable = disconnectable; @@ -85,26 +91,41 @@ public AuthorizeHandler(String connectPath, CancelableScheduler scheduler, Confi this.clientsBox = clientsBox; } + @Override + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, ctx.channel()); + scheduler.schedule(key, new Runnable() { + @Override + public void run() { + ctx.channel().close(); + log.debug("Client with ip {} opened channel but doesn't send any data! Channel closed!", ctx.channel().remoteAddress()); + } + }, configuration.getFirstDataTimeout(), TimeUnit.MILLISECONDS); + super.channelActive(ctx); + } + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, ctx.channel()); + scheduler.cancel(key); + if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; Channel channel = ctx.channel(); - QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); + QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); if (!configuration.isAllowCustomRequests() && !queryDecoder.path().startsWith(connectPath)) { HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST); channel.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); req.release(); - log.warn("Blocked wrong request! url: {}, ip: {}", queryDecoder.path(), channel.remoteAddress()); return; } List sid = queryDecoder.parameters().get("sid"); if (queryDecoder.path().equals(connectPath) && sid == null) { - String origin = req.headers().get(HttpHeaders.Names.ORIGIN); + String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (!authorize(ctx, channel, origin, queryDecoder.parameters(), req)) { req.release(); return; @@ -123,13 +144,17 @@ private boolean authorize(ChannelHandlerContext ctx, Channel channel, String ori headers.put(name, values); } - HandshakeData data = new HandshakeData(headers, params, - (InetSocketAddress)channel.remoteAddress(), - req.getUri(), origin != null && !origin.equalsIgnoreCase("null")); + HandshakeData data = new HandshakeData(req.headers(), params, + (InetSocketAddress)channel.remoteAddress(), + (InetSocketAddress)channel.localAddress(), + req.uri(), origin != null && !origin.equalsIgnoreCase("null")); boolean result = false; + Map storeParams = Collections.emptyMap(); try { - result = configuration.getAuthorizationListener().isAuthorized(data); + AuthorizationResult authResult = configuration.getAuthorizationListener().getAuthorizationResult(data); + result = authResult.isAuthorized(); + storeParams = authResult.getStoreParams(); } catch (Exception e) { log.error("Authorization error", e); } @@ -142,56 +167,118 @@ private boolean authorize(ChannelHandlerContext ctx, Channel channel, String ori return false; } - // TODO try to get sessionId from cookie - UUID sessionId = UUID.randomUUID(); + UUID sessionId = null; + if (configuration.isRandomSession()) { + sessionId = UUID.randomUUID(); + } else { + sessionId = this.generateOrGetSessionIdFromRequest(req.headers()); + } List transportValue = params.get("transport"); if (transportValue == null) { - log.warn("Got no transports for request {}", req.getUri()); + log.error("Got no transports for request {}", req.uri()); + writeAndFlushTransportError(channel, origin); + return false; + } - HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.UNAUTHORIZED); - channel.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); + Transport transport = null; + try { + transport = Transport.valueOf(transportValue.get(0).toUpperCase()); + } catch (IllegalArgumentException e) { + log.error("Unknown transport for request {}", req.uri()); + writeAndFlushTransportError(channel, origin); + return false; + } + if (!configuration.getTransports().contains(transport)) { + log.error("Unsupported transport for request {}", req.uri()); + writeAndFlushTransportError(channel, origin); return false; } - Transport transport = Transport.byName(transportValue.get(0)); - ClientHead client = new ClientHead(sessionId, ackManager, disconnectable, storeFactory, data, clientsBox, transport, disconnectScheduler, configuration); + ClientHead client = new ClientHead(sessionId, ackManager, disconnectable, storeFactory, data, clientsBox, transport, scheduler, configuration, params); + Store store = client.getStore(); + storeParams.forEach(store::set); channel.attr(ClientHead.CLIENT).set(client); clientsBox.addClient(client); - String[] transports = {}; - if (configuration.getTransports().contains(Transport.WEBSOCKET)) { - transports = new String[] {"websocket"}; + //:TODO lyjnew Current WEBSOCKET retrun upgrade[] engine-io protocol + // the test case line + // https://github.com/socketio/engine.io-protocol/blob/de247df875ddcd4778d1165829c8644301750e9f/test-suite/test-suite.js#L131C43-L131C43 + if (configuration.getTransports().contains(Transport.WEBSOCKET) && + !(EngineIOVersion.V4.equals(client.getEngineIOVersion()) && Transport.WEBSOCKET.equals(client.getCurrentTransport()))) { + transports = new String[]{"websocket"}; } AuthPacket authPacket = new AuthPacket(sessionId, transports, configuration.getPingInterval(), - configuration.getPingTimeout()); - Packet packet = new Packet(PacketType.OPEN); + configuration.getPingTimeout()); + Packet packet = new Packet(PacketType.OPEN, client.getEngineIOVersion()); packet.setData(authPacket); client.send(packet); + client.schedulePing(); client.schedulePingTimeout(); log.debug("Handshake authorized for sessionId: {}, query params: {} headers: {}", sessionId, params, headers); return true; } + private void writeAndFlushTransportError(Channel channel, String origin) { + Map errorData = new HashMap(); + errorData.put("code", 0); + errorData.put("message", "Transport unknown"); + + channel.attr(EncoderHandler.ORIGIN).set(origin); + channel.writeAndFlush(new HttpErrorMessage(errorData)); + } + + /** + * This method will either generate a new random sessionId or will retrieve the value stored + * in the "io" cookie. Failures to parse will cause a logging warning to be generated and a + * random uuid to be generated instead (same as not passing a cookie in the first place). + */ + private UUID generateOrGetSessionIdFromRequest(HttpHeaders headers) { + List values = headers.getAll("io"); + if (values.size() == 1) { + try { + return UUID.fromString(values.get(0)); + } catch (IllegalArgumentException iaex) { + log.warn("Malformed UUID received for session! io=" + values.get(0)); + } + } + + for (String cookieHeader : headers.getAll(HttpHeaderNames.COOKIE)) { + Set cookies = ServerCookieDecoder.LAX.decode(cookieHeader); + + for (Cookie cookie : cookies) { + if (cookie.name().equals("io")) { + try { + return UUID.fromString(cookie.value()); + } catch (IllegalArgumentException iaex) { + log.warn("Malformed UUID received for session! io=" + cookie.value()); + } + } + } + } + + return UUID.randomUUID(); + } + public void connect(UUID sessionId) { SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, sessionId); - disconnectScheduler.cancel(key); + scheduler.cancel(key); } public void connect(ClientHead client) { Namespace ns = namespacesHub.get(Namespace.DEFAULT_NAME); if (!client.getNamespaces().contains(ns)) { -// connect(client.getSessionId()); - - Packet packet = new Packet(PacketType.MESSAGE); + Packet packet = new Packet(PacketType.MESSAGE, client.getEngineIOVersion()); packet.setSubType(PacketType.CONNECT); - client.send(packet); + //::TODO lyjnew V4 delay send connect packet ON client add Namecapse + if (!EngineIOVersion.V4.equals(client.getEngineIOVersion())) + client.send(packet); - configuration.getStoreFactory().pubSubStore().publish(PubSubStore.CONNECT, new ConnectMessage(client.getSessionId())); + configuration.getStoreFactory().pubSubStore().publish(PubSubType.CONNECT, new ConnectMessage(client.getSessionId())); SocketIOClient nsClient = client.addNamespaceClient(ns); ns.onConnect(nsClient); diff --git a/src/main/java/com/corundumstudio/socketio/handler/ClientHead.java b/src/main/java/com/corundumstudio/socketio/handler/ClientHead.java index 99c7f85b6..dfb1220e0 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/ClientHead.java +++ b/src/main/java/com/corundumstudio/socketio/handler/ClientHead.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,6 @@ */ package com.corundumstudio.socketio.handler; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.util.AttributeKey; - -import java.net.SocketAddress; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.DisconnectableHub; import com.corundumstudio.socketio.HandshakeData; @@ -43,6 +22,7 @@ import com.corundumstudio.socketio.ack.AckManager; import com.corundumstudio.socketio.messages.OutPacketMessage; import com.corundumstudio.socketio.namespace.Namespace; +import com.corundumstudio.socketio.protocol.EngineIOVersion; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; import com.corundumstudio.socketio.scheduler.CancelableScheduler; @@ -51,24 +31,40 @@ import com.corundumstudio.socketio.store.Store; import com.corundumstudio.socketio.store.StoreFactory; import com.corundumstudio.socketio.transport.NamespaceClient; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.util.AttributeKey; +import io.netty.util.internal.PlatformDependent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.SocketAddress; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public class ClientHead { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(ClientHead.class); public static final AttributeKey CLIENT = AttributeKey.valueOf("client"); private final AtomicBoolean disconnected = new AtomicBoolean(); - private final Map namespaceClients = new ConcurrentHashMap(); - private final Map channels = new HashMap(); + private final Map namespaceClients = PlatformDependent.newConcurrentHashMap(); + private final Map channels = new HashMap(2); private final HandshakeData handshakeData; private final UUID sessionId; + private final EngineIOVersion engineIOVersion; + private final Store store; private final DisconnectableHub disconnectableHub; private final AckManager ackManager; private ClientsBox clientsBox; - private final CancelableScheduler disconnectScheduler; + private final CancelableScheduler scheduler; private final Configuration configuration; private Packet lastBinaryPacket; @@ -77,8 +73,8 @@ public class ClientHead { private volatile Transport currentTransport; public ClientHead(UUID sessionId, AckManager ackManager, DisconnectableHub disconnectable, - StoreFactory storeFactory, HandshakeData handshakeData, ClientsBox clientsBox, Transport transport, CancelableScheduler disconnectScheduler, - Configuration configuration) { + StoreFactory storeFactory, HandshakeData handshakeData, ClientsBox clientsBox, Transport transport, CancelableScheduler scheduler, + Configuration configuration, Map> params) { this.sessionId = sessionId; this.ackManager = ackManager; this.disconnectableHub = disconnectable; @@ -86,11 +82,18 @@ public ClientHead(UUID sessionId, AckManager ackManager, DisconnectableHub disco this.handshakeData = handshakeData; this.clientsBox = clientsBox; this.currentTransport = transport; - this.disconnectScheduler = disconnectScheduler; + this.scheduler = scheduler; this.configuration = configuration; channels.put(Transport.POLLING, new TransportState()); channels.put(Transport.WEBSOCKET, new TransportState()); + + List versions = params.getOrDefault(EngineIOVersion.EIO, new ArrayList()); + if (versions.isEmpty()) { + engineIOVersion = EngineIOVersion.UNKNOWN; + } else { + engineIOVersion = EngineIOVersion.fromValue(versions.get(0)); + } } public void bindChannel(Channel channel, Transport transport) { @@ -106,28 +109,59 @@ public void bindChannel(Channel channel, Transport transport) { sendPackets(transport, channel); } + public void releasePollingChannel(Channel channel) { + TransportState state = channels.get(Transport.POLLING); + if(channel.equals(state.getChannel())) { + clientsBox.remove(channel); + state.update(null); + } + } + public String getOrigin() { - return handshakeData.getSingleHeader(HttpHeaders.Names.ORIGIN); + return handshakeData.getHttpHeaders().get(HttpHeaderNames.ORIGIN); } public ChannelFuture send(Packet packet) { return send(packet, getCurrentTransport()); } + public void cancelPing() { + SchedulerKey key = new SchedulerKey(Type.PING, sessionId); + scheduler.cancel(key); + } public void cancelPingTimeout() { SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, sessionId); - disconnectScheduler.cancel(key); + scheduler.cancel(key); + } + + public void schedulePing() { + cancelPing(); + final SchedulerKey key = new SchedulerKey(Type.PING, sessionId); + scheduler.schedule(key, new Runnable() { + @Override + public void run() { + ClientHead client = clientsBox.get(sessionId); + if (client != null) { + EngineIOVersion version = client.getEngineIOVersion(); + //only send ping packet for engine.io version 4 + if (EngineIOVersion.V4.equals(version)) { + client.send(new Packet(PacketType.PING, version)); + } + schedulePing(); + } + } + }, configuration.getPingInterval(), TimeUnit.MILLISECONDS); } public void schedulePingTimeout() { cancelPingTimeout(); SchedulerKey key = new SchedulerKey(Type.PING_TIMEOUT, sessionId); - disconnectScheduler.schedule(key, new Runnable() { + scheduler.schedule(key, new Runnable() { @Override public void run() { ClientHead client = clientsBox.get(sessionId); if (client != null) { - client.onChannelDisconnect(); + client.disconnect(); log.debug("{} removed due to ping timeout", sessionId); } } @@ -147,7 +181,6 @@ public ChannelFuture send(Packet packet, Transport transport) { } private ChannelFuture sendPackets(Transport transport, Channel channel) { - // TODO promise handling return channel.writeAndFlush(new OutPacketMessage(this, transport)); } @@ -177,6 +210,7 @@ public boolean isConnected() { } public void onChannelDisconnect() { + cancelPing(); cancelPingTimeout(); disconnected.set(true); @@ -207,8 +241,12 @@ public SocketAddress getRemoteAddress() { } public void disconnect() { - ChannelFuture future = send(new Packet(PacketType.DISCONNECT)); - future.addListener(ChannelFutureListener.CLOSE); + Packet packet = new Packet(PacketType.MESSAGE, engineIOVersion); + packet.setSubType(PacketType.DISCONNECT); + ChannelFuture future = send(packet); + if(future != null) { + future.addListener(ChannelFutureListener.CLOSE); + } onChannelDisconnect(); } @@ -267,4 +305,20 @@ public Packet getLastBinaryPacket() { return lastBinaryPacket; } + public EngineIOVersion getEngineIOVersion() { + return engineIOVersion; + } + + /** + * Returns true if and only if the I/O thread will perform the requested write operation immediately. + * Any write requests made when this method returns false are queued until the I/O thread is ready to process the queued write requests. + * @return + */ + public boolean isWritable() { + TransportState state = channels.get(getCurrentTransport()); + Channel channel = state.getChannel(); + return channel != null && channel.isWritable(); + } + + } diff --git a/src/main/java/com/corundumstudio/socketio/handler/ClientsBox.java b/src/main/java/com/corundumstudio/socketio/handler/ClientsBox.java index fc3725a47..beafd2cef 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/ClientsBox.java +++ b/src/main/java/com/corundumstudio/socketio/handler/ClientsBox.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ package com.corundumstudio.socketio.handler; import io.netty.channel.Channel; +import io.netty.util.internal.PlatformDependent; import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import com.corundumstudio.socketio.HandshakeData; public class ClientsBox { - private final Map uuid2clients = new ConcurrentHashMap(); - private final Map channel2clients = new ConcurrentHashMap(); + private final Map uuid2clients = PlatformDependent.newConcurrentHashMap(); + private final Map channel2clients = PlatformDependent.newConcurrentHashMap(); // TODO use storeFactory public HandshakeData getHandshakeData(UUID sessionId) { @@ -42,8 +42,8 @@ public void addClient(ClientHead clientHead) { uuid2clients.put(clientHead.getSessionId(), clientHead); } - public void removeClient(UUID sessionId) { - uuid2clients.remove(sessionId); + public ClientHead removeClient(UUID sessionId) { + return uuid2clients.remove(sessionId); } public ClientHead get(UUID sessionId) { diff --git a/src/main/java/com/corundumstudio/socketio/handler/EncoderHandler.java b/src/main/java/com/corundumstudio/socketio/handler/EncoderHandler.java index b9fa45178..4d538a716 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/EncoderHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/EncoderHandler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,53 +15,56 @@ */ package com.corundumstudio.socketio.handler; -import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS; -import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS; -import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN; -import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; -import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpHeaders.Values.KEEP_ALIVE; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import com.corundumstudio.socketio.Configuration; +import com.corundumstudio.socketio.Transport; +import com.corundumstudio.socketio.messages.HttpErrorMessage; +import com.corundumstudio.socketio.messages.HttpMessage; +import com.corundumstudio.socketio.messages.OutPacketMessage; +import com.corundumstudio.socketio.messages.XHROptionsMessage; +import com.corundumstudio.socketio.messages.XHRPostMessage; +import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.protocol.PacketEncoder; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.ByteBufUtil; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpResponse; -import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.util.Attribute; import io.netty.util.AttributeKey; import io.netty.util.CharsetUtil; - +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; import java.io.IOException; +import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import java.util.Queue; import java.util.jar.Attributes; import java.util.jar.Manifest; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.corundumstudio.socketio.Configuration; -import com.corundumstudio.socketio.Transport; -import com.corundumstudio.socketio.messages.BaseMessage; -import com.corundumstudio.socketio.messages.HttpMessage; -import com.corundumstudio.socketio.messages.OutPacketMessage; -import com.corundumstudio.socketio.messages.XHROptionsMessage; -import com.corundumstudio.socketio.messages.XHRPostMessage; -import com.corundumstudio.socketio.protocol.Packet; -import com.corundumstudio.socketio.protocol.PacketEncoder; - @Sharable public class EncoderHandler extends ChannelOutboundHandlerAdapter { @@ -73,7 +76,7 @@ public class EncoderHandler extends ChannelOutboundHandlerAdapter { public static final AttributeKey JSONP_INDEX = AttributeKey.valueOf("jsonpIndex"); public static final AttributeKey WRITE_ONCE = AttributeKey.valueOf("writeOnce"); - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(EncoderHandler.class); private final PacketEncoder encoder; @@ -92,8 +95,8 @@ public EncoderHandler(Configuration configuration, PacketEncoder encoder) throws private void readVersion() throws IOException { Enumeration resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); while (resources.hasMoreElements()) { - try { - Manifest manifest = new Manifest(resources.nextElement().openStream()); + try (InputStream inputStream = resources.nextElement().openStream()){ + Manifest manifest = new Manifest(inputStream); Attributes attrs = manifest.getMainAttributes(); if (attrs == null) { continue; @@ -109,32 +112,39 @@ private void readVersion() throws IOException { } } - private void write(XHROptionsMessage msg, ChannelHandlerContext ctx) { + private void write(XHROptionsMessage msg, ChannelHandlerContext ctx, ChannelPromise promise) { HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); - HttpHeaders.addHeader(res, "Set-Cookie", "io=" + msg.getSessionId()); - HttpHeaders.addHeader(res, CONNECTION, KEEP_ALIVE); - HttpHeaders.addHeader(res, ACCESS_CONTROL_ALLOW_HEADERS, CONTENT_TYPE); - addOriginHeaders(ctx.channel(), res); + res.headers().add(HttpHeaderNames.SET_COOKIE, "io=" + msg.getSessionId()) + .add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE) + .add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaderNames.CONTENT_TYPE); + + String origin = ctx.channel().attr(ORIGIN).get(); + addOriginHeaders(origin, res); ByteBuf out = encoder.allocateBuffer(ctx.alloc()); - sendMessage(msg, ctx.channel(), out, res); + sendMessage(msg, ctx.channel(), out, res, promise); } - private void write(XHRPostMessage msg, ChannelHandlerContext ctx) { + private void write(XHRPostMessage msg, ChannelHandlerContext ctx, ChannelPromise promise) { ByteBuf out = encoder.allocateBuffer(ctx.alloc()); out.writeBytes(OK); - sendMessage(msg, ctx.channel(), out, "text/html"); + sendMessage(msg, ctx.channel(), out, "text/html", promise, HttpResponseStatus.OK); } - private void sendMessage(HttpMessage msg, Channel channel, ByteBuf out, String type) { - HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); + private void sendMessage(HttpMessage msg, Channel channel, ByteBuf out, String type, ChannelPromise promise, HttpResponseStatus status) { + HttpResponse res = new DefaultHttpResponse(HTTP_1_1, status); + + res.headers().add(HttpHeaderNames.CONTENT_TYPE, type) + .add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + if (msg.getSessionId() != null) { + res.headers().add(HttpHeaderNames.SET_COOKIE, "io=" + msg.getSessionId()); + } - res.headers().add(CONTENT_TYPE, type).add("Set-Cookie", "io=" + msg.getSessionId()) - .add(CONNECTION, KEEP_ALIVE); + String origin = channel.attr(ORIGIN).get(); + addOriginHeaders(origin, res); - addOriginHeaders(channel, res); - HttpHeaders.setContentLength(res, out.readableBytes()); + HttpUtil.setContentLength(res, out.readableBytes()); // prevent XSS warnings on IE // https://github.com/LearnBoost/socket.io/pull/1333 @@ -143,46 +153,62 @@ private void sendMessage(HttpMessage msg, Channel channel, ByteBuf out, String t res.headers().add("X-XSS-Protection", "0"); } - sendMessage(msg, channel, out, res); + sendMessage(msg, channel, out, res, promise); } - private void sendMessage(HttpMessage msg, Channel channel, ByteBuf out, HttpResponse res) { + private void sendMessage(HttpMessage msg, Channel channel, ByteBuf out, HttpResponse res, ChannelPromise promise) { channel.write(res); if (log.isTraceEnabled()) { - log.trace("Out message: {} - sessionId: {}", out.toString(CharsetUtil.UTF_8), msg.getSessionId()); + if (msg.getSessionId() != null) { + log.trace("Out message: {} - sessionId: {}", out.toString(CharsetUtil.UTF_8), msg.getSessionId()); + } else { + log.trace("Out message: {}", out.toString(CharsetUtil.UTF_8)); + } } if (out.isReadable()) { - channel.write(out); + channel.write(new DefaultHttpContent(out)); } else { out.release(); } - channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).addListener(ChannelFutureListener.CLOSE); + channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT, promise).addListener(ChannelFutureListener.CLOSE); + } + private void sendError(HttpErrorMessage errorMsg, ChannelHandlerContext ctx, ChannelPromise promise) throws IOException { + final ByteBuf encBuf = encoder.allocateBuffer(ctx.alloc()); + ByteBufOutputStream out = new ByteBufOutputStream(encBuf); + encoder.getJsonSupport().writeValue(out, errorMsg.getData()); + + sendMessage(errorMsg, ctx.channel(), encBuf, "application/json", promise, HttpResponseStatus.BAD_REQUEST); } - private void addOriginHeaders(Channel channel, HttpResponse res) { + private void addOriginHeaders(String origin, HttpResponse res) { if (version != null) { - res.headers().add(HttpHeaders.Names.SERVER, version); + res.headers().add(HttpHeaderNames.SERVER, version); } - if (configuration.getOrigin() != null) { - HttpHeaders.addHeader(res, ACCESS_CONTROL_ALLOW_ORIGIN, configuration.getOrigin()); - } else { - String origin = channel.attr(ORIGIN).get(); - if (origin != null) { - HttpHeaders.addHeader(res, ACCESS_CONTROL_ALLOW_ORIGIN, origin); - HttpHeaders.addHeader(res, ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE); + if (configuration.isEnableCors()) { + if (configuration.getOrigin() != null) { + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, configuration.getOrigin()); + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE); } else { - HttpHeaders.addHeader(res, ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + if (origin != null) { + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, origin); + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE); + } else { + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + } + } + if (configuration.getAllowHeaders() != null) { + res.headers().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, configuration.getAllowHeaders()); } } } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (!(msg instanceof BaseMessage)) { + if (!(msg instanceof HttpMessage)) { super.write(ctx, msg, promise); return; } @@ -190,35 +216,59 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) if (msg instanceof OutPacketMessage) { OutPacketMessage m = (OutPacketMessage) msg; if (m.getTransport() == Transport.WEBSOCKET) { - handleWebsocket((OutPacketMessage) msg, ctx); + handleWebsocket((OutPacketMessage) msg, ctx, promise); } if (m.getTransport() == Transport.POLLING) { - handleHTTP((OutPacketMessage) msg, ctx); + handleHTTP((OutPacketMessage) msg, ctx, promise); } } else if (msg instanceof XHROptionsMessage) { - write((XHROptionsMessage) msg, ctx); + write((XHROptionsMessage) msg, ctx, promise); } else if (msg instanceof XHRPostMessage) { - write((XHRPostMessage) msg, ctx); + write((XHRPostMessage) msg, ctx, promise); + } else if (msg instanceof HttpErrorMessage) { + sendError((HttpErrorMessage) msg, ctx, promise); } } - private void handleWebsocket(final OutPacketMessage msg, ChannelHandlerContext ctx) throws IOException { + + private static final int FRAME_BUFFER_SIZE = 8192; + + + private void handleWebsocket(final OutPacketMessage msg, ChannelHandlerContext ctx, ChannelPromise promise) throws IOException { + ChannelFutureList writeFutureList = new ChannelFutureList(); + while (true) { Queue queue = msg.getClientHead().getPacketsQueue(msg.getTransport()); Packet packet = queue.poll(); if (packet == null) { + writeFutureList.setChannelPromise(promise); break; } - final ByteBuf out = encoder.allocateBuffer(ctx.alloc()); - encoder.encodePacket(packet, out, ctx.alloc(), true, false); + ByteBuf out = encoder.allocateBuffer(ctx.alloc()); + encoder.encodePacket(packet, out, ctx.alloc(), true); - WebSocketFrame res = new TextWebSocketFrame(out); if (log.isTraceEnabled()) { log.trace("Out message: {} sessionId: {}", out.toString(CharsetUtil.UTF_8), msg.getSessionId()); } - ctx.channel().writeAndFlush(res); - if (!out.isReadable()) { + if (out.isReadable() && out.readableBytes() > configuration.getMaxFramePayloadLength()) { + ByteBuf dstStart = out.readSlice(FRAME_BUFFER_SIZE); + dstStart.retain(); + WebSocketFrame start = new TextWebSocketFrame(false, 0, dstStart); + ctx.channel().write(start); + while (out.isReadable()) { + int re = Math.min(out.readableBytes(), FRAME_BUFFER_SIZE); + ByteBuf dst = out.readSlice(re); + dst.retain(); + WebSocketFrame res = new ContinuationWebSocketFrame(!out.isReadable(), 0, dst); + ctx.channel().write(res); + } + out.release(); + ctx.channel().flush(); + } else if (out.isReadable()){ + WebSocketFrame res = new TextWebSocketFrame(out); + ctx.channel().writeAndFlush(res); + } else { out.release(); } @@ -229,18 +279,19 @@ private void handleWebsocket(final OutPacketMessage msg, ChannelHandlerContext c if (log.isTraceEnabled()) { log.trace("Out attachment: {} sessionId: {}", ByteBufUtil.hexDump(outBuf), msg.getSessionId()); } - ctx.channel().writeAndFlush(new BinaryWebSocketFrame(outBuf)); + writeFutureList.add(ctx.channel().writeAndFlush(new BinaryWebSocketFrame(outBuf))); } } } - private void handleHTTP(OutPacketMessage msg, ChannelHandlerContext ctx) throws IOException { + private void handleHTTP(OutPacketMessage msg, ChannelHandlerContext ctx, ChannelPromise promise) throws IOException { Channel channel = ctx.channel(); Attribute attr = channel.attr(WRITE_ONCE); Queue queue = msg.getClientHead().getPacketsQueue(msg.getTransport()); if (!channel.isActive() || queue.isEmpty() || !attr.compareAndSet(null, true)) { + promise.trySuccess(); return; } @@ -249,10 +300,67 @@ private void handleHTTP(OutPacketMessage msg, ChannelHandlerContext ctx) throws if (b64 != null && b64) { Integer jsonpIndex = ctx.channel().attr(EncoderHandler.JSONP_INDEX).get(); encoder.encodeJsonP(jsonpIndex, queue, out, ctx.alloc(), 50); - sendMessage(msg, channel, out, "application/javascript"); + String type = "application/javascript"; + if (jsonpIndex == null) { + type = "text/plain"; + } + sendMessage(msg, channel, out, type, promise, HttpResponseStatus.OK); } else { encoder.encodePackets(queue, out, ctx.alloc(), 50); - sendMessage(msg, channel, out, "application/octet-stream"); + sendMessage(msg, channel, out, "application/octet-stream", promise, HttpResponseStatus.OK); + } + } + + /** + * Helper class for the handleWebsocket method, handles a list of ChannelFutures and + * sets the status of a promise when + * - any of the operations fail + * - all of the operations succeed + * The setChannelPromise method should be called after all the futures are added + */ + private static class ChannelFutureList implements GenericFutureListener> { + + private List futureList = new ArrayList(); + private ChannelPromise promise = null; + + private void cleanup() { + promise = null; + for (ChannelFuture f : futureList) f.removeListener(this); + } + + private void validate() { + boolean allSuccess = true; + for (ChannelFuture f : futureList) { + if (f.isDone()) { + if (!f.isSuccess()) { + promise.tryFailure(f.cause()); + cleanup(); + return; + } + } + else { + allSuccess = false; + } + } + if (allSuccess) { + promise.trySuccess(); + cleanup(); + } + } + + public void add(ChannelFuture f) { + futureList.add(f); + f.addListener(this); + } + + public void setChannelPromise(ChannelPromise p) { + promise = p; + validate(); + } + + @Override + public void operationComplete(Future voidFuture) throws Exception { + if (promise != null) validate(); } } diff --git a/src/main/java/com/corundumstudio/socketio/handler/InPacketHandler.java b/src/main/java/com/corundumstudio/socketio/handler/InPacketHandler.java index 2e8def00b..5e8503154 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/InPacketHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/InPacketHandler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,28 +15,29 @@ */ package com.corundumstudio.socketio.handler; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.util.CharsetUtil; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import com.corundumstudio.socketio.AuthTokenResult; import com.corundumstudio.socketio.listener.ExceptionListener; import com.corundumstudio.socketio.messages.PacketsMessage; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; +import com.corundumstudio.socketio.protocol.ConnPacket; +import com.corundumstudio.socketio.protocol.EngineIOVersion; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketDecoder; import com.corundumstudio.socketio.protocol.PacketType; import com.corundumstudio.socketio.transport.NamespaceClient; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.CharsetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Sharable public class InPacketHandler extends SimpleChannelInboundHandler { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(InPacketHandler.class); private final PacketListener packetListener; private final PacketDecoder decoder; @@ -63,17 +64,51 @@ protected void channelRead0(io.netty.channel.ChannelHandlerContext ctx, PacketsM while (content.isReadable()) { try { Packet packet = decoder.decodePackets(content, client); - if (packet.hasAttachments() && !packet.isAttachmentsLoaded()) { - return; - } + Namespace ns = namespacesHub.get(packet.getNsp()); if (ns == null) { + if (packet.getSubType() == PacketType.CONNECT) { + Packet p = new Packet(PacketType.MESSAGE, client.getEngineIOVersion()); + p.setSubType(PacketType.ERROR); + p.setNsp(packet.getNsp()); + p.setData("Invalid namespace"); + client.send(p); + return; + } log.debug("Can't find namespace for endpoint: {}, sessionId: {} probably it was removed.", packet.getNsp(), client.getSessionId()); return; } if (packet.getSubType() == PacketType.CONNECT) { client.addNamespaceClient(ns); + NamespaceClient nClient = client.getChildClient(ns); + //:TODO lyjnew client namespace send connect packet 0+namespace socket io v4 + // https://socket.io/docs/v4/socket-io-protocol/#connection-to-a-namespace + if (EngineIOVersion.V4.equals(client.getEngineIOVersion())) { + // Check for an auth token + if (packet.getData() != null) { + final Object authData = packet.getData(); + client.getHandshakeData().setAuthToken(authData); + // Call all authTokenListeners to see if one denies it + final AuthTokenResult allowAuth = ns.onAuthData(nClient, authData); + if (!allowAuth.isSuccess()) { + Packet p = new Packet(PacketType.MESSAGE, client.getEngineIOVersion()); + p.setSubType(PacketType.ERROR); + p.setNsp(packet.getNsp()); + final Object errorData = allowAuth.getErrorData(); + if (errorData != null) { + p.setData(errorData); + } + client.send(p); + return; + } + } + Packet p = new Packet(PacketType.MESSAGE, client.getEngineIOVersion()); + p.setSubType(PacketType.CONNECT); + p.setNsp(packet.getNsp()); + p.setData(new ConnPacket(client.getSessionId())); + client.send(p); + } } NamespaceClient nClient = client.getChildClient(ns); @@ -81,11 +116,14 @@ protected void channelRead0(io.netty.channel.ChannelHandlerContext ctx, PacketsM log.debug("Can't find namespace client in namespace: {}, sessionId: {} probably it was disconnected.", ns.getName(), client.getSessionId()); return; } + if (packet.hasAttachments() && !packet.isAttachmentsLoaded()) { + return; + } packetListener.onPacket(packet, nClient, message.getTransport()); } catch (Exception ex) { String c = content.toString(CharsetUtil.UTF_8); log.error("Error during data processing. Client sessionId: " + client.getSessionId() + ", data: " + c, ex); - return; + throw ex; } } } diff --git a/src/main/java/com/corundumstudio/socketio/handler/PacketListener.java b/src/main/java/com/corundumstudio/socketio/handler/PacketListener.java index c3583ff3e..d0446339f 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/PacketListener.java +++ b/src/main/java/com/corundumstudio/socketio/handler/PacketListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import com.corundumstudio.socketio.ack.AckManager; import com.corundumstudio.socketio.namespace.Namespace; import com.corundumstudio.socketio.namespace.NamespacesHub; +import com.corundumstudio.socketio.protocol.EngineIOVersion; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; import com.corundumstudio.socketio.scheduler.CancelableScheduler; @@ -52,16 +53,23 @@ public void onPacket(Packet packet, NamespaceClient client, Transport transport) switch (packet.getType()) { case PING: { - Packet outPacket = new Packet(PacketType.PONG); + Packet outPacket = new Packet(PacketType.PONG, client.getEngineIOVersion()); outPacket.setData(packet.getData()); // TODO use future client.getBaseClient().send(outPacket, transport); - if ("probe".equals(packet.getData())) { - client.getBaseClient().send(new Packet(PacketType.NOOP), Transport.POLLING); + client.getBaseClient().send(new Packet(PacketType.NOOP, client.getEngineIOVersion()), Transport.POLLING); } else { client.getBaseClient().schedulePingTimeout(); } + Namespace namespace = namespacesHub.get(packet.getNsp()); + namespace.onPing(client); + break; + } + case PONG: { + client.getBaseClient().schedulePingTimeout(); + Namespace namespace = namespacesHub.get(packet.getNsp()); + namespace.onPong(client); break; } @@ -86,10 +94,13 @@ public void onPacket(Packet packet, NamespaceClient client, Transport transport) Namespace namespace = namespacesHub.get(packet.getNsp()); namespace.onConnect(client); // send connect handshake packet back to client - client.getBaseClient().send(packet, transport); + if (!EngineIOVersion.V4.equals(client.getEngineIOVersion())) { + client.getBaseClient().send(packet, transport); + } } - if (packet.getSubType() == PacketType.ACK) { + if (packet.getSubType() == PacketType.ACK + || packet.getSubType() == PacketType.BINARY_ACK) { ackManager.onAck(client, packet); } diff --git a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java b/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java deleted file mode 100644 index 47eeb1878..000000000 --- a/src/main/java/com/corundumstudio/socketio/handler/ResourceHandler.java +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright 2012 Nikita Koksharov - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.corundumstudio.socketio.handler; - -import static io.netty.handler.codec.http.HttpHeaders.setContentLength; -import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; -import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.stream.ChunkedStream; -import io.netty.handler.stream.ChunkedWriteHandler; -import io.netty.util.CharsetUtil; - -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -import javax.activation.MimetypesFileTypeMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.corundumstudio.socketio.SocketIOChannelInitializer; - -@Sharable -public class ResourceHandler extends ChannelInboundHandlerAdapter { - - private final Logger log = LoggerFactory.getLogger(getClass()); - - public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; - public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; - public static final int HTTP_CACHE_SECONDS = 60; - - private final Map resources = new HashMap(); - - public ResourceHandler(String context) { - addResource(context + "/static/flashsocket/WebSocketMain.swf", "/static/flashsocket/WebSocketMain.swf"); - addResource(context + "/static/flashsocket/WebSocketMainInsecure.swf", "/static/flashsocket/WebSocketMainInsecure.swf"); - } - - public void addResource(String pathPart, String resourcePath) { - URL resUrl = getClass().getResource(resourcePath); - if (resUrl == null) { - log.error("The specified resource was not found: " + resourcePath); - return; - } - resources.put(pathPart, resUrl); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof FullHttpRequest) { - FullHttpRequest req = (FullHttpRequest) msg; - QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); - URL resUrl = resources.get(queryDecoder.path()); - if (resUrl != null) { - URLConnection fileUrl = resUrl.openConnection(); - long lastModified = fileUrl.getLastModified(); - // check if file has been modified since last request - if (isNotModified(req, lastModified)) { - sendNotModified(ctx); - req.release(); - return; - } - // create resource input-stream and check existence - final InputStream is = fileUrl.getInputStream(); - if (is == null) { - sendError(ctx, NOT_FOUND); - return; - } - // create ok response - HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); - // set Content-Length header - setContentLength(res, fileUrl.getContentLength()); - // set Content-Type header - setContentTypeHeader(res, fileUrl); - // set Date, Expires, Cache-Control and Last-Modified headers - setDateAndCacheHeaders(res, lastModified); - // write initial response header - ctx.write(res); - - // write the content stream - ctx.pipeline().addBefore(SocketIOChannelInitializer.RESOURCE_HANDLER, "chunkedWriter", new ChunkedWriteHandler()); - ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is, fileUrl.getContentLength())); - // add operation complete listener so we can close the channel and the input stream - writeFuture.addListener(ChannelFutureListener.CLOSE); - return; - } - } - ctx.fireChannelRead(msg); - } - - /* - * Checks if the content has been modified sicne the date provided by the IF_MODIFIED_SINCE http header - * */ - private boolean isNotModified(HttpRequest request, long lastModified) throws ParseException { - String ifModifiedSince = request.headers().get(HttpHeaders.Names.IF_MODIFIED_SINCE); - if (ifModifiedSince != null && !ifModifiedSince.equals("")) { - SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); - Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); - - // Only compare up to the second because the datetime format we send to the client does - // not have milliseconds - long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; - long fileLastModifiedSeconds = lastModified / 1000; - return ifModifiedSinceDateSeconds == fileLastModifiedSeconds; - } - return false; - } - - /* - * Sends a Not Modified response to the client - * - * */ - private void sendNotModified(ChannelHandlerContext ctx) { - HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED); - setDateHeader(response); - - // Close the connection as soon as the error message is sent. - ctx.channel().write(response).addListener(ChannelFutureListener.CLOSE); - } - - /** - * Sets the Date header for the HTTP response - * - * @param response - * HTTP response - */ - private void setDateHeader(HttpResponse response) { - SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); - dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); - - Calendar time = new GregorianCalendar(); - HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); - } - - - /** - * Sends an Error response with status message - * - * @param ctx - * @param status - */ - private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { - HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); - HttpHeaders.setHeader(response, CONTENT_TYPE, "text/plain; charset=UTF-8"); - ByteBuf content = Unpooled.copiedBuffer( "Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8); - - ctx.channel().write(response); - // Close the connection as soon as the error message is sent. - ctx.channel().write(content).addListener(ChannelFutureListener.CLOSE); - } - - /** - * Sets the Date and Cache headers for the HTTP Response - * - * @param response - * HTTP response - * @param fileToCache - * file to extract content type - */ - private void setDateAndCacheHeaders(HttpResponse response, long lastModified) { - SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); - dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); - - // Date header - Calendar time = new GregorianCalendar(); - HttpHeaders.setHeader(response, HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); - - // Add cache headers - time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); - HttpHeaders.setHeader(response, HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime())); - HttpHeaders.setHeader(response, HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); - HttpHeaders.setHeader(response, HttpHeaders.Names.LAST_MODIFIED, dateFormatter.format(new Date(lastModified))); - } - - /** - * Sets the content type header for the HTTP Response - * - * @param response - * HTTP response - * @param file - * file to extract content type - */ - private void setContentTypeHeader(HttpResponse response, URLConnection resUrlConnection) { - MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); - String resName = resUrlConnection.getURL().getFile(); - HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(resName)); - } -} diff --git a/src/main/java/com/corundumstudio/socketio/handler/SocketIOException.java b/src/main/java/com/corundumstudio/socketio/handler/SocketIOException.java index b5638e798..dbbfbde43 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/SocketIOException.java +++ b/src/main/java/com/corundumstudio/socketio/handler/SocketIOException.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/handler/SuccessAuthorizationListener.java b/src/main/java/com/corundumstudio/socketio/handler/SuccessAuthorizationListener.java index dfded95ce..eb536f293 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/SuccessAuthorizationListener.java +++ b/src/main/java/com/corundumstudio/socketio/handler/SuccessAuthorizationListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package com.corundumstudio.socketio.handler; import com.corundumstudio.socketio.AuthorizationListener; +import com.corundumstudio.socketio.AuthorizationResult; import com.corundumstudio.socketio.HandshakeData; public class SuccessAuthorizationListener implements AuthorizationListener { @Override - public boolean isAuthorized(HandshakeData data) { - return true; + public AuthorizationResult getAuthorizationResult(HandshakeData data) { + return AuthorizationResult.SUCCESSFUL_AUTHORIZATION; } } diff --git a/src/main/java/com/corundumstudio/socketio/handler/TransportState.java b/src/main/java/com/corundumstudio/socketio/handler/TransportState.java index 4850639e0..6134aacfe 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/TransportState.java +++ b/src/main/java/com/corundumstudio/socketio/handler/TransportState.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/handler/WrongUrlHandler.java b/src/main/java/com/corundumstudio/socketio/handler/WrongUrlHandler.java index 078767f66..bd76f783a 100644 --- a/src/main/java/com/corundumstudio/socketio/handler/WrongUrlHandler.java +++ b/src/main/java/com/corundumstudio/socketio/handler/WrongUrlHandler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,9 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelHandler.Sharable; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponse; @@ -35,20 +35,23 @@ @Sharable public class WrongUrlHandler extends ChannelInboundHandlerAdapter { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(WrongUrlHandler.class); + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; Channel channel = ctx.channel(); - QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); + QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST); ChannelFuture f = channel.writeAndFlush(res); f.addListener(ChannelFutureListener.CLOSE); req.release(); log.warn("Blocked wrong socket.io-context request! url: {}, params: {}, ip: {}", queryDecoder.path(), queryDecoder.parameters(), channel.remoteAddress()); + return; } + super.channelRead(ctx, msg); } } diff --git a/src/main/java/com/corundumstudio/socketio/listener/ClientListeners.java b/src/main/java/com/corundumstudio/socketio/listener/ClientListeners.java index 284092043..a3f24c919 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/ClientListeners.java +++ b/src/main/java/com/corundumstudio/socketio/listener/ClientListeners.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,36 @@ */ package com.corundumstudio.socketio.listener; + public interface ClientListeners { void addMultiTypeEventListener(String eventName, MultiTypeEventListener listener, Class ... eventClass); void addEventListener(String eventName, Class eventClass, DataListener listener); + void addEventInterceptor(EventInterceptor eventInterceptor); + void addDisconnectListener(DisconnectListener listener); void addConnectListener(ConnectListener listener); + /** + * from v4, ping will always be sent by server except probe ping packet sent from client, + * and pong will always be responded by client while receiving ping except probe pong packet responded from server + * it makes no more sense to listen to ping packet, instead you can listen to pong packet + * @deprecated use addPongListener instead + * @param listener + */ + @Deprecated + void addPingListener(PingListener listener); + void addPongListener(PongListener listener); + void addListeners(Object listeners); - void addListeners(Object listeners, Class listenersClass); + void addListeners(Iterable listeners); + + void addListeners(Object listeners, Class listenersClass); + void removeAllListeners(String eventName); + } diff --git a/src/main/java/com/corundumstudio/socketio/listener/ConnectListener.java b/src/main/java/com/corundumstudio/socketio/listener/ConnectListener.java index fff737383..f4f4b22f8 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/ConnectListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/ConnectListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/listener/DataListener.java b/src/main/java/com/corundumstudio/socketio/listener/DataListener.java index c0f100de6..4809da3af 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/DataListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/DataListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ public interface DataListener { * * @param client - receiver * @param data - received object + * @param ackSender - ack request + * */ void onData(SocketIOClient client, T data, AckRequest ackSender) throws Exception; diff --git a/src/main/java/com/corundumstudio/socketio/listener/DefaultExceptionListener.java b/src/main/java/com/corundumstudio/socketio/listener/DefaultExceptionListener.java index 15b1969f8..1662708f9 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/DefaultExceptionListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/DefaultExceptionListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ public class DefaultExceptionListener extends ExceptionListenerAdapter { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(DefaultExceptionListener.class); @Override public void onEventException(Exception e, List args, SocketIOClient client) { @@ -44,18 +44,24 @@ public void onConnectException(Exception e, SocketIOClient client) { } @Override - public void onMessageException(Exception e, String data, SocketIOClient client) { + public void onPingException(Exception e, SocketIOClient client) { log.error(e.getMessage(), e); } @Override - public void onJsonException(Exception e, Object data, SocketIOClient client) { + public void onPongException(Exception e, SocketIOClient client) { log.error(e.getMessage(), e); } + @Override public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception { log.error(e.getMessage(), e); return true; } + @Override + public void onAuthException(Throwable e, SocketIOClient client) { + log.error(e.getMessage(), e); + } + } diff --git a/src/main/java/com/corundumstudio/socketio/listener/DisconnectListener.java b/src/main/java/com/corundumstudio/socketio/listener/DisconnectListener.java index 1e62bb3fc..24ae8707d 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/DisconnectListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/DisconnectListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/listener/EventInterceptor.java b/src/main/java/com/corundumstudio/socketio/listener/EventInterceptor.java new file mode 100644 index 000000000..c96295dd1 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/listener/EventInterceptor.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.listener; + +import com.corundumstudio.socketio.AckRequest; +import com.corundumstudio.socketio.transport.NamespaceClient; +import java.util.List; + +public interface EventInterceptor { + void onEvent(NamespaceClient client, String eventName, List args, AckRequest ackRequest); +} diff --git a/src/main/java/com/corundumstudio/socketio/listener/ExceptionListener.java b/src/main/java/com/corundumstudio/socketio/listener/ExceptionListener.java index cbad76ed2..04d7f4387 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/ExceptionListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/ExceptionListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,12 @@ public interface ExceptionListener { void onConnectException(Exception e, SocketIOClient client); - void onMessageException(Exception e, String data, SocketIOClient client); + @Deprecated + void onPingException(Exception e, SocketIOClient client); - void onJsonException(Exception e, Object data, SocketIOClient client); + void onPongException(Exception e, SocketIOClient client); boolean exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception; + void onAuthException(Throwable e, SocketIOClient client); } diff --git a/src/main/java/com/corundumstudio/socketio/listener/ExceptionListenerAdapter.java b/src/main/java/com/corundumstudio/socketio/listener/ExceptionListenerAdapter.java index 0faa1569a..b4eaeb67b 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/ExceptionListenerAdapter.java +++ b/src/main/java/com/corundumstudio/socketio/listener/ExceptionListenerAdapter.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,16 +41,18 @@ public void onConnectException(Exception e, SocketIOClient client) { } @Override - public void onMessageException(Exception e, String data, SocketIOClient client) { + public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception { + return false; } @Override - public void onJsonException(Exception e, Object data, SocketIOClient client) { + public void onPingException(Exception e, SocketIOClient client) { + } @Override - public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception { - return false; + public void onPongException(Exception e, SocketIOClient client) { + } } diff --git a/src/main/java/com/corundumstudio/socketio/listener/MultiTypeEventListener.java b/src/main/java/com/corundumstudio/socketio/listener/MultiTypeEventListener.java index 84be764e7..9cd448a9a 100644 --- a/src/main/java/com/corundumstudio/socketio/listener/MultiTypeEventListener.java +++ b/src/main/java/com/corundumstudio/socketio/listener/MultiTypeEventListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/transport/BaseTransport.java b/src/main/java/com/corundumstudio/socketio/listener/PingListener.java similarity index 66% rename from src/main/java/com/corundumstudio/socketio/transport/BaseTransport.java rename to src/main/java/com/corundumstudio/socketio/listener/PingListener.java index 4dd6bfd92..c318f811c 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/BaseTransport.java +++ b/src/main/java/com/corundumstudio/socketio/listener/PingListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.corundumstudio.socketio.transport; +package com.corundumstudio.socketio.listener; -import io.netty.channel.ChannelInboundHandlerAdapter; - -import com.corundumstudio.socketio.Disconnectable; +import com.corundumstudio.socketio.SocketIOClient; @Deprecated -public abstract class BaseTransport extends ChannelInboundHandlerAdapter implements Disconnectable { +public interface PingListener { + + void onPing(SocketIOClient client); } diff --git a/src/main/java/com/corundumstudio/socketio/messages/BaseMessage.java b/src/main/java/com/corundumstudio/socketio/listener/PongListener.java similarity index 72% rename from src/main/java/com/corundumstudio/socketio/messages/BaseMessage.java rename to src/main/java/com/corundumstudio/socketio/listener/PongListener.java index c434fc420..46d78843b 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/BaseMessage.java +++ b/src/main/java/com/corundumstudio/socketio/listener/PongListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.corundumstudio.socketio.messages; +package com.corundumstudio.socketio.listener; +import com.corundumstudio.socketio.SocketIOClient; -public abstract class BaseMessage { +public interface PongListener { + + void onPong(SocketIOClient client); } diff --git a/src/main/java/com/corundumstudio/socketio/messages/HttpErrorMessage.java b/src/main/java/com/corundumstudio/socketio/messages/HttpErrorMessage.java new file mode 100644 index 000000000..ac34d5096 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/messages/HttpErrorMessage.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.messages; + +import java.util.Map; + +public class HttpErrorMessage extends HttpMessage { + + private final Map data; + + public HttpErrorMessage(Map data) { + super(null, null); + this.data = data; + } + + public Map getData() { + return data; + } + +} diff --git a/src/main/java/com/corundumstudio/socketio/messages/HttpMessage.java b/src/main/java/com/corundumstudio/socketio/messages/HttpMessage.java index 3f706b680..830ece1f8 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/HttpMessage.java +++ b/src/main/java/com/corundumstudio/socketio/messages/HttpMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import java.util.UUID; -public abstract class HttpMessage extends BaseMessage { +public abstract class HttpMessage { private final String origin; private final UUID sessionId; diff --git a/src/main/java/com/corundumstudio/socketio/messages/OutPacketMessage.java b/src/main/java/com/corundumstudio/socketio/messages/OutPacketMessage.java index 16d8f527c..14ca15ace 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/OutPacketMessage.java +++ b/src/main/java/com/corundumstudio/socketio/messages/OutPacketMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ public class OutPacketMessage extends HttpMessage { - ClientHead clientHead; - Transport transport; + private final ClientHead clientHead; + private final Transport transport; public OutPacketMessage(ClientHead clientHead, Transport transport) { super(clientHead.getOrigin(), clientHead.getSessionId()); diff --git a/src/main/java/com/corundumstudio/socketio/messages/PacketsMessage.java b/src/main/java/com/corundumstudio/socketio/messages/PacketsMessage.java index 38d676995..d8cb94df8 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/PacketsMessage.java +++ b/src/main/java/com/corundumstudio/socketio/messages/PacketsMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/messages/XHROptionsMessage.java b/src/main/java/com/corundumstudio/socketio/messages/XHROptionsMessage.java index 9d753f3cf..9b3522d96 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/XHROptionsMessage.java +++ b/src/main/java/com/corundumstudio/socketio/messages/XHROptionsMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/messages/XHRPostMessage.java b/src/main/java/com/corundumstudio/socketio/messages/XHRPostMessage.java index b137bfab1..0b0cf5891 100644 --- a/src/main/java/com/corundumstudio/socketio/messages/XHRPostMessage.java +++ b/src/main/java/com/corundumstudio/socketio/messages/XHRPostMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/misc/CompositeIterable.java b/src/main/java/com/corundumstudio/socketio/misc/CompositeIterable.java index 73d2b4b89..961233696 100644 --- a/src/main/java/com/corundumstudio/socketio/misc/CompositeIterable.java +++ b/src/main/java/com/corundumstudio/socketio/misc/CompositeIterable.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,11 @@ import java.util.Iterator; import java.util.List; -public class CompositeIterable implements Iterable, Iterator { +public class CompositeIterable implements Iterable { private List> iterablesList; private Iterable[] iterables; - private Iterator> listIterator; - private Iterator currentIterator; - public CompositeIterable(List> iterables) { this.iterablesList = iterables; } @@ -52,35 +49,8 @@ public Iterator iterator() { iterators.add(iterable.iterator()); } } - listIterator = iterators.iterator(); - currentIterator = null; - return this; - } - - @Override - public boolean hasNext() { - if (currentIterator == null || !currentIterator.hasNext()) { - while (listIterator.hasNext()) { - Iterator iterator = listIterator.next(); - if (iterator.hasNext()) { - currentIterator = iterator; - return true; - } - } - return false; - } - return currentIterator.hasNext(); + return new CompositeIterator(iterators.iterator()); } - @Override - public T next() { - hasNext(); - return currentIterator.next(); - } - - @Override - public void remove() { - currentIterator.remove(); - } } diff --git a/src/main/java/com/corundumstudio/socketio/misc/CompositeIterator.java b/src/main/java/com/corundumstudio/socketio/misc/CompositeIterator.java new file mode 100644 index 000000000..4be9991e8 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/misc/CompositeIterator.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.misc; + +import java.util.Iterator; + +public class CompositeIterator implements Iterator { + + private final Iterator> listIterator; + private Iterator currentIterator; + + public CompositeIterator(Iterator> listIterator) { + this.currentIterator = null; + this.listIterator = listIterator; + } + + @Override + public boolean hasNext() { + if (currentIterator == null || !currentIterator.hasNext()) { + while (listIterator.hasNext()) { + Iterator iterator = listIterator.next(); + if (iterator.hasNext()) { + currentIterator = iterator; + return true; + } + } + return false; + } + return true; // can only be reached when currentIterator.hasNext() is true + } + + @Override + public T next() { + hasNext(); + return currentIterator.next(); + } + + @Override + public void remove() { + currentIterator.remove(); + } +} diff --git a/src/main/java/com/corundumstudio/socketio/misc/IterableCollection.java b/src/main/java/com/corundumstudio/socketio/misc/IterableCollection.java index a0e8e2aa5..7663d3cf4 100644 --- a/src/main/java/com/corundumstudio/socketio/misc/IterableCollection.java +++ b/src/main/java/com/corundumstudio/socketio/misc/IterableCollection.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/namespace/EventEntry.java b/src/main/java/com/corundumstudio/socketio/namespace/EventEntry.java index 36bfef3b7..a5a4be93e 100644 --- a/src/main/java/com/corundumstudio/socketio/namespace/EventEntry.java +++ b/src/main/java/com/corundumstudio/socketio/namespace/EventEntry.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/namespace/Namespace.java b/src/main/java/com/corundumstudio/socketio/namespace/Namespace.java index 7bb478a57..cd78123c3 100644 --- a/src/main/java/com/corundumstudio/socketio/namespace/Namespace.java +++ b/src/main/java/com/corundumstudio/socketio/namespace/Namespace.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,37 +15,21 @@ */ package com.corundumstudio.socketio.namespace; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ConcurrentMap; - -import com.corundumstudio.socketio.AckMode; -import com.corundumstudio.socketio.AckRequest; -import com.corundumstudio.socketio.BroadcastOperations; -import com.corundumstudio.socketio.Configuration; -import com.corundumstudio.socketio.MultiTypeArgs; -import com.corundumstudio.socketio.SocketIOClient; -import com.corundumstudio.socketio.SocketIONamespace; +import com.corundumstudio.socketio.*; import com.corundumstudio.socketio.annotation.ScannerEngine; -import com.corundumstudio.socketio.listener.ConnectListener; -import com.corundumstudio.socketio.listener.DataListener; -import com.corundumstudio.socketio.listener.DisconnectListener; -import com.corundumstudio.socketio.listener.ExceptionListener; -import com.corundumstudio.socketio.listener.MultiTypeEventListener; +import com.corundumstudio.socketio.listener.*; import com.corundumstudio.socketio.protocol.JsonSupport; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.store.StoreFactory; +import com.corundumstudio.socketio.store.pubsub.BulkJoinLeaveMessage; import com.corundumstudio.socketio.store.pubsub.JoinLeaveMessage; -import com.corundumstudio.socketio.store.pubsub.PubSubStore; +import com.corundumstudio.socketio.store.pubsub.PubSubType; import com.corundumstudio.socketio.transport.NamespaceClient; +import io.netty.util.internal.PlatformDependent; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; /** * Hub object for all clients in one namespace. @@ -58,14 +42,18 @@ public class Namespace implements SocketIONamespace { public static final String DEFAULT_NAME = ""; private final ScannerEngine engine = new ScannerEngine(); - private final ConcurrentMap> eventListeners = - new ConcurrentHashMap>(); + private final ConcurrentMap> eventListeners = PlatformDependent.newConcurrentHashMap(); private final Queue connectListeners = new ConcurrentLinkedQueue(); private final Queue disconnectListeners = new ConcurrentLinkedQueue(); + private final Queue pingListeners = new ConcurrentLinkedQueue(); + private final Queue pongListeners = new ConcurrentLinkedQueue(); + private final Queue eventInterceptors = new ConcurrentLinkedQueue(); + + private final Queue authDataInterceptors = new ConcurrentLinkedQueue<>(); - private final Map allClients = new ConcurrentHashMap(); - private final ConcurrentMap> roomClients = new ConcurrentHashMap>(); - private final ConcurrentMap> clientRooms = new ConcurrentHashMap>(); + private final Map allClients = PlatformDependent.newConcurrentHashMap(); + private final ConcurrentMap> roomClients = PlatformDependent.newConcurrentHashMap(); + private final ConcurrentMap> clientRooms = PlatformDependent.newConcurrentHashMap(); private final String name; private final AckMode ackMode; @@ -86,6 +74,7 @@ public void addClient(SocketIOClient client) { allClients.put(client.getSessionId(), client); } + @Override public String getName() { return name; } @@ -104,6 +93,14 @@ public void addMultiTypeEventListener(String eventName, MultiTypeEventListener l entry.addListener(listener); jsonSupport.addEventMapping(name, eventName, eventClass); } + + @Override + public void removeAllListeners(String eventName) { + EventEntry entry = eventListeners.remove(eventName); + if (entry != null) { + jsonSupport.removeEventMapping(name, eventName); + } + } @Override @SuppressWarnings({"unchecked", "rawtypes"}) @@ -120,6 +117,11 @@ public void addEventListener(String eventName, Class eventClass, DataList jsonSupport.addEventMapping(name, eventName, eventClass); } + @Override + public void addEventInterceptor(EventInterceptor eventInterceptor) { + eventInterceptors.add(eventInterceptor); + } + @SuppressWarnings({"rawtypes", "unchecked"}) public void onEvent(NamespaceClient client, String eventName, List args, AckRequest ackRequest) { EventEntry entry = eventListeners.get(eventName); @@ -133,6 +135,10 @@ public void onEvent(NamespaceClient client, String eventName, List args, Object data = getEventData(args, dataListener); dataListener.onData(client, data, ackRequest); } + + for (EventInterceptor eventInterceptor : eventInterceptors) { + eventInterceptor.onEvent(client, eventName, args, ackRequest); + } } catch (Exception e) { exceptionListener.onEventException(e, args, client); if (ackMode == AckMode.AUTO_SUCCESS_ONLY) { @@ -151,7 +157,7 @@ private void sendAck(AckRequest ackRequest) { } } - private Object getEventData(List args, DataListener dataListener) { + private Object getEventData(List args, DataListener dataListener) { if (dataListener instanceof MultiTypeEventListener) { return new MultiTypeArgs(args); } else { @@ -168,10 +174,16 @@ public void addDisconnectListener(DisconnectListener listener) { } public void onDisconnect(SocketIOClient client) { + Set joinedRooms = client.getAllRooms(); allClients.remove(client.getSessionId()); + final Set roomsToLeave = new HashSet<>(joinedRooms); - leave(getName(), client.getSessionId()); - storeFactory.pubSubStore().publish(PubSubStore.LEAVE, new JoinLeaveMessage(client.getSessionId(), getName(), getName())); + // client must leave all rooms and publish the leave msg one by one on disconnect. + for (String joinedRoom : joinedRooms) { + leave(roomClients, joinedRoom, client.getSessionId()); + } + clientRooms.remove(client.getSessionId()); + storeFactory.pubSubStore().publish(PubSubType.BULK_LEAVE, new BulkJoinLeaveMessage(client.getSessionId(), roomsToLeave, getName())); try { for (DisconnectListener listener : disconnectListeners) { @@ -188,8 +200,13 @@ public void addConnectListener(ConnectListener listener) { } public void onConnect(SocketIOClient client) { + if (roomClients.containsKey(getName()) && + roomClients.get(getName()).contains(client.getSessionId())) { + return; + } + join(getName(), client.getSessionId()); - storeFactory.pubSubStore().publish(PubSubStore.JOIN, new JoinLeaveMessage(client.getSessionId(), getName(), getName())); + storeFactory.pubSubStore().publish(PubSubType.JOIN, new JoinLeaveMessage(client.getSessionId(), getName(), getName())); try { for (ConnectListener listener : connectListeners) { @@ -200,17 +217,56 @@ public void onConnect(SocketIOClient client) { } } + @Override + public void addPingListener(PingListener listener) { + pingListeners.add(listener); + } + + @Override + public void addPongListener(PongListener listener) { + pongListeners.add(listener); + } + + public void onPing(SocketIOClient client) { + try { + for (PingListener listener : pingListeners) { + listener.onPing(client); + } + } catch (Exception e) { + exceptionListener.onPingException(e, client); + } + } + + public void onPong(SocketIOClient client) { + try { + for (PongListener listener : pongListeners) { + listener.onPong(client); + } + } catch (Exception e) { + exceptionListener.onPingException(e, client); + } + } + @Override public BroadcastOperations getBroadcastOperations() { - return new BroadcastOperations(allClients.values(), storeFactory); + return new SingleRoomBroadcastOperations(getName(), getName(), allClients.values(), storeFactory); } @Override public BroadcastOperations getRoomOperations(String room) { - return new BroadcastOperations(getRoomClients(room), storeFactory); + return new SingleRoomBroadcastOperations(getName(), room, getRoomClients(room), storeFactory); } - @Override + @Override + public BroadcastOperations getRoomOperations(String... rooms) { + List list = new ArrayList<>(); + for( String room : rooms ) { + list.add( new SingleRoomBroadcastOperations(getName(), room, getRoomClients(room), storeFactory) ); + } + return new MultiRoomBroadcastOperations( list ); + } + + @Override public int hashCode() { final int prime = 31; int result = 1; @@ -237,17 +293,39 @@ public boolean equals(Object obj) { @Override public void addListeners(Object listeners) { + if (listeners instanceof Iterable) { + addListeners((Iterable) listeners); + return; + } addListeners(listeners, listeners.getClass()); } @Override - public void addListeners(Object listeners, Class listenersClass) { + public void addListeners(Iterable listeners) { + for (L next : listeners) { + addListeners(next, next.getClass()); + } + } + + @Override + public void addListeners(Object listeners, Class listenersClass) { + if (listeners instanceof Iterable) { + addListeners((Iterable) listeners); + return; + } engine.scan(this, listeners, listenersClass); } public void joinRoom(String room, UUID sessionId) { join(room, sessionId); - storeFactory.pubSubStore().publish(PubSubStore.JOIN, new JoinLeaveMessage(sessionId, room, getName())); + storeFactory.pubSubStore().publish(PubSubType.JOIN, new JoinLeaveMessage(sessionId, room, getName())); + } + + public void joinRooms(Set rooms, final UUID sessionId) { + for (String room : rooms) { + join(room, sessionId); + } + storeFactory.pubSubStore().publish(PubSubType.BULK_JOIN, new BulkJoinLeaveMessage(sessionId, rooms, getName())); } public void dispatch(String room, Packet packet) { @@ -261,7 +339,7 @@ public void dispatch(String room, Packet packet) { private void join(ConcurrentMap> map, K key, V value) { Set clients = map.get(key); if (clients == null) { - clients = Collections.newSetFromMap(new ConcurrentHashMap()); + clients = Collections.newSetFromMap(PlatformDependent.newConcurrentHashMap()); Set oldClients = map.putIfAbsent(key, clients); if (oldClients != null) { clients = oldClients; @@ -282,7 +360,14 @@ public void join(String room, UUID sessionId) { public void leaveRoom(String room, UUID sessionId) { leave(room, sessionId); - storeFactory.pubSubStore().publish(PubSubStore.LEAVE, new JoinLeaveMessage(sessionId, room, getName())); + storeFactory.pubSubStore().publish(PubSubType.LEAVE, new JoinLeaveMessage(sessionId, room, getName())); + } + + public void leaveRooms(Set rooms, final UUID sessionId) { + for (String room : rooms) { + leave(room, sessionId); + } + storeFactory.pubSubStore().publish(PubSubType.BULK_LEAVE, new BulkJoinLeaveMessage(sessionId, rooms, getName())); } private void leave(ConcurrentMap> map, K room, V sessionId) { @@ -310,6 +395,10 @@ public Set getRooms(SocketIOClient client) { return Collections.unmodifiableSet(res); } + public Set getRooms() { + return roomClients.keySet(); + } + public Iterable getRoomClients(String room) { Set sessionIds = roomClients.get(room); @@ -327,6 +416,12 @@ public Iterable getRoomClients(String room) { return result; } + public int getRoomClientsInCluster(String room) { + Set sessionIds = roomClients.get(room); + return sessionIds == null ? 0 : sessionIds.size(); + } + + @Override public Collection getAllClients() { return Collections.unmodifiableCollection(allClients.values()); } @@ -335,8 +430,28 @@ public JsonSupport getJsonSupport() { return jsonSupport; } + @Override public SocketIOClient getClient(UUID uuid) { return allClients.get(uuid); } + @Override + public void addAuthTokenListener(final AuthTokenListener listener) { + this.authDataInterceptors.add(listener); + } + + public AuthTokenResult onAuthData(SocketIOClient client, Object authData) { + try { + for (AuthTokenListener listener : authDataInterceptors) { + final AuthTokenResult result = listener.getAuthTokenResult(authData, client); + if (!result.isSuccess()) { + return result; + } + } + return AuthTokenResult.AuthTokenResultSuccess; + } catch (Exception e) { + exceptionListener.onAuthException(e, client); + } + return new AuthTokenResult(false, "Internal error"); + } } diff --git a/src/main/java/com/corundumstudio/socketio/namespace/NamespacesHub.java b/src/main/java/com/corundumstudio/socketio/namespace/NamespacesHub.java index 03cea4915..54da9b7b0 100644 --- a/src/main/java/com/corundumstudio/socketio/namespace/NamespacesHub.java +++ b/src/main/java/com/corundumstudio/socketio/namespace/NamespacesHub.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,11 @@ */ package com.corundumstudio.socketio.namespace; +import io.netty.util.internal.PlatformDependent; + import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.corundumstudio.socketio.Configuration; @@ -28,7 +29,7 @@ public class NamespacesHub { - private final ConcurrentMap namespaces = new ConcurrentHashMap(); + private final ConcurrentMap namespaces = PlatformDependent.newConcurrentHashMap(); private final Configuration configuration; public NamespacesHub(Configuration configuration) { diff --git a/src/main/java/com/corundumstudio/socketio/protocol/AckArgs.java b/src/main/java/com/corundumstudio/socketio/protocol/AckArgs.java index f7a65577c..b25230151 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/AckArgs.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/AckArgs.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/protocol/AuthPacket.java b/src/main/java/com/corundumstudio/socketio/protocol/AuthPacket.java index ebb1156dd..88cfa77ae 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/AuthPacket.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/AuthPacket.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/protocol/DecoderException.java b/src/main/java/com/corundumstudio/socketio/protocol/ConnPacket.java similarity index 70% rename from src/main/java/com/corundumstudio/socketio/protocol/DecoderException.java rename to src/main/java/com/corundumstudio/socketio/protocol/ConnPacket.java index 487b75de6..0e151c242 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/DecoderException.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/ConnPacket.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,17 @@ */ package com.corundumstudio.socketio.protocol; -// TODO use SocketIOException -public class DecoderException extends RuntimeException { +import java.util.UUID; - private static final long serialVersionUID = -312474299994609579L; +public class ConnPacket { - public DecoderException(String message) { - super(message); + private final UUID sid; + + public ConnPacket(UUID sid) { + this.sid = sid; } + public UUID getSid() { + return sid; + } } diff --git a/src/main/java/com/corundumstudio/socketio/protocol/EngineIOVersion.java b/src/main/java/com/corundumstudio/socketio/protocol/EngineIOVersion.java new file mode 100644 index 000000000..c0a6d7f49 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/protocol/EngineIOVersion.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.protocol; + +import java.util.HashMap; +import java.util.Map; + +/** + * Engine.IO protocol version + */ +public enum EngineIOVersion { + /** + * @link Engine.IO version 2 + */ + V2("2"), + /** + * @link Engine.IO version 3 + */ + V3("3"), + /** + * current version + * @link Engine.IO version 4 + */ + V4("4"), + + UNKNOWN(""), + ; + + public static final String EIO = "EIO"; + + private static final Map VERSIONS = new HashMap<>(); + + static { + for (EngineIOVersion value : values()) { + VERSIONS.put(value.getValue(), value); + } + } + + private final String value; + + private EngineIOVersion(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static EngineIOVersion fromValue(String value) { + EngineIOVersion engineIOVersion = VERSIONS.get(value); + if (engineIOVersion != null) { + return engineIOVersion; + } + return UNKNOWN; + } +} diff --git a/src/main/java/com/corundumstudio/socketio/protocol/Event.java b/src/main/java/com/corundumstudio/socketio/protocol/Event.java index 525e0394e..3392ae883 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/Event.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/Event.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import java.util.List; -class Event { +public class Event { private String name; private List args; diff --git a/src/main/java/com/corundumstudio/socketio/protocol/JacksonJsonSupport.java b/src/main/java/com/corundumstudio/socketio/protocol/JacksonJsonSupport.java index 1730e8e03..5c7fa53c5 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/JacksonJsonSupport.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/JacksonJsonSupport.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,9 @@ */ package com.corundumstudio.socketio.protocol; -import io.netty.buffer.ByteBufInputStream; -import io.netty.buffer.ByteBufOutputStream; - import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; @@ -27,7 +26,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,9 +39,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.SerializableString; -import com.fasterxml.jackson.core.io.CharacterEscapes; -import com.fasterxml.jackson.core.io.SerializedString; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -67,39 +62,13 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.type.ArrayType; -public class JacksonJsonSupport implements JsonSupport { - - public static class HTMLCharacterEscapes extends CharacterEscapes - { - private static final long serialVersionUID = 6159367941232231133L; - - private final int[] asciiEscapes; - - public HTMLCharacterEscapes() - { - // start with set of characters known to require escaping (double-quote, backslash etc) - int[] esc = CharacterEscapes.standardAsciiEscapesForJSON(); - // and force escaping of a few others: - esc['"'] = CharacterEscapes.ESCAPE_CUSTOM; - asciiEscapes = esc; - } - - // this method gets called for character codes 0 - 127 - @Override - public int[] getEscapeCodesForAscii() { - return asciiEscapes; - } +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.util.internal.PlatformDependent; - @Override - public SerializableString getEscapeSequence(int ch) { - if (ch == '"') { - return new SerializedString("\\\\" + (char)ch); - } - return null; - } - } +public class JacksonJsonSupport implements JsonSupport { - private class AckArgsDeserializer extends StdDeserializer { + protected class AckArgsDeserializer extends StdDeserializer { private static final long serialVersionUID = 7810461017389946707L; @@ -185,11 +154,11 @@ public boolean equals(Object obj) { } - private class EventDeserializer extends StdDeserializer { + protected class EventDeserializer extends StdDeserializer { private static final long serialVersionUID = 8178797221017768689L; - final Map>> eventMapping = new ConcurrentHashMap>>(); + final Map>> eventMapping = PlatformDependent.newConcurrentHashMap(); protected EventDeserializer() { @@ -236,7 +205,10 @@ public Event deserialize(JsonParser jp, DeserializationContext ctxt) throws IOEx public static class ByteArraySerializer extends StdSerializer { + private static final long serialVersionUID = 3420082888596468148L; + private final ThreadLocal> arrays = new ThreadLocal>() { + @Override protected List initialValue() { return new ArrayList(); }; @@ -301,7 +273,7 @@ public void clear() { } - private class ExBeanSerializerModifier extends BeanSerializerModifier { + protected static class ExBeanSerializerModifier extends BeanSerializerModifier { private final ByteArraySerializer serializer = new ByteArraySerializer(); @@ -321,24 +293,24 @@ public ByteArraySerializer getSerializer() { } - private final ExBeanSerializerModifier modifier = new ExBeanSerializerModifier(); - private final ThreadLocal namespaceClass = new ThreadLocal(); - private final ThreadLocal> currentAckClass = new ThreadLocal>(); - private final ObjectMapper objectMapper = new ObjectMapper(); - private final ObjectMapper jsonpObjectMapper = new ObjectMapper(); - private final EventDeserializer eventDeserializer = new EventDeserializer(); - private final AckArgsDeserializer ackArgsDeserializer = new AckArgsDeserializer(); + protected final ExBeanSerializerModifier modifier = new ExBeanSerializerModifier(); + protected final ThreadLocal namespaceClass = new ThreadLocal(); + protected final ThreadLocal> currentAckClass = new ThreadLocal>(); + protected final ObjectMapper objectMapper = new ObjectMapper(); + protected final EventDeserializer eventDeserializer = new EventDeserializer(); + protected final AckArgsDeserializer ackArgsDeserializer = new AckArgsDeserializer(); - private final Logger log = LoggerFactory.getLogger(getClass()); + protected static final Logger log = LoggerFactory.getLogger(JacksonJsonSupport.class); + + public JacksonJsonSupport() { + this(new Module[] {}); + } public JacksonJsonSupport(Module... modules) { if (modules != null && modules.length > 0) { objectMapper.registerModules(modules); - jsonpObjectMapper.registerModules(modules); } init(objectMapper); - init(jsonpObjectMapper); - jsonpObjectMapper.getFactory().setCharacterEscapes(new HTMLCharacterEscapes()); } protected void init(ObjectMapper objectMapper) { @@ -367,25 +339,19 @@ public void removeEventMapping(String namespaceName, String eventName) { @Override public T readValue(String namespaceName, ByteBufInputStream src, Class valueType) throws IOException { namespaceClass.set(namespaceName); - return objectMapper.readValue(src, valueType); + return objectMapper.readValue((InputStream)src, valueType); } @Override public AckArgs readAckArgs(ByteBufInputStream src, AckCallback callback) throws IOException { currentAckClass.set(callback); - return objectMapper.readValue(src, AckArgs.class); + return objectMapper.readValue((InputStream)src, AckArgs.class); } @Override public void writeValue(ByteBufOutputStream out, Object value) throws IOException { modifier.getSerializer().clear(); - objectMapper.writeValue(out, value); - } - - @Override - public void writeJsonpValue(ByteBufOutputStream out, Object value) throws IOException { - modifier.getSerializer().clear(); - jsonpObjectMapper.writeValue(out, value); + objectMapper.writeValue((OutputStream)out, value); } @Override diff --git a/src/main/java/com/corundumstudio/socketio/protocol/JsonSupport.java b/src/main/java/com/corundumstudio/socketio/protocol/JsonSupport.java index 92b3d328e..537715fb3 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/JsonSupport.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/JsonSupport.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,6 @@ */ public interface JsonSupport { - void writeJsonpValue(ByteBufOutputStream out, Object value) throws IOException; - AckArgs readAckArgs(ByteBufInputStream src, AckCallback callback) throws IOException; T readValue(String namespaceName, ByteBufInputStream src, Class valueType) throws IOException; diff --git a/src/main/java/com/corundumstudio/socketio/protocol/Packet.java b/src/main/java/com/corundumstudio/socketio/protocol/Packet.java index 2d81f42fd..368d5b9c1 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/Packet.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/Packet.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ public class Packet implements Serializable { private static final long serialVersionUID = 4560159536486711426L; private PacketType type; + private EngineIOVersion engineIOVersion; private PacketType subType; private Long ackId; private String name; @@ -42,10 +43,15 @@ public class Packet implements Serializable { protected Packet() { } + //only for tests public Packet(PacketType type) { super(); this.type = type; } + public Packet(PacketType type, EngineIOVersion engineIOVersion) { + this(type); + this.engineIOVersion = engineIOVersion; + } public PacketType getSubType() { return subType; @@ -65,16 +71,49 @@ public void setData(Object data) { /** * Get packet data + * + * @param the type data + * *
-     * @return json object for {@link PacketType.JSON} type
-     * message for {@link PacketType.MESSAGE} type
+     * @return json object for PacketType.JSON type
+     * message for PacketType.MESSAGE type
      * 
*/ public T getData() { return (T)data; } + /** + * Creates a copy of #{@link Packet} with new namespace set + * if it differs from current namespace. + * Otherwise, returns original object unchanged + * + * @param namespace + * @param engineIOVersion + * @return packet + */ + public Packet withNsp(String namespace, EngineIOVersion engineIOVersion) { + if (this.nsp.equalsIgnoreCase(namespace)) { + return this; + } else { + Packet newPacket = new Packet(this.type, engineIOVersion); + newPacket.setAckId(this.ackId); + newPacket.setData(this.data); + newPacket.setDataSource(this.dataSource); + newPacket.setName(this.name); + newPacket.setSubType(this.subType); + newPacket.setNsp(namespace); + newPacket.attachments = this.attachments; + newPacket.attachmentsCount = this.attachmentsCount; + return newPacket; + } + } + public void setNsp(String endpoint) { + //patch for #903 + if (endpoint.equals("{}")){ + endpoint=""; + } this.nsp = endpoint; } @@ -104,7 +143,7 @@ public boolean isAckRequested() { public void initAttachments(int attachmentsCount) { this.attachmentsCount = attachmentsCount; - this.attachments = new ArrayList(attachmentsCount); + this.attachments = new ArrayList<>(); } public void addAttachment(ByteBuf attachment) { if (this.attachments.size() < attachmentsCount) { @@ -128,6 +167,14 @@ public void setDataSource(ByteBuf dataSource) { this.dataSource = dataSource; } + public EngineIOVersion getEngineIOVersion() { + return engineIOVersion; + } + + public void setEngineIOVersion(EngineIOVersion engineIOVersion) { + this.engineIOVersion = engineIOVersion; + } + @Override public String toString() { return "Packet [type=" + type + ", ackId=" + ackId + "]"; diff --git a/src/main/java/com/corundumstudio/socketio/protocol/PacketDecoder.java b/src/main/java/com/corundumstudio/socketio/protocol/PacketDecoder.java index ea261e6d0..bb60ed31a 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/PacketDecoder.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/PacketDecoder.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,54 +15,55 @@ */ package com.corundumstudio.socketio.protocol; +import com.corundumstudio.socketio.AckCallback; +import com.corundumstudio.socketio.ack.AckManager; +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.namespace.Namespace; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; import io.netty.handler.codec.base64.Base64; -import io.netty.handler.codec.base64.Base64Dialect; import io.netty.util.CharsetUtil; - import java.io.IOException; import java.net.URLDecoder; import java.util.LinkedList; -import java.util.UUID; - -import com.corundumstudio.socketio.AckCallback; -import com.corundumstudio.socketio.ack.AckManager; -import com.corundumstudio.socketio.handler.ClientHead; -import com.corundumstudio.socketio.namespace.NamespacesHub; +import java.util.Map; public class PacketDecoder { + private final UTF8CharsScanner utf8scanner = new UTF8CharsScanner(); + private final ByteBuf QUOTES = Unpooled.copiedBuffer("\"", CharsetUtil.UTF_8); private final JsonSupport jsonSupport; private final AckManager ackManager; - private final NamespacesHub nspHub; - public PacketDecoder(JsonSupport jsonSupport, NamespacesHub nspHub, AckManager ackManager) { + public PacketDecoder(JsonSupport jsonSupport, AckManager ackManager) { this.jsonSupport = jsonSupport; this.ackManager = ackManager; - this.nspHub = nspHub; + } + + private boolean isStringPacket(ByteBuf content) { + return content.getByte(content.readerIndex()) == 0x0; } // TODO optimize public ByteBuf preprocessJson(Integer jsonIndex, ByteBuf content) throws IOException { String packet = URLDecoder.decode(content.toString(CharsetUtil.UTF_8), CharsetUtil.UTF_8.name()); - int startPos = 0; if (jsonIndex != null) { + /** + * double escaping is required for escaped new lines because unescaping of new lines can be done safely on server-side + * (c) socket.io.js + * + * @see https://github.com/Automattic/socket.io-client/blob/1.3.3/socket.io.js#L2682 + */ + packet = packet.replace("\\\\n", "\\n"); + // skip "d=" - startPos = 2; + packet = packet.substring(2); } - int splitIndex = packet.indexOf(":"); - String len = packet.substring(startPos, splitIndex); - Integer length = Integer.valueOf(len); - - packet = packet.substring(splitIndex+1, splitIndex+length+1); - packet = new String(packet.getBytes(CharsetUtil.ISO_8859_1), CharsetUtil.UTF_8); - return Unpooled.wrappedBuffer(packet.getBytes(CharsetUtil.UTF_8)); } @@ -90,22 +91,39 @@ private PacketType readInnerType(ByteBuf buffer) { return PacketType.valueOfInner(typeId); } - @Deprecated - public Packet decodePacket(String string, UUID uuid) throws IOException { - ByteBuf buf = Unpooled.copiedBuffer(string, CharsetUtil.UTF_8); - try { - return null; - } finally { - buf.release(); + private boolean hasLengthHeader(ByteBuf buffer) { + for (int i = 0; i < Math.min(buffer.readableBytes(), 10); i++) { + byte b = buffer.getByte(buffer.readerIndex() + i); + if (b == (byte)':' && i > 0) { + return true; + } + if (b > 57 || b < 48) { + return false; + } } + return false; } public Packet decodePackets(ByteBuf buffer, ClientHead client) throws IOException { - boolean isString = buffer.getByte(buffer.readerIndex()) == 0x0; - if (isString) { - int headEndIndex = buffer.bytesBefore((byte)-1); + if (isStringPacket(buffer)) { + // TODO refactor + int maxLength = Math.min(buffer.readableBytes(), 10); + int headEndIndex = buffer.bytesBefore(maxLength, (byte)-1); + if (headEndIndex == -1) { + headEndIndex = buffer.bytesBefore(maxLength, (byte)0x3f); + } int len = (int) readLong(buffer, headEndIndex); + ByteBuf frame = buffer.slice(buffer.readerIndex() + 1, len); + // skip this frame + buffer.readerIndex(buffer.readerIndex() + 1 + len); + return decode(client, frame); + } else if (hasLengthHeader(buffer)) { + // TODO refactor + int lengthEndIndex = buffer.bytesBefore((byte)':'); + int lenHeader = (int) readLong(buffer, lengthEndIndex); + int len = utf8scanner.getActualLength(buffer, lenHeader); + ByteBuf frame = buffer.slice(buffer.readerIndex() + 1, len); // skip this frame buffer.readerIndex(buffer.readerIndex() + 1 + len); @@ -125,27 +143,42 @@ private String readString(ByteBuf frame, int size) { } private Packet decode(ClientHead head, ByteBuf frame) throws IOException { - if ((frame.getByte(0) == 'b' && frame.getByte(1) == '4') - || frame.getByte(0) == 4 || frame.getByte(0) == 1) { - return parseBinary(head, frame); + + Packet lastPacket = head.getLastBinaryPacket(); + // Assume attachments follow. + if (lastPacket != null) { + if (lastPacket.hasAttachments() && !lastPacket.isAttachmentsLoaded()) { + return addAttachment(head, frame, lastPacket); + } } - PacketType type = readType(frame); - Packet packet = new Packet(type); + + final int separatorPos = frame.bytesBefore((byte) 0x1E); + final ByteBuf packetBuf; + if (separatorPos > 0) { + // Multiple packets in one, copy out the next packet to parse + packetBuf = frame.copy(frame.readerIndex(), separatorPos); + frame.skipBytes(separatorPos + 1); + } else { + packetBuf = frame; + } + + PacketType type = readType(packetBuf); + Packet packet = new Packet(type, head.getEngineIOVersion()); if (type == PacketType.PING) { - packet.setData(readString(frame)); + packet.setData(readString(packetBuf)); return packet; } - if (!frame.isReadable()) { + if (!packetBuf.isReadable()) { return packet; } - PacketType innerType = readInnerType(frame); + PacketType innerType = readInnerType(packetBuf); packet.setSubType(innerType); - parseHeader(frame, packet, innerType); - parseBody(head, frame, packet); + parseHeader(packetBuf, packet, innerType); + parseBody(head, packetBuf, packet); return packet; } @@ -157,7 +190,8 @@ private void parseHeader(ByteBuf frame, Packet packet, PacketType innerType) { int attachmentsDividerIndex = frame.bytesBefore(endIndex, (byte)'-'); boolean hasAttachments = attachmentsDividerIndex != -1; - if (hasAttachments && PacketType.BINARY_EVENT.equals(innerType)) { + if (hasAttachments && (PacketType.BINARY_EVENT.equals(innerType) + || PacketType.BINARY_ACK.equals(innerType))) { int attachments = (int) readLong(frame, attachmentsDividerIndex); packet.initAttachments(attachments); frame.readerIndex(frame.readerIndex() + 1); @@ -185,91 +219,123 @@ private void parseHeader(ByteBuf frame, Packet packet, PacketType innerType) { } } - private Packet parseBinary(ClientHead head, ByteBuf frame) throws IOException { - if (frame.getByte(0) == 1) { - frame.readByte(); - int headEndIndex = frame.bytesBefore((byte)-1); - int len = (int) readLong(frame, headEndIndex); - ByteBuf oldFrame = frame; - frame = frame.slice(oldFrame.readerIndex() + 1, len); - oldFrame.readerIndex(oldFrame.readerIndex() + 1 + len); - } - - if (frame.getByte(0) == 'b' && frame.getByte(1) == '4') { - frame.readShort(); - } else if (frame.getByte(0) == 4) { - frame.readByte(); - } - - Packet binaryPacket = head.getLastBinaryPacket(); - if (binaryPacket != null) { - ByteBuf attachBuf; - if (frame.getByte(0) == 'b' && frame.getByte(1) == '4') { - attachBuf = frame; - } else { - attachBuf = Base64.encode(frame); - } - binaryPacket.addAttachment(Unpooled.copiedBuffer(attachBuf)); - frame.readerIndex(frame.readerIndex() + frame.readableBytes()); - - if (binaryPacket.isAttachmentsLoaded()) { - LinkedList slices = new LinkedList(); - ByteBuf source = binaryPacket.getDataSource(); - for (int i = 0; i < binaryPacket.getAttachments().size(); i++) { - ByteBuf attachment = binaryPacket.getAttachments().get(i); - ByteBuf scanValue = Unpooled.copiedBuffer("{\"_placeholder\":true,\"num\":" + i + "}", CharsetUtil.UTF_8); - int pos = PacketEncoder.find(source, scanValue); + private Packet addAttachment(ClientHead head, ByteBuf frame, Packet binaryPacket) throws IOException { + ByteBuf attachBuf = Base64.encode(frame); + binaryPacket.addAttachment(Unpooled.copiedBuffer(attachBuf)); + attachBuf.release(); + frame.skipBytes(frame.readableBytes()); + + if (binaryPacket.isAttachmentsLoaded()) { + LinkedList slices = new LinkedList(); + ByteBuf source = binaryPacket.getDataSource(); + for (int i = 0; i < binaryPacket.getAttachments().size(); i++) { + ByteBuf attachment = binaryPacket.getAttachments().get(i); + ByteBuf scanValue = Unpooled.copiedBuffer("{\"_placeholder\":true,\"num\":" + i + "}", CharsetUtil.UTF_8); + int pos = PacketEncoder.find(source, scanValue); + if (pos == -1) { + scanValue = Unpooled.copiedBuffer("{\"num\":" + i + ",\"_placeholder\":true}", CharsetUtil.UTF_8); + pos = PacketEncoder.find(source, scanValue); if (pos == -1) { throw new IllegalStateException("Can't find attachment by index: " + i + " in packet source"); } - - ByteBuf prefixBuf = source.slice(source.readerIndex(), pos - source.readerIndex()); - slices.add(prefixBuf); - slices.add(QUOTES); - slices.add(attachment); - slices.add(QUOTES); - - source.readerIndex(pos + scanValue.readableBytes()); } - slices.add(source.slice()); - ByteBuf compositeBuf = Unpooled.wrappedBuffer(slices.toArray(new ByteBuf[slices.size()])); - parseBody(head, compositeBuf, binaryPacket); - head.setLastBinaryPacket(null); - return binaryPacket; + ByteBuf prefixBuf = source.slice(source.readerIndex(), pos - source.readerIndex()); + slices.add(prefixBuf); + slices.add(QUOTES); + slices.add(attachment); + slices.add(QUOTES); + + source.readerIndex(pos + scanValue.readableBytes()); } + slices.add(source.slice()); + + ByteBuf compositeBuf = Unpooled.wrappedBuffer(slices.toArray(new ByteBuf[0])); + parseBody(head, compositeBuf, binaryPacket); + head.setLastBinaryPacket(null); + return binaryPacket; } - return new Packet(PacketType.MESSAGE); + return new Packet(PacketType.MESSAGE, head.getEngineIOVersion()); } private void parseBody(ClientHead head, ByteBuf frame, Packet packet) throws IOException { if (packet.getType() == PacketType.MESSAGE) { if (packet.getSubType() == PacketType.CONNECT || packet.getSubType() == PacketType.DISCONNECT) { - packet.setNsp(readString(frame)); + packet.setNsp(readNamespace(frame, false)); + if (packet.getSubType() == PacketType.CONNECT && frame.readableBytes() > 0) { + final Object authArgs = jsonSupport.readValue(packet.getNsp(), new ByteBufInputStream(frame), Map.class); + packet.setData(authArgs); + } } - if (packet.getSubType() == PacketType.ACK) { - ByteBufInputStream in = new ByteBufInputStream(frame); + if (packet.hasAttachments() && !packet.isAttachmentsLoaded()) { + packet.setDataSource(Unpooled.copiedBuffer(frame)); + frame.skipBytes(frame.readableBytes()); + head.setLastBinaryPacket(packet); + return; + } + + if (packet.getSubType() == PacketType.ACK + || packet.getSubType() == PacketType.BINARY_ACK) { AckCallback callback = ackManager.getCallback(head.getSessionId(), packet.getAckId()); - AckArgs args = jsonSupport.readAckArgs(in, callback); - packet.setData(args.getArgs()); + if (callback != null) { + ByteBufInputStream in = new ByteBufInputStream(frame); + AckArgs args = jsonSupport.readAckArgs(in, callback); + packet.setData(args.getArgs()); + }else { + frame.clear(); + } } if (packet.getSubType() == PacketType.EVENT || packet.getSubType() == PacketType.BINARY_EVENT) { - if (packet.hasAttachments() && !packet.isAttachmentsLoaded()) { - packet.setDataSource(Unpooled.copiedBuffer(frame)); - frame.readerIndex(frame.readableBytes()); - head.setLastBinaryPacket(packet); - } else { - ByteBufInputStream in = new ByteBufInputStream(frame); - Event event = jsonSupport.readValue(packet.getNsp(), in, Event.class); - packet.setName(event.getName()); - packet.setData(event.getArgs()); - } + ByteBufInputStream in = new ByteBufInputStream(frame); + Event event = jsonSupport.readValue(packet.getNsp(), in, Event.class); + packet.setName(event.getName()); + packet.setData(event.getArgs()); } } } + private String readNamespace(ByteBuf frame, final boolean defaultToAll) { + + /** + * namespace post request with url queryString, like + * /message (v1) + * /message?a=1, (v2) + * /message, (v3,v4) + */ + ByteBuf buffer = frame.slice(); + + boolean withSpecialChar = false; + + int namespaceFieldEndIndex = buffer.bytesBefore((byte) ','); + if (namespaceFieldEndIndex > 0) { + withSpecialChar = true; + } else { + namespaceFieldEndIndex = buffer.readableBytes(); + } + + int namespaceEndIndex = buffer.bytesBefore((byte) '?'); + if (namespaceEndIndex > 0) { + withSpecialChar = true; + } else { + namespaceEndIndex = namespaceFieldEndIndex; + } + + String namespace = readString(buffer, namespaceEndIndex); + if (namespace.startsWith("/")) { + frame.skipBytes(namespaceFieldEndIndex + (withSpecialChar ? 1 : 0)); + return namespace; + } + + if (defaultToAll) { + // skip this frame + frame.skipBytes(frame.readableBytes()); + return readString(buffer); + } + return Namespace.DEFAULT_NAME; + } + } diff --git a/src/main/java/com/corundumstudio/socketio/protocol/PacketEncoder.java b/src/main/java/com/corundumstudio/socketio/protocol/PacketEncoder.java index 1626efa14..00f903f6f 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/PacketEncoder.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/PacketEncoder.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package com.corundumstudio.socketio.protocol; +import com.corundumstudio.socketio.Configuration; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufOutputStream; @@ -27,18 +28,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; -import java.util.regex.Pattern; - -import com.corundumstudio.socketio.Configuration; public class PacketEncoder { - private static final Pattern QUOTES_PATTERN = Pattern.compile("\"", Pattern.LITERAL); private static final byte[] BINARY_HEADER = "b4".getBytes(CharsetUtil.UTF_8); private static final byte[] B64_DELIMITER = new byte[] {':'}; private static final byte[] JSONP_HEAD = "___eio[".getBytes(CharsetUtil.UTF_8); - private static final byte[] JSONP_START = "](\"".getBytes(CharsetUtil.UTF_8); - private static final byte[] JSONP_END = "\");".getBytes(CharsetUtil.UTF_8); + private static final byte[] JSONP_START = "]('".getBytes(CharsetUtil.UTF_8); + private static final byte[] JSONP_END = "');".getBytes(CharsetUtil.UTF_8); private final JsonSupport jsonSupport; private final Configuration configuration; @@ -48,6 +45,10 @@ public PacketEncoder(Configuration configuration, JsonSupport jsonSupport) { this.configuration = configuration; } + public JsonSupport getJsonSupport() { + return jsonSupport; + } + public ByteBuf allocateBuffer(ByteBufAllocator allocator) { if (configuration.isPreferDirectBuffer()) { return allocator.ioBuffer(); @@ -59,10 +60,7 @@ public ByteBuf allocateBuffer(ByteBufAllocator allocator) { public void encodeJsonP(Integer jsonpIndex, Queue packets, ByteBuf out, ByteBufAllocator allocator, int limit) throws IOException { boolean jsonpMode = jsonpIndex != null; - ByteBuf buf = out; - if (jsonpMode) { - buf = allocateBuffer(allocator); - } + ByteBuf buf = allocateBuffer(allocator); int i = 0; while (true) { @@ -72,15 +70,9 @@ public void encodeJsonP(Integer jsonpIndex, Queue packets, ByteBuf out, } ByteBuf packetBuf = allocateBuffer(allocator); - encodePacket(packet, packetBuf, allocator, true, jsonpMode); + encodePacket(packet, packetBuf, allocator, true); int packetSize = packetBuf.writerIndex(); - if (jsonpMode) { - // scan for \\\" - int count = count(packetBuf, Unpooled.copiedBuffer("\\\"", CharsetUtil.UTF_8)); - packetSize -= count; - } - buf.writeBytes(toChars(packetSize)); buf.writeBytes(B64_DELIMITER); buf.writeBytes(packetBuf); @@ -102,26 +94,47 @@ public void encodeJsonP(Integer jsonpIndex, Queue packets, ByteBuf out, out.writeBytes(JSONP_HEAD); out.writeBytes(toChars(jsonpIndex)); out.writeBytes(JSONP_START); + } - String packet = buf.toString(CharsetUtil.UTF_8); - buf.release(); - // TODO optimize - packet = QUOTES_PATTERN.matcher(packet).replaceAll("\\\\\""); - packet = new String(packet.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1); - out.writeBytes(packet.getBytes(CharsetUtil.UTF_8)); + processUtf8(buf, out, jsonpMode); + buf.release(); + if (jsonpMode) { out.writeBytes(JSONP_END); } } + private void processUtf8(ByteBuf in, ByteBuf out, boolean jsonpMode) { + while (in.isReadable()) { + short value = (short) (in.readByte() & 0xFF); + if (value >>> 7 == 0) { + if (jsonpMode && (value == '\\' || value == '\'')) { + out.writeByte('\\'); + } + out.writeByte(value); + } else { + out.writeByte(((value >>> 6) | 0xC0)); + out.writeByte(((value & 0x3F) | 0x80)); + } + } + } + public void encodePackets(Queue packets, ByteBuf buffer, ByteBufAllocator allocator, int limit) throws IOException { int i = 0; + boolean hasPrecedingPacket = false; while (true) { Packet packet = packets.poll(); if (packet == null || i == limit) { break; } - encodePacket(packet, buffer, allocator, false, false); + // Multiple packets are separated by 0x1e from protocol version 3 on + // see https://socket.io/docs/v4/socket-io-protocol/#sample-session + final boolean isV3OrNewer = EngineIOVersion.V4.equals(packet.getEngineIOVersion()) || + EngineIOVersion.V3.equals(packet.getEngineIOVersion()); + if (hasPrecedingPacket && isV3OrNewer) { + buffer.writeByte(0x1e); + } + encodePacket(packet, buffer, allocator, false); i++; @@ -132,6 +145,7 @@ public void encodePackets(Queue packets, ByteBuf buffer, ByteBufAllocato buffer.writeByte(4); buffer.writeBytes(attachment); } + hasPrecedingPacket = true; } } @@ -139,25 +153,25 @@ private byte toChar(int number) { return (byte) (number ^ 0x30); } - final static char[] DigitTens = {'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', + static final char[] DigitTens = {'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9',}; - final static char[] DigitOnes = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', + static final char[] DigitOnes = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',}; - final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', + static final char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - final static int[] sizeTable = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, + static final int[] sizeTable = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE}; // Requires positive x @@ -221,7 +235,7 @@ public static byte[] longToBytes(long number) { return res; } - public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocator, boolean binary, boolean jsonp) throws IOException { + public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocator, boolean binary) throws IOException { ByteBuf buf = buffer; if (!binary) { buf = allocateBuffer(allocator); @@ -239,11 +253,7 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat case OPEN: { ByteBufOutputStream out = new ByteBufOutputStream(buf); - if (jsonp) { - jsonSupport.writeJsonpValue(out, packet.getData()); - } else { - jsonSupport.writeValue(out, packet.getData()); - } + jsonSupport.writeValue(out, packet.getData()); break; } @@ -251,14 +261,18 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat ByteBuf encBuf = null; + if (packet.getSubType() == PacketType.ERROR) { + encBuf = allocateBuffer(allocator); + + ByteBufOutputStream out = new ByteBufOutputStream(encBuf); + jsonSupport.writeValue(out, packet.getData()); + } if (packet.getSubType() == PacketType.EVENT - || packet.getSubType() == PacketType.ACK - || packet.getSubType() == PacketType.ERROR) { + || packet.getSubType() == PacketType.ACK) { List values = new ArrayList(); - if (packet.getSubType() == PacketType.EVENT - || packet.getSubType() == PacketType.ERROR) { + if (packet.getSubType() == PacketType.EVENT) { values.add(packet.getName()); } @@ -267,18 +281,15 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat List args = packet.getData(); values.addAll(args); ByteBufOutputStream out = new ByteBufOutputStream(encBuf); - if (jsonp) { - jsonSupport.writeJsonpValue(out, values); - } else { - jsonSupport.writeValue(out, values); - } + jsonSupport.writeValue(out, values); if (!jsonSupport.getArrays().isEmpty()) { packet.initAttachments(jsonSupport.getArrays().size()); for (byte[] array : jsonSupport.getArrays()) { packet.addAttachment(Unpooled.wrappedBuffer(array)); } - packet.setSubType(PacketType.BINARY_EVENT); + packet.setSubType(packet.getSubType() == PacketType.ACK + ? PacketType.BINARY_ACK : PacketType.BINARY_EVENT); } } @@ -295,6 +306,16 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat if (!packet.getNsp().isEmpty()) { buf.writeBytes(packet.getNsp().getBytes(CharsetUtil.UTF_8)); } + //:TODO lyjnew tmp change V4 add “,” + if (EngineIOVersion.V4.equals(packet.getEngineIOVersion()) + && packet.getData() != null) { + + if (!packet.getNsp().isEmpty()) { + buf.writeByte(','); + } + ByteBufOutputStream out = new ByteBufOutputStream(buf); + jsonSupport.writeValue(out, packet.getData()); + } } else { if (!packet.getNsp().isEmpty()) { buf.writeBytes(packet.getNsp().getBytes(CharsetUtil.UTF_8)); @@ -318,10 +339,12 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat } finally { // we need to write a buffer in any case if (!binary) { - buffer.writeByte(0); - int length = buf.writerIndex(); - buffer.writeBytes(longToBytes(length)); - buffer.writeByte(0xff); + if (!EngineIOVersion.V4.equals(packet.getEngineIOVersion())){ + buffer.writeByte(0); + int length = buf.writerIndex(); + buffer.writeBytes(longToBytes(length)); + buffer.writeByte(0xff); + } buffer.writeBytes(buf); buf.release(); @@ -329,16 +352,6 @@ public void encodePacket(Packet packet, ByteBuf buffer, ByteBufAllocator allocat } } - private int count(ByteBuf buffer, ByteBuf searchValue) { - int count = 0; - for (int i = 0; i < buffer.readableBytes(); i++) { - if (isValueFound(buffer, i, searchValue)) { - count++; - } - } - return count; - } - public static int find(ByteBuf buffer, ByteBuf searchValue) { for (int i = buffer.readerIndex(); i < buffer.readerIndex() + buffer.readableBytes(); i++) { if (isValueFound(buffer, i, searchValue)) { diff --git a/src/main/java/com/corundumstudio/socketio/protocol/PacketType.java b/src/main/java/com/corundumstudio/socketio/protocol/PacketType.java index d642314bc..5a00c6209 100644 --- a/src/main/java/com/corundumstudio/socketio/protocol/PacketType.java +++ b/src/main/java/com/corundumstudio/socketio/protocol/PacketType.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ public enum PacketType { OPEN(0), CLOSE(1), PING(2), PONG(3), MESSAGE(4), UPGRADE(5), NOOP(6), - CONNECT(0, true), DISCONNECT(1, true), EVENT(2, true), ACK(3, true), ERROR(4, true), BINARY_EVENT(5, true); + CONNECT(0, true), DISCONNECT(1, true), EVENT(2, true), ACK(3, true), ERROR(4, true), BINARY_EVENT(5, true), BINARY_ACK(6, true); public static final PacketType[] VALUES = values(); private final int value; @@ -54,7 +54,7 @@ public static PacketType valueOfInner(int value) { return type; } } - throw new IllegalStateException(); + throw new IllegalArgumentException("Can't parse " + value); } } diff --git a/src/main/java/com/corundumstudio/socketio/protocol/UTF8CharsScanner.java b/src/main/java/com/corundumstudio/socketio/protocol/UTF8CharsScanner.java new file mode 100644 index 000000000..303556d90 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/protocol/UTF8CharsScanner.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.protocol; + +import io.netty.buffer.ByteBuf; + +public class UTF8CharsScanner { + + /** + * Lookup table used for determining which input characters need special + * handling when contained in text segment. + */ + static final int[] sInputCodes; + static { + /* + * 96 would do for most cases (backslash is ascii 94) but if we want to + * do lookups by raw bytes it's better to have full table + */ + int[] table = new int[256]; + // Control chars and non-space white space are not allowed unquoted + for (int i = 0; i < 32; ++i) { + table[i] = -1; + } + // And then string end and quote markers are special too + table['"'] = 1; + table['\\'] = 1; + sInputCodes = table; + } + + /** + * Additionally we can combine UTF-8 decoding info into similar data table. + */ + static final int[] sInputCodesUtf8; + static { + int[] table = new int[sInputCodes.length]; + System.arraycopy(sInputCodes, 0, table, 0, sInputCodes.length); + for (int c = 128; c < 256; ++c) { + int code; + + // We'll add number of bytes needed for decoding + if ((c & 0xE0) == 0xC0) { // 2 bytes (0x0080 - 0x07FF) + code = 2; + } else if ((c & 0xF0) == 0xE0) { // 3 bytes (0x0800 - 0xFFFF) + code = 3; + } else if ((c & 0xF8) == 0xF0) { + // 4 bytes; double-char with surrogates and all... + code = 4; + } else { + // And -1 seems like a good "universal" error marker... + code = -1; + } + table[c] = code; + } + sInputCodesUtf8 = table; + } + + private int getCharTailIndex(ByteBuf inputBuffer, int i) { + int c = (int) inputBuffer.getByte(i) & 0xFF; + switch (sInputCodesUtf8[c]) { + case 2: // 2-byte UTF + i += 2; + break; + case 3: // 3-byte UTF + i += 3; + break; + case 4: // 4-byte UTF + i += 4; + break; + default: + i++; + break; + } + return i; + } + + public int getActualLength(ByteBuf inputBuffer, int length) { + int len = 0; + int start = inputBuffer.readerIndex(); + for (int i = inputBuffer.readerIndex(); i < inputBuffer.readableBytes() + inputBuffer.readerIndex();) { + i = getCharTailIndex(inputBuffer, i); + len++; + if (length == len) { + return i-start; + } + } + throw new IllegalStateException(); + } + +} diff --git a/src/main/java/com/corundumstudio/socketio/scheduler/CancelableScheduler.java b/src/main/java/com/corundumstudio/socketio/scheduler/CancelableScheduler.java index 73ad57826..6926f13c1 100644 --- a/src/main/java/com/corundumstudio/socketio/scheduler/CancelableScheduler.java +++ b/src/main/java/com/corundumstudio/socketio/scheduler/CancelableScheduler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelScheduler.java b/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelScheduler.java index c4e6ad820..dc357aa5b 100644 --- a/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelScheduler.java +++ b/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelScheduler.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,19 +15,28 @@ */ package com.corundumstudio.socketio.scheduler; +import java.util.Map; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + import io.netty.channel.ChannelHandlerContext; import io.netty.util.HashedWheelTimer; import io.netty.util.Timeout; import io.netty.util.TimerTask; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; +import io.netty.util.internal.PlatformDependent; public class HashedWheelScheduler implements CancelableScheduler { - private final Map scheduledFutures = new ConcurrentHashMap(); - private final HashedWheelTimer executorService = new HashedWheelTimer(); + private final Map scheduledFutures = PlatformDependent.newConcurrentHashMap(); + private final HashedWheelTimer executorService; + + public HashedWheelScheduler() { + executorService = new HashedWheelTimer(); + } + + public HashedWheelScheduler(ThreadFactory threadFactory) { + executorService = new HashedWheelTimer(threadFactory); + } private volatile ChannelHandlerContext ctx; @@ -36,6 +45,7 @@ public void update(ChannelHandlerContext ctx) { this.ctx = ctx; } + @Override public void cancel(SchedulerKey key) { Timeout timeout = scheduledFutures.remove(key); if (timeout != null) { @@ -43,6 +53,7 @@ public void cancel(SchedulerKey key) { } } + @Override public void schedule(final Runnable runnable, long delay, TimeUnit unit) { executorService.newTimeout(new TimerTask() { @Override @@ -52,6 +63,7 @@ public void run(Timeout timeout) throws Exception { }, delay, unit); } + @Override public void scheduleCallback(final SchedulerKey key, final Runnable runnable, long delay, TimeUnit unit) { Timeout timeout = executorService.newTimeout(new TimerTask() { @Override @@ -74,6 +86,7 @@ public void run() { } } + @Override public void schedule(final SchedulerKey key, final Runnable runnable, long delay, TimeUnit unit) { Timeout timeout = executorService.newTimeout(new TimerTask() { @Override @@ -91,6 +104,7 @@ public void run(Timeout timeout) throws Exception { } } + @Override public void shutdown() { executorService.stop(); } diff --git a/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelTimeoutScheduler.java b/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelTimeoutScheduler.java new file mode 100644 index 000000000..27b905104 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/scheduler/HashedWheelTimeoutScheduler.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Modified version of HashedWheelScheduler specially for timeouts handling. + * Difference: + * - handling old timeout with same key after adding new one + * fixes multithreaded problem that appears in highly concurrent non-atomic sequence cancel() -> schedule() + * + * (c) Alim Akbashev, 2015-02-11 + */ + +package com.corundumstudio.socketio.scheduler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.TimerTask; +import io.netty.util.internal.PlatformDependent; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public class HashedWheelTimeoutScheduler implements CancelableScheduler { + + private final ConcurrentMap scheduledFutures = PlatformDependent.newConcurrentHashMap(); + private final HashedWheelTimer executorService; + + private volatile ChannelHandlerContext ctx; + + public HashedWheelTimeoutScheduler() { + executorService = new HashedWheelTimer(); + } + + public HashedWheelTimeoutScheduler(ThreadFactory threadFactory) { + executorService = new HashedWheelTimer(threadFactory); + } + + @Override + public void update(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public void cancel(SchedulerKey key) { + Timeout timeout = scheduledFutures.remove(key); + if (timeout != null) { + timeout.cancel(); + } + } + + @Override + public void schedule(final Runnable runnable, long delay, TimeUnit unit) { + executorService.newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) throws Exception { + runnable.run(); + } + }, delay, unit); + } + + @Override + public void scheduleCallback(final SchedulerKey key, final Runnable runnable, long delay, TimeUnit unit) { + Timeout timeout = executorService.newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) throws Exception { + ctx.executor().execute(new Runnable() { + @Override + public void run() { + scheduledFutures.remove(key); + runnable.run(); + } + }); + } + }, delay, unit); + + replaceScheduledFuture(key, timeout); + } + + @Override + public void schedule(final SchedulerKey key, final Runnable runnable, long delay, TimeUnit unit) { + Timeout timeout = executorService.newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) throws Exception { + scheduledFutures.remove(key); + runnable.run(); + } + }, delay, unit); + + replaceScheduledFuture(key, timeout); + } + + @Override + public void shutdown() { + executorService.stop(); + } + + private void replaceScheduledFuture(final SchedulerKey key, final Timeout newTimeout) { + final Timeout oldTimeout; + + if (newTimeout.isExpired()) { + // no need to put already expired timeout to scheduledFutures map. + // simply remove old timeout + oldTimeout = scheduledFutures.remove(key); + } else { + oldTimeout = scheduledFutures.put(key, newTimeout); + } + + // if there was old timeout, cancel it + if (oldTimeout != null) { + oldTimeout.cancel(); + } + } +} diff --git a/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java b/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java index 2bfb5bb13..e1d5fb155 100644 --- a/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java +++ b/src/main/java/com/corundumstudio/socketio/scheduler/SchedulerKey.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,15 @@ */ package com.corundumstudio.socketio.scheduler; -import java.util.UUID; public class SchedulerKey { - public enum Type {POLLING, HEARBEAT_TIMEOUT, PING_TIMEOUT, ACK_TIMEOUT, UPGRADE_TIMEOUT}; + public enum Type {PING, PING_TIMEOUT, ACK_TIMEOUT, UPGRADE_TIMEOUT}; private final Type type; - private final UUID sessionId; + private final Object sessionId; - public SchedulerKey(Type type, UUID sessionId) { + public SchedulerKey(Type type, Object sessionId) { this.type = type; this.sessionId = sessionId; } diff --git a/src/main/java/com/corundumstudio/socketio/store/HazelcastPubSubStore.java b/src/main/java/com/corundumstudio/socketio/store/HazelcastPubSubStore.java index 6fbbff2b7..9dcf87e5f 100644 --- a/src/main/java/com/corundumstudio/socketio/store/HazelcastPubSubStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/HazelcastPubSubStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,16 @@ */ package com.corundumstudio.socketio.store; +import io.netty.util.internal.PlatformDependent; + import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import com.corundumstudio.socketio.store.pubsub.PubSubListener; import com.corundumstudio.socketio.store.pubsub.PubSubMessage; import com.corundumstudio.socketio.store.pubsub.PubSubStore; +import com.corundumstudio.socketio.store.pubsub.PubSubType; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.ITopic; import com.hazelcast.core.Message; @@ -35,8 +37,7 @@ public class HazelcastPubSubStore implements PubSubStore { private final HazelcastInstance hazelcastSub; private final Long nodeId; - private final ConcurrentMap> map = - new ConcurrentHashMap>(); + private final ConcurrentMap> map = PlatformDependent.newConcurrentHashMap(); public HazelcastPubSubStore(HazelcastInstance hazelcastPub, HazelcastInstance hazelcastSub, Long nodeId) { this.hazelcastPub = hazelcastPub; @@ -45,13 +46,14 @@ public HazelcastPubSubStore(HazelcastInstance hazelcastPub, HazelcastInstance ha } @Override - public void publish(String name, PubSubMessage msg) { + public void publish(PubSubType type, PubSubMessage msg) { msg.setNodeId(nodeId); - hazelcastPub.getTopic(name).publish(msg); + hazelcastPub.getTopic(type.toString()).publish(msg); } @Override - public void subscribe(String name, final PubSubListener listener, Class clazz) { + public void subscribe(PubSubType type, final PubSubListener listener, Class clazz) { + String name = type.toString(); ITopic topic = hazelcastSub.getTopic(name); String regId = topic.addMessageListener(new MessageListener() { @Override @@ -75,7 +77,8 @@ public void onMessage(Message message) { } @Override - public void unsubscribe(String name) { + public void unsubscribe(PubSubType type) { + String name = type.toString(); Queue regIds = map.remove(name); ITopic topic = hazelcastSub.getTopic(name); for (String id : regIds) { diff --git a/src/main/java/com/corundumstudio/socketio/store/HazelcastStore.java b/src/main/java/com/corundumstudio/socketio/store/HazelcastStore.java index cd599ad9a..e63b084d1 100644 --- a/src/main/java/com/corundumstudio/socketio/store/HazelcastStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/HazelcastStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/HazelcastStoreFactory.java b/src/main/java/com/corundumstudio/socketio/store/HazelcastStoreFactory.java index 91e8e0acc..4ec58c500 100644 --- a/src/main/java/com/corundumstudio/socketio/store/HazelcastStoreFactory.java +++ b/src/main/java/com/corundumstudio/socketio/store/HazelcastStoreFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/MemoryPubSubStore.java b/src/main/java/com/corundumstudio/socketio/store/MemoryPubSubStore.java index c912eb5f1..cbf444154 100644 --- a/src/main/java/com/corundumstudio/socketio/store/MemoryPubSubStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/MemoryPubSubStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,20 @@ import com.corundumstudio.socketio.store.pubsub.PubSubListener; import com.corundumstudio.socketio.store.pubsub.PubSubMessage; import com.corundumstudio.socketio.store.pubsub.PubSubStore; +import com.corundumstudio.socketio.store.pubsub.PubSubType; public class MemoryPubSubStore implements PubSubStore { @Override - public void publish(String name, PubSubMessage msg) { + public void publish(PubSubType type, PubSubMessage msg) { } @Override - public void subscribe(String name, PubSubListener listener, Class clazz) { + public void subscribe(PubSubType type, PubSubListener listener, Class clazz) { } @Override - public void unsubscribe(String name) { + public void unsubscribe(PubSubType type) { } @Override diff --git a/src/main/java/com/corundumstudio/socketio/store/MemoryStore.java b/src/main/java/com/corundumstudio/socketio/store/MemoryStore.java index b37651d12..6a76a5388 100644 --- a/src/main/java/com/corundumstudio/socketio/store/MemoryStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/MemoryStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ package com.corundumstudio.socketio.store; +import io.netty.util.internal.PlatformDependent; + import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; public class MemoryStore implements Store { - private final Map store = new ConcurrentHashMap(); + private final Map store = PlatformDependent.newConcurrentHashMap(); @Override public void set(String key, Object value) { diff --git a/src/main/java/com/corundumstudio/socketio/store/MemoryStoreFactory.java b/src/main/java/com/corundumstudio/socketio/store/MemoryStoreFactory.java index a5c4dc657..47eb52b6d 100644 --- a/src/main/java/com/corundumstudio/socketio/store/MemoryStoreFactory.java +++ b/src/main/java/com/corundumstudio/socketio/store/MemoryStoreFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,10 @@ */ package com.corundumstudio.socketio.store; +import io.netty.util.internal.PlatformDependent; + import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import com.corundumstudio.socketio.store.pubsub.BaseStoreFactory; import com.corundumstudio.socketio.store.pubsub.PubSubStore; @@ -47,7 +48,7 @@ public String toString() { @Override public Map createMap(String name) { - return new ConcurrentHashMap(); + return PlatformDependent.newConcurrentHashMap(); } } diff --git a/src/main/java/com/corundumstudio/socketio/store/RedissonPubSubStore.java b/src/main/java/com/corundumstudio/socketio/store/RedissonPubSubStore.java index 7332d1cdb..2e4535994 100644 --- a/src/main/java/com/corundumstudio/socketio/store/RedissonPubSubStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/RedissonPubSubStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,47 +16,49 @@ package com.corundumstudio.socketio.store; import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import org.redisson.Redisson; -import org.redisson.core.MessageListener; -import org.redisson.core.RTopic; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.redisson.api.listener.MessageListener; import com.corundumstudio.socketio.store.pubsub.PubSubListener; import com.corundumstudio.socketio.store.pubsub.PubSubMessage; import com.corundumstudio.socketio.store.pubsub.PubSubStore; +import com.corundumstudio.socketio.store.pubsub.PubSubType; + +import io.netty.util.internal.PlatformDependent; public class RedissonPubSubStore implements PubSubStore { - private final Redisson redissonPub; - private final Redisson redissonSub; + private final RedissonClient redissonPub; + private final RedissonClient redissonSub; private final Long nodeId; - private final ConcurrentMap> map = - new ConcurrentHashMap>(); + private final ConcurrentMap> map = PlatformDependent.newConcurrentHashMap(); - public RedissonPubSubStore(Redisson redissonPub, Redisson redissonSub, Long nodeId) { + public RedissonPubSubStore(RedissonClient redissonPub, RedissonClient redissonSub, Long nodeId) { this.redissonPub = redissonPub; this.redissonSub = redissonSub; this.nodeId = nodeId; } @Override - public void publish(String name, PubSubMessage msg) { + public void publish(PubSubType type, PubSubMessage msg) { msg.setNodeId(nodeId); - redissonPub.getTopic(name).publish(msg); + redissonPub.getTopic(type.toString()).publish(msg); } @Override - public void subscribe(String name, final PubSubListener listener, Class clazz) { - RTopic topic = redissonSub.getTopic(name); - int regId = topic.addListener(new MessageListener() { + public void subscribe(PubSubType type, final PubSubListener listener, Class clazz) { + String name = type.toString(); + RTopic topic = redissonSub.getTopic(name); + int regId = topic.addListener(PubSubMessage.class, new MessageListener() { @Override - public void onMessage(T msg) { + public void onMessage(CharSequence channel, PubSubMessage msg) { if (!nodeId.equals(msg.getNodeId())) { - listener.onMessage(msg); + listener.onMessage((T)msg); } } }); @@ -73,9 +75,10 @@ public void onMessage(T msg) { } @Override - public void unsubscribe(String name) { + public void unsubscribe(PubSubType type) { + String name = type.toString(); Queue regIds = map.remove(name); - RTopic topic = redissonSub.getTopic(name); + RTopic topic = redissonSub.getTopic(name); for (Integer id : regIds) { topic.removeListener(id); } diff --git a/src/main/java/com/corundumstudio/socketio/store/RedissonStore.java b/src/main/java/com/corundumstudio/socketio/store/RedissonStore.java index 5723488f3..f7585a619 100644 --- a/src/main/java/com/corundumstudio/socketio/store/RedissonStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/RedissonStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,13 @@ import java.util.Map; import java.util.UUID; -import org.redisson.Redisson; +import org.redisson.api.RedissonClient; public class RedissonStore implements Store { private final Map map; - public RedissonStore(UUID sessionId, Redisson redisson) { + public RedissonStore(UUID sessionId, RedissonClient redisson) { this.map = redisson.getMap(sessionId.toString()); } diff --git a/src/main/java/com/corundumstudio/socketio/store/RedissonStoreFactory.java b/src/main/java/com/corundumstudio/socketio/store/RedissonStoreFactory.java index ab7f09244..9e2e3127c 100644 --- a/src/main/java/com/corundumstudio/socketio/store/RedissonStoreFactory.java +++ b/src/main/java/com/corundumstudio/socketio/store/RedissonStoreFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +19,16 @@ import java.util.UUID; import org.redisson.Redisson; +import org.redisson.api.RedissonClient; import com.corundumstudio.socketio.store.pubsub.BaseStoreFactory; import com.corundumstudio.socketio.store.pubsub.PubSubStore; public class RedissonStoreFactory extends BaseStoreFactory { - private final Redisson redisClient; - private final Redisson redisPub; - private final Redisson redisSub; + private final RedissonClient redisClient; + private final RedissonClient redisPub; + private final RedissonClient redisSub; private final PubSubStore pubSubStore; @@ -35,7 +36,7 @@ public RedissonStoreFactory() { this(Redisson.create()); } - public RedissonStoreFactory(Redisson redisson) { + public RedissonStoreFactory(RedissonClient redisson) { this.redisClient = redisson; this.redisPub = redisson; this.redisSub = redisson; @@ -56,6 +57,7 @@ public Store createStore(UUID sessionId) { return new RedissonStore(sessionId, redisClient); } + @Override public PubSubStore pubSubStore() { return pubSubStore; } diff --git a/src/main/java/com/corundumstudio/socketio/store/Store.java b/src/main/java/com/corundumstudio/socketio/store/Store.java index 54654c77d..5ac60b86c 100644 --- a/src/main/java/com/corundumstudio/socketio/store/Store.java +++ b/src/main/java/com/corundumstudio/socketio/store/Store.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/StoreFactory.java b/src/main/java/com/corundumstudio/socketio/store/StoreFactory.java index a3f5186cb..c8c212861 100644 --- a/src/main/java/com/corundumstudio/socketio/store/StoreFactory.java +++ b/src/main/java/com/corundumstudio/socketio/store/StoreFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/BaseStoreFactory.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/BaseStoreFactory.java index 8e4a13cb0..6250368bb 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/BaseStoreFactory.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/BaseStoreFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package com.corundumstudio.socketio.store.pubsub; +import java.util.Set; + +import com.corundumstudio.socketio.namespace.Namespace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,51 +37,91 @@ protected Long getNodeId() { return nodeId; } + @Override public void init(final NamespacesHub namespacesHub, final AuthorizeHandler authorizeHandler, JsonSupport jsonSupport) { - pubSubStore().subscribe(PubSubStore.DISCONNECT, new PubSubListener() { + pubSubStore().subscribe(PubSubType.DISCONNECT, new PubSubListener() { @Override public void onMessage(DisconnectMessage msg) { - log.debug("{} sessionId: {}", PubSubStore.DISCONNECT, msg.getSessionId()); + log.debug("{} sessionId: {}", PubSubType.DISCONNECT, msg.getSessionId()); } }, DisconnectMessage.class); - pubSubStore().subscribe(PubSubStore.CONNECT, new PubSubListener() { + pubSubStore().subscribe(PubSubType.CONNECT, new PubSubListener() { @Override public void onMessage(ConnectMessage msg) { authorizeHandler.connect(msg.getSessionId()); - log.debug("{} sessionId: {}", PubSubStore.CONNECT, msg.getSessionId()); + log.debug("{} sessionId: {}", PubSubType.CONNECT, msg.getSessionId()); } }, ConnectMessage.class); - pubSubStore().subscribe(PubSubStore.DISPATCH, new PubSubListener() { + pubSubStore().subscribe(PubSubType.DISPATCH, new PubSubListener() { @Override public void onMessage(DispatchMessage msg) { String name = msg.getRoom(); - namespacesHub.get(msg.getNamespace()).dispatch(name, msg.getPacket()); - log.debug("{} packet: {}", PubSubStore.DISPATCH, msg.getPacket()); + Namespace n = namespacesHub.get(msg.getNamespace()); + if (n != null) { + n.dispatch(name, msg.getPacket()); + } + log.debug("{} packet: {}", PubSubType.DISPATCH, msg.getPacket()); } }, DispatchMessage.class); - pubSubStore().subscribe(PubSubStore.JOIN, new PubSubListener() { + pubSubStore().subscribe(PubSubType.JOIN, new PubSubListener() { @Override public void onMessage(JoinLeaveMessage msg) { String name = msg.getRoom(); - namespacesHub.get(msg.getNamespace()).join(name, msg.getSessionId()); - log.debug("{} sessionId: {}", PubSubStore.JOIN, msg.getSessionId()); + Namespace n = namespacesHub.get(msg.getNamespace()); + if (n != null) { + n.join(name, msg.getSessionId()); + } + log.debug("{} sessionId: {}", PubSubType.JOIN, msg.getSessionId()); } }, JoinLeaveMessage.class); - pubSubStore().subscribe(PubSubStore.LEAVE, new PubSubListener() { + pubSubStore().subscribe(PubSubType.BULK_JOIN, new PubSubListener() { + @Override + public void onMessage(BulkJoinLeaveMessage msg) { + Set rooms = msg.getRooms(); + + for (String room : rooms) { + Namespace n = namespacesHub.get(msg.getNamespace()); + if (n != null) { + n.join(room, msg.getSessionId()); + } + } + log.debug("{} sessionId: {}", PubSubType.BULK_JOIN, msg.getSessionId()); + } + }, BulkJoinLeaveMessage.class); + + pubSubStore().subscribe(PubSubType.LEAVE, new PubSubListener() { @Override public void onMessage(JoinLeaveMessage msg) { String name = msg.getRoom(); - namespacesHub.get(msg.getNamespace()).leave(name, msg.getSessionId()); - log.debug("{} sessionId: {}", PubSubStore.LEAVE, msg.getSessionId()); + Namespace n = namespacesHub.get(msg.getNamespace()); + if (n != null) { + n.leave(name, msg.getSessionId()); + } + log.debug("{} sessionId: {}", PubSubType.LEAVE, msg.getSessionId()); } }, JoinLeaveMessage.class); + + pubSubStore().subscribe(PubSubType.BULK_LEAVE, new PubSubListener() { + @Override + public void onMessage(BulkJoinLeaveMessage msg) { + Set rooms = msg.getRooms(); + + for (String room : rooms) { + Namespace n = namespacesHub.get(msg.getNamespace()); + if (n != null) { + n.leave(room, msg.getSessionId()); + } + } + log.debug("{} sessionId: {}", PubSubType.BULK_LEAVE, msg.getSessionId()); + } + }, BulkJoinLeaveMessage.class); } @Override diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/BulkJoinLeaveMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/BulkJoinLeaveMessage.java new file mode 100644 index 000000000..4a2ce2a91 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/BulkJoinLeaveMessage.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.store.pubsub; + +import java.util.Set; +import java.util.UUID; + +public class BulkJoinLeaveMessage extends PubSubMessage { + + private static final long serialVersionUID = 7506016762607624388L; + + private UUID sessionId; + private String namespace; + private Set rooms; + + public BulkJoinLeaveMessage() { + } + + public BulkJoinLeaveMessage(UUID id, Set rooms, String namespace) { + super(); + this.sessionId = id; + this.rooms = rooms; + this.namespace = namespace; + } + + public String getNamespace() { + return namespace; + } + + public UUID getSessionId() { + return sessionId; + } + + public Set getRooms() { + return rooms; + } + +} diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/ConnectMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/ConnectMessage.java index 04d716b9e..9bdca591c 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/ConnectMessage.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/ConnectMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/DisconnectMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/DisconnectMessage.java index 3067a30c8..0a638f0e1 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/DisconnectMessage.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/DisconnectMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/DispatchMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/DispatchMessage.java index 8b750d11f..723a91cf6 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/DispatchMessage.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/DispatchMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/JoinLeaveMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/JoinLeaveMessage.java index 20ce881fb..1469c1e12 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/JoinLeaveMessage.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/JoinLeaveMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubListener.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubListener.java index 9b4881045..e76efde51 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubListener.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubListener.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubMessage.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubMessage.java index 1ed84edc7..229a740a0 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubMessage.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubMessage.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubStore.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubStore.java index c845db15d..1ffec8253 100644 --- a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubStore.java +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubStore.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,23 +18,11 @@ public interface PubSubStore { - // TODO refactor to enum - String CONNECT = "connect"; + void publish(PubSubType type, PubSubMessage msg); - String DISCONNECT = "disconnect"; + void subscribe(PubSubType type, PubSubListener listener, Class clazz); - String JOIN = "join"; - - String LEAVE = "leave"; - - String DISPATCH = "dispatch"; - - - void publish(String name, PubSubMessage msg); - - void subscribe(String name, PubSubListener listener, Class clazz); - - void unsubscribe(String name); + void unsubscribe(PubSubType type); void shutdown(); diff --git a/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubType.java b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubType.java new file mode 100644 index 000000000..6675d95d6 --- /dev/null +++ b/src/main/java/com/corundumstudio/socketio/store/pubsub/PubSubType.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.store.pubsub; + +public enum PubSubType { + + CONNECT, DISCONNECT, JOIN, BULK_JOIN, LEAVE, BULK_LEAVE, DISPATCH; + + @Override + public String toString() { + return name().toLowerCase(); + } + +} diff --git a/src/main/java/com/corundumstudio/socketio/transport/NamespaceClient.java b/src/main/java/com/corundumstudio/socketio/transport/NamespaceClient.java index f3344e618..012abcdaa 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/NamespaceClient.java +++ b/src/main/java/com/corundumstudio/socketio/transport/NamespaceClient.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; +import com.corundumstudio.socketio.protocol.EngineIOVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,7 +36,7 @@ public class NamespaceClient implements SocketIOClient { - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(NamespaceClient.class); private final AtomicBoolean disconnected = new AtomicBoolean(); private final ClientHead baseClient; @@ -56,6 +57,11 @@ public Transport getTransport() { return baseClient.getCurrentTransport(); } + @Override + public EngineIOVersion getEngineIOVersion() { + return baseClient.getEngineIOVersion(); + } + @Override public boolean isChannelOpen() { return baseClient.isChannelOpen(); @@ -68,7 +74,7 @@ public Namespace getNamespace() { @Override public void sendEvent(String name, Object ... data) { - Packet packet = new Packet(PacketType.MESSAGE); + Packet packet = new Packet(PacketType.MESSAGE, getEngineIOVersion()); packet.setSubType(PacketType.EVENT); packet.setName(name); packet.setData(Arrays.asList(data)); @@ -77,7 +83,7 @@ public void sendEvent(String name, Object ... data) { @Override public void sendEvent(String name, AckCallback ackCallback, Object ... data) { - Packet packet = new Packet(PacketType.MESSAGE); + Packet packet = new Packet(PacketType.MESSAGE, getEngineIOVersion()); packet.setSubType(PacketType.EVENT); packet.setName(name); packet.setData(Arrays.asList(data)); @@ -88,6 +94,11 @@ private boolean isConnected() { return !disconnected.get() && baseClient.isConnected(); } + @Override + public boolean isWritable() { + return isConnected() && this.baseClient.isWritable(); + } + @Override public void send(Packet packet, AckCallback ackCallback) { if (!isConnected()) { @@ -104,8 +115,8 @@ public void send(Packet packet) { if (!isConnected()) { return; } - packet.setNsp(namespace.getName()); - baseClient.send(packet); + + baseClient.send(packet.withNsp(namespace.getName(), baseClient.getEngineIOVersion())); } public void onDisconnect() { @@ -119,7 +130,7 @@ public void onDisconnect() { @Override public void disconnect() { - Packet packet = new Packet(PacketType.MESSAGE); + Packet packet = new Packet(PacketType.MESSAGE, getEngineIOVersion()); packet.setSubType(PacketType.DISCONNECT); send(packet); // onDisconnect(); @@ -172,11 +183,21 @@ public void joinRoom(String room) { namespace.joinRoom(room, getSessionId()); } + @Override + public void joinRooms(Set rooms) { + namespace.joinRooms(rooms, getSessionId()); + } + @Override public void leaveRoom(String room) { namespace.leaveRoom(room, getSessionId()); } + @Override + public void leaveRooms(Set rooms) { + namespace.leaveRooms(rooms, getSessionId()); + } + @Override public void set(String key, Object val) { baseClient.getStore().set(key, val); @@ -202,6 +223,11 @@ public Set getAllRooms() { return namespace.getRooms(this); } + @Override + public int getCurrentRoomSize(String room) { + return namespace.getRoomClientsInCluster(room); + } + @Override public HandshakeData getHandshakeData() { return baseClient.getHandshakeData(); diff --git a/src/main/java/com/corundumstudio/socketio/transport/PollingTransport.java b/src/main/java/com/corundumstudio/socketio/transport/PollingTransport.java index 2c9aec059..9b3b1f19f 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/PollingTransport.java +++ b/src/main/java/com/corundumstudio/socketio/transport/PollingTransport.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,43 +15,37 @@ */ package com.corundumstudio.socketio.transport; -import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import com.corundumstudio.socketio.Transport; +import com.corundumstudio.socketio.handler.AuthorizeHandler; +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.handler.ClientsBox; +import com.corundumstudio.socketio.handler.EncoderHandler; +import com.corundumstudio.socketio.messages.PacketsMessage; +import com.corundumstudio.socketio.messages.XHROptionsMessage; +import com.corundumstudio.socketio.messages.XHRPostMessage; +import com.corundumstudio.socketio.protocol.PacketDecoder; import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.corundumstudio.socketio.Transport; -import com.corundumstudio.socketio.handler.AuthorizeHandler; -import com.corundumstudio.socketio.handler.ClientHead; -import com.corundumstudio.socketio.handler.ClientsBox; -import com.corundumstudio.socketio.handler.EncoderHandler; -import com.corundumstudio.socketio.messages.PacketsMessage; -import com.corundumstudio.socketio.messages.XHROptionsMessage; -import com.corundumstudio.socketio.messages.XHRPostMessage; -import com.corundumstudio.socketio.protocol.PacketDecoder; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @Sharable public class PollingTransport extends ChannelInboundHandlerAdapter { public static final String NAME = "polling"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(PollingTransport.class); private final PacketDecoder decoder; private final ClientsBox clientsBox; @@ -67,7 +61,7 @@ public PollingTransport(PacketDecoder decoder, AuthorizeHandler authorizeHandler public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; - QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); + QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); List transport = queryDecoder.parameters().get("transport"); @@ -76,10 +70,10 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception List j = queryDecoder.parameters().get("j"); List b64 = queryDecoder.parameters().get("b64"); - String origin = req.headers().get(HttpHeaders.Names.ORIGIN); + String origin = req.headers().get(HttpHeaderNames.ORIGIN); ctx.channel().attr(EncoderHandler.ORIGIN).set(origin); - String userAgent = req.headers().get(HttpHeaders.Names.USER_AGENT); + String userAgent = req.headers().get(HttpHeaderNames.USER_AGENT); ctx.channel().attr(EncoderHandler.USER_AGENT).set(userAgent); if (j != null && j.get(0) != null) { @@ -87,7 +81,13 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception ctx.channel().attr(EncoderHandler.JSONP_INDEX).set(index); } if (b64 != null && b64.get(0) != null) { - Integer enable = Integer.valueOf(b64.get(0)); + String flag = b64.get(0); + if ("true".equals(flag)) { + flag = "1"; + } else if ("false".equals(flag)) { + flag = "0"; + } + Integer enable = Integer.valueOf(flag); ctx.channel().attr(EncoderHandler.B64).set(enable == 1); } @@ -98,7 +98,9 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } else { // first connection ClientHead client = ctx.channel().attr(ClientHead.CLIENT).get(); - handleMessage(req, client.getSessionId(), queryDecoder, ctx); + if (client != null) { + handleMessage(req, client.getSessionId(), queryDecoder, ctx); + } } } finally { req.release(); @@ -111,19 +113,19 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception private void handleMessage(FullHttpRequest req, UUID sessionId, QueryStringDecoder queryDecoder, ChannelHandlerContext ctx) throws IOException { - String origin = req.headers().get(HttpHeaders.Names.ORIGIN); + String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (queryDecoder.parameters().containsKey("disconnect")) { ClientHead client = clientsBox.get(sessionId); client.onChannelDisconnect(); ctx.channel().writeAndFlush(new XHRPostMessage(origin, sessionId)); - } else if (HttpMethod.POST.equals(req.getMethod())) { + } else if (HttpMethod.POST.equals(req.method())) { onPost(sessionId, ctx, origin, req.content()); - } else if (HttpMethod.GET.equals(req.getMethod())) { + } else if (HttpMethod.GET.equals(req.method())) { onGet(sessionId, ctx, origin); - } else if (HttpMethod.OPTIONS.equals(req.getMethod())) { + } else if (HttpMethod.OPTIONS.equals(req.method())) { onOptions(sessionId, ctx, origin); } else { - log.error("Wrong {} method invocation for {}", req.getMethod(), sessionId); + log.error("Wrong {} method invocation for {}", req.method(), sessionId); sendError(ctx); } } @@ -179,4 +181,15 @@ private void sendError(ChannelHandlerContext ctx) { ctx.channel().writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); } + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + final Channel channel = ctx.channel(); + ClientHead client = clientsBox.get(channel); + if (client != null && client.isTransportChannel(ctx.channel(), Transport.POLLING)) { + log.debug("channel inactive {}", client.getSessionId()); + client.releasePollingChannel(channel); + } + super.channelInactive(ctx); + } + } diff --git a/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java b/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java index 438706278..677050373 100644 --- a/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java +++ b/src/main/java/com/corundumstudio/socketio/transport/WebSocketTransport.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,46 +15,39 @@ */ package com.corundumstudio.socketio.transport; +import com.corundumstudio.socketio.Configuration; +import com.corundumstudio.socketio.SocketIOChannelInitializer; +import com.corundumstudio.socketio.Transport; +import com.corundumstudio.socketio.handler.AuthorizeHandler; +import com.corundumstudio.socketio.handler.ClientHead; +import com.corundumstudio.socketio.handler.ClientsBox; +import com.corundumstudio.socketio.messages.PacketsMessage; +import com.corundumstudio.socketio.protocol.EngineIOVersion; +import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.protocol.PacketType; +import com.corundumstudio.socketio.scheduler.CancelableScheduler; +import com.corundumstudio.socketio.scheduler.SchedulerKey; import io.netty.buffer.ByteBufHolder; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; +import io.netty.channel.*; import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; -import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; -import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; -import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; -import io.netty.util.ReferenceCountUtil; +import io.netty.handler.codec.http.websocketx.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.corundumstudio.socketio.Configuration; -import com.corundumstudio.socketio.Transport; -import com.corundumstudio.socketio.handler.AuthorizeHandler; -import com.corundumstudio.socketio.handler.ClientHead; -import com.corundumstudio.socketio.handler.ClientsBox; -import com.corundumstudio.socketio.messages.PacketsMessage; -import com.corundumstudio.socketio.scheduler.CancelableScheduler; -import com.corundumstudio.socketio.scheduler.SchedulerKey; - @Sharable public class WebSocketTransport extends ChannelInboundHandlerAdapter { public static final String NAME = "websocket"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger log = LoggerFactory.getLogger(WebSocketTransport.class); private final AuthorizeHandler authorizeHandler; private final CancelableScheduler scheduler; @@ -76,8 +69,7 @@ public WebSocketTransport(boolean isSsl, @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof CloseWebSocketFrame) { - ctx.channel().close(); - ReferenceCountUtil.release(msg); + ctx.channel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE); } else if (msg instanceof BinaryWebSocketFrame || msg instanceof TextWebSocketFrame) { ByteBufHolder frame = (ByteBufHolder) msg; @@ -93,7 +85,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception frame.release(); } else if (msg instanceof FullHttpRequest) { FullHttpRequest req = (FullHttpRequest) msg; - QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); + QueryStringDecoder queryDecoder = new QueryStringDecoder(req.uri()); String path = queryDecoder.path(); List transport = queryDecoder.parameters().get("transport"); List sid = queryDecoder.parameters().get("sid"); @@ -111,7 +103,9 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } else { ClientHead client = ctx.channel().attr(ClientHead.CLIENT).get(); // first connection - handshake(ctx, client.getSessionId(), path, req); + if (client != null) { + handshake(ctx, client.getSessionId(), path, req); + } } } finally { req.release(); @@ -136,43 +130,73 @@ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - ClientHead client = clientsBox.get(ctx.channel()); + final Channel channel = ctx.channel(); + ClientHead client = clientsBox.get(channel); + Packet packet = new Packet(PacketType.MESSAGE, client != null ? client.getEngineIOVersion() : EngineIOVersion.UNKNOWN); + packet.setSubType(PacketType.DISCONNECT); if (client != null && client.isTransportChannel(ctx.channel(), Transport.WEBSOCKET)) { log.debug("channel inactive {}", client.getSessionId()); client.onChannelDisconnect(); } super.channelInactive(ctx); + if (client != null) { + client.send(packet); + } + channel.close(); + ctx.close(); } private void handshake(ChannelHandlerContext ctx, final UUID sessionId, String path, FullHttpRequest req) { final Channel channel = ctx.channel(); WebSocketServerHandshakerFactory factory = - new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false, configuration.getMaxFramePayloadLength()); + new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, true, configuration.getMaxFramePayloadLength()); WebSocketServerHandshaker handshaker = factory.newHandshaker(req); if (handshaker != null) { - ChannelFuture f = handshaker.handshake(channel, req); - f.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - log.error("Can't handshake " + sessionId, future.cause()); - return; + try { + ChannelFuture f = handshaker.handshake(channel, req); + f.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + log.error("Can't handshake {}", sessionId, future.cause()); + closeClient(sessionId, channel); + return; + } + channel.pipeline().addBefore(SocketIOChannelInitializer.WEB_SOCKET_TRANSPORT, SocketIOChannelInitializer.WEB_SOCKET_AGGREGATOR, + new WebSocketFrameAggregator(configuration.getMaxFramePayloadLength())); + connectClient(channel, sessionId); } - connectClient(channel, sessionId); - } - }); + }); + } catch (Throwable e) { + log.warn("Can't handshake {}, {}", sessionId, e.getMessage(), e); + closeClient(sessionId, channel); + } } else { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } } + private void closeClient(UUID sessionId, Channel channel) { + try { + channel.close(); + } catch (Throwable t) { + log.warn("Can't close channel for sessionId: {}", sessionId, t); + } + ClientHead clientHead = clientsBox.get(sessionId); + if (clientHead != null && clientHead.getNamespaces().isEmpty()) { + clientsBox.removeClient(sessionId); + clientHead.disconnect(); + } + log.info("Client with sessionId: {} was disconnected", sessionId); + } + private void connectClient(final Channel channel, final UUID sessionId) { ClientHead client = clientsBox.get(sessionId); if (client == null) { log.warn("Unauthorized client with sessionId: {} with ip: {}. Channel closed!", sessionId, channel.remoteAddress()); - channel.close(); + closeClient(sessionId, channel); return; } @@ -204,7 +228,7 @@ private String getWebSocketLocation(HttpRequest req) { if (isSsl) { protocol = "wss://"; } - return protocol + req.headers().get(HttpHeaders.Names.HOST) + req.getUri(); + return protocol + req.headers().get(HttpHeaderNames.HOST) + req.uri(); } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 000000000..eceaa0e5b --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,32 @@ +module netty.socketio { + exports com.corundumstudio.socketio; + exports com.corundumstudio.socketio.ack; + exports com.corundumstudio.socketio.annotation; + exports com.corundumstudio.socketio.handler; + exports com.corundumstudio.socketio.listener; + exports com.corundumstudio.socketio.namespace; + exports com.corundumstudio.socketio.misc; + exports com.corundumstudio.socketio.messages; + exports com.corundumstudio.socketio.protocol; + + requires static spring.beans; + requires static spring.core; + + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; + + requires static com.hazelcast.core; + requires static com.hazelcast.client; + + requires static redisson; + + requires static io.netty.transport.classes.epoll; + requires io.netty.codec; + requires io.netty.transport; + requires io.netty.buffer; + requires io.netty.common; + requires io.netty.handler; + requires io.netty.codec.http; + requires org.slf4j; +} diff --git a/src/test/java/com/corundumstudio/socketio/JoinIteratorsTest.java b/src/test/java/com/corundumstudio/socketio/JoinIteratorsTest.java index 18bc3cedb..8b869fb3b 100644 --- a/src/test/java/com/corundumstudio/socketio/JoinIteratorsTest.java +++ b/src/test/java/com/corundumstudio/socketio/JoinIteratorsTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import org.junit.Assert; diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderAckPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderAckPacketTest.java index e7c11a598..bf20f8ecb 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderAckPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderAckPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,28 @@ package com.corundumstudio.socketio.parser; import java.io.IOException; -import java.util.Arrays; +import java.nio.charset.Charset; import java.util.UUID; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import mockit.Expectations; -import com.fasterxml.jackson.core.JsonParseException; - import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import com.corundumstudio.socketio.AckCallback; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; +import com.fasterxml.jackson.core.JsonParseException; +@Ignore public class DecoderAckPacketTest extends DecoderBaseTest { @Test public void testDecode() throws IOException { - Packet packet = decoder.decodePacket("6:::140", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("6:::140", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.ACK, packet.getType()); Assert.assertEquals(140, (long)packet.getAckId()); // Assert.assertTrue(packet.getArgs().isEmpty()); @@ -44,7 +47,7 @@ public void testDecode() throws IOException { public void testDecodeWithArgs() throws IOException { initExpectations(); - Packet packet = decoder.decodePacket("6:::12+[\"woot\",\"wa\"]", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("6:::12+[\"woot\",\"wa\"]", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.ACK, packet.getType()); Assert.assertEquals(12, (long)packet.getAckId()); // Assert.assertEquals(Arrays.asList("woot", "wa"), packet.getArgs()); @@ -64,7 +67,7 @@ public void onSuccess(String result) { @Test(expected = JsonParseException.class) public void testDecodeWithBadJson() throws IOException { initExpectations(); - decoder.decodePacket("6:::1+{\"++]", null); + decoder.decodePackets(Unpooled.copiedBuffer("6:::1+{\"++]", CharsetUtil.UTF_8), null); } } diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderBaseTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderBaseTest.java index 96d90bf97..d31a3051f 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderBaseTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderBaseTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,13 @@ */ package com.corundumstudio.socketio.parser; -import com.corundumstudio.socketio.namespace.NamespacesHub; -import org.junit.Before; - import mockit.Mocked; -import com.corundumstudio.socketio.Configuration; +import org.junit.Before; + import com.corundumstudio.socketio.ack.AckManager; -import com.corundumstudio.socketio.protocol.PacketDecoder; import com.corundumstudio.socketio.protocol.JacksonJsonSupport; +import com.corundumstudio.socketio.protocol.PacketDecoder; public class DecoderBaseTest { @@ -35,7 +33,7 @@ public class DecoderBaseTest { @Before public void before() { - decoder = new PacketDecoder(new JacksonJsonSupport(), new NamespacesHub(new Configuration()), ackManager); + decoder = new PacketDecoder(new JacksonJsonSupport(), ackManager); } } diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderConnectionPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderConnectionPacketTest.java index c2cdbf2e3..f820f7643 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderConnectionPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderConnectionPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,30 +17,34 @@ import java.io.IOException; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; +@Ignore public class DecoderConnectionPacketTest extends DecoderBaseTest { @Test public void testDecodeHeartbeat() throws IOException { - Packet packet = decoder.decodePacket("2:::", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("2:::", CharsetUtil.UTF_8), null); // Assert.assertEquals(PacketType.HEARTBEAT, packet.getType()); } @Test public void testDecode() throws IOException { - Packet packet = decoder.decodePacket("1::/tobi", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("1::/tobi", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.CONNECT, packet.getType()); Assert.assertEquals("/tobi", packet.getNsp()); } @Test public void testDecodeWithQueryString() throws IOException { - Packet packet = decoder.decodePacket("1::/test:?test=1", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("1::/test:?test=1", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.CONNECT, packet.getType()); Assert.assertEquals("/test", packet.getNsp()); // Assert.assertEquals("?test=1", packet.getQs()); @@ -48,7 +52,7 @@ public void testDecodeWithQueryString() throws IOException { @Test public void testDecodeDisconnection() throws IOException { - Packet packet = decoder.decodePacket("0::/woot", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("0::/woot", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.DISCONNECT, packet.getType()); Assert.assertEquals("/woot", packet.getNsp()); } diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderEventPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderEventPacketTest.java index ad8990fff..79cbdece9 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderEventPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderEventPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,30 +17,31 @@ import java.io.IOException; import java.util.HashMap; -import java.util.Map; -import com.corundumstudio.socketio.namespace.NamespacesHub; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; -import com.corundumstudio.socketio.Configuration; -import com.corundumstudio.socketio.protocol.PacketDecoder; import com.corundumstudio.socketio.protocol.JacksonJsonSupport; import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.protocol.PacketDecoder; import com.corundumstudio.socketio.protocol.PacketType; +@Ignore public class DecoderEventPacketTest extends DecoderBaseTest { @Test public void testDecode() throws IOException { - Packet packet = decoder.decodePacket("5:::{\"name\":\"woot\"}", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("5:::{\"name\":\"woot\"}", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.EVENT, packet.getType()); Assert.assertEquals("woot", packet.getName()); } @Test public void testDecodeWithMessageIdAndAck() throws IOException { - Packet packet = decoder.decodePacket("5:1+::{\"name\":\"tobi\"}", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("5:1+::{\"name\":\"tobi\"}", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.EVENT, packet.getType()); // Assert.assertEquals(1, (long)packet.getId()); // Assert.assertEquals(Packet.ACK_DATA, packet.getAck()); @@ -51,9 +52,9 @@ public void testDecodeWithMessageIdAndAck() throws IOException { public void testDecodeWithData() throws IOException { JacksonJsonSupport jsonSupport = new JacksonJsonSupport(); jsonSupport.addEventMapping("", "edwald", HashMap.class, Integer.class, String.class); - PacketDecoder decoder = new PacketDecoder(jsonSupport, new NamespacesHub(new Configuration()), ackManager); + PacketDecoder decoder = new PacketDecoder(jsonSupport, ackManager); - Packet packet = decoder.decodePacket("5:::{\"name\":\"edwald\",\"args\":[{\"a\": \"b\"},2,\"3\"]}", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("5:::{\"name\":\"edwald\",\"args\":[{\"a\": \"b\"},2,\"3\"]}", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.EVENT, packet.getType()); Assert.assertEquals("edwald", packet.getName()); // Assert.assertEquals(3, packet.getArgs().size()); diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderJsonPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderJsonPacketTest.java index dcf8475ff..cb3bc5e25 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderJsonPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderJsonPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,30 +18,34 @@ import java.io.IOException; import java.util.Map; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import com.corundumstudio.socketio.protocol.Packet; +@Ignore public class DecoderJsonPacketTest extends DecoderBaseTest { @Test public void testUTF8Decode() throws IOException { - Packet packet = decoder.decodePacket("4:::\"Привет\"", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("4:::\"Привет\"", CharsetUtil.UTF_8), null); // Assert.assertEquals(PacketType.JSON, packet.getType()); Assert.assertEquals("Привет", packet.getData()); } @Test public void testDecode() throws IOException { - Packet packet = decoder.decodePacket("4:::\"2\"", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("4:::\"2\"", CharsetUtil.UTF_8), null); // Assert.assertEquals(PacketType.JSON, packet.getType()); Assert.assertEquals("2", packet.getData()); } @Test public void testDecodeWithMessageIdAndAckData() throws IOException { - Packet packet = decoder.decodePacket("4:1+::{\"a\":\"b\"}", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("4:1+::{\"a\":\"b\"}", CharsetUtil.UTF_8), null); // Assert.assertEquals(PacketType.JSON, packet.getType()); // Assert.assertEquals(1, (long)packet.getId()); // Assert.assertEquals(Packet.ACK_DATA, packet.getAck()); diff --git a/src/test/java/com/corundumstudio/socketio/parser/DecoderMessagePacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/DecoderMessagePacketTest.java index 6de51f7f8..d52427357 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/DecoderMessagePacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/DecoderMessagePacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,21 @@ import java.io.IOException; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; +@Ignore public class DecoderMessagePacketTest extends DecoderBaseTest { @Test public void testDecodeId() throws IOException { - Packet packet = decoder.decodePacket("3:1::asdfasdf", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("3:1::asdfasdf", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.MESSAGE, packet.getType()); // Assert.assertEquals(1, (long)packet.getId()); // Assert.assertTrue(packet.getArgs().isEmpty()); @@ -36,14 +40,14 @@ public void testDecodeId() throws IOException { @Test public void testDecode() throws IOException { - Packet packet = decoder.decodePacket("3:::woot", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("3:::woot", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.MESSAGE, packet.getType()); Assert.assertEquals("woot", packet.getData()); } @Test public void testDecodeWithIdAndEndpoint() throws IOException { - Packet packet = decoder.decodePacket("3:5:/tobi", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("3:5:/tobi", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.MESSAGE, packet.getType()); // Assert.assertEquals(5, (long)packet.getId()); // Assert.assertEquals(true, packet.getAck()); diff --git a/src/test/java/com/corundumstudio/socketio/parser/EncoderAckPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/EncoderAckPacketTest.java index f867ea384..b888d4447 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/EncoderAckPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/EncoderAckPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import io.netty.util.CharsetUtil; import java.io.IOException; -import java.util.Arrays; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/com/corundumstudio/socketio/parser/EncoderBaseTest.java b/src/test/java/com/corundumstudio/socketio/parser/EncoderBaseTest.java index 03297a46e..dbba57458 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/EncoderBaseTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/EncoderBaseTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/corundumstudio/socketio/parser/EncoderConnectionPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/EncoderConnectionPacketTest.java index f4461daaf..7c626dd42 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/EncoderConnectionPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/EncoderConnectionPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/corundumstudio/socketio/parser/EncoderEventPacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/EncoderEventPacketTest.java index 70054baa4..0675841fe 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/EncoderEventPacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/EncoderEventPacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import io.netty.util.CharsetUtil; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/com/corundumstudio/socketio/parser/EncoderMessagePacketTest.java b/src/test/java/com/corundumstudio/socketio/parser/EncoderMessagePacketTest.java index f8c322514..9782c5a61 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/EncoderMessagePacketTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/EncoderMessagePacketTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/corundumstudio/socketio/parser/PayloadTest.java b/src/test/java/com/corundumstudio/socketio/parser/PayloadTest.java index 763eb83f0..9feb61e1c 100644 --- a/src/test/java/com/corundumstudio/socketio/parser/PayloadTest.java +++ b/src/test/java/com/corundumstudio/socketio/parser/PayloadTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2012 Nikita Koksharov + * Copyright (c) 2012-2023 Nikita Koksharov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,8 @@ */ package com.corundumstudio.socketio.parser; -import com.corundumstudio.socketio.namespace.NamespacesHub; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.util.CharsetUtil; import java.io.IOException; @@ -28,19 +26,21 @@ import java.util.concurrent.ConcurrentLinkedQueue; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import com.corundumstudio.socketio.Configuration; -import com.corundumstudio.socketio.protocol.PacketDecoder; -import com.corundumstudio.socketio.protocol.PacketEncoder; import com.corundumstudio.socketio.protocol.JacksonJsonSupport; import com.corundumstudio.socketio.protocol.Packet; +import com.corundumstudio.socketio.protocol.PacketDecoder; +import com.corundumstudio.socketio.protocol.PacketEncoder; import com.corundumstudio.socketio.protocol.PacketType; +@Ignore public class PayloadTest { private final JacksonJsonSupport support = new JacksonJsonSupport(); - private final PacketDecoder decoder = new PacketDecoder(support, new NamespacesHub(new Configuration()), null); + private final PacketDecoder decoder = new PacketDecoder(support, null); private final PacketEncoder encoder = new PacketEncoder(new Configuration(), support); @Test @@ -81,7 +81,7 @@ public void testPayloadEncode() throws IOException { @Test public void testDecodingNewline() throws IOException { - Packet packet = decoder.decodePacket("3:::\n", null); + Packet packet = decoder.decodePackets(Unpooled.copiedBuffer("3:::\n", CharsetUtil.UTF_8), null); Assert.assertEquals(PacketType.MESSAGE, packet.getType()); Assert.assertEquals("\n", packet.getData()); } diff --git a/src/test/java/com/corundumstudio/socketio/protocol/PacketTest.java b/src/test/java/com/corundumstudio/socketio/protocol/PacketTest.java new file mode 100644 index 000000000..38d1cf909 --- /dev/null +++ b/src/test/java/com/corundumstudio/socketio/protocol/PacketTest.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.protocol; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import io.netty.buffer.Unpooled; +import org.junit.Test; + +public class PacketTest { + + @Test + public void packetCopyIsCreatedWhenNamespaceDiffers() { + Packet oldPacket = createPacket(); + + String newNs = "new"; + Packet newPacket = oldPacket.withNsp(newNs, EngineIOVersion.UNKNOWN); + assertEquals(newNs, newPacket.getNsp()); + assertPacketCopied(oldPacket, newPacket); + } + + @Test + public void packetCopyIsCreatedWhenNewNamespaceDiffersAndIsNull() { + Packet packet = createPacket(); + Packet newPacket = packet.withNsp(null, EngineIOVersion.UNKNOWN); + assertNull(newPacket.getNsp()); + assertPacketCopied(packet, newPacket); + } + + @Test + public void originalPacketReturnedIfNamespaceIsTheSame() { + Packet packet = new Packet(PacketType.MESSAGE); + assertSame(packet, packet.withNsp("", EngineIOVersion.UNKNOWN)); + } + + private void assertPacketCopied(Packet oldPacket, Packet newPacket) { + assertNotSame(newPacket, oldPacket); + assertEquals(oldPacket.getName(), newPacket.getName()); + assertEquals(oldPacket.getType(), newPacket.getType()); + assertEquals(oldPacket.getSubType(), newPacket.getSubType()); + assertEquals(oldPacket.getAckId(), newPacket.getAckId()); + assertEquals(oldPacket.getAttachments().size(), newPacket.getAttachments().size()); + assertSame(oldPacket.getAttachments(), newPacket.getAttachments()); + assertEquals(oldPacket.getData(), newPacket.getData()); + assertSame(oldPacket.getDataSource(), newPacket.getDataSource()); + } + + private Packet createPacket() { + Packet packet = new Packet(PacketType.MESSAGE); + packet.setSubType(PacketType.EVENT); + packet.setName("packetName"); + packet.setData("data"); + packet.setAckId(1L); + packet.setNsp("old"); + packet.setDataSource(Unpooled.wrappedBuffer(new byte[]{10})); + packet.initAttachments(1); + packet.addAttachment(Unpooled.wrappedBuffer(new byte[]{20})); + return packet; + } +} \ No newline at end of file diff --git a/src/test/java/com/corundumstudio/socketio/transport/HttpTransportTest.java b/src/test/java/com/corundumstudio/socketio/transport/HttpTransportTest.java new file mode 100644 index 000000000..0e1f60b6a --- /dev/null +++ b/src/test/java/com/corundumstudio/socketio/transport/HttpTransportTest.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.corundumstudio.socketio.transport; + +import com.corundumstudio.socketio.Configuration; +import com.corundumstudio.socketio.SocketConfig; +import com.corundumstudio.socketio.SocketIOClient; +import com.corundumstudio.socketio.SocketIOServer; +import com.corundumstudio.socketio.Transport; +import com.corundumstudio.socketio.listener.ExceptionListener; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandlerContext; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpTransportTest { + + private SocketIOServer server; + + private ObjectMapper mapper = new ObjectMapper(); + + private Pattern responseJsonMatcher = Pattern.compile("([0-9]+)(\\{.*\\})?"); + + private Pattern multiResponsePattern = Pattern.compile("((?[0-9])(?[0-9]*)(?.+)\\x{1E})*(?[0-9])(?[0-9]*)(?.+)"); + + private final String packetSeparator = new String(new byte[] { 0x1e }); + + private Logger logger = LoggerFactory.getLogger(HttpTransportTest.class); + + @Before + public void createTestServer() { + final int port = findFreePort(); + final Configuration config = new Configuration(); + config.setRandomSession(true); + config.setTransports(Transport.POLLING); + config.setPort(port); + config.setExceptionListener(new ExceptionListener() { + @Override + public void onEventException(Exception e, List args, SocketIOClient client) { + logger.error("eventException", e); + } + + @Override + public void onDisconnectException(Exception e, SocketIOClient client) { + logger.error("disconnectException", e); + } + + @Override + public void onConnectException(Exception e, SocketIOClient client) { + logger.error("connectException", e); + } + + @Override + public void onPingException(Exception e, SocketIOClient client) { + logger.error("pingException", e); + } + + @Override + public void onPongException(Exception e, SocketIOClient client) { + logger.error("pongException", e); + } + + @Override + public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception { + return false; + } + + @Override + public void onAuthException(Throwable e, SocketIOClient client) { + logger.error("authException", e); + } + }); + + final SocketConfig socketConfig = new SocketConfig(); + socketConfig.setReuseAddress(true); + config.setSocketConfig(socketConfig); + + this.server = new SocketIOServer(config); + this.server.start(); + } + + @After + public void cleanupTestServer() { + this.server.stop(); + } + + private URI createTestServerUri(final String query) throws URISyntaxException { + return new URI("http", null , "localhost", server.getConfiguration().getPort(), server.getConfiguration().getContext() + "/", + query, null); + } + + private String makeSocketIoRequest(final String sessionId, final String bodyForPost) + throws URISyntaxException, IOException, InterruptedException { + final URI uri = createTestServerUri("EIO=4&transport=polling&t=Oqd9eWh" + (sessionId == null ? "" : "&sid=" + sessionId)); + + URLConnection con = uri.toURL().openConnection(); + HttpURLConnection http = (HttpURLConnection)con; + if (bodyForPost != null) { + http.setRequestMethod("POST"); // PUT is another valid option + http.setDoOutput(true); + } + + if (bodyForPost != null) { + byte[] out = bodyForPost.toString().getBytes(StandardCharsets.UTF_8); + http.setFixedLengthStreamingMode(out.length); + http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + http.connect(); + try (OutputStream os = http.getOutputStream()) { + os.write(out); + } + } else { + http.connect(); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(http.getInputStream(), StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } + + private void postMessage(final String sessionId, final String body) + throws URISyntaxException, IOException, InterruptedException { + final String responseStr = makeSocketIoRequest(sessionId, body); + Assert.assertEquals(responseStr, "ok"); + } + + private String[] pollForListOfResponses(final String sessionId) + throws URISyntaxException, IOException, InterruptedException { + final String responseStr = makeSocketIoRequest(sessionId, null); + return responseStr.split(packetSeparator); + } + + private String connectForSessionId(final String sessionId) + throws URISyntaxException, IOException, InterruptedException { + final String firstMessage = pollForListOfResponses(sessionId)[0]; + final Matcher jsonMatcher = responseJsonMatcher.matcher(firstMessage); + Assert.assertTrue(jsonMatcher.find()); + Assert.assertEquals(jsonMatcher.group(1), "0"); + final JsonNode node = mapper.readTree(jsonMatcher.group(2)); + return node.get("sid").asText(); + } + + @Test + public void testConnect() throws URISyntaxException, IOException, InterruptedException { + final String sessionId = connectForSessionId(null); + Assert.assertNotNull(sessionId); + } + + @Test + public void testMultipleMessages() throws URISyntaxException, IOException, InterruptedException { + server.addEventListener("hello", String.class, (client, data, ackSender) -> + ackSender.sendAckData(data)); + final String sessionId = connectForSessionId(null); + final ArrayList events = new ArrayList<>(); + events.add("420[\"hello\", \"world\"]"); + events.add("421[\"hello\", \"socketio\"]"); + events.add("422[\"hello\", \"socketio\"]"); + postMessage(sessionId, events.stream().collect(Collectors.joining(packetSeparator))); + final String[] responses = pollForListOfResponses(sessionId); + Assert.assertEquals(responses.length, 3); + } + + /** + * Returns a free port number on localhost. + * + * Heavily inspired from org.eclipse.jdt.launching.SocketUtil (to avoid a dependency to JDT just because of this). + * Slightly improved with close() missing in JDT. And throws exception instead of returning -1. + * + * @return a free port number on localhost + * @throws IllegalStateException if unable to find a free port + */ + private static int findFreePort() { + ServerSocket socket = null; + try { + socket = new ServerSocket(0); + socket.setReuseAddress(true); + return socket.getLocalPort(); + } catch (IOException ignored) { + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException ignored) { + } + } + } + throw new IllegalStateException("Could not find a free TCP/IP port to start embedded SocketIO Server on"); + } + +} diff --git a/src/test/java/com/corundumstudio/socketio/transport/WebSocketTransportTest.java b/src/test/java/com/corundumstudio/socketio/transport/WebSocketTransportTest.java new file mode 100644 index 000000000..de7581c1a --- /dev/null +++ b/src/test/java/com/corundumstudio/socketio/transport/WebSocketTransportTest.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2012-2023 Nikita Koksharov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * @(#)WebSocketTransportTest.java 2018. 5. 23. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.corundumstudio.socketio.transport; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; + +/** + * @author hangsu.cho@navercorp.com + * + */ +public class WebSocketTransportTest { + + /** + * Test method for {@link com.corundumstudio.socketio.transport.WebSocketTransport#channelRead()}. + */ + @Test + public void testCloseFrame() { + EmbeddedChannel channel = createChannel(); + + channel.writeInbound(new CloseWebSocketFrame()); + Object msg = channel.readOutbound(); + + // https://tools.ietf.org/html/rfc6455#section-5.5.1 + // If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint + // MUST send a Close frame in response. + assertTrue(msg instanceof CloseWebSocketFrame); + } + + private EmbeddedChannel createChannel() { + return new EmbeddedChannel(new WebSocketTransport(false, null, null, null, null) { + /* + * (non-Javadoc) + * + * @see + * com.corundumstudio.socketio.transport.WebSocketTransport#channelInactive(io.netty.channel. + * ChannelHandlerContext) + */ + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception {} + }); + } + +}