├── .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 extends Filter> 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 extends Servlet> 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 extends EventListener> 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 |
--------------------------------------------------------------------------------