Skip to content

Commit

Permalink
Add support for WebSocket subprotocols
Browse files Browse the repository at this point in the history
  • Loading branch information
weavejester committed Sep 15, 2023
1 parent 48c0388 commit 1632c47
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 9 deletions.
28 changes: 22 additions & 6 deletions SPEC-alpha.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,28 +222,44 @@ For example:
## 3. Websockets

A HTTP request can be promoted into a websocket by means of an
"upgrade" header.
""upgrade" header.

In this situation, a Ring handler may choose to respond with a
websocket response instead of a HTTP response.

### 3.1. Websocket Responses

A websocket response is a map that has the `:ring.websocket/listener`
key, which maps to a websocket listener, described in section 3.2.
A websocket response is a map that represents a WebSocket, and may be
returned from a handler in place of a response map.

```clojure
(fn [request]
#:ring.websocket{:listener websocket-listener})
```

A websocket response may be returned from a synchronous listener, or
via the response callback of an asynchronous listener.
It may also be used from an asynchronous handler.

```clojure
(fn [request respond raise]
(respond #:ring.websocket{:listener websocket-listener}))
```

A websocket response contains the following keys. Any key not marked as
**required** may be omitted.

| Key | Type | Required |
| ------------------------ | ----------------------- | -------- |
|`:ring.websocket/listener`|`ring.websocket/Listener`| Yes |
|`:ring.websocket/protocol`|`String` | |

#### :ring.websocket/listener

An event listener that satisfies the `ring.websocket/Listener` protocol,
as described in section 3.2.

#### :ring.websocket/protocol

An optional websocket subprotocol. Must be one of the values listed in
the `Sec-Websocket-Protocol` header on the request.

### 3.2. Websocket Listeners

Expand Down
9 changes: 9 additions & 0 deletions ring-core/src/ring/websocket.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns ring.websocket
"Protocols and utility functions for websocket support."
(:refer-clojure :exclude [send])
(:require [clojure.string :as str])
(:import [java.nio ByteBuffer]))

(defprotocol Listener
Expand Down Expand Up @@ -134,3 +135,11 @@
"Returns true if the response contains a websocket listener."
[response]
(contains? response ::listener))

(defn request-protocols
"Returns a collection of websocket subprotocols from a request map."
[request]
(some-> (:headers request)
(get "sec-websocket-protocol")
(str/split #",")
(as-> ps (map str/trim ps))))
11 changes: 11 additions & 0 deletions ring-core/test/ring/test/websocket.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
(ns ring.test.websocket
(:require [clojure.test :refer [deftest is testing]]
[ring.websocket :as ws]))

(deftest test-request-protocols
(is (empty? (ws/request-protocols {:headers {}})))
(is (= ["mqtt"]
(ws/request-protocols {:headers {"sec-websocket-protocol" "mqtt"}})))
(is (= ["mqtt" "soap"]
(ws/request-protocols
{:headers {"sec-websocket-protocol" "mqtt, soap"}}))))
12 changes: 10 additions & 2 deletions ring-jetty-adapter/src/ring/adapter/jetty.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
(:require [ring.util.jakarta.servlet :as servlet]
[ring.websocket :as ws])
(:import [java.nio ByteBuffer]
[java.util ArrayList]
[org.eclipse.jetty.server
Request
Server
Expand All @@ -19,9 +20,12 @@
[org.eclipse.jetty.util.thread ThreadPool QueuedThreadPool]
[org.eclipse.jetty.util.ssl SslContextFactory$Server KeyStoreScanner]
[org.eclipse.jetty.websocket.server
JettyServerUpgradeRequest
JettyServerUpgradeResponse
JettyWebSocketServerContainer
JettyWebSocketCreator]
[org.eclipse.jetty.websocket.api
ExtensionConfig
Session
WebSocketConnectionListener
WebSocketListener
Expand Down Expand Up @@ -79,9 +83,13 @@
(onWebSocketPong [_ payload]
(ws/on-pong listener @socket payload)))))

(defn- websocket-creator [{listener ::ws/listener}]
(defn- ^JettyWebSocketCreator websocket-creator
[{:keys [::ws/listener ::ws/protocol]}]
(reify JettyWebSocketCreator
(createWebSocket [_ _ _]
(createWebSocket [_ ^JettyServerUpgradeRequest _req
^JettyServerUpgradeResponse resp]
(when protocol
(.setAcceptedSubProtocol resp protocol))
(websocket-listener listener))))

(defn- upgrade-to-websocket [^HttpServletRequest request response response-map]
Expand Down
20 changes: 19 additions & 1 deletion ring-jetty-adapter/test/ring/adapter/test/jetty.clj
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@
(ws/close sock)
(swap! log conj [:open? (ws/open? sock)]))
(on-message [_ _ _])
(on-pong [_ _ data])
(on-pong [_ _ _])
(on-error [_ _ _])
(on-close [_ _ code reason]
(swap! log conj [:close])))})]
Expand All @@ -738,6 +738,24 @@
(is (= [[:open? true] [:open? false] [:close]]
@log))))

(testing "subprotocols"
(let [log (atom [])
handler (constantly
{::ws/protocol "mqtt"
::ws/listener
(reify ws/Listener
(on-open [_ _])
(on-message [_ _ _])
(on-pong [_ _ _])
(on-error [_ _ _])
(on-close [_ _ _ _]))})]
(with-server handler {:port test-port}
(let [ws @(hato/websocket test-websocket-url
{:subprotocols ["soap" "mqtt"]})]
(is (= "mqtt" (.getSubprotocol ^java.net.http.WebSocket ws)))
@(hato/close! ws)
(Thread/sleep 100)))))

(testing "sending websocket messages asynchronously"
(let [log (atom [])
handler (constantly
Expand Down

0 comments on commit 1632c47

Please sign in to comment.