├── .gitignore ├── README.md ├── library ├── pom.xml ├── protobuf │ ├── Makefile │ └── SubProtocol.proto └── src │ ├── main │ └── java │ │ └── org │ │ └── whispersystems │ │ └── websocket │ │ ├── WebSocketClient.java │ │ ├── WebSocketResourceProvider.java │ │ ├── WebSocketResourceProviderFactory.java │ │ ├── auth │ │ ├── AuthenticationException.java │ │ ├── WebSocketAuthenticator.java │ │ └── internal │ │ │ └── WebSocketAuthValueFactoryProvider.java │ │ ├── configuration │ │ └── WebSocketConfiguration.java │ │ ├── messages │ │ ├── InvalidMessageException.java │ │ ├── WebSocketMessage.java │ │ ├── WebSocketMessageFactory.java │ │ ├── WebSocketRequestMessage.java │ │ ├── WebSocketResponseMessage.java │ │ └── protobuf │ │ │ ├── ProtobufWebSocketMessage.java │ │ │ ├── ProtobufWebSocketMessageFactory.java │ │ │ ├── ProtobufWebSocketRequestMessage.java │ │ │ ├── ProtobufWebSocketResponseMessage.java │ │ │ └── SubProtocol.java │ │ ├── servlet │ │ ├── BufferingServletInputStream.java │ │ ├── BufferingServletOutputStream.java │ │ ├── LoggableRequest.java │ │ ├── LoggableResponse.java │ │ ├── NullServletOutputStream.java │ │ ├── NullServletResponse.java │ │ ├── WebSocketServletRequest.java │ │ └── WebSocketServletResponse.java │ │ ├── session │ │ ├── WebSocketSession.java │ │ ├── WebSocketSessionContext.java │ │ └── WebSocketSessionContextValueFactoryProvider.java │ │ ├── setup │ │ ├── WebSocketConnectListener.java │ │ └── WebSocketEnvironment.java │ │ └── util │ │ └── Base64.java │ └── test │ └── java │ └── org │ └── whispersystems │ └── websocket │ ├── LoggableRequestResponseTest.java │ ├── WebSocketResourceProviderFactoryTest.java │ └── WebSocketResourceProviderTest.java ├── pom.xml ├── sample-client ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── whispersystems │ └── websocket │ └── client │ ├── Client.java │ └── WebSocketInterface.java └── sample-server ├── config └── config.yml ├── pom.xml └── src ├── main └── java │ └── org │ └── whispersystems │ └── websocket │ └── sample │ ├── Server.java │ ├── ServerConfiguration.java │ ├── auth │ ├── HelloAccount.java │ ├── HelloAccountBasicAuthenticator.java │ └── HelloAccountWebSocketAuthenticator.java │ └── resources │ └── HelloResource.java └── test └── java └── org └── whispersystems └── websocket ├── HelloServerTest.java └── SynchronousClient.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | .idea 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket-Resources 2 | 3 | A Dropwizard library that lets you use Jersey-style Resources over WebSockets. 4 | 5 | Install from maven central: 6 | 7 | ``` 8 | 9 | org.whispersystems 10 | websocket-resources 11 | ${latest_version} 12 | 13 | ``` 14 | 15 | ## The problem 16 | 17 | In the standard HTTP world, we might use Jersey to define a set of REST APIs: 18 | 19 | ``` 20 | @Path("/api/v1/mail") 21 | public class MailResource { 22 | 23 | @Timed 24 | @POST 25 | @Path("/{destination}/") 26 | @Consumes(MediaType.APPLICATION_JSON_TYPE) 27 | public void sendMessage(@Auth Account sender, 28 | @PathParam("destination") String destination, 29 | @Valid Message message) 30 | { 31 | ... 32 | } 33 | } 34 | ``` 35 | 36 | Using JAX-RS annotations and some Dropwizard glue, we can easily define a set of resource methods 37 | that allow an authenticated sender to POST a JSON Message object. All of the routing, parsing, 38 | validation, and authentication are taken care of, and the resource method can focus on the business 39 | logic. 40 | 41 | What if we want to expose a similar API over a WebSocket? It's not pretty. We have to define our 42 | own sub-protocol, do all of the parsing and validation ourselves, keep track of the connection state, 43 | and do our own routing. It's basically the equivalent of writing a raw servlet, but worse. 44 | 45 | ## The WebSocket-Resources model 46 | 47 | WebSocket-Resources is designed to make exposing an API over a WebSocket as simple as writing a 48 | Jersey resource. The library is based on the premise that the WebSocket client and the 49 | WebSocket server should each be modeled as both a HTTP client and server simultaneously. 50 | 51 | That is, the WebSocket server receives HTTP-style requests and issues HTTP-style responses, but it 52 | can also issue HTTP-style requests to the client, and expects HTTP-style responses from the client. 53 | This allows us to write Jersey-style resources, while also initiating bi-directional communication 54 | from the server. 55 | 56 | What if we wanted to make the exact same resource above available over a WebSocket using 57 | WebSocket-Resources? In your standard Dropwizard service run method, just initialize 58 | WebSocket-Resources and register a standard Jersey resource: 59 | 60 | ``` 61 | @Override 62 | public void run(WhisperServerConfiguration config, Environment environment) 63 | throws Exception 64 | { 65 | WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config); 66 | webSocketEnvironment.jersey().register(new MailResource()); 67 | webSocketEnvironment.setAuthenticator(new MyWebSocketAuthenticator()); 68 | 69 | WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment); 70 | ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet); 71 | 72 | websocket.addMapping("/api/v1/websocket/*"); 73 | websocket.setAsyncSupported(true); 74 | servlet.start(); 75 | 76 | ... 77 | } 78 | ``` 79 | 80 | It's as simple as creating a `WebSocketEnvironment` from the Dropwizard `Environment` and registering 81 | Jersey resources. 82 | 83 | ## Making requests 84 | 85 | In order to call the Jersey resource we just registered from a client, we need to know how to format 86 | client requests. It's possible to either define our own subprotocol, or to use the default subprotocol 87 | packaged with WebSocket-Resources, which is based in protobuf. 88 | 89 | A subprotocol is composed of `Request`s and `Response`s. A `Request` has four parts: 90 | 91 | 1. An `id`. 92 | 1. A `method`. 93 | 1. A `path`. 94 | 1. An optional `body`. 95 | 96 | A `Response` has four parts: 97 | 98 | 1. The request `id` it is in response to. 99 | 1. A `status code`. 100 | 1. A `status message`. 101 | 1. An optional `body`. 102 | 103 | This should seem strongly reminiscent of HTTP. By default, WebSocket-Resources will use a protobuf 104 | formatted subprotocol: 105 | 106 | ``` 107 | message WebSocketRequestMessage { 108 | optional string verb = 1; 109 | optional string path = 2; 110 | optional bytes body = 3; 111 | optional uint64 id = 4; 112 | } 113 | 114 | message WebSocketResponseMessage { 115 | optional uint64 id = 1; 116 | optional uint32 status = 2; 117 | optional string message = 3; 118 | optional bytes body = 4; 119 | } 120 | 121 | message WebSocketMessage { 122 | enum Type { 123 | UNKNOWN = 0; 124 | REQUEST = 1; 125 | RESPONSE = 2; 126 | } 127 | 128 | optional Type type = 1; 129 | optional WebSocketRequestMessage request = 2; 130 | optional WebSocketResponseMessage response = 3; 131 | } 132 | ``` 133 | 134 | To use a custom wire format, it's as simple as implementing a custom `WebSocketMessageFactory` and 135 | registering it at initialization time: 136 | 137 | ``` 138 | @Override 139 | public void run(WhisperServerConfiguration config, Environment environment) 140 | throws Exception 141 | { 142 | WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment); 143 | webSocketEnvironment.setMessageFactory(MyMessageFactory()); 144 | ... 145 | } 146 | ``` 147 | 148 | ## Making requests from the server 149 | 150 | To issue requests from the server, use `WebSocketClient`. There are two ways to get a `WebSocketClient` 151 | instance: a resource annotation or a connection listener. 152 | 153 | Resource annotation: 154 | 155 | ``` 156 | @Path("/api/v1/mail") 157 | public class MailResource { 158 | 159 | @Timed 160 | @POST 161 | @Path("/{destination}/") 162 | @Consumes(MediaType.APPLICATION_JSON_TYPE) 163 | public void sendMessage(@Auth Account sender, 164 | @WebSocketSession WebSocketSessionContext context, 165 | @PathParam("destination") String destination, 166 | @Valid Message message) 167 | { 168 | WebSocketClient client = context.getClient(); 169 | ... 170 | } 171 | } 172 | 173 | ``` 174 | 175 | Or a connect listener: 176 | 177 | ``` 178 | @Override 179 | public void run(WhisperServerConfiguration config, Environment environment) 180 | throws Exception 181 | { 182 | WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment); 183 | webSocketEnvironment.setConnectListener(new WebSocketConnectListener() { 184 | @Override 185 | public void onConnect(WebSocketSessionContext context) { 186 | WebSocketClient client = context.getClient(); 187 | ... 188 | } 189 | }); 190 | ... 191 | } 192 | ``` 193 | 194 | A WebSocketClient can then be issued to transmit requests: 195 | 196 | ``` 197 | WebSocketClient client = context.getClient(); 198 | 199 | ListenableFuture response = client.sendRequest("PUT", "/api/v1/message", body); 200 | 201 | Futures.addCallback(response, new FutureCallback() { 202 | @Override 203 | public void onSuccess(@Nullable WebSocketResponseMessage response) { 204 | ... 205 | } 206 | 207 | @Override 208 | public void onFailure(@Nonnull Throwable throwable) { 209 | ... 210 | } 211 | }); 212 | ``` 213 | 214 | License 215 | --------------------- 216 | 217 | Copyright 2014 Open Whisper Systems 218 | 219 | Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html 220 | -------------------------------------------------------------------------------- /library/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 3.0.0 9 | 10 | 11 | org.whispersystems 12 | websocket-resources 13 | 0.5.10 14 | 15 | WebSocket-Resources 16 | A Dropwizard library that lets you use Jersey-style Resources over WebSockets 17 | https://github.com/WhisperSystems/WebSocket-Resources 18 | 19 | 20 | 21 | AGPLv3 22 | https://www.gnu.org/licenses/agpl-3.0.html 23 | repo 24 | 25 | 26 | 27 | 28 | 29 | Moxie Marlinspike 30 | 31 | 32 | 33 | 34 | https://github.com/WhisperSystems/WebSocket-Resources 35 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 36 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 37 | 38 | 39 | 40 | 1.3.9 41 | 42 | 43 | 44 | 45 | io.dropwizard 46 | dropwizard-core 47 | ${dropwizard.version} 48 | 49 | 50 | io.dropwizard 51 | dropwizard-auth 52 | ${dropwizard.version} 53 | 54 | 55 | io.dropwizard 56 | dropwizard-client 57 | ${dropwizard.version} 58 | 59 | 60 | io.dropwizard 61 | dropwizard-servlets 62 | ${dropwizard.version} 63 | 64 | 65 | 66 | org.eclipse.jetty.websocket 67 | websocket-server 68 | 9.4.14.v20181114 69 | 70 | 71 | 72 | com.google.protobuf 73 | protobuf-java 74 | 2.6.1 75 | 76 | 77 | 78 | io.dropwizard 79 | dropwizard-testing 80 | ${dropwizard.version} 81 | 82 | 83 | org.mockito 84 | mockito-core 85 | 2.7.22 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-compiler-plugin 95 | 96 | 1.8 97 | 1.8 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-source-plugin 103 | 2.2.1 104 | 105 | 106 | attach-sources 107 | 108 | jar 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-jar-plugin 116 | 2.4 117 | 118 | 119 | 120 | true 121 | 122 | 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-gpg-plugin 128 | 129 | 130 | sign-artifacts 131 | verify 132 | 133 | sign 134 | 135 | 136 | E5BA37AD 137 | 138 | 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-javadoc-plugin 144 | 2.8.1 145 | 146 | -Xdoclint:none 147 | 148 | 149 | 150 | attach-javadocs 151 | 152 | jar 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-deploy-plugin 160 | 2.4 161 | 162 | false 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | ossrh 171 | https://oss.sonatype.org/content/repositories/snapshots 172 | 173 | 174 | ossrh 175 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /library/protobuf/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | protoc --java_out=../src/main/java/ SubProtocol.proto 4 | -------------------------------------------------------------------------------- /library/protobuf/SubProtocol.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package textsecure; 18 | 19 | option java_package = "org.whispersystems.websocket.messages.protobuf"; 20 | 21 | message WebSocketRequestMessage { 22 | optional string verb = 1; 23 | optional string path = 2; 24 | repeated string headers = 5; 25 | optional bytes body = 3; 26 | optional uint64 id = 4; 27 | } 28 | 29 | message WebSocketResponseMessage { 30 | optional uint64 id = 1; 31 | optional uint32 status = 2; 32 | optional string message = 3; 33 | repeated string headers = 5; 34 | optional bytes body = 4; 35 | } 36 | 37 | message WebSocketMessage { 38 | enum Type { 39 | UNKNOWN = 0; 40 | REQUEST = 1; 41 | RESPONSE = 2; 42 | } 43 | 44 | optional Type type = 1; 45 | optional WebSocketRequestMessage request = 2; 46 | optional WebSocketResponseMessage response = 3; 47 | } -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/WebSocketClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket; 18 | 19 | import com.google.common.util.concurrent.ListenableFuture; 20 | import com.google.common.util.concurrent.SettableFuture; 21 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 22 | import org.eclipse.jetty.websocket.api.Session; 23 | import org.eclipse.jetty.websocket.api.WebSocketException; 24 | import org.eclipse.jetty.websocket.api.WriteCallback; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.whispersystems.websocket.messages.WebSocketMessage; 28 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 29 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 30 | 31 | import java.io.IOException; 32 | import java.nio.ByteBuffer; 33 | import java.security.SecureRandom; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.Optional; 37 | 38 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 39 | public class WebSocketClient { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(WebSocketClient.class); 42 | 43 | private final Session session; 44 | private final RemoteEndpoint remoteEndpoint; 45 | private final WebSocketMessageFactory messageFactory; 46 | private final Map> pendingRequestMapper; 47 | 48 | public WebSocketClient(Session session, RemoteEndpoint remoteEndpoint, 49 | WebSocketMessageFactory messageFactory, 50 | Map> pendingRequestMapper) 51 | { 52 | this.session = session; 53 | this.remoteEndpoint = remoteEndpoint; 54 | this.messageFactory = messageFactory; 55 | this.pendingRequestMapper = pendingRequestMapper; 56 | } 57 | 58 | public ListenableFuture sendRequest(String verb, String path, 59 | List headers, 60 | Optional body) 61 | { 62 | final long requestId = generateRequestId(); 63 | final SettableFuture future = SettableFuture.create(); 64 | 65 | pendingRequestMapper.put(requestId, future); 66 | 67 | WebSocketMessage requestMessage = messageFactory.createRequest(Optional.of(requestId), verb, path, headers, body); 68 | 69 | try { 70 | remoteEndpoint.sendBytes(ByteBuffer.wrap(requestMessage.toByteArray()), new WriteCallback() { 71 | @Override 72 | public void writeFailed(Throwable x) { 73 | logger.debug("Write failed", x); 74 | pendingRequestMapper.remove(requestId); 75 | future.setException(x); 76 | } 77 | 78 | @Override 79 | public void writeSuccess() {} 80 | }); 81 | } catch (WebSocketException e) { 82 | logger.debug("Write", e); 83 | pendingRequestMapper.remove(requestId); 84 | future.setException(e); 85 | } 86 | 87 | return future; 88 | } 89 | 90 | public void close(int code, String message) { 91 | session.close(code, message); 92 | } 93 | 94 | public void hardDisconnectQuietly() { 95 | try { 96 | session.disconnect(); 97 | } catch (IOException e) { 98 | // quietly we said 99 | } 100 | } 101 | 102 | private long generateRequestId() { 103 | return Math.abs(new SecureRandom().nextLong()); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/WebSocketResourceProvider.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket; 18 | 19 | import com.google.common.annotations.VisibleForTesting; 20 | import com.google.common.util.concurrent.SettableFuture; 21 | import org.eclipse.jetty.server.RequestLog; 22 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 23 | import org.eclipse.jetty.websocket.api.Session; 24 | import org.eclipse.jetty.websocket.api.WebSocketListener; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.whispersystems.websocket.messages.InvalidMessageException; 28 | import org.whispersystems.websocket.messages.WebSocketMessage; 29 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 30 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 31 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 32 | import org.whispersystems.websocket.servlet.LoggableRequest; 33 | import org.whispersystems.websocket.servlet.LoggableResponse; 34 | import org.whispersystems.websocket.servlet.NullServletResponse; 35 | import org.whispersystems.websocket.servlet.WebSocketServletRequest; 36 | import org.whispersystems.websocket.servlet.WebSocketServletResponse; 37 | import org.whispersystems.websocket.session.WebSocketSessionContext; 38 | import org.whispersystems.websocket.setup.WebSocketConnectListener; 39 | 40 | import javax.servlet.ServletException; 41 | import javax.servlet.http.HttpServlet; 42 | import javax.servlet.http.HttpServletRequest; 43 | import javax.servlet.http.HttpServletResponse; 44 | import javax.ws.rs.core.Response; 45 | import java.io.IOException; 46 | import java.nio.ByteBuffer; 47 | import java.util.LinkedList; 48 | import java.util.List; 49 | import java.util.Map; 50 | import java.util.Optional; 51 | import java.util.concurrent.ConcurrentHashMap; 52 | 53 | 54 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 55 | public class WebSocketResourceProvider implements WebSocketListener { 56 | 57 | private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProvider.class); 58 | 59 | private final Map> requestMap = new ConcurrentHashMap<>(); 60 | 61 | private final Object authenticated; 62 | private final WebSocketMessageFactory messageFactory; 63 | private final Optional connectListener; 64 | private final HttpServlet servlet; 65 | private final RequestLog requestLog; 66 | private final long idleTimeoutMillis; 67 | 68 | private Session session; 69 | private RemoteEndpoint remoteEndpoint; 70 | private WebSocketSessionContext context; 71 | 72 | public WebSocketResourceProvider(HttpServlet servlet, 73 | RequestLog requestLog, 74 | Object authenticated, 75 | WebSocketMessageFactory messageFactory, 76 | Optional connectListener, 77 | long idleTimeoutMillis) 78 | { 79 | this.servlet = servlet; 80 | this.requestLog = requestLog; 81 | this.authenticated = authenticated; 82 | this.messageFactory = messageFactory; 83 | this.connectListener = connectListener; 84 | this.idleTimeoutMillis = idleTimeoutMillis; 85 | } 86 | 87 | @Override 88 | public void onWebSocketConnect(Session session) { 89 | this.session = session; 90 | this.remoteEndpoint = session.getRemote(); 91 | this.context = new WebSocketSessionContext(new WebSocketClient(session, remoteEndpoint, messageFactory, requestMap)); 92 | this.context.setAuthenticated(authenticated); 93 | this.session.setIdleTimeout(idleTimeoutMillis); 94 | 95 | if (connectListener.isPresent()) { 96 | connectListener.get().onWebSocketConnect(this.context); 97 | } 98 | } 99 | 100 | @Override 101 | public void onWebSocketError(Throwable cause) { 102 | logger.debug("onWebSocketError", cause); 103 | close(session, 1011, "Server error"); 104 | } 105 | 106 | @Override 107 | public void onWebSocketBinary(byte[] payload, int offset, int length) { 108 | try { 109 | WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length); 110 | 111 | switch (webSocketMessage.getType()) { 112 | case REQUEST_MESSAGE: 113 | handleRequest(webSocketMessage.getRequestMessage()); 114 | break; 115 | case RESPONSE_MESSAGE: 116 | handleResponse(webSocketMessage.getResponseMessage()); 117 | break; 118 | default: 119 | close(session, 1018, "Badly formatted"); 120 | break; 121 | } 122 | } catch (InvalidMessageException e) { 123 | logger.debug("Parsing", e); 124 | close(session, 1018, "Badly formatted"); 125 | } 126 | } 127 | 128 | @Override 129 | public void onWebSocketClose(int statusCode, String reason) { 130 | if (context != null) { 131 | context.notifyClosed(statusCode, reason); 132 | 133 | for (long requestId : requestMap.keySet()) { 134 | SettableFuture outstandingRequest = requestMap.remove(requestId); 135 | 136 | if (outstandingRequest != null) { 137 | outstandingRequest.setException(new IOException("Connection closed!")); 138 | } 139 | } 140 | } 141 | } 142 | 143 | @Override 144 | public void onWebSocketText(String message) { 145 | logger.debug("onWebSocketText!"); 146 | } 147 | 148 | private void handleRequest(WebSocketRequestMessage requestMessage) { 149 | try { 150 | HttpServletRequest servletRequest = createRequest(requestMessage, context); 151 | HttpServletResponse servletResponse = createResponse(requestMessage); 152 | 153 | servlet.service(servletRequest, servletResponse); 154 | servletResponse.flushBuffer(); 155 | requestLog.log(new LoggableRequest(servletRequest), new LoggableResponse(servletResponse)); 156 | } catch (IOException | ServletException e) { 157 | logger.warn("Servlet Error: " + requestMessage.getVerb() + " " + requestMessage.getPath() + "\n" + requestMessage.getBody(), e); 158 | sendErrorResponse(requestMessage, Response.status(500).build()); 159 | } 160 | } 161 | 162 | private void handleResponse(WebSocketResponseMessage responseMessage) { 163 | SettableFuture future = requestMap.remove(responseMessage.getRequestId()); 164 | 165 | if (future != null) { 166 | future.set(responseMessage); 167 | } 168 | } 169 | 170 | private void close(Session session, int status, String message) { 171 | session.close(status, message); 172 | } 173 | 174 | private HttpServletRequest createRequest(WebSocketRequestMessage message, 175 | WebSocketSessionContext context) 176 | { 177 | return new WebSocketServletRequest(context, message, servlet.getServletContext()); 178 | } 179 | 180 | private HttpServletResponse createResponse(WebSocketRequestMessage message) { 181 | if (message.hasRequestId()) { 182 | return new WebSocketServletResponse(remoteEndpoint, message.getRequestId(), messageFactory); 183 | } else { 184 | return new NullServletResponse(); 185 | } 186 | } 187 | 188 | private void sendErrorResponse(WebSocketRequestMessage requestMessage, Response error) { 189 | if (requestMessage.hasRequestId()) { 190 | List headers = new LinkedList<>(); 191 | 192 | for (String key : error.getStringHeaders().keySet()) { 193 | headers.add(key + ":" + error.getStringHeaders().getFirst(key)); 194 | } 195 | 196 | WebSocketMessage response = messageFactory.createResponse(requestMessage.getRequestId(), 197 | error.getStatus(), 198 | "Error response", 199 | headers, 200 | Optional.empty()); 201 | 202 | remoteEndpoint.sendBytesByFuture(ByteBuffer.wrap(response.toByteArray())); 203 | } 204 | } 205 | 206 | @VisibleForTesting 207 | WebSocketSessionContext getContext() { 208 | return context; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/WebSocketResourceProviderFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket; 18 | 19 | import org.eclipse.jetty.server.Server; 20 | import org.eclipse.jetty.util.AttributesMap; 21 | import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; 22 | import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; 23 | import org.eclipse.jetty.websocket.servlet.WebSocketCreator; 24 | import org.eclipse.jetty.websocket.servlet.WebSocketServlet; 25 | import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | import org.whispersystems.websocket.auth.AuthenticationException; 29 | import org.whispersystems.websocket.auth.WebSocketAuthenticator; 30 | import org.whispersystems.websocket.auth.WebSocketAuthenticator.AuthenticationResult; 31 | import org.whispersystems.websocket.auth.internal.WebSocketAuthValueFactoryProvider; 32 | import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; 33 | import org.whispersystems.websocket.setup.WebSocketEnvironment; 34 | 35 | import javax.servlet.Filter; 36 | import javax.servlet.FilterRegistration; 37 | import javax.servlet.RequestDispatcher; 38 | import javax.servlet.Servlet; 39 | import javax.servlet.ServletConfig; 40 | import javax.servlet.ServletContext; 41 | import javax.servlet.ServletException; 42 | import javax.servlet.ServletRegistration; 43 | import javax.servlet.SessionCookieConfig; 44 | import javax.servlet.SessionTrackingMode; 45 | import javax.servlet.descriptor.JspConfigDescriptor; 46 | import java.io.IOException; 47 | import java.io.InputStream; 48 | import java.net.MalformedURLException; 49 | import java.net.URL; 50 | import java.security.AccessController; 51 | import java.util.Collections; 52 | import java.util.Enumeration; 53 | import java.util.EventListener; 54 | import java.util.Map; 55 | import java.util.Optional; 56 | import java.util.Set; 57 | 58 | import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; 59 | 60 | public class WebSocketResourceProviderFactory extends WebSocketServlet implements WebSocketCreator { 61 | 62 | private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProviderFactory.class); 63 | 64 | private final WebSocketEnvironment environment; 65 | 66 | public WebSocketResourceProviderFactory(WebSocketEnvironment environment) 67 | throws ServletException 68 | { 69 | this.environment = environment; 70 | 71 | environment.jersey().register(new WebSocketSessionContextValueFactoryProvider.Binder()); 72 | environment.jersey().register(new WebSocketAuthValueFactoryProvider.Binder()); 73 | environment.jersey().register(new JacksonMessageBodyProvider(environment.getObjectMapper())); 74 | } 75 | 76 | public void start() throws ServletException { 77 | this.environment.getJerseyServletContainer().init(new WServletConfig()); 78 | } 79 | 80 | @Override 81 | public Object createWebSocket(ServletUpgradeRequest request, ServletUpgradeResponse response) { 82 | try { 83 | Optional authenticator = Optional.ofNullable(environment.getAuthenticator()); 84 | Object authenticated = null; 85 | 86 | if (authenticator.isPresent()) { 87 | AuthenticationResult authenticationResult = authenticator.get().authenticate(request); 88 | 89 | if (!authenticationResult.getUser().isPresent() && authenticationResult.isRequired()) { 90 | response.sendForbidden("Unauthorized"); 91 | return null; 92 | } else { 93 | authenticated = authenticationResult.getUser().orElse(null); 94 | } 95 | } 96 | 97 | return new WebSocketResourceProvider(this.environment.getJerseyServletContainer(), 98 | this.environment.getRequestLog(), 99 | authenticated, 100 | this.environment.getMessageFactory(), 101 | Optional.ofNullable(this.environment.getConnectListener()), 102 | this.environment.getIdleTimeoutMillis()); 103 | } catch (AuthenticationException | IOException e) { 104 | logger.warn("Authentication failure", e); 105 | return null; 106 | } 107 | } 108 | 109 | @Override 110 | public void configure(WebSocketServletFactory factory) { 111 | factory.setCreator(this); 112 | } 113 | 114 | private static class WServletConfig implements ServletConfig { 115 | 116 | private final ServletContext context = new NoContext(); 117 | 118 | @Override 119 | public String getServletName() { 120 | return "WebSocketResourceServlet"; 121 | } 122 | 123 | @Override 124 | public ServletContext getServletContext() { 125 | return context; 126 | } 127 | 128 | @Override 129 | public String getInitParameter(String name) { 130 | return null; 131 | } 132 | 133 | @Override 134 | public Enumeration getInitParameterNames() { 135 | return new Enumeration() { 136 | @Override 137 | public boolean hasMoreElements() { 138 | return false; 139 | } 140 | 141 | @Override 142 | public String nextElement() { 143 | return null; 144 | } 145 | }; 146 | } 147 | } 148 | 149 | public static class NoContext extends AttributesMap implements ServletContext 150 | { 151 | 152 | private int effectiveMajorVersion = 3; 153 | private int effectiveMinorVersion = 0; 154 | 155 | @Override 156 | public ServletContext getContext(String uripath) 157 | { 158 | return null; 159 | } 160 | 161 | @Override 162 | public int getMajorVersion() 163 | { 164 | return 3; 165 | } 166 | 167 | @Override 168 | public String getMimeType(String file) 169 | { 170 | return null; 171 | } 172 | 173 | @Override 174 | public int getMinorVersion() 175 | { 176 | return 0; 177 | } 178 | 179 | @Override 180 | public RequestDispatcher getNamedDispatcher(String name) 181 | { 182 | return null; 183 | } 184 | 185 | @Override 186 | public RequestDispatcher getRequestDispatcher(String uriInContext) 187 | { 188 | return null; 189 | } 190 | 191 | @Override 192 | public String getRealPath(String path) 193 | { 194 | return null; 195 | } 196 | 197 | @Override 198 | public URL getResource(String path) throws MalformedURLException 199 | { 200 | return null; 201 | } 202 | 203 | @Override 204 | public InputStream getResourceAsStream(String path) 205 | { 206 | return null; 207 | } 208 | 209 | @Override 210 | public Set getResourcePaths(String path) 211 | { 212 | return null; 213 | } 214 | 215 | @Override 216 | public String getServerInfo() 217 | { 218 | return "websocketresources/" + Server.getVersion(); 219 | } 220 | 221 | @Override 222 | @Deprecated 223 | public Servlet getServlet(String name) throws ServletException 224 | { 225 | return null; 226 | } 227 | 228 | @SuppressWarnings("unchecked") 229 | @Override 230 | @Deprecated 231 | public Enumeration getServletNames() 232 | { 233 | return Collections.enumeration(Collections.EMPTY_LIST); 234 | } 235 | 236 | @SuppressWarnings("unchecked") 237 | @Override 238 | @Deprecated 239 | public Enumeration getServlets() 240 | { 241 | return Collections.enumeration(Collections.EMPTY_LIST); 242 | } 243 | 244 | @Override 245 | public void log(Exception exception, String msg) 246 | { 247 | logger.warn(msg,exception); 248 | } 249 | 250 | @Override 251 | public void log(String msg) 252 | { 253 | logger.info(msg); 254 | } 255 | 256 | @Override 257 | public void log(String message, Throwable throwable) 258 | { 259 | logger.warn(message,throwable); 260 | } 261 | 262 | @Override 263 | public String getInitParameter(String name) 264 | { 265 | return null; 266 | } 267 | 268 | @SuppressWarnings("unchecked") 269 | @Override 270 | public Enumeration getInitParameterNames() 271 | { 272 | return Collections.enumeration(Collections.EMPTY_LIST); 273 | } 274 | 275 | 276 | @Override 277 | public String getServletContextName() 278 | { 279 | return "No Context"; 280 | } 281 | 282 | @Override 283 | public String getContextPath() 284 | { 285 | return null; 286 | } 287 | 288 | 289 | @Override 290 | public boolean setInitParameter(String name, String value) 291 | { 292 | return false; 293 | } 294 | 295 | @Override 296 | public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) 297 | { 298 | return null; 299 | } 300 | 301 | @Override 302 | public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) 303 | { 304 | return null; 305 | } 306 | 307 | @Override 308 | public FilterRegistration.Dynamic addFilter(String filterName, String className) 309 | { 310 | return null; 311 | } 312 | 313 | @Override 314 | public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) 315 | { 316 | return null; 317 | } 318 | 319 | @Override 320 | public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) 321 | { 322 | return null; 323 | } 324 | 325 | @Override 326 | public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String className) 327 | { 328 | return null; 329 | } 330 | 331 | @Override 332 | public T createFilter(Class c) throws ServletException 333 | { 334 | return null; 335 | } 336 | 337 | @Override 338 | public T createServlet(Class c) throws ServletException 339 | { 340 | return null; 341 | } 342 | 343 | @Override 344 | public Set getDefaultSessionTrackingModes() 345 | { 346 | return null; 347 | } 348 | 349 | @Override 350 | public Set getEffectiveSessionTrackingModes() 351 | { 352 | return null; 353 | } 354 | 355 | @Override 356 | public FilterRegistration getFilterRegistration(String filterName) 357 | { 358 | return null; 359 | } 360 | 361 | @Override 362 | public Map getFilterRegistrations() 363 | { 364 | return null; 365 | } 366 | 367 | @Override 368 | public ServletRegistration getServletRegistration(String servletName) 369 | { 370 | return null; 371 | } 372 | 373 | @Override 374 | public Map getServletRegistrations() 375 | { 376 | return null; 377 | } 378 | 379 | @Override 380 | public SessionCookieConfig getSessionCookieConfig() 381 | { 382 | return null; 383 | } 384 | 385 | @Override 386 | public void setSessionTrackingModes(Set sessionTrackingModes) 387 | { 388 | } 389 | 390 | @Override 391 | public void addListener(String className) 392 | { 393 | } 394 | 395 | @Override 396 | public void addListener(T t) 397 | { 398 | } 399 | 400 | @Override 401 | public void addListener(Class listenerClass) 402 | { 403 | } 404 | 405 | @Override 406 | public T createListener(Class clazz) throws ServletException 407 | { 408 | try 409 | { 410 | return clazz.newInstance(); 411 | } 412 | catch (InstantiationException e) 413 | { 414 | throw new ServletException(e); 415 | } 416 | catch (IllegalAccessException e) 417 | { 418 | throw new ServletException(e); 419 | } 420 | } 421 | 422 | @Override 423 | public ClassLoader getClassLoader() 424 | { 425 | AccessController.checkPermission(new RuntimePermission("getClassLoader")); 426 | return WebSocketResourceProviderFactory.class.getClassLoader(); 427 | } 428 | 429 | @Override 430 | public int getEffectiveMajorVersion() 431 | { 432 | return effectiveMajorVersion; 433 | } 434 | 435 | @Override 436 | public int getEffectiveMinorVersion() 437 | { 438 | return effectiveMinorVersion; 439 | } 440 | 441 | public void setEffectiveMajorVersion (int v) 442 | { 443 | this.effectiveMajorVersion = v; 444 | } 445 | 446 | public void setEffectiveMinorVersion (int v) 447 | { 448 | this.effectiveMinorVersion = v; 449 | } 450 | 451 | @Override 452 | public JspConfigDescriptor getJspConfigDescriptor() 453 | { 454 | return null; 455 | } 456 | 457 | @Override 458 | public void declareRoles(String... roleNames) 459 | { 460 | } 461 | 462 | @Override 463 | public String getVirtualServerName() { 464 | return null; 465 | } 466 | } 467 | 468 | } 469 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/auth/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.auth; 18 | 19 | public class AuthenticationException extends Exception { 20 | 21 | public AuthenticationException(String s) { 22 | super(s); 23 | } 24 | 25 | public AuthenticationException(Exception e) { 26 | super(e); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/auth/WebSocketAuthenticator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.auth; 18 | 19 | import org.eclipse.jetty.server.Authentication; 20 | import org.eclipse.jetty.websocket.api.UpgradeRequest; 21 | 22 | import java.util.Optional; 23 | 24 | public interface WebSocketAuthenticator { 25 | AuthenticationResult authenticate(UpgradeRequest request) throws AuthenticationException; 26 | 27 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 28 | public class AuthenticationResult { 29 | private final Optional user; 30 | private final boolean required; 31 | 32 | public AuthenticationResult(Optional user, boolean required) { 33 | this.user = user; 34 | this.required = required; 35 | } 36 | 37 | public Optional getUser() { 38 | return user; 39 | } 40 | 41 | public boolean isRequired() { 42 | return required; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/auth/internal/WebSocketAuthValueFactoryProvider.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.auth.internal; 2 | 3 | import org.glassfish.hk2.api.InjectionResolver; 4 | import org.glassfish.hk2.api.ServiceLocator; 5 | import org.glassfish.hk2.api.TypeLiteral; 6 | import org.glassfish.hk2.utilities.binding.AbstractBinder; 7 | import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; 8 | import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; 9 | import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; 10 | import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; 11 | import org.glassfish.jersey.server.model.Parameter; 12 | import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; 13 | import org.whispersystems.websocket.servlet.WebSocketServletRequest; 14 | 15 | import javax.inject.Inject; 16 | import javax.inject.Singleton; 17 | import javax.ws.rs.WebApplicationException; 18 | import java.security.Principal; 19 | import java.util.Optional; 20 | 21 | import io.dropwizard.auth.Auth; 22 | 23 | @Singleton 24 | public class WebSocketAuthValueFactoryProvider extends AbstractValueFactoryProvider { 25 | 26 | @Inject 27 | public WebSocketAuthValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, 28 | ServiceLocator injector) 29 | { 30 | super(mpep, injector, Parameter.Source.UNKNOWN); 31 | } 32 | 33 | @Override 34 | public AbstractContainerRequestValueFactory createValueFactory(final Parameter parameter) { 35 | if (parameter.getAnnotation(Auth.class) == null) { 36 | return null; 37 | } 38 | 39 | if (parameter.getRawType() == Optional.class) { 40 | return new OptionalContainerRequestValueFactory(parameter); 41 | } else { 42 | return new StandardContainerRequestValueFactory(parameter); 43 | } 44 | } 45 | 46 | private static class OptionalContainerRequestValueFactory extends AbstractContainerRequestValueFactory { 47 | private final Parameter parameter; 48 | 49 | private OptionalContainerRequestValueFactory(Parameter parameter) { 50 | this.parameter = parameter; 51 | } 52 | 53 | @Override 54 | public Object provide() { 55 | Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal(); 56 | 57 | if (principal != null && !(principal instanceof WebSocketServletRequest.ContextPrincipal)) { 58 | throw new IllegalArgumentException("Can't inject non-ContextPrincipal into request"); 59 | } 60 | 61 | if (principal == null) return Optional.empty(); 62 | else return Optional.ofNullable(((WebSocketServletRequest.ContextPrincipal)principal).getContext().getAuthenticated()); 63 | 64 | } 65 | } 66 | 67 | private static class StandardContainerRequestValueFactory extends AbstractContainerRequestValueFactory { 68 | private final Parameter parameter; 69 | 70 | private StandardContainerRequestValueFactory(Parameter parameter) { 71 | this.parameter = parameter; 72 | } 73 | 74 | @Override 75 | public Object provide() { 76 | Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal(); 77 | 78 | if (principal == null) { 79 | throw new IllegalStateException("Cannot inject a custom principal into unauthenticated request"); 80 | } 81 | 82 | if (!(principal instanceof WebSocketServletRequest.ContextPrincipal)) { 83 | throw new IllegalArgumentException("Cannot inject a non-WebSocket AuthPrincipal into request"); 84 | } 85 | 86 | Object authenticated = ((WebSocketServletRequest.ContextPrincipal)principal).getContext().getAuthenticated(); 87 | 88 | if (authenticated == null) { 89 | throw new WebApplicationException("Authenticated resource", 401); 90 | } 91 | 92 | if (!parameter.getRawType().isAssignableFrom(authenticated.getClass())) { 93 | throw new IllegalArgumentException("Authenticated principal is of the wrong type: " + authenticated.getClass() + " looking for: " + parameter.getRawType()); 94 | } 95 | 96 | return parameter.getRawType().cast(authenticated); 97 | } 98 | } 99 | 100 | @Singleton 101 | private static class AuthInjectionResolver extends ParamInjectionResolver { 102 | public AuthInjectionResolver() { 103 | super(WebSocketAuthValueFactoryProvider.class); 104 | } 105 | } 106 | 107 | public static class Binder extends AbstractBinder { 108 | 109 | 110 | public Binder() { 111 | } 112 | 113 | @Override 114 | protected void configure() { 115 | bind(WebSocketAuthValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); 116 | bind(AuthInjectionResolver.class).to(new TypeLiteral>() { 117 | }).in(Singleton.class); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/configuration/WebSocketConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.configuration; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import javax.validation.Valid; 6 | import javax.validation.constraints.NotNull; 7 | 8 | import io.dropwizard.request.logging.LogbackAccessRequestLogFactory; 9 | import io.dropwizard.request.logging.RequestLogFactory; 10 | 11 | public class WebSocketConfiguration { 12 | 13 | @Valid 14 | @NotNull 15 | @JsonProperty 16 | private RequestLogFactory requestLog = new LogbackAccessRequestLogFactory(); 17 | 18 | public RequestLogFactory getRequestLog() { 19 | return requestLog; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/InvalidMessageException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages; 18 | 19 | public class InvalidMessageException extends Exception { 20 | public InvalidMessageException(String s) { 21 | super(s); 22 | } 23 | 24 | public InvalidMessageException(Exception e) { 25 | super(e); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/WebSocketMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages; 18 | 19 | public interface WebSocketMessage { 20 | 21 | public enum Type { 22 | UNKNOWN_MESSAGE, 23 | REQUEST_MESSAGE, 24 | RESPONSE_MESSAGE 25 | } 26 | 27 | public Type getType(); 28 | public WebSocketRequestMessage getRequestMessage(); 29 | public WebSocketResponseMessage getResponseMessage(); 30 | public byte[] toByteArray(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/WebSocketMessageFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages; 18 | 19 | 20 | import java.util.List; 21 | import java.util.Optional; 22 | 23 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 24 | public interface WebSocketMessageFactory { 25 | 26 | public WebSocketMessage parseMessage(byte[] serialized, int offset, int len) 27 | throws InvalidMessageException; 28 | 29 | public WebSocketMessage createRequest(Optional requestId, 30 | String verb, String path, 31 | List headers, 32 | Optional body); 33 | 34 | public WebSocketMessage createResponse(long requestId, int status, String message, 35 | List headers, 36 | Optional body); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/WebSocketRequestMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages; 18 | 19 | 20 | 21 | import java.util.Map; 22 | import java.util.Optional; 23 | 24 | public interface WebSocketRequestMessage { 25 | 26 | public String getVerb(); 27 | public String getPath(); 28 | public Map getHeaders(); 29 | public Optional getBody(); 30 | public long getRequestId(); 31 | public boolean hasRequestId(); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/WebSocketResponseMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages; 18 | 19 | 20 | import java.util.Map; 21 | import java.util.Optional; 22 | 23 | public interface WebSocketResponseMessage { 24 | public long getRequestId(); 25 | public int getStatus(); 26 | public String getMessage(); 27 | public Map getHeaders(); 28 | public Optional getBody(); 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages.protobuf; 18 | 19 | import com.google.protobuf.ByteString; 20 | import com.google.protobuf.InvalidProtocolBufferException; 21 | import org.whispersystems.websocket.messages.InvalidMessageException; 22 | import org.whispersystems.websocket.messages.WebSocketMessage; 23 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 24 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 25 | 26 | public class ProtobufWebSocketMessage implements WebSocketMessage { 27 | 28 | private final SubProtocol.WebSocketMessage message; 29 | 30 | ProtobufWebSocketMessage(byte[] buffer, int offset, int length) throws InvalidMessageException { 31 | try { 32 | this.message = SubProtocol.WebSocketMessage.parseFrom(ByteString.copyFrom(buffer, offset, length)); 33 | 34 | if (getType() == Type.REQUEST_MESSAGE) { 35 | if (!message.getRequest().hasVerb() || !message.getRequest().hasPath()) { 36 | throw new InvalidMessageException("Missing required request attributes!"); 37 | } 38 | } else if (getType() == Type.RESPONSE_MESSAGE) { 39 | if (!message.getResponse().hasId() || !message.getResponse().hasStatus() || !message.getResponse().hasMessage()) { 40 | throw new InvalidMessageException("Missing required response attributes!"); 41 | } 42 | } 43 | } catch (InvalidProtocolBufferException e) { 44 | throw new InvalidMessageException(e); 45 | } 46 | } 47 | 48 | ProtobufWebSocketMessage(SubProtocol.WebSocketMessage message) { 49 | this.message = message; 50 | } 51 | 52 | @Override 53 | public Type getType() { 54 | if (message.getType().getNumber() == SubProtocol.WebSocketMessage.Type.REQUEST_VALUE && 55 | message.hasRequest()) 56 | { 57 | return Type.REQUEST_MESSAGE; 58 | } else if (message.getType().getNumber() == SubProtocol.WebSocketMessage.Type.RESPONSE_VALUE && 59 | message.hasResponse()) 60 | { 61 | return Type.RESPONSE_MESSAGE; 62 | } else { 63 | return Type.UNKNOWN_MESSAGE; 64 | } 65 | } 66 | 67 | @Override 68 | public WebSocketRequestMessage getRequestMessage() { 69 | return new ProtobufWebSocketRequestMessage(message.getRequest()); 70 | } 71 | 72 | @Override 73 | public WebSocketResponseMessage getResponseMessage() { 74 | return new ProtobufWebSocketResponseMessage(message.getResponse()); 75 | } 76 | 77 | @Override 78 | public byte[] toByteArray() { 79 | return message.toByteArray(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessageFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.websocket.messages.protobuf; 19 | 20 | import com.google.protobuf.ByteString; 21 | import org.whispersystems.websocket.messages.InvalidMessageException; 22 | import org.whispersystems.websocket.messages.WebSocketMessage; 23 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 24 | 25 | import java.util.List; 26 | import java.util.Optional; 27 | 28 | public class ProtobufWebSocketMessageFactory implements WebSocketMessageFactory { 29 | 30 | @Override 31 | public WebSocketMessage parseMessage(byte[] serialized, int offset, int len) 32 | throws InvalidMessageException 33 | { 34 | return new ProtobufWebSocketMessage(serialized, offset, len); 35 | } 36 | 37 | @Override 38 | public WebSocketMessage createRequest(Optional requestId, 39 | String verb, String path, 40 | List headers, 41 | Optional body) 42 | { 43 | SubProtocol.WebSocketRequestMessage.Builder requestMessage = 44 | SubProtocol.WebSocketRequestMessage.newBuilder() 45 | .setVerb(verb) 46 | .setPath(path); 47 | 48 | if (requestId.isPresent()) { 49 | requestMessage.setId(requestId.get()); 50 | } 51 | 52 | if (body.isPresent()) { 53 | requestMessage.setBody(ByteString.copyFrom(body.get())); 54 | } 55 | 56 | if (headers != null) { 57 | requestMessage.addAllHeaders(headers); 58 | } 59 | 60 | SubProtocol.WebSocketMessage message 61 | = SubProtocol.WebSocketMessage.newBuilder() 62 | .setType(SubProtocol.WebSocketMessage.Type.REQUEST) 63 | .setRequest(requestMessage) 64 | .build(); 65 | 66 | return new ProtobufWebSocketMessage(message); 67 | } 68 | 69 | @Override 70 | public WebSocketMessage createResponse(long requestId, int status, String messageString, List headers, Optional body) { 71 | SubProtocol.WebSocketResponseMessage.Builder responseMessage = 72 | SubProtocol.WebSocketResponseMessage.newBuilder() 73 | .setId(requestId) 74 | .setStatus(status) 75 | .setMessage(messageString); 76 | 77 | if (body.isPresent()) { 78 | responseMessage.setBody(ByteString.copyFrom(body.get())); 79 | } 80 | 81 | if (headers != null) { 82 | responseMessage.addAllHeaders(headers); 83 | } 84 | 85 | SubProtocol.WebSocketMessage message = 86 | SubProtocol.WebSocketMessage.newBuilder() 87 | .setType(SubProtocol.WebSocketMessage.Type.RESPONSE) 88 | .setResponse(responseMessage) 89 | .build(); 90 | 91 | return new ProtobufWebSocketMessage(message); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketRequestMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages.protobuf; 18 | 19 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | 25 | public class ProtobufWebSocketRequestMessage implements WebSocketRequestMessage { 26 | 27 | private final SubProtocol.WebSocketRequestMessage message; 28 | 29 | ProtobufWebSocketRequestMessage(SubProtocol.WebSocketRequestMessage message) { 30 | this.message = message; 31 | } 32 | 33 | @Override 34 | public String getVerb() { 35 | return message.getVerb(); 36 | } 37 | 38 | @Override 39 | public String getPath() { 40 | return message.getPath(); 41 | } 42 | 43 | @Override 44 | public Optional getBody() { 45 | if (message.hasBody()) { 46 | return Optional.of(message.getBody().toByteArray()); 47 | } else { 48 | return Optional.empty(); 49 | } 50 | } 51 | 52 | @Override 53 | public long getRequestId() { 54 | return message.getId(); 55 | } 56 | 57 | @Override 58 | public boolean hasRequestId() { 59 | return message.hasId(); 60 | } 61 | 62 | @Override 63 | public Map getHeaders() { 64 | Map results = new HashMap<>(); 65 | 66 | for (String header : message.getHeadersList()) { 67 | String[] tokenized = header.split(":"); 68 | 69 | if (tokenized.length == 2 && tokenized[0] != null && tokenized[1] != null) { 70 | results.put(tokenized[0].trim().toLowerCase(), tokenized[1].trim()); 71 | } 72 | } 73 | 74 | return results; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketResponseMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.messages.protobuf; 18 | 19 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | 25 | public class ProtobufWebSocketResponseMessage implements WebSocketResponseMessage { 26 | 27 | private final SubProtocol.WebSocketResponseMessage message; 28 | 29 | public ProtobufWebSocketResponseMessage(SubProtocol.WebSocketResponseMessage message) { 30 | this.message = message; 31 | } 32 | 33 | @Override 34 | public long getRequestId() { 35 | return message.getId(); 36 | } 37 | 38 | @Override 39 | public int getStatus() { 40 | return message.getStatus(); 41 | } 42 | 43 | @Override 44 | public String getMessage() { 45 | return message.getMessage(); 46 | } 47 | 48 | @Override 49 | public Optional getBody() { 50 | if (message.hasBody()) { 51 | return Optional.of(message.getBody().toByteArray()); 52 | } else { 53 | return Optional.empty(); 54 | } 55 | } 56 | 57 | @Override 58 | public Map getHeaders() { 59 | Map results = new HashMap<>(); 60 | 61 | for (String header : message.getHeadersList()) { 62 | String[] tokenized = header.split(":"); 63 | 64 | if (tokenized.length == 2 && tokenized[0] != null && tokenized[1] != null) { 65 | results.put(tokenized[0].trim().toLowerCase(), tokenized[1].trim()); 66 | } 67 | } 68 | 69 | return results; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/BufferingServletInputStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import javax.servlet.ReadListener; 20 | import javax.servlet.ServletInputStream; 21 | import java.io.ByteArrayInputStream; 22 | import java.io.IOException; 23 | 24 | public class BufferingServletInputStream extends ServletInputStream { 25 | 26 | private final ByteArrayInputStream buffer; 27 | 28 | public BufferingServletInputStream(byte[] body) { 29 | this.buffer = new ByteArrayInputStream(body); 30 | } 31 | 32 | @Override 33 | public int read(byte[] buf, int offset, int length) { 34 | return buffer.read(buf, offset, length); 35 | } 36 | 37 | @Override 38 | public int read(byte[] buf) { 39 | return read(buf, 0, buf.length); 40 | } 41 | 42 | @Override 43 | public int read() throws IOException { 44 | return buffer.read(); 45 | } 46 | 47 | @Override 48 | public int available() { 49 | return buffer.available(); 50 | } 51 | 52 | @Override 53 | public boolean isFinished() { 54 | return available() > 0; 55 | } 56 | 57 | @Override 58 | public boolean isReady() { 59 | return true; 60 | } 61 | 62 | @Override 63 | public void setReadListener(ReadListener readListener) { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/BufferingServletOutputStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import javax.servlet.ServletOutputStream; 20 | import javax.servlet.WriteListener; 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.IOException; 23 | 24 | public class BufferingServletOutputStream extends ServletOutputStream { 25 | 26 | private final ByteArrayOutputStream buffer; 27 | 28 | public BufferingServletOutputStream(ByteArrayOutputStream buffer) { 29 | this.buffer = buffer; 30 | } 31 | 32 | @Override 33 | public void write(byte[] buf, int offset, int length) { 34 | buffer.write(buf, offset, length); 35 | } 36 | 37 | @Override 38 | public void write(byte[] buf) { 39 | write(buf, 0, buf.length); 40 | } 41 | 42 | @Override 43 | public void write(int b) throws IOException { 44 | buffer.write(b); 45 | } 46 | 47 | @Override 48 | public void flush() { 49 | 50 | } 51 | 52 | @Override 53 | public void close() { 54 | 55 | } 56 | 57 | @Override 58 | public boolean isReady() { 59 | return true; 60 | } 61 | 62 | @Override 63 | public void setWriteListener(WriteListener writeListener) { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/LoggableRequest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.servlet; 2 | 3 | import org.eclipse.jetty.http.HttpFields; 4 | import org.eclipse.jetty.http.HttpURI; 5 | import org.eclipse.jetty.http.HttpVersion; 6 | import org.eclipse.jetty.server.Authentication; 7 | import org.eclipse.jetty.server.HttpChannel; 8 | import org.eclipse.jetty.server.HttpChannelState; 9 | import org.eclipse.jetty.server.HttpInput; 10 | import org.eclipse.jetty.server.Request; 11 | import org.eclipse.jetty.server.Response; 12 | import org.eclipse.jetty.server.UserIdentity; 13 | import org.eclipse.jetty.server.handler.ContextHandler; 14 | import org.eclipse.jetty.util.Attributes; 15 | 16 | import javax.servlet.AsyncContext; 17 | import javax.servlet.DispatcherType; 18 | import javax.servlet.RequestDispatcher; 19 | import javax.servlet.ServletContext; 20 | import javax.servlet.ServletException; 21 | import javax.servlet.ServletInputStream; 22 | import javax.servlet.ServletRequest; 23 | import javax.servlet.ServletResponse; 24 | import javax.servlet.http.Cookie; 25 | import javax.servlet.http.HttpServletRequest; 26 | import javax.servlet.http.HttpServletResponse; 27 | import javax.servlet.http.HttpSession; 28 | import javax.servlet.http.Part; 29 | import java.io.BufferedReader; 30 | import java.io.IOException; 31 | import java.io.UnsupportedEncodingException; 32 | import java.net.InetSocketAddress; 33 | import java.security.Principal; 34 | import java.util.Collection; 35 | import java.util.Enumeration; 36 | import java.util.EventListener; 37 | import java.util.Locale; 38 | import java.util.Map; 39 | 40 | public class LoggableRequest extends Request { 41 | 42 | private final HttpServletRequest request; 43 | 44 | public LoggableRequest(HttpServletRequest request) { 45 | super(null, null); 46 | this.request = request; 47 | } 48 | 49 | @Override 50 | public HttpFields getHttpFields() { 51 | throw new AssertionError(); 52 | } 53 | 54 | @Override 55 | public HttpInput getHttpInput() { 56 | throw new AssertionError(); 57 | } 58 | 59 | @Override 60 | public void addEventListener(EventListener listener) { 61 | throw new AssertionError(); 62 | } 63 | 64 | @Override 65 | public AsyncContext getAsyncContext() { 66 | throw new AssertionError(); 67 | } 68 | 69 | @Override 70 | public HttpChannelState getHttpChannelState() { 71 | throw new AssertionError(); 72 | } 73 | 74 | @Override 75 | public Object getAttribute(String name) { 76 | return request.getAttribute(name); 77 | } 78 | 79 | @Override 80 | public Enumeration getAttributeNames() { 81 | return request.getAttributeNames(); 82 | } 83 | 84 | @Override 85 | public Attributes getAttributes() { 86 | throw new AssertionError(); 87 | } 88 | 89 | @Override 90 | public Authentication getAuthentication() { 91 | return null; 92 | } 93 | 94 | @Override 95 | public String getAuthType() { 96 | return request.getAuthType(); 97 | } 98 | 99 | @Override 100 | public String getCharacterEncoding() { 101 | return request.getCharacterEncoding(); 102 | } 103 | 104 | @Override 105 | public HttpChannel getHttpChannel() { 106 | throw new AssertionError(); 107 | } 108 | 109 | @Override 110 | public int getContentLength() { 111 | return request.getContentLength(); 112 | } 113 | 114 | @Override 115 | public String getContentType() { 116 | return request.getContentType(); 117 | } 118 | 119 | @Override 120 | public ContextHandler.Context getContext() { 121 | throw new AssertionError(); 122 | } 123 | 124 | @Override 125 | public String getContextPath() { 126 | return request.getContextPath(); 127 | } 128 | 129 | @Override 130 | public Cookie[] getCookies() { 131 | return request.getCookies(); 132 | } 133 | 134 | @Override 135 | public long getDateHeader(String name) { 136 | return request.getDateHeader(name); 137 | } 138 | 139 | @Override 140 | public DispatcherType getDispatcherType() { 141 | return request.getDispatcherType(); 142 | } 143 | 144 | @Override 145 | public String getHeader(String name) { 146 | return request.getHeader(name); 147 | } 148 | 149 | @Override 150 | public Enumeration getHeaderNames() { 151 | return request.getHeaderNames(); 152 | } 153 | 154 | @Override 155 | public Enumeration getHeaders(String name) { 156 | return request.getHeaders(name); 157 | } 158 | 159 | @Override 160 | public int getInputState() { 161 | throw new AssertionError(); 162 | } 163 | 164 | @Override 165 | public ServletInputStream getInputStream() throws IOException { 166 | return request.getInputStream(); 167 | } 168 | 169 | @Override 170 | public int getIntHeader(String name) { 171 | return request.getIntHeader(name); 172 | } 173 | 174 | @Override 175 | public Locale getLocale() { 176 | return request.getLocale(); 177 | } 178 | 179 | @Override 180 | public Enumeration getLocales() { 181 | return request.getLocales(); 182 | } 183 | 184 | @Override 185 | public String getLocalAddr() { 186 | return request.getLocalAddr(); 187 | } 188 | 189 | @Override 190 | public String getLocalName() { 191 | return request.getLocalName(); 192 | } 193 | 194 | @Override 195 | public int getLocalPort() { 196 | return request.getLocalPort(); 197 | } 198 | 199 | @Override 200 | public String getMethod() { 201 | return request.getMethod(); 202 | } 203 | 204 | @Override 205 | public String getParameter(String name) { 206 | return request.getParameter(name); 207 | } 208 | 209 | @Override 210 | public Map getParameterMap() { 211 | return request.getParameterMap(); 212 | } 213 | 214 | @Override 215 | public Enumeration getParameterNames() { 216 | return request.getParameterNames(); 217 | } 218 | 219 | @Override 220 | public String[] getParameterValues(String name) { 221 | return request.getParameterValues(name); 222 | } 223 | 224 | @Override 225 | public String getPathInfo() { 226 | return request.getPathInfo(); 227 | } 228 | 229 | @Override 230 | public String getPathTranslated() { 231 | return request.getPathTranslated(); 232 | } 233 | 234 | @Override 235 | public String getProtocol() { 236 | return request.getProtocol(); 237 | } 238 | 239 | @Override 240 | public HttpVersion getHttpVersion() { 241 | throw new AssertionError(); 242 | } 243 | 244 | @Override 245 | public String getQueryEncoding() { 246 | throw new AssertionError(); 247 | } 248 | 249 | @Override 250 | public String getQueryString() { 251 | return request.getQueryString(); 252 | } 253 | 254 | @Override 255 | public BufferedReader getReader() throws IOException { 256 | throw new AssertionError(); 257 | } 258 | 259 | @Override 260 | public String getRealPath(String path) { 261 | return request.getRealPath(path); 262 | } 263 | 264 | @Override 265 | public String getRemoteAddr() { 266 | return request.getRemoteAddr(); 267 | } 268 | 269 | @Override 270 | public String getRemoteHost() { 271 | return request.getRemoteHost(); 272 | } 273 | 274 | @Override 275 | public int getRemotePort() { 276 | return request.getRemotePort(); 277 | } 278 | 279 | @Override 280 | public String getRemoteUser() { 281 | return request.getRemoteUser(); 282 | } 283 | 284 | @Override 285 | public RequestDispatcher getRequestDispatcher(String path) { 286 | return request.getRequestDispatcher(path); 287 | } 288 | 289 | @Override 290 | public String getRequestedSessionId() { 291 | return request.getRequestedSessionId(); 292 | } 293 | 294 | @Override 295 | public String getRequestURI() { 296 | return request.getRequestURI(); 297 | } 298 | 299 | @Override 300 | public StringBuffer getRequestURL() { 301 | return request.getRequestURL(); 302 | } 303 | 304 | @Override 305 | public Response getResponse() { 306 | throw new AssertionError(); 307 | } 308 | 309 | @Override 310 | public StringBuilder getRootURL() { 311 | throw new AssertionError(); 312 | } 313 | 314 | @Override 315 | public String getScheme() { 316 | return request.getScheme(); 317 | } 318 | 319 | @Override 320 | public String getServerName() { 321 | return request.getServerName(); 322 | } 323 | 324 | @Override 325 | public int getServerPort() { 326 | return request.getServerPort(); 327 | } 328 | 329 | @Override 330 | public ServletContext getServletContext() { 331 | return request.getServletContext(); 332 | } 333 | 334 | @Override 335 | public String getServletName() { 336 | throw new AssertionError(); 337 | } 338 | 339 | @Override 340 | public String getServletPath() { 341 | return request.getServletPath(); 342 | } 343 | 344 | @Override 345 | public ServletResponse getServletResponse() { 346 | throw new AssertionError(); 347 | } 348 | 349 | @Override 350 | public String changeSessionId() { 351 | throw new AssertionError(); 352 | } 353 | 354 | @Override 355 | public HttpSession getSession() { 356 | return request.getSession(); 357 | } 358 | 359 | @Override 360 | public HttpSession getSession(boolean create) { 361 | return request.getSession(create); 362 | } 363 | 364 | @Override 365 | public long getTimeStamp() { 366 | return System.currentTimeMillis(); 367 | } 368 | 369 | @Override 370 | public HttpURI getHttpURI() { 371 | return new HttpURI(getRequestURI()); 372 | } 373 | 374 | @Override 375 | public UserIdentity getUserIdentity() { 376 | throw new AssertionError(); 377 | } 378 | 379 | @Override 380 | public UserIdentity getResolvedUserIdentity() { 381 | throw new AssertionError(); 382 | } 383 | 384 | @Override 385 | public UserIdentity.Scope getUserIdentityScope() { 386 | throw new AssertionError(); 387 | } 388 | 389 | @Override 390 | public Principal getUserPrincipal() { 391 | throw new AssertionError(); 392 | } 393 | 394 | @Override 395 | public boolean isHandled() { 396 | throw new AssertionError(); 397 | } 398 | 399 | @Override 400 | public boolean isAsyncStarted() { 401 | return request.isAsyncStarted(); 402 | } 403 | 404 | @Override 405 | public boolean isAsyncSupported() { 406 | return request.isAsyncSupported(); 407 | } 408 | 409 | @Override 410 | public boolean isRequestedSessionIdFromCookie() { 411 | return request.isRequestedSessionIdFromCookie(); 412 | } 413 | 414 | @Override 415 | public boolean isRequestedSessionIdFromUrl() { 416 | return request.isRequestedSessionIdFromUrl(); 417 | } 418 | 419 | @Override 420 | public boolean isRequestedSessionIdFromURL() { 421 | return request.isRequestedSessionIdFromURL(); 422 | } 423 | 424 | @Override 425 | public boolean isRequestedSessionIdValid() { 426 | return request.isRequestedSessionIdValid(); 427 | } 428 | 429 | @Override 430 | public boolean isSecure() { 431 | return request.isSecure(); 432 | } 433 | 434 | @Override 435 | public void setSecure(boolean secure) { 436 | throw new AssertionError(); 437 | } 438 | 439 | @Override 440 | public boolean isUserInRole(String role) { 441 | return request.isUserInRole(role); 442 | } 443 | 444 | @Override 445 | public void removeAttribute(String name) { 446 | request.removeAttribute(name); 447 | } 448 | 449 | @Override 450 | public void removeEventListener(EventListener listener) { 451 | throw new AssertionError(); 452 | } 453 | 454 | @Override 455 | public void setAsyncSupported(boolean supported, String source) { 456 | throw new AssertionError(); 457 | } 458 | 459 | @Override 460 | public void setAttribute(String name, Object value) { 461 | throw new AssertionError(); 462 | } 463 | 464 | @Override 465 | public void setAttributes(Attributes attributes) { 466 | throw new AssertionError(); 467 | } 468 | 469 | @Override 470 | public void setAuthentication(Authentication authentication) { 471 | throw new AssertionError(); 472 | } 473 | 474 | @Override 475 | public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { 476 | throw new AssertionError(); 477 | } 478 | 479 | @Override 480 | public void setCharacterEncodingUnchecked(String encoding) { 481 | throw new AssertionError(); 482 | } 483 | 484 | @Override 485 | public void setContentType(String contentType) { 486 | throw new AssertionError(); 487 | } 488 | 489 | @Override 490 | public void setContext(ContextHandler.Context context) { 491 | throw new AssertionError(); 492 | } 493 | 494 | @Override 495 | public boolean takeNewContext() { 496 | throw new AssertionError(); 497 | } 498 | 499 | @Override 500 | public void setContextPath(String contextPath) { 501 | throw new AssertionError(); 502 | } 503 | 504 | @Override 505 | public void setCookies(Cookie[] cookies) { 506 | throw new AssertionError(); 507 | } 508 | 509 | @Override 510 | public void setDispatcherType(DispatcherType type) { 511 | throw new AssertionError(); 512 | } 513 | 514 | @Override 515 | public void setHandled(boolean h) { 516 | throw new AssertionError(); 517 | } 518 | 519 | @Override 520 | public boolean isHead() { 521 | throw new AssertionError(); 522 | } 523 | 524 | @Override 525 | public void setPathInfo(String pathInfo) { 526 | throw new AssertionError(); 527 | } 528 | 529 | @Override 530 | public void setHttpVersion(HttpVersion version) { 531 | throw new AssertionError(); 532 | } 533 | 534 | @Override 535 | public void setQueryEncoding(String queryEncoding) { 536 | throw new AssertionError(); 537 | } 538 | 539 | @Override 540 | public void setQueryString(String queryString) { 541 | throw new AssertionError(); 542 | } 543 | 544 | @Override 545 | public void setRemoteAddr(InetSocketAddress addr) { 546 | throw new AssertionError(); 547 | } 548 | 549 | @Override 550 | public void setRequestedSessionId(String requestedSessionId) { 551 | throw new AssertionError(); 552 | } 553 | 554 | @Override 555 | public void setRequestedSessionIdFromCookie(boolean requestedSessionIdCookie) { 556 | throw new AssertionError(); 557 | } 558 | 559 | @Override 560 | public void setScheme(String scheme) { 561 | throw new AssertionError(); 562 | } 563 | 564 | @Override 565 | public void setServletPath(String servletPath) { 566 | throw new AssertionError(); 567 | } 568 | 569 | @Override 570 | public void setSession(HttpSession session) { 571 | throw new AssertionError(); 572 | } 573 | 574 | @Override 575 | public void setTimeStamp(long ts) { 576 | throw new AssertionError(); 577 | } 578 | 579 | @Override 580 | public void setHttpURI(HttpURI uri) { 581 | throw new AssertionError(); 582 | } 583 | 584 | @Override 585 | public void setUserIdentityScope(UserIdentity.Scope scope) { 586 | throw new AssertionError(); 587 | } 588 | 589 | @Override 590 | public AsyncContext startAsync() throws IllegalStateException { 591 | throw new AssertionError(); 592 | } 593 | 594 | @Override 595 | public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { 596 | throw new AssertionError(); 597 | } 598 | 599 | @Override 600 | public String toString() { 601 | return request.toString(); 602 | } 603 | 604 | @Override 605 | public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { 606 | throw new AssertionError(); 607 | } 608 | 609 | @Override 610 | public Part getPart(String name) throws IOException, ServletException { 611 | return request.getPart(name); 612 | } 613 | 614 | @Override 615 | public Collection getParts() throws IOException, ServletException { 616 | return request.getParts(); 617 | } 618 | 619 | @Override 620 | public void login(String username, String password) throws ServletException { 621 | throw new AssertionError(); 622 | } 623 | 624 | @Override 625 | public void logout() throws ServletException { 626 | throw new AssertionError(); 627 | } 628 | 629 | } 630 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/LoggableResponse.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.servlet; 2 | 3 | import org.eclipse.jetty.http.HttpContent; 4 | import org.eclipse.jetty.http.HttpCookie; 5 | import org.eclipse.jetty.http.HttpFields; 6 | import org.eclipse.jetty.http.HttpHeader; 7 | import org.eclipse.jetty.http.HttpVersion; 8 | import org.eclipse.jetty.http.MetaData; 9 | import org.eclipse.jetty.io.Connection; 10 | import org.eclipse.jetty.io.EndPoint; 11 | import org.eclipse.jetty.server.Connector; 12 | import org.eclipse.jetty.server.HttpChannel; 13 | import org.eclipse.jetty.server.HttpConfiguration; 14 | import org.eclipse.jetty.server.HttpOutput; 15 | import org.eclipse.jetty.server.HttpTransport; 16 | import org.eclipse.jetty.server.Response; 17 | import org.eclipse.jetty.util.Callback; 18 | 19 | import javax.servlet.ServletOutputStream; 20 | import javax.servlet.http.Cookie; 21 | import javax.servlet.http.HttpServletResponse; 22 | import java.io.IOException; 23 | import java.io.PrintWriter; 24 | import java.net.InetSocketAddress; 25 | import java.nio.ByteBuffer; 26 | import java.nio.channels.ReadPendingException; 27 | import java.nio.channels.WritePendingException; 28 | import java.util.Collection; 29 | import java.util.Locale; 30 | 31 | public class LoggableResponse extends Response { 32 | 33 | private final HttpServletResponse response; 34 | 35 | public LoggableResponse(HttpServletResponse response) { 36 | super(null, null); 37 | this.response = response; 38 | } 39 | 40 | @Override 41 | public void putHeaders(HttpContent httpContent, long contentLength, boolean etag) { 42 | throw new AssertionError(); 43 | } 44 | 45 | @Override 46 | public HttpOutput getHttpOutput() { 47 | throw new AssertionError(); 48 | } 49 | 50 | @Override 51 | public boolean isIncluding() { 52 | throw new AssertionError(); 53 | } 54 | 55 | @Override 56 | public void include() { 57 | throw new AssertionError(); 58 | } 59 | 60 | @Override 61 | public void included() { 62 | throw new AssertionError(); 63 | } 64 | 65 | @Override 66 | public void addCookie(HttpCookie cookie) { 67 | throw new AssertionError(); 68 | } 69 | 70 | @Override 71 | public void addCookie(Cookie cookie) { 72 | throw new AssertionError(); 73 | } 74 | 75 | @Override 76 | public boolean containsHeader(String name) { 77 | return response.containsHeader(name); 78 | } 79 | 80 | @Override 81 | public String encodeURL(String url) { 82 | return response.encodeURL(url); 83 | } 84 | 85 | @Override 86 | public String encodeRedirectURL(String url) { 87 | return response.encodeRedirectURL(url); 88 | } 89 | 90 | @Override 91 | public String encodeUrl(String url) { 92 | return response.encodeUrl(url); 93 | } 94 | 95 | @Override 96 | public String encodeRedirectUrl(String url) { 97 | return response.encodeRedirectUrl(url); 98 | } 99 | 100 | @Override 101 | public void sendError(int sc) throws IOException { 102 | throw new AssertionError(); 103 | } 104 | 105 | @Override 106 | public void sendError(int code, String message) throws IOException { 107 | throw new AssertionError(); 108 | } 109 | 110 | @Override 111 | public void sendProcessing() throws IOException { 112 | throw new AssertionError(); 113 | } 114 | 115 | @Override 116 | public void sendRedirect(String location) throws IOException { 117 | throw new AssertionError(); 118 | } 119 | 120 | @Override 121 | public void setDateHeader(String name, long date) { 122 | throw new AssertionError(); 123 | } 124 | 125 | @Override 126 | public void addDateHeader(String name, long date) { 127 | throw new AssertionError(); 128 | } 129 | 130 | @Override 131 | public void setHeader(HttpHeader name, String value) { 132 | throw new AssertionError(); 133 | } 134 | 135 | @Override 136 | public void setHeader(String name, String value) { 137 | throw new AssertionError(); 138 | } 139 | 140 | @Override 141 | public Collection getHeaderNames() { 142 | return response.getHeaderNames(); 143 | } 144 | 145 | @Override 146 | public String getHeader(String name) { 147 | return response.getHeader(name); 148 | } 149 | 150 | @Override 151 | public Collection getHeaders(String name) { 152 | return response.getHeaders(name); 153 | } 154 | 155 | @Override 156 | public void addHeader(String name, String value) { 157 | throw new AssertionError(); 158 | } 159 | 160 | @Override 161 | public void setIntHeader(String name, int value) { 162 | throw new AssertionError(); 163 | } 164 | 165 | @Override 166 | public void addIntHeader(String name, int value) { 167 | throw new AssertionError(); 168 | } 169 | 170 | @Override 171 | public void setStatus(int sc) { 172 | throw new AssertionError(); 173 | } 174 | 175 | @Override 176 | public void setStatus(int sc, String sm) { 177 | throw new AssertionError(); 178 | } 179 | 180 | @Override 181 | public void setStatusWithReason(int sc, String sm) { 182 | throw new AssertionError(); 183 | } 184 | 185 | @Override 186 | public String getCharacterEncoding() { 187 | return response.getCharacterEncoding(); 188 | } 189 | 190 | @Override 191 | public String getContentType() { 192 | return response.getContentType(); 193 | } 194 | 195 | @Override 196 | public ServletOutputStream getOutputStream() throws IOException { 197 | throw new AssertionError(); 198 | } 199 | 200 | @Override 201 | public boolean isWriting() { 202 | throw new AssertionError(); 203 | } 204 | 205 | @Override 206 | public PrintWriter getWriter() throws IOException { 207 | throw new AssertionError(); 208 | } 209 | 210 | @Override 211 | public void setContentLength(int len) { 212 | throw new AssertionError(); 213 | } 214 | 215 | @Override 216 | public boolean isAllContentWritten(long written) { 217 | throw new AssertionError(); 218 | } 219 | 220 | @Override 221 | public void closeOutput() throws IOException { 222 | throw new AssertionError(); 223 | } 224 | 225 | @Override 226 | public long getLongContentLength() { 227 | return response.getBufferSize(); 228 | } 229 | 230 | @Override 231 | public void setLongContentLength(long len) { 232 | throw new AssertionError(); 233 | } 234 | 235 | @Override 236 | public void setCharacterEncoding(String encoding) { 237 | throw new AssertionError(); 238 | } 239 | 240 | @Override 241 | public void setContentType(String contentType) { 242 | throw new AssertionError(); 243 | } 244 | 245 | @Override 246 | public void setBufferSize(int size) { 247 | throw new AssertionError(); 248 | } 249 | 250 | @Override 251 | public int getBufferSize() { 252 | return response.getBufferSize(); 253 | } 254 | 255 | @Override 256 | public void flushBuffer() throws IOException { 257 | throw new AssertionError(); 258 | } 259 | 260 | @Override 261 | public void reset() { 262 | throw new AssertionError(); 263 | } 264 | 265 | @Override 266 | public void reset(boolean preserveCookies) { 267 | throw new AssertionError(); 268 | } 269 | 270 | @Override 271 | public void resetForForward() { 272 | throw new AssertionError(); 273 | } 274 | 275 | @Override 276 | public void resetBuffer() { 277 | throw new AssertionError(); 278 | } 279 | 280 | @Override 281 | public boolean isCommitted() { 282 | throw new AssertionError(); 283 | } 284 | 285 | @Override 286 | public void setLocale(Locale locale) { 287 | throw new AssertionError(); 288 | } 289 | 290 | @Override 291 | public Locale getLocale() { 292 | return response.getLocale(); 293 | } 294 | 295 | @Override 296 | public int getStatus() { 297 | return response.getStatus(); 298 | } 299 | 300 | @Override 301 | public String getReason() { 302 | throw new AssertionError(); 303 | } 304 | 305 | @Override 306 | public HttpFields getHttpFields() { 307 | return new HttpFields(); 308 | } 309 | 310 | @Override 311 | public long getContentCount() { 312 | return 0; 313 | } 314 | 315 | @Override 316 | public String toString() { 317 | return response.toString(); 318 | } 319 | 320 | @Override 321 | public MetaData.Response getCommittedMetaData() { 322 | return new MetaData.Response(HttpVersion.HTTP_2, getStatus(), null); 323 | } 324 | 325 | @Override 326 | public HttpChannel getHttpChannel() 327 | { 328 | return new HttpChannel(null, new HttpConfiguration(), new NullEndPoint(), null); 329 | } 330 | 331 | private static class NullEndPoint implements EndPoint { 332 | 333 | @Override 334 | public InetSocketAddress getLocalAddress() { 335 | return null; 336 | } 337 | 338 | @Override 339 | public InetSocketAddress getRemoteAddress() { 340 | return null; 341 | } 342 | 343 | @Override 344 | public boolean isOpen() { 345 | return false; 346 | } 347 | 348 | @Override 349 | public long getCreatedTimeStamp() { 350 | return 0; 351 | } 352 | 353 | @Override 354 | public void shutdownOutput() { 355 | 356 | } 357 | 358 | @Override 359 | public boolean isOutputShutdown() { 360 | return false; 361 | } 362 | 363 | @Override 364 | public boolean isInputShutdown() { 365 | return false; 366 | } 367 | 368 | @Override 369 | public void close() { 370 | 371 | } 372 | 373 | @Override 374 | public int fill(ByteBuffer buffer) throws IOException { 375 | return 0; 376 | } 377 | 378 | @Override 379 | public boolean flush(ByteBuffer... buffer) throws IOException { 380 | return false; 381 | } 382 | 383 | @Override 384 | public Object getTransport() { 385 | return null; 386 | } 387 | 388 | @Override 389 | public long getIdleTimeout() { 390 | return 0; 391 | } 392 | 393 | @Override 394 | public void setIdleTimeout(long idleTimeout) { 395 | 396 | } 397 | 398 | @Override 399 | public void fillInterested(Callback callback) throws ReadPendingException { 400 | 401 | } 402 | 403 | @Override 404 | public boolean tryFillInterested(Callback callback) { 405 | return false; 406 | } 407 | 408 | @Override 409 | public boolean isFillInterested() { 410 | return false; 411 | } 412 | 413 | @Override 414 | public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException { 415 | 416 | } 417 | 418 | @Override 419 | public Connection getConnection() { 420 | return null; 421 | } 422 | 423 | @Override 424 | public void setConnection(Connection connection) { 425 | 426 | } 427 | 428 | @Override 429 | public void onOpen() { 430 | 431 | } 432 | 433 | @Override 434 | public void onClose() { 435 | 436 | } 437 | 438 | @Override 439 | public boolean isOptimizedForDirectBuffers() { 440 | return false; 441 | } 442 | 443 | @Override 444 | public void upgrade(Connection newConnection) { 445 | 446 | } 447 | } 448 | 449 | } 450 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/NullServletOutputStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import javax.servlet.ServletOutputStream; 20 | import javax.servlet.WriteListener; 21 | import java.io.IOException; 22 | 23 | public class NullServletOutputStream extends ServletOutputStream { 24 | @Override 25 | public void write(int b) throws IOException {} 26 | 27 | @Override 28 | public void write(byte[] buf) {} 29 | 30 | @Override 31 | public void write(byte[] buf, int offset, int len) {} 32 | 33 | @Override 34 | public boolean isReady() { 35 | return false; 36 | } 37 | 38 | @Override 39 | public void setWriteListener(WriteListener writeListener) { 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/NullServletResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import javax.servlet.ServletOutputStream; 20 | import javax.servlet.http.Cookie; 21 | import javax.servlet.http.HttpServletResponse; 22 | import java.io.IOException; 23 | import java.io.PrintWriter; 24 | import java.util.Collection; 25 | import java.util.LinkedList; 26 | import java.util.Locale; 27 | 28 | public class NullServletResponse implements HttpServletResponse { 29 | @Override 30 | public void addCookie(Cookie cookie) {} 31 | 32 | @Override 33 | public boolean containsHeader(String name) { 34 | return false; 35 | } 36 | 37 | @Override 38 | public String encodeURL(String url) { 39 | return url; 40 | } 41 | 42 | @Override 43 | public String encodeRedirectURL(String url) { 44 | return url; 45 | } 46 | 47 | @Override 48 | public String encodeUrl(String url) { 49 | return url; 50 | } 51 | 52 | @Override 53 | public String encodeRedirectUrl(String url) { 54 | return url; 55 | } 56 | 57 | @Override 58 | public void sendError(int sc, String msg) throws IOException {} 59 | 60 | @Override 61 | public void sendError(int sc) throws IOException {} 62 | 63 | @Override 64 | public void sendRedirect(String location) throws IOException {} 65 | 66 | @Override 67 | public void setDateHeader(String name, long date) {} 68 | 69 | @Override 70 | public void addDateHeader(String name, long date) {} 71 | 72 | @Override 73 | public void setHeader(String name, String value) {} 74 | 75 | @Override 76 | public void addHeader(String name, String value) {} 77 | 78 | @Override 79 | public void setIntHeader(String name, int value) {} 80 | 81 | @Override 82 | public void addIntHeader(String name, int value) {} 83 | 84 | @Override 85 | public void setStatus(int sc) {} 86 | 87 | @Override 88 | public void setStatus(int sc, String sm) {} 89 | 90 | @Override 91 | public int getStatus() { 92 | return 200; 93 | } 94 | 95 | @Override 96 | public String getHeader(String name) { 97 | return null; 98 | } 99 | 100 | @Override 101 | public Collection getHeaders(String name) { 102 | return new LinkedList<>(); 103 | } 104 | 105 | @Override 106 | public Collection getHeaderNames() { 107 | return new LinkedList<>(); 108 | } 109 | 110 | @Override 111 | public String getCharacterEncoding() { 112 | return "UTF-8"; 113 | } 114 | 115 | @Override 116 | public String getContentType() { 117 | return null; 118 | } 119 | 120 | @Override 121 | public ServletOutputStream getOutputStream() throws IOException { 122 | return new NullServletOutputStream(); 123 | } 124 | 125 | @Override 126 | public PrintWriter getWriter() throws IOException { 127 | return new PrintWriter(new NullServletOutputStream()); 128 | } 129 | 130 | @Override 131 | public void setCharacterEncoding(String charset) {} 132 | 133 | @Override 134 | public void setContentLength(int len) {} 135 | 136 | @Override 137 | public void setContentLengthLong(long len) {} 138 | 139 | @Override 140 | public void setContentType(String type) {} 141 | 142 | @Override 143 | public void setBufferSize(int size) {} 144 | 145 | @Override 146 | public int getBufferSize() { 147 | return 0; 148 | } 149 | 150 | @Override 151 | public void flushBuffer() throws IOException {} 152 | 153 | @Override 154 | public void resetBuffer() {} 155 | 156 | @Override 157 | public boolean isCommitted() { 158 | return true; 159 | } 160 | 161 | @Override 162 | public void reset() {} 163 | 164 | @Override 165 | public void setLocale(Locale loc) {} 166 | 167 | @Override 168 | public Locale getLocale() { 169 | return Locale.US; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/WebSocketServletRequest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 20 | import org.whispersystems.websocket.session.WebSocketSessionContext; 21 | 22 | import javax.servlet.AsyncContext; 23 | import javax.servlet.DispatcherType; 24 | import javax.servlet.RequestDispatcher; 25 | import javax.servlet.ServletContext; 26 | import javax.servlet.ServletException; 27 | import javax.servlet.ServletInputStream; 28 | import javax.servlet.ServletRequest; 29 | import javax.servlet.ServletResponse; 30 | import javax.servlet.http.Cookie; 31 | import javax.servlet.http.HttpServletRequest; 32 | import javax.servlet.http.HttpServletResponse; 33 | import javax.servlet.http.HttpSession; 34 | import javax.servlet.http.HttpUpgradeHandler; 35 | import javax.servlet.http.Part; 36 | import java.io.BufferedReader; 37 | import java.io.IOException; 38 | import java.io.InputStreamReader; 39 | import java.io.UnsupportedEncodingException; 40 | import java.security.Principal; 41 | import java.util.Collection; 42 | import java.util.Enumeration; 43 | import java.util.HashMap; 44 | import java.util.LinkedList; 45 | import java.util.Locale; 46 | import java.util.Map; 47 | import java.util.Vector; 48 | 49 | 50 | public class WebSocketServletRequest implements HttpServletRequest { 51 | 52 | private final Map headers = new HashMap<>(); 53 | private final Map attributes = new HashMap<>(); 54 | 55 | private final WebSocketRequestMessage requestMessage; 56 | private final ServletInputStream inputStream; 57 | private final ServletContext servletContext; 58 | private final WebSocketSessionContext sessionContext; 59 | 60 | public WebSocketServletRequest(WebSocketSessionContext sessionContext, 61 | WebSocketRequestMessage requestMessage, 62 | ServletContext servletContext) 63 | { 64 | this.requestMessage = requestMessage; 65 | this.servletContext = servletContext; 66 | this.sessionContext = sessionContext; 67 | 68 | if (requestMessage.getBody().isPresent()) { 69 | inputStream = new BufferingServletInputStream(requestMessage.getBody().get()); 70 | } else { 71 | inputStream = new BufferingServletInputStream(new byte[0]); 72 | } 73 | 74 | headers.putAll(requestMessage.getHeaders()); 75 | } 76 | 77 | @Override 78 | public String getAuthType() { 79 | return BASIC_AUTH; 80 | } 81 | 82 | @Override 83 | public Cookie[] getCookies() { 84 | return new Cookie[0]; 85 | } 86 | 87 | @Override 88 | public long getDateHeader(String name) { 89 | return -1; 90 | } 91 | 92 | @Override 93 | public String getHeader(String name) { 94 | return headers.get(name.toLowerCase()); 95 | } 96 | 97 | @Override 98 | public Enumeration getHeaders(String name) { 99 | String header = this.headers.get(name.toLowerCase()); 100 | Vector results = new Vector<>(); 101 | 102 | if (header != null) { 103 | results.add(header); 104 | } 105 | 106 | return results.elements(); 107 | } 108 | 109 | @Override 110 | public Enumeration getHeaderNames() { 111 | return new Vector<>(headers.keySet()).elements(); 112 | } 113 | 114 | @Override 115 | public int getIntHeader(String name) { 116 | return -1; 117 | } 118 | 119 | @Override 120 | public String getMethod() { 121 | return requestMessage.getVerb(); 122 | } 123 | 124 | @Override 125 | public String getPathInfo() { 126 | return requestMessage.getPath(); 127 | } 128 | 129 | @Override 130 | public String getPathTranslated() { 131 | return requestMessage.getPath(); 132 | } 133 | 134 | @Override 135 | public String getContextPath() { 136 | return ""; 137 | } 138 | 139 | @Override 140 | public String getQueryString() { 141 | if (requestMessage.getPath().contains("?")) { 142 | return requestMessage.getPath().substring(requestMessage.getPath().indexOf("?") + 1); 143 | } 144 | 145 | return null; 146 | } 147 | 148 | @Override 149 | public String getRemoteUser() { 150 | return null; 151 | } 152 | 153 | @Override 154 | public boolean isUserInRole(String role) { 155 | return false; 156 | } 157 | 158 | @Override 159 | public Principal getUserPrincipal() { 160 | return new ContextPrincipal(sessionContext); 161 | } 162 | 163 | @Override 164 | public String getRequestedSessionId() { 165 | return null; 166 | } 167 | 168 | @Override 169 | public String getRequestURI() { 170 | if (requestMessage.getPath().contains("?")) { 171 | return requestMessage.getPath().substring(0, requestMessage.getPath().indexOf("?")); 172 | } else { 173 | return requestMessage.getPath(); 174 | } 175 | } 176 | 177 | @Override 178 | public StringBuffer getRequestURL() { 179 | StringBuffer stringBuffer = new StringBuffer(); 180 | stringBuffer.append("http://websocket"); 181 | stringBuffer.append(getRequestURI()); 182 | 183 | return stringBuffer; 184 | } 185 | 186 | @Override 187 | public String getServletPath() { 188 | return ""; 189 | } 190 | 191 | @Override 192 | public HttpSession getSession(boolean create) { 193 | return null; 194 | } 195 | 196 | @Override 197 | public HttpSession getSession() { 198 | return null; 199 | } 200 | 201 | @Override 202 | public String changeSessionId() { 203 | return null; 204 | } 205 | 206 | @Override 207 | public boolean isRequestedSessionIdValid() { 208 | return false; 209 | } 210 | 211 | @Override 212 | public boolean isRequestedSessionIdFromCookie() { 213 | return false; 214 | } 215 | 216 | @Override 217 | public boolean isRequestedSessionIdFromURL() { 218 | return false; 219 | } 220 | 221 | @Override 222 | public boolean isRequestedSessionIdFromUrl() { 223 | return false; 224 | } 225 | 226 | @Override 227 | public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { 228 | return false; 229 | } 230 | 231 | @Override 232 | public void login(String username, String password) throws ServletException { 233 | 234 | } 235 | 236 | @Override 237 | public void logout() throws ServletException { 238 | 239 | } 240 | 241 | @Override 242 | public Collection getParts() throws IOException, ServletException { 243 | return new LinkedList<>(); 244 | } 245 | 246 | @Override 247 | public Part getPart(String name) throws IOException, ServletException { 248 | return null; 249 | } 250 | 251 | @Override 252 | public T upgrade(Class handlerClass) throws IOException, ServletException { 253 | return null; 254 | } 255 | 256 | @Override 257 | public Object getAttribute(String name) { 258 | return attributes.get(name); 259 | } 260 | 261 | @Override 262 | public Enumeration getAttributeNames() { 263 | return new Vector<>(attributes.keySet()).elements(); 264 | } 265 | 266 | @Override 267 | public String getCharacterEncoding() { 268 | return null; 269 | } 270 | 271 | @Override 272 | public void setCharacterEncoding(String env) throws UnsupportedEncodingException {} 273 | 274 | @Override 275 | public int getContentLength() { 276 | if (requestMessage.getBody().isPresent()) { 277 | return requestMessage.getBody().get().length; 278 | } else { 279 | return 0; 280 | } 281 | } 282 | 283 | @Override 284 | public long getContentLengthLong() { 285 | return getContentLength(); 286 | } 287 | 288 | @Override 289 | public String getContentType() { 290 | if (requestMessage.getBody().isPresent()) { 291 | return "application/json"; 292 | } else { 293 | return null; 294 | } 295 | } 296 | 297 | @Override 298 | public ServletInputStream getInputStream() throws IOException { 299 | return inputStream; 300 | } 301 | 302 | @Override 303 | public String getParameter(String name) { 304 | String[] result = getParameterMap().get(name); 305 | 306 | if (result != null && result.length > 0) { 307 | return result[0]; 308 | } 309 | 310 | return null; 311 | } 312 | 313 | @Override 314 | public Enumeration getParameterNames() { 315 | return new Vector<>(getParameterMap().keySet()).elements(); 316 | } 317 | 318 | @Override 319 | public String[] getParameterValues(String name) { 320 | return getParameterMap().get(name); 321 | } 322 | 323 | @Override 324 | public Map getParameterMap() { 325 | Map parameterMap = new HashMap<>(); 326 | String queryParameters = getQueryString(); 327 | 328 | if (queryParameters == null) { 329 | return parameterMap; 330 | } 331 | 332 | String[] tokens = queryParameters.split("&"); 333 | 334 | for (String token : tokens) { 335 | String[] parts = token.split("="); 336 | 337 | if (parts != null && parts.length > 1) { 338 | parameterMap.put(parts[0], new String[] {parts[1]}); 339 | } 340 | } 341 | 342 | return parameterMap; 343 | } 344 | 345 | @Override 346 | public String getProtocol() { 347 | return "HTTP/1.0"; 348 | } 349 | 350 | @Override 351 | public String getScheme() { 352 | return "http"; 353 | } 354 | 355 | @Override 356 | public String getServerName() { 357 | return "websocket"; 358 | } 359 | 360 | @Override 361 | public int getServerPort() { 362 | return 8080; 363 | } 364 | 365 | @Override 366 | public BufferedReader getReader() throws IOException { 367 | return new BufferedReader(new InputStreamReader(inputStream)); 368 | } 369 | 370 | @Override 371 | public String getRemoteAddr() { 372 | return "127.0.0.1"; 373 | } 374 | 375 | @Override 376 | public String getRemoteHost() { 377 | return "localhost"; 378 | } 379 | 380 | @Override 381 | public void setAttribute(String name, Object o) { 382 | if (o != null) attributes.put(name, o); 383 | else removeAttribute(name); 384 | } 385 | 386 | @Override 387 | public void removeAttribute(String name) { 388 | attributes.remove(name); 389 | } 390 | 391 | @Override 392 | public Locale getLocale() { 393 | return Locale.US; 394 | } 395 | 396 | @Override 397 | public Enumeration getLocales() { 398 | Vector results = new Vector<>(); 399 | results.add(getLocale()); 400 | return results.elements(); 401 | } 402 | 403 | @Override 404 | public boolean isSecure() { 405 | return false; 406 | } 407 | 408 | @Override 409 | public RequestDispatcher getRequestDispatcher(String path) { 410 | return servletContext.getRequestDispatcher(path); 411 | } 412 | 413 | @Override 414 | public String getRealPath(String path) { 415 | return path; 416 | } 417 | 418 | @Override 419 | public int getRemotePort() { 420 | return 31337; 421 | } 422 | 423 | @Override 424 | public String getLocalName() { 425 | return "localhost"; 426 | } 427 | 428 | @Override 429 | public String getLocalAddr() { 430 | return "127.0.0.1"; 431 | } 432 | 433 | @Override 434 | public int getLocalPort() { 435 | return 8080; 436 | } 437 | 438 | @Override 439 | public ServletContext getServletContext() { 440 | return servletContext; 441 | } 442 | 443 | @Override 444 | public AsyncContext startAsync() throws IllegalStateException { 445 | throw new AssertionError("nyi"); 446 | } 447 | 448 | @Override 449 | public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { 450 | throw new AssertionError("nyi"); 451 | } 452 | 453 | @Override 454 | public boolean isAsyncStarted() { 455 | return false; 456 | } 457 | 458 | @Override 459 | public boolean isAsyncSupported() { 460 | return false; 461 | } 462 | 463 | @Override 464 | public AsyncContext getAsyncContext() { 465 | return null; 466 | } 467 | 468 | @Override 469 | public DispatcherType getDispatcherType() { 470 | return DispatcherType.REQUEST; 471 | } 472 | 473 | public static class ContextPrincipal implements Principal { 474 | 475 | private final WebSocketSessionContext context; 476 | 477 | public ContextPrincipal(WebSocketSessionContext context) { 478 | this.context = context; 479 | } 480 | 481 | @Override 482 | public boolean equals(Object another) { 483 | return another instanceof ContextPrincipal && 484 | context.equals(((ContextPrincipal) another).context); 485 | } 486 | 487 | @Override 488 | public String toString() { 489 | return super.toString(); 490 | } 491 | 492 | @Override 493 | public int hashCode() { 494 | return context.hashCode(); 495 | } 496 | 497 | @Override 498 | public String getName() { 499 | return "WebSocketSessionContext"; 500 | } 501 | 502 | public WebSocketSessionContext getContext() { 503 | return context; 504 | } 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/servlet/WebSocketServletResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.servlet; 18 | 19 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 23 | 24 | import javax.servlet.ServletOutputStream; 25 | import javax.servlet.http.Cookie; 26 | import javax.servlet.http.HttpServletResponse; 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.IOException; 29 | import java.io.PrintWriter; 30 | import java.nio.ByteBuffer; 31 | import java.util.Collection; 32 | import java.util.LinkedList; 33 | import java.util.Locale; 34 | import java.util.Optional; 35 | 36 | 37 | public class WebSocketServletResponse implements HttpServletResponse { 38 | 39 | @SuppressWarnings("unused") 40 | private static final Logger logger = LoggerFactory.getLogger(WebSocketServletResponse.class); 41 | 42 | private final RemoteEndpoint endPoint; 43 | private final long requestId; 44 | private final WebSocketMessageFactory messageFactory; 45 | 46 | private ResponseBuilder responseBuilder = new ResponseBuilder(); 47 | private ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); 48 | private ServletOutputStream servletOutputStream = new BufferingServletOutputStream(responseBody); 49 | private boolean isCommitted = false; 50 | 51 | public WebSocketServletResponse(RemoteEndpoint endPoint, long requestId, 52 | WebSocketMessageFactory messageFactory) 53 | { 54 | this.endPoint = endPoint; 55 | this.requestId = requestId; 56 | this.messageFactory = messageFactory; 57 | 58 | this.responseBuilder.setRequestId(requestId); 59 | } 60 | 61 | @Override 62 | public void addCookie(Cookie cookie) {} 63 | 64 | @Override 65 | public boolean containsHeader(String name) { 66 | return false; 67 | } 68 | 69 | @Override 70 | public String encodeURL(String url) { 71 | return url; 72 | } 73 | 74 | @Override 75 | public String encodeRedirectURL(String url) { 76 | return url; 77 | } 78 | 79 | @Override 80 | public String encodeUrl(String url) { 81 | return url; 82 | } 83 | 84 | @Override 85 | public String encodeRedirectUrl(String url) { 86 | return url; 87 | } 88 | 89 | @Override 90 | public void sendError(int sc, String msg) throws IOException { 91 | setStatus(sc, msg); 92 | } 93 | 94 | @Override 95 | public void sendError(int sc) throws IOException { 96 | setStatus(sc); 97 | } 98 | 99 | @Override 100 | public void sendRedirect(String location) throws IOException { 101 | throw new IOException("Not supported!"); 102 | } 103 | 104 | @Override 105 | public void setDateHeader(String name, long date) {} 106 | 107 | @Override 108 | public void addDateHeader(String name, long date) {} 109 | 110 | @Override 111 | public void setHeader(String name, String value) {} 112 | 113 | @Override 114 | public void addHeader(String name, String value) {} 115 | 116 | @Override 117 | public void setIntHeader(String name, int value) {} 118 | 119 | @Override 120 | public void addIntHeader(String name, int value) {} 121 | 122 | @Override 123 | public void setStatus(int sc) { 124 | setStatus(sc, ""); 125 | } 126 | 127 | @Override 128 | public void setStatus(int sc, String sm) { 129 | this.responseBuilder.setStatusCode(sc); 130 | this.responseBuilder.setMessage(sm); 131 | } 132 | 133 | @Override 134 | public int getStatus() { 135 | return this.responseBuilder.getStatusCode(); 136 | } 137 | 138 | @Override 139 | public String getHeader(String name) { 140 | return null; 141 | } 142 | 143 | @Override 144 | public Collection getHeaders(String name) { 145 | return new LinkedList<>(); 146 | } 147 | 148 | @Override 149 | public Collection getHeaderNames() { 150 | return new LinkedList<>(); 151 | } 152 | 153 | @Override 154 | public String getCharacterEncoding() { 155 | return "UTF-8"; 156 | } 157 | 158 | @Override 159 | public String getContentType() { 160 | return null; 161 | } 162 | 163 | @Override 164 | public ServletOutputStream getOutputStream() throws IOException { 165 | return servletOutputStream; 166 | } 167 | 168 | @Override 169 | public PrintWriter getWriter() throws IOException { 170 | return new PrintWriter(servletOutputStream); 171 | } 172 | 173 | @Override 174 | public void setCharacterEncoding(String charset) {} 175 | 176 | @Override 177 | public void setContentLength(int len) {} 178 | 179 | @Override 180 | public void setContentLengthLong(long len) {} 181 | 182 | @Override 183 | public void setContentType(String type) {} 184 | 185 | @Override 186 | public void setBufferSize(int size) {} 187 | 188 | @Override 189 | public int getBufferSize() { 190 | return 0; 191 | } 192 | 193 | @Override 194 | public void flushBuffer() throws IOException { 195 | if (!isCommitted) { 196 | byte[] body = responseBody.toByteArray(); 197 | 198 | if (body.length <= 0) { 199 | body = null; 200 | } 201 | 202 | byte[] response = messageFactory.createResponse(responseBuilder.getRequestId(), 203 | responseBuilder.getStatusCode(), 204 | responseBuilder.getMessage(), 205 | new LinkedList<>(), 206 | Optional.ofNullable(body)) 207 | .toByteArray(); 208 | 209 | endPoint.sendBytesByFuture(ByteBuffer.wrap(response)); 210 | isCommitted = true; 211 | } 212 | } 213 | 214 | @Override 215 | public void resetBuffer() { 216 | if (isCommitted) throw new IllegalStateException("Buffer already flushed!"); 217 | responseBody.reset(); 218 | } 219 | 220 | @Override 221 | public boolean isCommitted() { 222 | return isCommitted; 223 | } 224 | 225 | @Override 226 | public void reset() { 227 | if (isCommitted) throw new IllegalStateException("Buffer already flushed!"); 228 | responseBuilder = new ResponseBuilder(); 229 | responseBuilder.setRequestId(requestId); 230 | resetBuffer(); 231 | } 232 | 233 | @Override 234 | public void setLocale(Locale loc) {} 235 | 236 | @Override 237 | public Locale getLocale() { 238 | return Locale.US; 239 | } 240 | 241 | private static class ResponseBuilder { 242 | private long requestId; 243 | private int statusCode; 244 | private String message = ""; 245 | 246 | public long getRequestId() { 247 | return requestId; 248 | } 249 | 250 | public void setRequestId(long requestId) { 251 | this.requestId = requestId; 252 | } 253 | 254 | public int getStatusCode() { 255 | return statusCode; 256 | } 257 | 258 | public void setStatusCode(int statusCode) { 259 | this.statusCode = statusCode; 260 | } 261 | 262 | public String getMessage() { 263 | return message; 264 | } 265 | 266 | public void setMessage(String message) { 267 | this.message = message; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/session/WebSocketSession.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.session; 18 | 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | @Retention(RetentionPolicy.RUNTIME) 25 | @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) 26 | public @interface WebSocketSession { 27 | } 28 | 29 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.session; 18 | 19 | import org.whispersystems.websocket.WebSocketClient; 20 | 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | 24 | public class WebSocketSessionContext { 25 | 26 | private final List closeListeners = new LinkedList<>(); 27 | 28 | private final WebSocketClient webSocketClient; 29 | 30 | private Object authenticated; 31 | private boolean closed; 32 | 33 | public WebSocketSessionContext(WebSocketClient webSocketClient) { 34 | this.webSocketClient = webSocketClient; 35 | } 36 | 37 | public void setAuthenticated(Object authenticated) { 38 | this.authenticated = authenticated; 39 | } 40 | 41 | public T getAuthenticated(Class clazz) { 42 | if (authenticated != null && clazz.equals(authenticated.getClass())) { 43 | return clazz.cast(authenticated); 44 | } 45 | 46 | throw new IllegalArgumentException("No authenticated type for: " + clazz + ", we have: " + authenticated); 47 | } 48 | 49 | public Object getAuthenticated() { 50 | return authenticated; 51 | } 52 | 53 | public synchronized void addListener(WebSocketEventListener listener) { 54 | if (!closed) this.closeListeners.add(listener); 55 | else listener.onWebSocketClose(this, 1000, "Closed"); 56 | } 57 | 58 | public WebSocketClient getClient() { 59 | return webSocketClient; 60 | } 61 | 62 | public synchronized void notifyClosed(int statusCode, String reason) { 63 | for (WebSocketEventListener listener : closeListeners) { 64 | listener.onWebSocketClose(this, statusCode, reason); 65 | } 66 | 67 | closed = true; 68 | } 69 | 70 | public interface WebSocketEventListener { 71 | public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason); 72 | } 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContextValueFactoryProvider.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.session; 2 | 3 | import org.glassfish.hk2.api.InjectionResolver; 4 | import org.glassfish.hk2.api.ServiceLocator; 5 | import org.glassfish.hk2.api.TypeLiteral; 6 | import org.glassfish.hk2.utilities.binding.AbstractBinder; 7 | import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; 8 | import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; 9 | import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; 10 | import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; 11 | import org.glassfish.jersey.server.model.Parameter; 12 | import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; 13 | import org.whispersystems.websocket.servlet.WebSocketServletRequest; 14 | 15 | import javax.inject.Inject; 16 | import javax.inject.Singleton; 17 | import java.security.Principal; 18 | 19 | 20 | @Singleton 21 | public class WebSocketSessionContextValueFactoryProvider extends AbstractValueFactoryProvider { 22 | 23 | @Inject 24 | public WebSocketSessionContextValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, 25 | ServiceLocator injector) 26 | { 27 | super(mpep, injector, Parameter.Source.UNKNOWN); 28 | } 29 | 30 | @Override 31 | public AbstractContainerRequestValueFactory createValueFactory(Parameter parameter) { 32 | if (parameter.getAnnotation(WebSocketSession.class) == null) { 33 | return null; 34 | } 35 | 36 | return new AbstractContainerRequestValueFactory() { 37 | 38 | public WebSocketSessionContext provide() { 39 | Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal(); 40 | 41 | if (principal == null) { 42 | throw new IllegalStateException("Cannot inject a custom principal into unauthenticated request"); 43 | } 44 | 45 | if (!(principal instanceof WebSocketServletRequest.ContextPrincipal)) { 46 | throw new IllegalArgumentException("Cannot inject a non-WebSocket AuthPrincipal into request"); 47 | } 48 | 49 | return ((WebSocketServletRequest.ContextPrincipal)principal).getContext(); 50 | } 51 | }; 52 | } 53 | 54 | @Singleton 55 | private static class WebSocketSessionInjectionResolver extends ParamInjectionResolver { 56 | public WebSocketSessionInjectionResolver() { 57 | super(WebSocketSessionContextValueFactoryProvider.class); 58 | } 59 | } 60 | 61 | public static class Binder extends AbstractBinder { 62 | 63 | public Binder() { 64 | } 65 | 66 | @Override 67 | protected void configure() { 68 | bind(WebSocketSessionContextValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); 69 | bind(WebSocketSessionInjectionResolver.class).to(new TypeLiteral>() { 70 | }).in(Singleton.class); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/setup/WebSocketConnectListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.setup; 18 | 19 | import org.whispersystems.websocket.session.WebSocketSessionContext; 20 | 21 | public interface WebSocketConnectListener { 22 | public void onWebSocketConnect(WebSocketSessionContext context); 23 | } 24 | -------------------------------------------------------------------------------- /library/src/main/java/org/whispersystems/websocket/setup/WebSocketEnvironment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package org.whispersystems.websocket.setup; 18 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import org.eclipse.jetty.server.RequestLog; 21 | import org.glassfish.jersey.servlet.ServletContainer; 22 | import org.whispersystems.websocket.auth.WebSocketAuthenticator; 23 | import org.whispersystems.websocket.configuration.WebSocketConfiguration; 24 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 25 | import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; 26 | 27 | import javax.servlet.http.HttpServlet; 28 | import javax.validation.Validator; 29 | 30 | import io.dropwizard.jersey.DropwizardResourceConfig; 31 | import io.dropwizard.jersey.setup.JerseyContainerHolder; 32 | import io.dropwizard.jersey.setup.JerseyEnvironment; 33 | import io.dropwizard.setup.Environment; 34 | 35 | public class WebSocketEnvironment { 36 | 37 | private final JerseyContainerHolder jerseyServletContainer; 38 | private final JerseyEnvironment jerseyEnvironment; 39 | private final ObjectMapper objectMapper; 40 | private final Validator validator; 41 | private final RequestLog requestLog; 42 | private final long idleTimeoutMillis; 43 | 44 | private WebSocketAuthenticator authenticator; 45 | private WebSocketMessageFactory messageFactory; 46 | private WebSocketConnectListener connectListener; 47 | 48 | public WebSocketEnvironment(Environment environment, WebSocketConfiguration configuration) { 49 | this(environment, configuration, 60000); 50 | } 51 | 52 | public WebSocketEnvironment(Environment environment, WebSocketConfiguration configuration, long idleTimeoutMillis) { 53 | this(environment, configuration.getRequestLog().build("websocket"), idleTimeoutMillis); 54 | } 55 | 56 | public WebSocketEnvironment(Environment environment, RequestLog requestLog, long idleTimeoutMillis) { 57 | DropwizardResourceConfig jerseyConfig = new DropwizardResourceConfig(environment.metrics()); 58 | 59 | this.objectMapper = environment.getObjectMapper(); 60 | this.validator = environment.getValidator(); 61 | this.requestLog = requestLog; 62 | this.jerseyServletContainer = new JerseyContainerHolder(new ServletContainer(jerseyConfig) ); 63 | this.jerseyEnvironment = new JerseyEnvironment(jerseyServletContainer, jerseyConfig); 64 | this.messageFactory = new ProtobufWebSocketMessageFactory(); 65 | this.idleTimeoutMillis = idleTimeoutMillis; 66 | } 67 | 68 | public JerseyEnvironment jersey() { 69 | return jerseyEnvironment; 70 | } 71 | 72 | public WebSocketAuthenticator getAuthenticator() { 73 | return authenticator; 74 | } 75 | 76 | public void setAuthenticator(WebSocketAuthenticator authenticator) { 77 | this.authenticator = authenticator; 78 | } 79 | 80 | public long getIdleTimeoutMillis() { 81 | return idleTimeoutMillis; 82 | } 83 | 84 | public ObjectMapper getObjectMapper() { 85 | return objectMapper; 86 | } 87 | 88 | public RequestLog getRequestLog() { 89 | return requestLog; 90 | } 91 | 92 | public Validator getValidator() { 93 | return validator; 94 | } 95 | 96 | public HttpServlet getJerseyServletContainer() { 97 | return (HttpServlet)jerseyServletContainer.getContainer(); 98 | } 99 | 100 | public WebSocketMessageFactory getMessageFactory() { 101 | return messageFactory; 102 | } 103 | 104 | public void setMessageFactory(WebSocketMessageFactory messageFactory) { 105 | this.messageFactory = messageFactory; 106 | } 107 | 108 | public WebSocketConnectListener getConnectListener() { 109 | return connectListener; 110 | } 111 | 112 | public void setConnectListener(WebSocketConnectListener connectListener) { 113 | this.connectListener = connectListener; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /library/src/test/java/org/whispersystems/websocket/LoggableRequestResponseTest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket; 2 | 3 | import org.eclipse.jetty.server.AbstractNCSARequestLog; 4 | import org.eclipse.jetty.server.NCSARequestLog; 5 | import org.eclipse.jetty.server.RequestLog; 6 | import org.eclipse.jetty.util.component.AbstractLifeCycle; 7 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 8 | import org.junit.Test; 9 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 10 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 11 | import org.whispersystems.websocket.servlet.LoggableRequest; 12 | import org.whispersystems.websocket.servlet.LoggableResponse; 13 | import org.whispersystems.websocket.servlet.WebSocketServletRequest; 14 | import org.whispersystems.websocket.servlet.WebSocketServletResponse; 15 | import org.whispersystems.websocket.session.WebSocketSessionContext; 16 | 17 | import javax.servlet.ServletContext; 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.servlet.http.HttpServletResponse; 20 | 21 | import java.util.HashMap; 22 | import java.util.Optional; 23 | 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.when; 26 | 27 | public class LoggableRequestResponseTest { 28 | 29 | @Test 30 | public void testLogging() { 31 | NCSARequestLog requestLog = new EnabledNCSARequestLog(); 32 | 33 | WebSocketClient webSocketClient = mock(WebSocketClient.class ); 34 | WebSocketRequestMessage requestMessage = mock(WebSocketRequestMessage.class); 35 | ServletContext servletContext = mock(ServletContext.class ); 36 | RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class ); 37 | WebSocketMessageFactory messageFactory = mock(WebSocketMessageFactory.class); 38 | 39 | when(requestMessage.getVerb()).thenReturn("GET"); 40 | when(requestMessage.getBody()).thenReturn(Optional.empty()); 41 | when(requestMessage.getHeaders()).thenReturn(new HashMap<>()); 42 | when(requestMessage.getPath()).thenReturn("/api/v1/test"); 43 | when(requestMessage.getRequestId()).thenReturn(1L); 44 | when(requestMessage.hasRequestId()).thenReturn(true); 45 | 46 | WebSocketSessionContext sessionContext = new WebSocketSessionContext (webSocketClient ); 47 | HttpServletRequest servletRequest = new WebSocketServletRequest (sessionContext, requestMessage, servletContext); 48 | HttpServletResponse servletResponse = new WebSocketServletResponse(remoteEndpoint, 1, messageFactory ); 49 | 50 | LoggableRequest loggableRequest = new LoggableRequest (servletRequest ); 51 | LoggableResponse loggableResponse = new LoggableResponse(servletResponse); 52 | 53 | requestLog.log(loggableRequest, loggableResponse); 54 | } 55 | 56 | 57 | private class EnabledNCSARequestLog extends NCSARequestLog { 58 | @Override 59 | public boolean isEnabled() { 60 | return true; 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /library/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket; 2 | 3 | 4 | import org.eclipse.jetty.websocket.api.Session; 5 | import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; 6 | import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; 7 | import org.junit.Test; 8 | import org.whispersystems.websocket.auth.AuthenticationException; 9 | import org.whispersystems.websocket.auth.WebSocketAuthenticator; 10 | import org.whispersystems.websocket.setup.WebSocketEnvironment; 11 | 12 | import javax.servlet.ServletException; 13 | import java.io.IOException; 14 | import java.util.Optional; 15 | 16 | import io.dropwizard.jersey.setup.JerseyEnvironment; 17 | import static org.junit.Assert.*; 18 | import static org.mockito.Matchers.eq; 19 | import static org.mockito.Mockito.*; 20 | 21 | public class WebSocketResourceProviderFactoryTest { 22 | 23 | @Test 24 | public void testUnauthorized() throws ServletException, AuthenticationException, IOException { 25 | JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class ); 26 | WebSocketEnvironment environment = mock(WebSocketEnvironment.class ); 27 | WebSocketAuthenticator authenticator = mock(WebSocketAuthenticator.class); 28 | ServletUpgradeRequest request = mock(ServletUpgradeRequest.class ); 29 | ServletUpgradeResponse response = mock(ServletUpgradeResponse.class); 30 | 31 | when(environment.getAuthenticator()).thenReturn(authenticator); 32 | when(authenticator.authenticate(eq(request))).thenReturn(new WebSocketAuthenticator.AuthenticationResult<>(Optional.empty(), true)); 33 | when(environment.jersey()).thenReturn(jerseyEnvironment); 34 | 35 | WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory(environment); 36 | Object connection = factory.createWebSocket(request, response); 37 | 38 | assertNull(connection); 39 | verify(response).sendForbidden(eq("Unauthorized")); 40 | verify(authenticator).authenticate(eq(request)); 41 | } 42 | 43 | @Test 44 | public void testValidAuthorization() throws AuthenticationException, ServletException { 45 | JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class ); 46 | WebSocketEnvironment environment = mock(WebSocketEnvironment.class ); 47 | WebSocketAuthenticator authenticator = mock(WebSocketAuthenticator.class); 48 | ServletUpgradeRequest request = mock(ServletUpgradeRequest.class ); 49 | ServletUpgradeResponse response = mock(ServletUpgradeResponse.class); 50 | Session session = mock(Session.class ); 51 | Account account = new Account(); 52 | 53 | when(environment.getAuthenticator()).thenReturn(authenticator); 54 | when(authenticator.authenticate(eq(request))).thenReturn(new WebSocketAuthenticator.AuthenticationResult<>(Optional.of(account), true)); 55 | when(environment.jersey()).thenReturn(jerseyEnvironment); 56 | 57 | WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory(environment); 58 | Object connection = factory.createWebSocket(request, response); 59 | 60 | assertNotNull(connection); 61 | verifyNoMoreInteractions(response); 62 | verify(authenticator).authenticate(eq(request)); 63 | 64 | ((WebSocketResourceProvider)connection).onWebSocketConnect(session); 65 | 66 | assertNotNull(((WebSocketResourceProvider) connection).getContext().getAuthenticated()); 67 | assertEquals(((WebSocketResourceProvider)connection).getContext().getAuthenticated(), account); 68 | } 69 | 70 | private static class Account {} 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /library/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderTest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket; 2 | 3 | import org.eclipse.jetty.server.RequestLog; 4 | import org.eclipse.jetty.websocket.api.CloseStatus; 5 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 6 | import org.eclipse.jetty.websocket.api.Session; 7 | import org.eclipse.jetty.websocket.api.UpgradeRequest; 8 | import org.junit.Test; 9 | import org.mockito.ArgumentCaptor; 10 | import org.whispersystems.websocket.WebSocketResourceProvider; 11 | import org.whispersystems.websocket.auth.AuthenticationException; 12 | import org.whispersystems.websocket.auth.WebSocketAuthenticator; 13 | import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; 14 | import org.whispersystems.websocket.setup.WebSocketConnectListener; 15 | 16 | import javax.servlet.http.HttpServlet; 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.io.IOException; 20 | import java.util.LinkedList; 21 | import java.util.Optional; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.mockito.Mockito.*; 25 | 26 | public class WebSocketResourceProviderTest { 27 | 28 | @Test 29 | public void testOnConnect() throws AuthenticationException, IOException { 30 | HttpServlet contextHandler = mock(HttpServlet.class); 31 | WebSocketAuthenticator authenticator = mock(WebSocketAuthenticator.class); 32 | RequestLog requestLog = mock(RequestLog.class); 33 | WebSocketResourceProvider provider = new WebSocketResourceProvider(contextHandler, requestLog, 34 | null, 35 | new ProtobufWebSocketMessageFactory(), 36 | Optional.empty(), 37 | 30000); 38 | 39 | Session session = mock(Session.class ); 40 | UpgradeRequest request = mock(UpgradeRequest.class); 41 | 42 | when(session.getUpgradeRequest()).thenReturn(request); 43 | when(authenticator.authenticate(request)).thenReturn(new WebSocketAuthenticator.AuthenticationResult<>(Optional.of("fooz"), true)); 44 | 45 | provider.onWebSocketConnect(session); 46 | 47 | verify(session, never()).close(anyInt(), anyString()); 48 | verify(session, never()).close(); 49 | verify(session, never()).close(any(CloseStatus.class)); 50 | } 51 | 52 | @Test 53 | public void testRouteMessage() throws Exception { 54 | HttpServlet servlet = mock(HttpServlet.class ); 55 | WebSocketAuthenticator authenticator = mock(WebSocketAuthenticator.class); 56 | RequestLog requestLog = mock(RequestLog.class ); 57 | WebSocketResourceProvider provider = new WebSocketResourceProvider(servlet, requestLog, Optional.of((WebSocketAuthenticator)authenticator), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); 58 | 59 | Session session = mock(Session.class ); 60 | RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); 61 | UpgradeRequest request = mock(UpgradeRequest.class); 62 | 63 | when(session.getUpgradeRequest()).thenReturn(request); 64 | when(session.getRemote()).thenReturn(remoteEndpoint); 65 | when(authenticator.authenticate(request)).thenReturn(new WebSocketAuthenticator.AuthenticationResult<>(Optional.of("foo"), true)); 66 | 67 | provider.onWebSocketConnect(session); 68 | 69 | verify(session, never()).close(anyInt(), anyString()); 70 | verify(session, never()).close(); 71 | verify(session, never()).close(any(CloseStatus.class)); 72 | 73 | byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/bar", new LinkedList(), Optional.of("hello world!".getBytes())).toByteArray(); 74 | 75 | provider.onWebSocketBinary(message, 0, message.length); 76 | 77 | ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); 78 | 79 | verify(servlet).service(requestCaptor.capture(), any(HttpServletResponse.class)); 80 | 81 | HttpServletRequest bundledRequest = requestCaptor.getValue(); 82 | 83 | byte[] expected = new byte[bundledRequest.getInputStream().available()]; 84 | int read = bundledRequest.getInputStream().read(expected); 85 | 86 | assertThat(read).isEqualTo(expected.length); 87 | assertThat(new String(expected)).isEqualTo("hello world!"); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 5 | 4.0.0 6 | 7 | org.whispersystems 8 | parent 9 | 0.5.9 10 | 11 | pom 12 | 13 | Dropwizard Websocket Resources 14 | 15 | 16 | library 17 | sample-server 18 | sample-client 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.apache.maven.plugins 26 | maven-compiler-plugin 27 | 28 | 1.8 29 | 1.8 30 | 31 | 32 | 33 | org.apache.maven.plugins 34 | maven-deploy-plugin 35 | 2.4 36 | 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ossrh 47 | https://oss.sonatype.org/content/repositories/snapshots 48 | 49 | 50 | ossrh 51 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /sample-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 3.0.0 9 | 10 | 11 | org.whispersystems 12 | sample-client 13 | 0.5.10 14 | 15 | WebSocket-Resources Sample Client Project 16 | https://github.com/WhisperSystems/WebSocket-Resources 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | AGPLv3 25 | https://www.gnu.org/licenses/agpl-3.0.html 26 | repo 27 | 28 | 29 | 30 | 31 | 32 | Moxie Marlinspike 33 | 34 | 35 | 36 | 37 | https://github.com/WhisperSystems/WebSocket-Resources 38 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 39 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 40 | 41 | 42 | 43 | 44 | org.whispersystems 45 | websocket-resources 46 | 0.5.10 47 | 48 | 49 | org.eclipse.jetty.websocket 50 | websocket-client 51 | 9.4.14.v20181114 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-compiler-plugin 61 | 62 | 1.8 63 | 1.8 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-source-plugin 69 | 2.2.1 70 | 71 | 72 | attach-sources 73 | 74 | jar 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-jar-plugin 82 | 2.4 83 | 84 | 85 | 86 | true 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-shade-plugin 94 | 1.6 95 | 96 | true 97 | 98 | 99 | *:* 100 | 101 | META-INF/*.SF 102 | META-INF/*.DSA 103 | META-INF/*.RSA 104 | 105 | 106 | 107 | 108 | 109 | 110 | package 111 | 112 | shade 113 | 114 | 115 | 116 | 117 | 118 | org.whispersystems.websocket.client.Client 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ossrh 131 | https://oss.sonatype.org/content/repositories/snapshots 132 | 133 | 134 | ossrh 135 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /sample-client/src/main/java/org/whispersystems/websocket/client/Client.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.client; 2 | 3 | import org.eclipse.jetty.util.log.Log; 4 | import org.eclipse.jetty.util.log.StdErrLog; 5 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 6 | import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; 7 | import org.eclipse.jetty.websocket.client.WebSocketClient; 8 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 9 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 10 | 11 | import java.io.IOException; 12 | import java.net.URI; 13 | import java.util.logging.Handler; 14 | import java.util.logging.Level; 15 | import java.util.logging.LogManager; 16 | import java.util.logging.Logger; 17 | 18 | @WebSocket(maxTextMessageSize = 64 * 1024) 19 | public class Client implements WebSocketInterface.Listener { 20 | 21 | private final WebSocketInterface webSocket; 22 | 23 | public Client(WebSocketInterface webSocket) { 24 | this.webSocket = webSocket; 25 | } 26 | 27 | public static void main(String[] argv) { 28 | WebSocketClient holder = new WebSocketClient(); 29 | WebSocketInterface webSocket = new WebSocketInterface(); 30 | Client client = new Client(webSocket); 31 | 32 | StdErrLog logger = new StdErrLog(); 33 | logger.setLevel(StdErrLog.LEVEL_OFF); 34 | Log.setLog(logger); 35 | 36 | try { 37 | webSocket.setListener(client); 38 | holder.start(); 39 | 40 | URI uri = new URI("ws://localhost:8080/websocket/?login=moxie&password=insecure"); 41 | ClientUpgradeRequest request = new ClientUpgradeRequest(); 42 | holder.connect(webSocket, uri, request); 43 | 44 | System.out.printf("Connecting..."); 45 | Thread.sleep(10000); 46 | } catch (Throwable t) { 47 | t.printStackTrace(); 48 | } 49 | } 50 | 51 | @Override 52 | public void onReceivedRequest(WebSocketRequestMessage requestMessage) { 53 | System.err.println("Got request"); 54 | 55 | try { 56 | webSocket.sendResponse(requestMessage.getRequestId(), 200, "OK", "world!".getBytes()); 57 | } catch (IOException e) { 58 | e.printStackTrace(); 59 | } 60 | } 61 | 62 | @Override 63 | public void onReceivedResponse(WebSocketResponseMessage responseMessage) { 64 | System.err.println("Got response: " + responseMessage.getStatus()); 65 | 66 | if (responseMessage.getBody().isPresent()) { 67 | System.err.println("Got response body: " + new String(responseMessage.getBody().get())); 68 | } 69 | } 70 | 71 | @Override 72 | public void onClosed() { 73 | System.err.println("onClosed()"); 74 | } 75 | 76 | @Override 77 | public void onConnected() { 78 | try { 79 | webSocket.sendRequest(1, "GET", "/hello"); 80 | webSocket.sendRequest(2, "GET", "/hello/named"); 81 | webSocket.sendRequest(3, "GET", "/hello/prompt"); 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /sample-client/src/main/java/org/whispersystems/websocket/client/WebSocketInterface.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.client; 2 | 3 | import org.eclipse.jetty.websocket.api.Session; 4 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; 5 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 6 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 7 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 8 | import org.whispersystems.websocket.messages.InvalidMessageException; 9 | import org.whispersystems.websocket.messages.WebSocketMessage; 10 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 11 | import org.whispersystems.websocket.messages.WebSocketRequestMessage; 12 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 13 | import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; 14 | 15 | import java.io.IOException; 16 | import java.nio.ByteBuffer; 17 | import java.util.LinkedList; 18 | import java.util.Optional; 19 | 20 | @WebSocket(maxTextMessageSize = 64 * 1024) 21 | public class WebSocketInterface { 22 | 23 | private final WebSocketMessageFactory factory = new ProtobufWebSocketMessageFactory(); 24 | 25 | private Listener listener; 26 | private Session session; 27 | 28 | public WebSocketInterface() {} 29 | 30 | public void setListener(Listener listener) { 31 | this.listener = listener; 32 | } 33 | 34 | @OnWebSocketClose 35 | public void onClose(int statusCode, String reason) { 36 | listener.onClosed(); 37 | } 38 | 39 | @OnWebSocketConnect 40 | public void onConnect(Session session) { 41 | this.session = session; 42 | listener.onConnected(); 43 | } 44 | 45 | @OnWebSocketMessage 46 | public void onMessage(byte[] buffer, int offset, int length) { 47 | try { 48 | WebSocketMessage message = factory.parseMessage(buffer, offset, length); 49 | 50 | if (message.getType() == WebSocketMessage.Type.REQUEST_MESSAGE) { 51 | listener.onReceivedRequest(message.getRequestMessage()); 52 | } else if (message.getType() == WebSocketMessage.Type.RESPONSE_MESSAGE) { 53 | listener.onReceivedResponse(message.getResponseMessage()); 54 | } else { 55 | System.out.println("Received websocket message of unknown type: " + message.getType()); 56 | } 57 | 58 | } catch (InvalidMessageException e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | 63 | public void sendRequest(long id, String verb, String path) throws IOException { 64 | WebSocketMessage message = factory.createRequest(Optional.of(id), verb, path, new LinkedList(), Optional.empty()); 65 | session.getRemote().sendBytes(ByteBuffer.wrap(message.toByteArray())); 66 | } 67 | 68 | public void sendResponse(long id, int code, String message, byte[] body) throws IOException { 69 | WebSocketMessage response = factory.createResponse(id, code, message, new LinkedList(), Optional.ofNullable(body)); 70 | session.getRemote().sendBytes(ByteBuffer.wrap(response.toByteArray())); 71 | } 72 | 73 | public interface Listener { 74 | public void onReceivedRequest(WebSocketRequestMessage requestMessage); 75 | public void onReceivedResponse(WebSocketResponseMessage responseMessage); 76 | public void onClosed(); 77 | public void onConnected(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sample-server/config/config.yml: -------------------------------------------------------------------------------- 1 | helloResponse: world 2 | -------------------------------------------------------------------------------- /sample-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 3.0.0 9 | 10 | 11 | org.whispersystems 12 | sample-server 13 | 0.5.10 14 | 15 | WebSocket-Resources Sample Server Project 16 | https://github.com/WhisperSystems/WebSocket-Resources 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | AGPLv3 25 | https://www.gnu.org/licenses/agpl-3.0.html 26 | repo 27 | 28 | 29 | 30 | 31 | 32 | Moxie Marlinspike 33 | 34 | 35 | 36 | 37 | https://github.com/WhisperSystems/WebSocket-Resources 38 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 39 | scm:git:https://github.com/WhisperSystems/WebSocket-Resources.git 40 | 41 | 42 | 43 | 44 | org.whispersystems 45 | websocket-resources 46 | 0.5.10 47 | 48 | 49 | org.whispersystems 50 | dropwizard-simpleauth 51 | 0.4.0 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-compiler-plugin 61 | 62 | 1.8 63 | 1.8 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-source-plugin 69 | 2.2.1 70 | 71 | 72 | attach-sources 73 | 74 | jar 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-jar-plugin 82 | 2.4 83 | 84 | 85 | 86 | true 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-shade-plugin 94 | 1.6 95 | 96 | true 97 | 98 | 99 | *:* 100 | 101 | META-INF/*.SF 102 | META-INF/*.DSA 103 | META-INF/*.RSA 104 | 105 | 106 | 107 | 108 | 109 | 110 | package 111 | 112 | shade 113 | 114 | 115 | 116 | 117 | 118 | org.whispersystems.websocket.sample.Server 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ossrh 131 | https://oss.sonatype.org/content/repositories/snapshots 132 | 133 | 134 | ossrh 135 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/Server.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample; 2 | 3 | import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature; 4 | import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; 5 | import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter; 6 | import org.whispersystems.websocket.WebSocketResourceProviderFactory; 7 | import org.whispersystems.websocket.sample.auth.HelloAccount; 8 | import org.whispersystems.websocket.sample.auth.HelloAccountBasicAuthenticator; 9 | import org.whispersystems.websocket.sample.auth.HelloAccountWebSocketAuthenticator; 10 | import org.whispersystems.websocket.sample.resources.HelloResource; 11 | import org.whispersystems.websocket.setup.WebSocketEnvironment; 12 | 13 | import javax.servlet.ServletRegistration; 14 | 15 | import io.dropwizard.Application; 16 | import io.dropwizard.setup.Environment; 17 | 18 | public class Server extends Application { 19 | 20 | @Override 21 | public void run(ServerConfiguration serverConfiguration, Environment environment) 22 | throws Exception 23 | { 24 | WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, serverConfiguration.getWebSocketConfiguration()); 25 | HelloResource helloResource = new HelloResource(serverConfiguration.getHelloResponse()); 26 | HelloAccountBasicAuthenticator helloBasicAuthenticator = new HelloAccountBasicAuthenticator(); 27 | 28 | 29 | environment.jersey().register(helloResource); 30 | environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder() 31 | .setAuthenticator(helloBasicAuthenticator) 32 | .setPrincipal(HelloAccount.class) 33 | .buildAuthFilter())); 34 | environment.jersey().register(new AuthValueFactoryProvider.Binder()); 35 | 36 | webSocketEnvironment.jersey().register(helloResource); 37 | webSocketEnvironment.setAuthenticator(new HelloAccountWebSocketAuthenticator(helloBasicAuthenticator)); 38 | 39 | WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment); 40 | ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet); 41 | 42 | websocket.addMapping("/websocket/*"); 43 | websocket.setAsyncSupported(true); 44 | servlet.start(); 45 | } 46 | 47 | public static void main(String[] args) throws Exception { 48 | new Server().run(args); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/ServerConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.hibernate.validator.constraints.NotEmpty; 5 | import org.whispersystems.websocket.configuration.WebSocketConfiguration; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotNull; 9 | 10 | import io.dropwizard.Configuration; 11 | 12 | public class ServerConfiguration extends Configuration { 13 | 14 | @NotEmpty 15 | private String helloResponse = "world!"; 16 | 17 | @Valid 18 | @NotNull 19 | @JsonProperty 20 | private WebSocketConfiguration webSocket = new WebSocketConfiguration(); 21 | 22 | public String getHelloResponse() { 23 | return helloResponse; 24 | } 25 | 26 | public WebSocketConfiguration getWebSocketConfiguration() { 27 | return webSocket; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/auth/HelloAccount.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample.auth; 2 | 3 | public class HelloAccount { 4 | 5 | private String username; 6 | 7 | public HelloAccount(String username) { 8 | this.username = username; 9 | } 10 | 11 | public String getUsername() { 12 | return username; 13 | } 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/auth/HelloAccountBasicAuthenticator.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample.auth; 2 | 3 | import org.whispersystems.dropwizard.simpleauth.Authenticator; 4 | 5 | import java.util.Optional; 6 | 7 | import io.dropwizard.auth.AuthenticationException; 8 | import io.dropwizard.auth.basic.BasicCredentials; 9 | 10 | public class HelloAccountBasicAuthenticator implements Authenticator { 11 | @Override 12 | public Optional authenticate(BasicCredentials credentials) 13 | throws AuthenticationException 14 | { 15 | if (credentials.getUsername().equals("moxie") && 16 | credentials.getPassword().equals("insecure")) 17 | { 18 | return Optional.of(new HelloAccount("moxie")); 19 | } 20 | 21 | return Optional.empty(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/auth/HelloAccountWebSocketAuthenticator.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample.auth; 2 | 3 | import org.eclipse.jetty.websocket.api.UpgradeRequest; 4 | import org.whispersystems.websocket.auth.AuthenticationException; 5 | import org.whispersystems.websocket.auth.WebSocketAuthenticator; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | 11 | import io.dropwizard.auth.basic.BasicCredentials; 12 | 13 | public class HelloAccountWebSocketAuthenticator implements WebSocketAuthenticator { 14 | 15 | private final HelloAccountBasicAuthenticator basicAuthenticator; 16 | 17 | public HelloAccountWebSocketAuthenticator(HelloAccountBasicAuthenticator basicAuthenticator) { 18 | this.basicAuthenticator = basicAuthenticator; 19 | } 20 | 21 | @Override 22 | public AuthenticationResult authenticate(UpgradeRequest request) 23 | throws AuthenticationException 24 | { 25 | try { 26 | Map> parameters = request.getParameterMap(); 27 | List usernames = parameters.get("login"); 28 | List passwords = parameters.get("password"); 29 | 30 | if (usernames == null || usernames.size() == 0 || 31 | passwords == null || passwords.size() == 0) 32 | { 33 | return new AuthenticationResult<>(Optional.empty(), false); 34 | } 35 | 36 | BasicCredentials credentials = new BasicCredentials(usernames.get(0), passwords.get(0)); 37 | return new AuthenticationResult<>(basicAuthenticator.authenticate(credentials), true); 38 | } catch (io.dropwizard.auth.AuthenticationException e) { 39 | throw new AuthenticationException(e); 40 | } 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /sample-server/src/main/java/org/whispersystems/websocket/sample/resources/HelloResource.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket.sample.resources; 2 | 3 | import com.codahale.metrics.annotation.Timed; 4 | import com.google.common.util.concurrent.FutureCallback; 5 | import com.google.common.util.concurrent.Futures; 6 | import com.google.common.util.concurrent.ListenableFuture; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 11 | import org.whispersystems.websocket.sample.auth.HelloAccount; 12 | import org.whispersystems.websocket.session.WebSocketSession; 13 | import org.whispersystems.websocket.session.WebSocketSessionContext; 14 | 15 | import javax.ws.rs.GET; 16 | import javax.ws.rs.Path; 17 | import javax.ws.rs.Produces; 18 | import javax.ws.rs.core.Response; 19 | 20 | import java.util.LinkedList; 21 | import java.util.Optional; 22 | 23 | import io.dropwizard.auth.Auth; 24 | 25 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 26 | @Path("/hello") 27 | public class HelloResource { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(HelloResource.class); 30 | 31 | private final String response; 32 | 33 | public HelloResource(String response) { 34 | this.response = response; 35 | } 36 | 37 | @GET 38 | @Timed 39 | @Produces("text/plain") 40 | public String sayHello() { 41 | return response; 42 | } 43 | 44 | @GET 45 | @Path("/named") 46 | @Timed 47 | @Produces("text/plain") 48 | public String saySpecialHello(@Auth HelloAccount account) { 49 | return "Hello " + account.getUsername(); 50 | } 51 | 52 | @GET 53 | @Path("/optional") 54 | @Timed 55 | @Produces("text/plain") 56 | public String sayOptionalHello(@Auth Optional account) { 57 | if (account.isPresent()) return account.get().getUsername(); 58 | else return "missing"; 59 | } 60 | 61 | @GET 62 | @Path("/prompt") 63 | public Response askMe(@Auth HelloAccount account, 64 | @WebSocketSession WebSocketSessionContext context) 65 | { 66 | ListenableFuture response = context.getClient().sendRequest("GET", "/hello", new LinkedList<>(), Optional.empty()); 67 | Futures.addCallback(response, new FutureCallback() { 68 | @Override 69 | public void onSuccess(WebSocketResponseMessage result) { 70 | logger.warn("Got response: " + new String(result.getBody().orElse(null))); 71 | } 72 | 73 | @Override 74 | public void onFailure(Throwable t) { 75 | logger.warn("Request error", t); 76 | } 77 | }); 78 | 79 | return Response.ok().build(); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /sample-server/src/test/java/org/whispersystems/websocket/HelloServerTest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket; 2 | 3 | import org.eclipse.jetty.websocket.api.UpgradeException; 4 | import org.junit.ClassRule; 5 | import org.junit.Test; 6 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 7 | import org.whispersystems.websocket.sample.Server; 8 | import org.whispersystems.websocket.sample.ServerConfiguration; 9 | 10 | import java.net.URI; 11 | import java.util.concurrent.ExecutionException; 12 | 13 | import io.dropwizard.testing.junit.DropwizardAppRule; 14 | import static junit.framework.TestCase.assertTrue; 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertFalse; 17 | 18 | public class HelloServerTest { 19 | 20 | @ClassRule 21 | public static final DropwizardAppRule RULE = new DropwizardAppRule<>(Server.class, new ServerConfiguration()); 22 | 23 | @Test 24 | public void testAuthenticatedQueries() throws Exception { 25 | SynchronousClient client = new SynchronousClient(new URI("ws://localhost:" + RULE.getLocalPort() + "/websocket/?login=moxie&password=insecure")); 26 | client.waitForConnected(5000); 27 | 28 | long requestId = client.sendRequest("GET", "/hello/named"); 29 | WebSocketResponseMessage response = client.readResponse(requestId, 5000); 30 | 31 | assertEquals(response.getStatus(), 200); 32 | assertTrue(response.getBody().isPresent()); 33 | assertEquals(new String(response.getBody().get()), "Hello moxie"); 34 | 35 | long optionalRequestId = client.sendRequest("GET", "/hello/optional"); 36 | WebSocketResponseMessage optionalResponse = client.readResponse(optionalRequestId, 5000); 37 | 38 | assertEquals(200, optionalResponse.getStatus()); 39 | assertTrue(optionalResponse.getBody().isPresent()); 40 | assertEquals(new String(optionalResponse.getBody().get()), "moxie"); 41 | } 42 | 43 | @Test 44 | public void testBadAuthentication() throws Exception { 45 | try { 46 | SynchronousClient client = new SynchronousClient(new URI("ws://localhost:" + RULE.getLocalPort() + "/websocket/?login=moxie&password=wrongpassword")); 47 | client.waitForConnected(5000); 48 | } catch (ExecutionException e) { 49 | assertTrue(e.getCause() instanceof UpgradeException); 50 | assertEquals(((UpgradeException)e.getCause()).getResponseStatusCode(), 403); 51 | return; 52 | } 53 | 54 | throw new AssertionError("authenticated"); 55 | } 56 | 57 | @Test 58 | public void testMissingAuthentication() throws Exception { 59 | SynchronousClient client = new SynchronousClient(new URI("ws://localhost:" + RULE.getLocalPort() + "/websocket/")); 60 | client.waitForConnected(5000); 61 | 62 | long requestId = client.sendRequest("GET", "/hello"); 63 | WebSocketResponseMessage response = client.readResponse(requestId, 5000); 64 | 65 | assertEquals(response.getStatus(), 200); 66 | assertTrue(response.getBody().isPresent()); 67 | assertEquals(new String(response.getBody().get()), "world!"); 68 | 69 | long badRequest = client.sendRequest("GET", "/hello/named"); 70 | WebSocketResponseMessage badResponse = client.readResponse(badRequest, 5000); 71 | 72 | assertEquals(401, badResponse.getStatus()); 73 | assertFalse(badResponse.getBody().isPresent()); 74 | 75 | long optionalRequest = client.sendRequest("GET", "/hello/optional"); 76 | WebSocketResponseMessage optionalResponse = client.readResponse(optionalRequest, 5000); 77 | 78 | assertEquals(200, optionalResponse.getStatus()); 79 | assertTrue(optionalResponse.getBody().isPresent()); 80 | assertEquals("missing", new String(optionalResponse.getBody().get())); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /sample-server/src/test/java/org/whispersystems/websocket/SynchronousClient.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.websocket; 2 | 3 | import com.google.common.util.concurrent.SettableFuture; 4 | import org.eclipse.jetty.websocket.api.Session; 5 | import org.eclipse.jetty.websocket.api.UpgradeRequest; 6 | import org.eclipse.jetty.websocket.api.UpgradeResponse; 7 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; 8 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 9 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; 10 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 11 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 12 | import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; 13 | import org.eclipse.jetty.websocket.client.WebSocketClient; 14 | import org.eclipse.jetty.websocket.client.io.UpgradeListener; 15 | import org.whispersystems.websocket.messages.InvalidMessageException; 16 | import org.whispersystems.websocket.messages.WebSocketMessage; 17 | import org.whispersystems.websocket.messages.WebSocketMessageFactory; 18 | import org.whispersystems.websocket.messages.WebSocketResponseMessage; 19 | import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; 20 | 21 | import java.io.IOException; 22 | import java.net.URI; 23 | import java.nio.ByteBuffer; 24 | import java.security.SecureRandom; 25 | import java.util.HashMap; 26 | import java.util.LinkedList; 27 | import java.util.Map; 28 | import java.util.Optional; 29 | import java.util.concurrent.ExecutionException; 30 | import java.util.concurrent.TimeUnit; 31 | import java.util.concurrent.TimeoutException; 32 | 33 | @WebSocket(maxTextMessageSize = 64 * 1024) 34 | public class SynchronousClient { 35 | 36 | private final SettableFuture connectFuture = SettableFuture.create(); 37 | private final Map responses = new HashMap<>(); 38 | 39 | private Session session; 40 | 41 | public SynchronousClient(URI uri) throws Exception { 42 | WebSocketClient client = new WebSocketClient(); 43 | client.start(); 44 | client.connect(this, uri, new ClientUpgradeRequest(), new UpgradeListener() { 45 | @Override 46 | public void onHandshakeRequest(UpgradeRequest upgradeRequest) { 47 | 48 | } 49 | 50 | @Override 51 | public void onHandshakeResponse(UpgradeResponse upgradeResponse) { 52 | System.out.println("Handshake response: " + upgradeResponse.getStatusCode()); 53 | } 54 | }); 55 | } 56 | 57 | @OnWebSocketError 58 | public void onError(Throwable error) { 59 | connectFuture.setException(error); 60 | error.printStackTrace(); 61 | } 62 | 63 | @OnWebSocketClose 64 | public void onClose(int statusCode, String reason) { 65 | System.out.println("onClose(" + statusCode + ", " + reason + ")"); 66 | } 67 | 68 | @OnWebSocketConnect 69 | public void onConnect(Session session) { 70 | this.session = session; 71 | connectFuture.set(session); 72 | } 73 | 74 | @OnWebSocketMessage 75 | public void onMessage(byte[] buffer, int offset, int length) { 76 | try { 77 | WebSocketMessageFactory factory = new ProtobufWebSocketMessageFactory(); 78 | WebSocketMessage message = factory.parseMessage(buffer, offset, length); 79 | 80 | if (message.getType() == WebSocketMessage.Type.RESPONSE_MESSAGE) { 81 | synchronized (responses) { 82 | responses.put(message.getResponseMessage().getRequestId(), message.getResponseMessage()); 83 | responses.notifyAll(); 84 | } 85 | } else { 86 | System.out.println("Received websocket message of unknown type: " + message.getType()); 87 | } 88 | 89 | } catch (InvalidMessageException e) { 90 | e.printStackTrace(); 91 | } 92 | } 93 | 94 | public void waitForConnected(int timeoutMillis) throws InterruptedException, ExecutionException, TimeoutException { 95 | connectFuture.get(timeoutMillis, TimeUnit.MILLISECONDS); 96 | } 97 | 98 | public long sendRequest(String verb, String path) throws IOException { 99 | WebSocketMessageFactory factory = new ProtobufWebSocketMessageFactory(); 100 | long id = new SecureRandom().nextLong(); 101 | 102 | WebSocketMessage message = factory.createRequest(Optional.of(id), verb, path, new LinkedList<>(), Optional.empty()); 103 | session.getRemote().sendBytes(ByteBuffer.wrap(message.toByteArray())); 104 | 105 | return id; 106 | } 107 | 108 | public WebSocketResponseMessage readResponse(long id, long timeoutMillis) throws InterruptedException { 109 | synchronized (responses){ 110 | while (!responses.containsKey(id)) responses.wait(timeoutMillis); 111 | return responses.get(id); 112 | } 113 | } 114 | 115 | } 116 | --------------------------------------------------------------------------------