├── target └── classes │ └── yetti │ └── util │ ├── ByteBufferHelpers.class │ ├── BufferedOutputStream.class │ └── WebSocketListenerWrapper.class ├── .gitignore ├── scripts └── repl ├── src ├── java │ └── yetti │ │ └── util │ │ ├── BufferedOutputStream.java │ │ ├── ByteBufferHelpers.java │ │ └── WebSocketListenerWrapper.java └── yetti │ ├── middleware.clj │ ├── response.clj │ ├── request.clj │ ├── util.clj │ ├── websocket.clj │ └── adapter.clj ├── deps.edn ├── dev └── user.clj ├── README.md └── LICENSE /target/classes/yetti/util/ByteBufferHelpers.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funcool/yetti/HEAD/target/classes/yetti/util/ByteBufferHelpers.class -------------------------------------------------------------------------------- /target/classes/yetti/util/BufferedOutputStream.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funcool/yetti/HEAD/target/classes/yetti/util/BufferedOutputStream.class -------------------------------------------------------------------------------- /target/classes/yetti/util/WebSocketListenerWrapper.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funcool/yetti/HEAD/target/classes/yetti/util/WebSocketListenerWrapper.class -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | .lein-env 14 | .cpcache 15 | .rebel_readline_history 16 | .idea/ 17 | *.iml 18 | /.nrepl-port 19 | -------------------------------------------------------------------------------- /scripts/repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export JAVA_OPTS="\ 4 | -Xms50m \ 5 | -Xmx512m 6 | -Djdk.attach.allowAttachSelf \ 7 | -Djdk.tracePinnedThreads=full \ 8 | -Djava.net.preferIPv4Stack=true \ 9 | -XX:+UseG1GC \ 10 | -XX:+UnlockExperimentalVMOptions \ 11 | -XX:-OmitStackTraceInFastThrow \ 12 | --sun-misc-unsafe-memory-access=allow \ 13 | --enable-preview \ 14 | --enable-native-access=ALL-UNNAMED"; 15 | 16 | export OPTIONS_EVAL="nil" 17 | 18 | set -ex 19 | exec clojure -A:dev -M -e "$OPTIONS_EVAL" -m rebel-readline.main 20 | -------------------------------------------------------------------------------- /src/java/yetti/util/BufferedOutputStream.java: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | // 5 | // Copyright © Andrey Antukh 6 | 7 | package yetti.util; 8 | 9 | import java.io.OutputStream; 10 | import java.io.IOException; 11 | 12 | public class BufferedOutputStream extends java.io.BufferedOutputStream { 13 | public BufferedOutputStream(OutputStream out) { 14 | super(out); 15 | } 16 | 17 | public BufferedOutputStream(OutputStream out, int size) { 18 | super(out, size); 19 | } 20 | 21 | @Override 22 | public void flush() { 23 | // DO NOTHING 24 | } 25 | 26 | @Override 27 | public void close() throws IOException { 28 | super.flush(); 29 | super.close(); 30 | } 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/java/yetti/util/ByteBufferHelpers.java: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | // 5 | // Copyright © Andrey Antukh 6 | 7 | package yetti.util; 8 | 9 | import java.nio.ByteBuffer; 10 | 11 | public class ByteBufferHelpers { 12 | public static ByteBuffer emptyByteBuffer = ByteBuffer.allocate(0); 13 | 14 | public static ByteBuffer copyMany(final ByteBuffer[] buffers) { 15 | int size = 0; 16 | for (int i=0; i 6 | 7 | (ns yetti.middleware 8 | "Yetti specific middlewates that works with the native Request type." 9 | (:require 10 | [clojure.java.io :as io] 11 | [clojure.string :as str] 12 | [yetti.request :as yrq] 13 | [yetti.util :as yu] 14 | [yetti.response :as-alias yrs])) 15 | 16 | (defn wrap-params 17 | ([handler] (wrap-params handler {})) 18 | ([handler options] 19 | (fn [request] 20 | (let [qparams (yu/parse-query-data request options) 21 | request (if (yrq/request? request) 22 | (-> request 23 | (assoc :query-params qparams) 24 | (update :params merge qparams)) 25 | (-> request 26 | (assoc ::yrq/query-params qparams) 27 | (update ::yrq/params merge qparams))) 28 | 29 | mtype (yrq/get-header request "content-type") 30 | request (if (and (string? mtype) 31 | (or (str/starts-with? mtype "application/x-www-form-urlencoded") 32 | (str/starts-with? mtype "multipart/form-data"))) 33 | (let [params (yu/parse-form-data request options)] 34 | (-> request 35 | (assoc :body-params params) 36 | (update :params merge params))) 37 | request)] 38 | (handler request))))) 39 | 40 | (defn wrap-server-timing 41 | [handler] 42 | (letfn [(get-age [start] 43 | (float (/ (- (System/nanoTime) start) 1000000000))) 44 | 45 | (update-headers [headers start] 46 | (assoc headers "Server-Timing" (str "total;dur=" (get-age start))))] 47 | 48 | (fn [request] 49 | (let [start (System/nanoTime)] 50 | (-> (handler request) 51 | (update ::yrs/headers update-headers start)))))) 52 | 53 | -------------------------------------------------------------------------------- /src/yetti/response.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | 7 | (ns yetti.response 8 | (:require 9 | [clojure.java.io :as io] 10 | [ring.core.protocols :as rcp] 11 | [yetti.util :as yu]) 12 | (:import 13 | io.undertow.server.HttpServerExchange 14 | io.undertow.util.HeaderMap 15 | io.undertow.util.HttpString)) 16 | 17 | (defprotocol IResponse 18 | "A protocol representing a HTTP response." 19 | (status [resp]) 20 | (headers [resp]) 21 | (body [resp])) 22 | 23 | (defprotocol IResponseCookies 24 | (cookies [resp])) 25 | 26 | (extend-protocol IResponse 27 | clojure.lang.IPersistentMap 28 | (status [resp] (or (::status resp) (:status resp))) 29 | (headers [resp] (or (::headers resp) (:headers resp))) 30 | (body [resp] (or (::body resp) (:body resp)))) 31 | 32 | (extend-protocol IResponseCookies 33 | clojure.lang.IPersistentMap 34 | (cookies [resp] 35 | (or (::cookies resp) 36 | (:cookies resp)))) 37 | 38 | (defn charset 39 | "Given a response map, return the charset of the content-type header." 40 | [response] 41 | (when-let [content-type (-> response headers (get "content-type"))] 42 | (second (re-find yu/re-charset content-type)))) 43 | 44 | (defn write-body-to-stream 45 | [body response output] 46 | (rcp/write-body-to-stream body response output)) 47 | 48 | (defn stream-body? 49 | [o] 50 | (satisfies? rcp/StreamableResponseBody o)) 51 | 52 | (defn stream-body 53 | "Coerce a function to an instance of StreamableResponseBody" 54 | [f] 55 | (reify rcp/StreamableResponseBody 56 | (write-body-to-stream [_ response output] 57 | (f response output)))) 58 | 59 | (defn write-response! 60 | "Update the HttpServerExchange using a response map." 61 | [^HttpServerExchange exchange response] 62 | (when-not (.isResponseStarted exchange) 63 | (.setStatusCode exchange (or (status response) 200)) 64 | (let [response-headers ^HeaderMap (.getResponseHeaders exchange)] 65 | (doseq [[key val-or-vals] (headers response)] 66 | (let [key (HttpString/tryFromString ^String key)] 67 | (if (coll? val-or-vals) 68 | (.putAll response-headers key ^Collection val-or-vals) 69 | (.put response-headers key ^String val-or-vals)))) 70 | (when-let [cookies (cookies response)] 71 | (yu/set-cookies! exchange cookies)) 72 | (let [output-stream (.getOutputStream exchange)] 73 | (rcp/write-body-to-stream (body response) response output-stream))))) 74 | -------------------------------------------------------------------------------- /src/java/yetti/util/WebSocketListenerWrapper.java: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | // 5 | // Copyright © Andrey Antukh 6 | 7 | package yetti.util; 8 | 9 | import clojure.lang.IFn; 10 | import io.undertow.websockets.core.AbstractReceiveListener; 11 | import io.undertow.websockets.core.BufferedBinaryMessage; 12 | import io.undertow.websockets.core.BufferedTextMessage; 13 | import io.undertow.websockets.core.CloseMessage; 14 | import io.undertow.websockets.core.WebSocketChannel; 15 | 16 | public class WebSocketListenerWrapper extends AbstractReceiveListener { 17 | private final IFn _onMessage; 18 | private final IFn _onPing; 19 | private final IFn _onPong; 20 | private final IFn _onClose; 21 | private final IFn _onError; 22 | 23 | public WebSocketListenerWrapper(IFn onMessage, IFn onPing, IFn onPong, IFn onError, IFn onClose) { 24 | super(); 25 | this._onMessage = onMessage; 26 | this._onPing = onPing; 27 | this._onPong = onPong; 28 | this._onError = onError; 29 | this._onClose = onClose; 30 | } 31 | 32 | public void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) throws java.io.IOException { 33 | if (this._onMessage != null) { 34 | this._onMessage.invoke(channel, message.getData()); 35 | } else { 36 | super.onFullTextMessage(channel, message); 37 | } 38 | } 39 | 40 | public void onFullBinaryMessage(WebSocketChannel channel, BufferedBinaryMessage message) throws java.io.IOException { 41 | if (this._onMessage != null) { 42 | var data = message.getData(); 43 | try { 44 | this._onMessage.invoke(channel, data.getResource()); 45 | } finally { 46 | data.free(); 47 | } 48 | } else { 49 | super.onFullBinaryMessage(channel, message); 50 | } 51 | } 52 | 53 | public void onFullPingMessage(WebSocketChannel channel, BufferedBinaryMessage message) throws java.io.IOException { 54 | if (this._onPing != null) { 55 | var data = message.getData(); 56 | try { 57 | this._onPing.invoke(channel, data.getResource()); 58 | } finally { 59 | data.free(); 60 | } 61 | } else { 62 | super.onFullPingMessage(channel, message); 63 | } 64 | } 65 | 66 | public void onFullPongMessage(WebSocketChannel channel, BufferedBinaryMessage message) throws java.io.IOException { 67 | if (this._onPong != null) { 68 | var data = message.getData(); 69 | try { 70 | this._onPong.invoke(channel, data.getResource()); 71 | } finally { 72 | data.free(); 73 | } 74 | } else { 75 | super.onFullPongMessage(channel, message); 76 | } 77 | } 78 | 79 | public void onCloseMessage(CloseMessage message, WebSocketChannel channel) { 80 | if (this._onClose != null) { 81 | this._onClose.invoke(channel, message.getCode(), message.getReason()); 82 | } else { 83 | super.onCloseMessage(message, channel); 84 | } 85 | } 86 | 87 | public void onError(WebSocketChannel channel, Throwable cause) { 88 | try { 89 | if (this._onError != null) { 90 | this._onError.invoke(channel, cause); 91 | } 92 | } finally { 93 | super.onError(channel, cause); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | 7 | (ns user 8 | (:require 9 | [clojure.java.io :as io] 10 | [clojure.pprint :refer [pprint print-table]] 11 | [clojure.repl :refer :all] 12 | [clojure.test :as test] 13 | [clojure.tools.namespace.repl :as repl] 14 | [clojure.walk :refer [macroexpand-all]] 15 | [criterium.core :as crit] 16 | [me.flowthing.pp :as pp] 17 | [promesa.core :as p] 18 | [promesa.exec :as px] 19 | [ring.core.protocols :as rcp] 20 | [taoensso.nippy :as nippy] 21 | [yetti.adapter :as yt] 22 | [yetti.middleware :as ymw] 23 | [yetti.request :as yrq] 24 | [yetti.response :as-alias yrs] 25 | [yetti.util :as yu]) 26 | (:import 27 | java.io.InputStream 28 | java.io.OutputStream 29 | java.util.concurrent.ForkJoinPool)) 30 | 31 | (defmacro run-quick-bench 32 | [& exprs] 33 | `(crit/with-progress-reporting (crit/quick-bench (do ~@exprs) :verbose))) 34 | 35 | (defn run-tests 36 | ([] (run-tests #"^yetti-test.*$")) 37 | ([o] 38 | (repl/refresh) 39 | (cond 40 | (instance? java.util.regex.Pattern o) 41 | (test/run-all-tests o) 42 | 43 | (symbol? o) 44 | (if-let [sns (namespace o)] 45 | (do (require (symbol sns)) 46 | (test/test-vars [(resolve o)])) 47 | (test/test-ns o))))) 48 | 49 | (defn hello-http-handler 50 | [request] 51 | 52 | {::yrs/status 200 53 | ::yrs/headers {"content-type" "text/plain" 54 | "test" "foooo" 55 | "x-foo-bar" ["baz" "foo"]} 56 | ::yrs/body (with-out-str 57 | (println "Values:") 58 | (prn (yu/parse-query-data request)) 59 | (prn (yrq/headers request))) 60 | 61 | ::yrs/cookies {"sample-cookie" {:value (rand-int 1000) 62 | :same-site :lax 63 | :path "/foo" 64 | :domain "localhost" 65 | :max-age 2000}}}) 66 | 67 | ;; (defn hello-http-handler 68 | ;; [request] 69 | ;; (prn "hello-world-handler" (Thread/currentThread)) 70 | ;; (prn "request" request) 71 | ;; (prn "request" "query-params:" (:query-params request)) 72 | ;; (prn "request" "body-params:" (:body-params request)) 73 | ;; (prn "request" "params:" (:params request)) 74 | ;; {::yrs/status 200 75 | ;; ::yrs/headers {"content-type" "application/octet-stream"} 76 | ;; ::yrs/body (reify rcp/StreamableResponseBody 77 | ;; (write-body-to-stream [_ _ output-stream] 78 | ;; (try 79 | ;; (with-open [^InputStream input (io/input-stream "caddy_linux_amd64")] 80 | ;; (io/copy input output-stream)) 81 | ;; (catch java.io.IOException _) 82 | ;; (finally 83 | ;; (.close ^OutputStream output-stream)))))}) 84 | 85 | (def server nil) 86 | 87 | (defn- on-error 88 | [cause request] 89 | (prn "on-error" cause)) 90 | 91 | (defn- start 92 | [] 93 | (let [options {:xnio/io-threads 2 94 | :xnio/direct-buffers true 95 | :http/on-error on-error 96 | :ring/compat :ring2} 97 | handler (-> hello-http-handler 98 | (ymw/wrap-server-timing) 99 | (ymw/wrap-params) 100 | )] 101 | 102 | (alter-var-root #'server (fn [server] 103 | (when server (yt/stop! server)) 104 | (-> (yt/server handler options) 105 | (yt/start!)))) 106 | :started)) 107 | 108 | (defn- stop 109 | [] 110 | (alter-var-root #'server (fn [server] 111 | (when server (yt/stop! server)) 112 | nil)) 113 | :stoped) 114 | 115 | (defn restart 116 | [] 117 | (stop) 118 | (repl/refresh :after 'user/start)) 119 | 120 | 121 | (defn -main 122 | [& args] 123 | (start)) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Undertow based HTTP server for Clojure 2 | 3 | A pragmatic and efficient ring adapter for the high performance 4 | **undertow** http server. 5 | 6 | Relevant characteristics: 7 | 8 | - Unified http and websocket handlers; there are no separate routings 9 | for http and http. Any http request can be upgraded to websocket. 10 | - No HTTPS support; this is intended to be used behind http proxy 11 | which is in most cases the responsible of handling TLS; this is a 12 | practical decission to not maintain code that is almost never used 13 | in practice (when serving content to the internet). 14 | - Based on ring-2.0 for performance reasons and it is the default, but 15 | it also can work in a ring1 compliant mode. 16 | - By default uses Virtual Threads for request dispatching so, it 17 | requires JDK >= 21 (although you have the option of using platform 18 | threads as well). 19 | - No ring-async support, with virtual threads there are no real need 20 | for callback based API, so we opted to directly to no support it. 21 | 22 | **NOTE: currently, the future of ring-2.0 is completely 23 | uncertain. Regardless of that, this library only depends on ring's 24 | stable APIs (protocols), and the rest of ring-2.0's ideas and 25 | proposals are bundled internally.** 26 | 27 | 28 | ## Usage 29 | 30 | ### Quick Start 31 | 32 | On deps.edn: 33 | 34 | ```clojure 35 | funcool/yetti 36 | {:git/tag "v11.0" 37 | :git/sha "e27eb51" 38 | :git/url "https://github.com/funcool/yetti.git"} 39 | ``` 40 | 41 | In the REPL: 42 | 43 | ```clojure 44 | (require '[yetti.adapter :as yt] 45 | '[yetti.response :as yrs]) 46 | 47 | ;; Using Response type 48 | 49 | (defn handler 50 | [request] 51 | {::yrs/status 200 52 | ::yrs/body "hello world"}) 53 | 54 | (-> handler 55 | (yt/server {:http/port 11010}) 56 | (yt/start!)) 57 | ``` 58 | 59 | If you want a ring1 compatible request: 60 | 61 | ```clojure 62 | (require '[yetti.adapter :as yt]) 63 | 64 | (defn handler 65 | [{:keys [request-method] :as request}] 66 | {:status 200 67 | :body (str "hello world " (name request-method))}) 68 | 69 | (-> handler 70 | (yt/server {:http/port 11010 :ring/compat :ring1}) 71 | (yt/start!)) 72 | ``` 73 | 74 | The possible values for `:ring/compat` are: 75 | 76 | - `:ring2`: the default, receives a ring2 compatible, map-like type 77 | (with main focus on performance) and expectes a ring2 or ring1 78 | response to be returned indistinctly 79 | - `:ring1`: receives a ring1 compliant map and expectes ring1 80 | response to be returned 81 | 82 | 83 | ### WebSocket 84 | 85 | Any handler can upgrade to websocket protocol, there is an example: 86 | 87 | ```clojure 88 | (require '[yetti.websocket :as yws] 89 | '[yetti.response :as-alias yrs] 90 | '[yetti.websocket :as-alias yws]) 91 | 92 | (defn handler 93 | [request] 94 | ;; We prefer use `yws/upgrade-request?` over `rws/upgrade-request?` 95 | ;; in case if you use ring2 requests, for performance reasons. 96 | (if (yws/upgrade-request? request) 97 | {::yws/listener {:on-open (fn [ws]) 98 | :on-error (fn [ws e]) 99 | :on-close (fn [ws status-code reason]) 100 | :on-message (fn [ws message]) 101 | :on-ping (fn [ws data]) 102 | :on-pong (fn [ws data])}} 103 | {::yrs/status 404})) 104 | ``` 105 | 106 | This is the main API for interacting with websocket channel/connection: 107 | 108 | - `(yws/open? ws msg)` 109 | - `(yws/send ws msg)` 110 | - `(yws/send ws msg callback)` 111 | - `(yws/ping ws msg)` 112 | - `(yws/pong ws msg)` 113 | - `(yws/close ws)` 114 | - `(yws/get-remote-addr ws)` 115 | - `(yws/set-idle-timeout! ws timeout)` 116 | 117 | 118 | Notice that we support different type of msg: 119 | 120 | * **byte[]** and **ByteBuffer**: send binary websocket message 121 | * **String**: send text websocket message 122 | 123 | All this internally uses the `ring-websocket-protocols` so it has full 124 | interop with already existing ring websocket code. 125 | 126 | 127 | ## License 128 | 129 | ``` 130 | This Source Code Form is subject to the terms of the Mozilla Public 131 | License, v. 2.0. If a copy of the MPL was not distributed with this 132 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 133 | 134 | Copyright © 2021-Now Andrey Antukh 135 | ``` 136 | -------------------------------------------------------------------------------- /src/yetti/request.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | ;; 7 | ;; Original code from ring branch:2.0 with small modifications. this 8 | ;; is temporal approach until ring-2.0 is released. 9 | ;; 10 | ;; Copyright (c) 2009-2010 Mark McGranaghan 11 | ;; Copyright (c) 2009-2018 James Reeves 12 | ;; 13 | ;; Permission is hereby granted, free of charge, to any person 14 | ;; obtaining a copy of this software and associated documentation 15 | ;; files (the "Software"), to deal in the Software without 16 | ;; restriction, including without limitation the rights to use, 17 | ;; copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | ;; copies of the Software, and to permit persons to whom the 19 | ;; Software is furnished to do so, subject to the following 20 | ;; conditions: 21 | ;; 22 | ;; The above copyright notice and this permission notice shall be 23 | ;; included in all copies or substantial portions of the Software. 24 | ;; 25 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | ;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | ;; OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | ;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | ;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | ;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | ;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | ;; OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | (ns yetti.request 35 | (:require 36 | [yetti.util :as yu]) 37 | (:import 38 | clojure.lang.Keyword 39 | org.xnio.XnioWorker 40 | java.util.concurrent.Executor 41 | io.undertow.server.ServerConnection 42 | io.undertow.server.HttpServerExchange)) 43 | 44 | (set! *warn-on-reflection* true) 45 | 46 | (defprotocol IRequest 47 | "A protocol representing a HTTP request." 48 | (server-port [req]) 49 | (server-name [req]) 50 | (remote-addr [req]) 51 | (ssl-client-cert [req]) 52 | (method [req]) 53 | (scheme [req]) 54 | (path [req]) 55 | (query [req]) 56 | (protocol [req]) 57 | (headers [req]) 58 | (body [req]) 59 | (get-header [req name])) 60 | 61 | (defprotocol IRequestCookies 62 | (cookies [req]) 63 | (get-cookie [req name])) 64 | 65 | (defprotocol IStreamableRequestBody 66 | "A protocol for reading the request body as an input stream." 67 | (-body-stream [body request])) 68 | 69 | (defn ^java.io.InputStream body-stream 70 | "Given a request map, return an input stream to read the body." 71 | [request] 72 | (-body-stream (body request) request)) 73 | 74 | (defrecord Request [^Keyword method ^String path ^HttpServerExchange exchange] 75 | IRequest 76 | (method [_] method) 77 | (path [_] path) 78 | (body [_] (.getInputStream exchange)) 79 | (headers [_] (yu/get-request-headers exchange)) 80 | (query [_] (.getQueryString exchange)) 81 | (server-port [_] (.. exchange getDestinationAddress getPort)) 82 | (server-name [_] (.getHostName exchange)) 83 | (remote-addr [_] (.. exchange getSourceAddress getAddress getHostAddress)) 84 | (scheme [_] (keyword (.. exchange getRequestScheme))) 85 | (protocol [_] (.. exchange getProtocol toString)) 86 | (get-header [_ name] (yu/get-request-header exchange name)) 87 | 88 | IRequestCookies 89 | (cookies [_] (yu/get-request-cookies exchange)) 90 | (get-cookie [_ name] (yu/get-request-cookie exchange name)) 91 | 92 | Executor 93 | (execute [_ r] 94 | (let [sconn (.getConnection exchange) 95 | exc (.getWorker ^ServerConnection sconn)] 96 | (.execute ^Executor exc ^Runnable r)))) 97 | 98 | (defn charset 99 | "Given a request map, return the charset of the content-type header." 100 | [request] 101 | (when-let [content-type (get-header request "content-type")] 102 | (second (re-find yu/re-charset content-type)))) 103 | 104 | 105 | (defn request? 106 | [o] 107 | (instance? Request o)) 108 | 109 | (defn exchange->ring1-request 110 | {:no-doc true} 111 | [^HttpServerExchange exchange] 112 | {:server-port (-> exchange .getDestinationAddress .getPort) 113 | :server-name (.getHostName exchange) 114 | :remote-addr (-> exchange .getSourceAddress .getAddress .getHostAddress) 115 | :uri (.getRequestURI exchange) 116 | :query-string (let [qs (.getQueryString exchange)] (if-not (.equals "" qs) qs)) 117 | :scheme (-> exchange .getRequestScheme keyword) 118 | :request-method (-> exchange .getRequestMethod .toString .toLowerCase keyword) 119 | :protocol (-> exchange .getProtocol .toString) 120 | :headers (yu/get-request-headers exchange) 121 | :body (if (.isBlocking exchange) (.getInputStream exchange))}) 122 | 123 | (defn exchange->ring2-request 124 | "Create the request from the HttpServerExchange." 125 | {:no-doc true} 126 | [^HttpServerExchange exchange] 127 | (let [method (keyword (.. exchange getRequestMethod toString toLowerCase)) 128 | path (.getRequestURI exchange)] 129 | (Request. ^Keyword method ^String path exchange))) 130 | 131 | (extend-protocol IRequest 132 | clojure.lang.IPersistentMap 133 | (server-port [req] (::server-port req (:server-port req))) 134 | (server-name [req] (::server-name req (:server-name req))) 135 | (remote-addr [req] (::remote-addr req (:remote-addr req))) 136 | (ssl-client-cert [req] (::ssl-client-cert req (:ssl-client-cert req))) 137 | (method [req] (::method req (:request-method req))) 138 | (scheme [req] (::scheme req (:scheme req))) 139 | (path [req] (::path req (:uri req))) 140 | (query [req] (::query req (:query-string req))) 141 | (protocol [req] (::protocol req (:protocol req))) 142 | (headers [req] (::headers req (:headers req))) 143 | (body [req] (::body req (:body req))) 144 | (get-header [req name] (get (headers req) name))) 145 | 146 | (extend-protocol IStreamableRequestBody 147 | (Class/forName "[B") 148 | (-body-stream [bs _] 149 | (java.io.ByteArrayInputStream. ^bytes bs)) 150 | java.io.InputStream 151 | (-body-stream [stream _] stream) 152 | 153 | String 154 | (-body-stream [^String s request] 155 | (java.io.ByteArrayInputStream. 156 | (if-let [encoding (charset request)] 157 | (.getBytes s ^String encoding) 158 | (.getBytes s "utf-8")))) 159 | nil 160 | (-body-stream [_ _] nil)) 161 | 162 | -------------------------------------------------------------------------------- /src/yetti/util.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | 7 | (ns yetti.util 8 | (:require 9 | [clojure.java.io :as io] 10 | [clojure.string :as str]) 11 | (:import 12 | io.undertow.server.HttpServerExchange 13 | io.undertow.server.handlers.Cookie 14 | io.undertow.server.handlers.CookieImpl 15 | io.undertow.server.handlers.form.FormData 16 | io.undertow.server.handlers.form.FormData$FileItem 17 | io.undertow.server.handlers.form.FormData$FormValue 18 | io.undertow.server.handlers.form.FormDataParser 19 | io.undertow.server.handlers.form.FormEncodedDataDefinition 20 | io.undertow.server.handlers.form.FormParserFactory 21 | io.undertow.server.handlers.form.MultiPartParserDefinition 22 | io.undertow.util.HeaderMap 23 | io.undertow.util.HeaderValues 24 | io.undertow.util.HttpString 25 | io.undertow.util.Headers 26 | java.lang.reflect.Field 27 | java.nio.file.Paths 28 | java.time.Duration 29 | java.time.Instant 30 | java.util.Collections 31 | java.util.Date 32 | java.util.Deque 33 | java.util.HashMap 34 | java.util.Iterator 35 | java.util.Map 36 | java.util.Map 37 | java.util.Map$Entry 38 | java.util.concurrent.Executor 39 | yetti.util.ByteBufferHelpers)) 40 | 41 | (set! *warn-on-reflection* true) 42 | 43 | (def default-temp-dir 44 | (Paths/get "/tmp/undertow/" (into-array String []))) 45 | 46 | (def re-charset 47 | #"(?x);(?:.*\s)?(?i:charset)=(?: 48 | ([!\#$%&'*\-+.0-9A-Z\^_`a-z\|~]+)| # token 49 | \"((?:\\\"|[^\"])*)\" # quoted 50 | )\s*(?:;|$)") 51 | 52 | (defn get-exchange 53 | [request] 54 | (or (:yetti.adapter/exchange request) 55 | (:exchange request))) 56 | 57 | (defn- headers->map 58 | [^HeaderMap headers] 59 | (loop [m {} 60 | c (.fastIterateNonEmpty headers)] 61 | (if (pos? c) 62 | (let [hvs ^HeaderValues (.fiCurrent headers c) 63 | hk (.. hvs getHeaderName toString toLowerCase) 64 | hv (if (= 1 (.size hvs)) (.getFirst hvs) (str/join "," hvs))] 65 | (recur (assoc m hk hv) 66 | (.fiNext headers c))) 67 | m))) 68 | 69 | (defn parser-factory 70 | [{:keys [item-max-size temp-dir executor] 71 | :or {item-max-size -1 72 | temp-dir default-temp-dir}}] 73 | (let [multipart (doto (MultiPartParserDefinition.) 74 | (.setFileSizeThreshold 0) 75 | (.setMaxIndividualFileSize item-max-size) 76 | (.setTempFileLocation temp-dir) 77 | (.setDefaultEncoding "UTF-8") 78 | (.setExecutor ^Executor executor)) 79 | xform (doto (FormEncodedDataDefinition.) 80 | (.setDefaultEncoding "UTF-8"))] 81 | (.. (FormParserFactory/builder) 82 | (withParsers [xform multipart]) 83 | (build)))) 84 | 85 | (defn parse-query-data 86 | ([request] (parse-query-data request {})) 87 | ([request {:keys [key-fn] :or {key-fn keyword}}] 88 | (when-let [^HttpServerExchange exchange (get-exchange request)] 89 | (let [params (.getQueryParameters exchange) 90 | entries (.entrySet ^Map params) 91 | it (.iterator entries)] 92 | (loop [rs {}] 93 | (if (.hasNext ^Iterator it) 94 | (let [item (.next ^Iterator it) 95 | k (.getKey ^Map$Entry item) 96 | v (.getValue ^Map$Entry item)] 97 | (if (= 1 (.size ^Deque v)) 98 | (recur (assoc rs (key-fn k) (.peek ^Deque v))) 99 | (recur (assoc rs (key-fn k) (into [] v))))) 100 | rs)))))) 101 | 102 | (defn set-cookies! 103 | [^HttpServerExchange exchange cookies] 104 | (let [^Map rcookies (.getResponseCookies exchange)] 105 | (doseq [[k cookie-map] cookies] 106 | (let [{:keys [path value domain max-age expires same-site secure http-only comment]} cookie-map 107 | item (doto (CookieImpl. ^String k ^String (str value)) 108 | (cond-> (boolean? secure) 109 | (.setSecure ^Boolean secure)) 110 | (cond-> (string? comment) 111 | (.setComment ^String comment)) 112 | (cond-> (string? path) 113 | (.setPath ^String path)) 114 | (cond-> (string? domain) 115 | (.setDomain ^String domain)) 116 | (cond-> (boolean? http-only) 117 | (.setHttpOnly ^Boolean http-only)) 118 | (cond-> (int? max-age) 119 | (.setMaxAge ^Integer (int max-age))) 120 | (cond-> (instance? Duration max-age) 121 | (.setMaxAge ^Integer (int (.getSeconds ^Duration max-age)))) 122 | (cond-> (instance? Instant expires) 123 | (.setExpires ^Date (Date/from expires))) 124 | (cond-> (instance? Date expires) 125 | (.setExpires ^Date expires)) 126 | (cond-> (keyword? same-site) 127 | (.setSameSiteMode (case same-site 128 | :lax "Lax" 129 | :strict "Strict" 130 | :none "None"))) 131 | (cond-> (string? same-site) 132 | (.setSameSiteMode ^Strict same-site)))] 133 | 134 | (.put ^Map rcookies ^String k ^Cookie item))))) 135 | 136 | (defn- parse-form-value 137 | [key ^FormData$FormValue fval] 138 | (if (.isFileItem fval) 139 | (let [^FormData$FileItem fitem (.getFileItem fval) 140 | headers (headers->map (.getHeaders fval)) 141 | mtype (get headers "content-type")] 142 | (cond-> {:name key 143 | :headers headers 144 | :filename (.getFileName fval) 145 | :path (.getFile fitem) 146 | :size (.getFileSize fitem)} 147 | (some? mtype) 148 | (assoc :mtype mtype))) 149 | (.getValue fval))) 150 | 151 | (defn- append-form-entry 152 | [val v] 153 | (cond 154 | (nil? val) 155 | v 156 | 157 | (vector? val) 158 | (conj val v) 159 | 160 | :else 161 | [val v])) 162 | 163 | (defn parse-form-data 164 | ([request] (parse-form-data request {})) 165 | ([request {:keys [key-fn] :or {key-fn keyword} :as options}] 166 | (when-let [exchange (get-exchange request)] 167 | (let [factory (parser-factory options) 168 | parser (.createParser ^FormParserFactory factory 169 | ^HttpServerExchange exchange) 170 | form (some-> parser .parseBlocking)] 171 | (reduce (fn [result key] 172 | (let [fkey (key-fn key) 173 | fval (.get ^FormData form ^String key)] 174 | (if (instance? FormData$FormValue fval) 175 | (update result fkey append-form-entry (parse-form-value key fval)) 176 | (reduce (fn [result fval] 177 | (update result fkey append-form-entry (parse-form-value key fval))) 178 | result 179 | fval)))) 180 | {} 181 | (seq form)))))) 182 | 183 | (defn get-request-header 184 | [^HttpServerExchange exchange ^String name] 185 | (let [^HeaderMap headers (.getRequestHeaders exchange)] 186 | (when-let [^HeaderValues entry (.get headers name)] 187 | (if (= 1 (.size entry)) 188 | (.getFirst entry) 189 | (str/join "," entry))))) 190 | 191 | (defn get-request-headers 192 | "Creates a name/value map of all the request headers." 193 | [^HttpServerExchange exchange] 194 | (headers->map (.getRequestHeaders exchange))) 195 | 196 | (defn- parse-cookie 197 | [^Cookie cookie] 198 | {:name (.getName cookie) 199 | :value (.getValue cookie)}) 200 | 201 | (defn get-request-cookies 202 | [^HttpServerExchange exchange] 203 | (into {} 204 | (map (fn [[k cookie]] 205 | [k (parse-cookie cookie)])) 206 | (.getRequestCookies ^HttpServerExchange exchange))) 207 | 208 | (defn get-request-cookie 209 | [^HttpServerExchange exchange ^String name] 210 | (let [^Map cookies (.getRequestCookies ^HttpServerExchange exchange)] 211 | (some-> (.get cookies name) parse-cookie))) 212 | 213 | (defn copy-many 214 | [data] 215 | (ByteBufferHelpers/copyMany data)) 216 | -------------------------------------------------------------------------------- /src/yetti/websocket.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | 7 | (ns yetti.websocket 8 | (:refer-clojure :exclude [send]) 9 | (:require 10 | [clojure.string :as str] 11 | [ring.websocket :as-alias rws] 12 | [ring.websocket.protocols :as rwp] 13 | [yetti.request :as yrq] 14 | [yetti.util :as yu]) 15 | (:import 16 | clojure.lang.IFn 17 | io.undertow.server.HttpServerExchange 18 | io.undertow.websockets.WebSocketConnectionCallback 19 | io.undertow.websockets.WebSocketProtocolHandshakeHandler 20 | io.undertow.websockets.core.AbstractReceiveListener 21 | io.undertow.websockets.core.BufferedBinaryMessage 22 | io.undertow.websockets.core.BufferedTextMessage 23 | io.undertow.websockets.core.CloseMessage 24 | io.undertow.websockets.core.WebSocketCallback 25 | io.undertow.websockets.core.WebSocketChannel 26 | io.undertow.websockets.core.WebSocketUtils 27 | io.undertow.websockets.core.WebSockets 28 | io.undertow.websockets.extensions.PerMessageDeflateHandshake 29 | io.undertow.websockets.spi.WebSocketHttpExchange 30 | java.nio.ByteBuffer 31 | java.util.concurrent.CompletableFuture 32 | java.util.concurrent.CompletionException 33 | java.util.concurrent.ExecutionException 34 | java.util.concurrent.atomic.AtomicBoolean 35 | org.xnio.Pooled 36 | yetti.util.WebSocketListenerWrapper)) 37 | 38 | (set! *warn-on-reflection* true) 39 | 40 | (extend-type clojure.lang.IPersistentMap 41 | rwp/Listener 42 | (on-open [m socket] 43 | (when-let [kv (find m :on-open)] ((val kv) socket))) 44 | (on-message [m socket message] 45 | (when-let [kv (find m :on-message)] ((val kv) socket message))) 46 | (on-pong [m socket data] 47 | (when-let [kv (find m :on-pong)] ((val kv) socket data))) 48 | (on-error [m socket throwable] 49 | (when-let [kv (find m :on-error)] ((val kv) socket throwable))) 50 | (on-close [m socket code reason] 51 | (when-let [kv (find m :on-close)] ((val kv) socket code reason))) 52 | 53 | rwp/PingListener 54 | (on-ping [m socket data] 55 | (if-let [kv (find m :on-ping)] 56 | ((val kv) socket data) 57 | (rwp/-pong socket data)))) 58 | 59 | (defn- fn->callback 60 | [succeed fail] 61 | (reify WebSocketCallback 62 | (complete [_ _ _] 63 | (succeed)) 64 | (onError [_ _ _ cause] 65 | (fail cause)))) 66 | 67 | (defn- cf->callback 68 | [^CompletableFuture ft] 69 | (reify WebSocketCallback 70 | (complete [_ _ _] 71 | (.complete ft nil)) 72 | (onError [_ _ _ cause] 73 | (.completeExceptionally ft cause)))) 74 | 75 | (extend-type WebSocketChannel 76 | rwp/Socket 77 | (-open? [this] 78 | (. this (isOpen))) 79 | 80 | (-send [this msg] 81 | (let [ft (CompletableFuture.)] 82 | (cond 83 | (string? msg) 84 | (WebSockets/sendText ^String msg 85 | ^WebSocketChannel this 86 | ^WebSocketCallback (cf->callback ft)) 87 | 88 | (bytes? msg) 89 | (WebSockets/sendBinary ^ByteBuffer (ByteBuffer/wrap ^bytes msg) 90 | ^WebSocketChannel this 91 | ^WebSocketCallback (cf->callback ft)) 92 | 93 | 94 | (instance? ByteBuffer msg) 95 | (WebSockets/sendBinary ^ByteBuffer (ByteBuffer/wrap ^bytes msg) 96 | ^WebSocketChannel this 97 | ^WebSocketCallback (cf->callback ft)) 98 | 99 | :else 100 | (throw (IllegalArgumentException. "invalid message"))) 101 | 102 | (.get ft))) 103 | 104 | (-ping [this msg] 105 | (let [ft (CompletableFuture.) 106 | msg (cond 107 | (bytes? msg) (ByteBuffer/wrap ^bytes msg) 108 | (instance? ByteBuffer msg) msg 109 | (string? msg) (WebSocketUtils/fromUtf8String ^String msg) 110 | :else (throw (IllegalArgumentException. "invalid mesage type")))] 111 | (WebSockets/sendPing ^ByteBuffer msg 112 | ^WebSocketChannel this 113 | ^WebSocketCallback (cf->callback ft)) 114 | (.get ft))) 115 | 116 | (-pong [this msg] 117 | (let [ft (CompletableFuture.) 118 | msg (cond 119 | (bytes? msg) (ByteBuffer/wrap ^bytes msg) 120 | (instance? ByteBuffer msg) msg 121 | (string? msg) (WebSocketUtils/fromUtf8String ^String msg) 122 | :else (throw (IllegalArgumentException. "invalid mesage type")))] 123 | (WebSockets/sendPong ^ByteBuffer msg 124 | ^WebSocketChannel this 125 | ^WebSocketCallback (cf->callback ft)) 126 | (.get ft))) 127 | 128 | (-close [this code reason] 129 | (when (some? code) 130 | (.setCloseCode this code)) 131 | (when (some? reason) 132 | (.setCloseReason this reason)) 133 | (.sendClose this) 134 | (.close this)) 135 | 136 | rwp/AsyncSocket 137 | (-send-async [this msg succeed fail] 138 | (cond 139 | (string? msg) 140 | (WebSockets/sendText ^String msg 141 | ^WebSocketChannel this 142 | ^WebSocketCallback (fn->callback succeed fail)) 143 | 144 | (bytes? msg) 145 | (WebSockets/sendBinary ^ByteBuffer (ByteBuffer/wrap ^bytes msg) 146 | ^WebSocketChannel this 147 | ^WebSocketCallback (fn->callback succeed fail)) 148 | 149 | (instance? ByteBuffer msg) 150 | (WebSockets/sendBinary ^ByteBuffer (ByteBuffer/wrap ^bytes msg) 151 | ^WebSocketChannel this 152 | ^WebSocketCallback (fn->callback succeed fail)) 153 | 154 | :else 155 | (throw (IllegalArgumentException. "invalid message"))))) 156 | 157 | (defn add-close-callback! 158 | "Adds on-close task to the websocket channel. Returns `channel` 159 | instance." 160 | [^WebSocketChannel channel callback] 161 | (.addCloseTask channel 162 | (reify org.xnio.ChannelListener 163 | (handleEvent [_ channel] 164 | (try 165 | (callback channel) 166 | (catch Throwable cause))))) 167 | channel) 168 | 169 | (defn set-idle-timeout! 170 | [^WebSocketChannel channel ms] 171 | (if (integer? ms) 172 | (.. channel (setIdleTimeout ^long ms)) 173 | (.. channel (setIdleTimeout ^long (inst-ms ms)))) 174 | channel) 175 | 176 | (defn get-remote-addr 177 | [^WebSocketChannel channel] 178 | (.. channel (getDestinationAddress))) 179 | 180 | (defn upgrade-request? 181 | "Checks if a request is a websocket upgrade request. 182 | 183 | This is a ring2 aware, more efficient version of 184 | `ring.websocket/upgrade-request?` function." 185 | [request] 186 | (let [upgrade (yrq/get-header request "upgrade") 187 | connection (yrq/get-header request "connection")] 188 | (and (string? upgrade) 189 | (string? connection) 190 | (str/includes? (str/lower-case upgrade) "websocket") 191 | (str/includes? (str/lower-case connection) "upgrade")))) 192 | 193 | (defn open? 194 | [socket] 195 | (boolean (rwp/-open? socket))) 196 | 197 | (defn send 198 | ([socket message] 199 | (rwp/-send socket message)) 200 | ([socket message succeed fail] 201 | (rwp/-send-async socket message succeed fail))) 202 | 203 | (defn ping 204 | ([socket] 205 | (rwp/-ping socket (ByteBuffer/allocate 0))) 206 | ([socket data] 207 | (rwp/-ping socket data))) 208 | 209 | (defn pong 210 | ([socket] 211 | (rwp/-pong socket (ByteBuffer/allocate 0))) 212 | ([socket data] 213 | (rwp/-pong socket data))) 214 | 215 | (defn close 216 | ([socket] 217 | (rwp/-close socket 1000 "")) 218 | ([socket code reason] 219 | (rwp/-close socket code reason))) 220 | 221 | (defn websocket-response? 222 | "Returns true if the response contains a websocket listener." 223 | [response] 224 | (or (contains? response ::listener) 225 | (contains? response ::rws/listener))) 226 | 227 | (defn request-protocols 228 | "Returns a collection of websocket subprotocols from a request map." 229 | [request] 230 | (some-> (:headers request) 231 | (get "sec-websocket-protocol") 232 | (str/split #"\s*,\s*"))) 233 | 234 | (defn- listener->handler 235 | [listener] 236 | (WebSocketProtocolHandshakeHandler. 237 | (reify WebSocketConnectionCallback 238 | (onConnect [_ exchange channel] 239 | (let [setter (.getReceiveSetter ^WebSocketChannel channel) 240 | closed (AtomicBoolean. false) 241 | 242 | on-message (fn [channel message] 243 | (rwp/on-message listener channel message)) 244 | on-pong (fn [channel buffers] 245 | (rwp/on-pong listener channel (yu/copy-many buffers))) 246 | on-ping (fn [channel buffers] 247 | (rwp/on-ping listener channel (yu/copy-many buffers))) 248 | on-error (fn [channel cause] 249 | (when (.compareAndSet ^AtomicBoolean closed false true) 250 | (rwp/on-error listener channel cause))) 251 | on-close (fn [channel code reason] 252 | (when (.compareAndSet ^AtomicBoolean closed false true) 253 | (rwp/on-close listener channel code reason)))] 254 | 255 | (rwp/on-open listener channel) 256 | 257 | (add-close-callback! channel #(on-close % -1 "connection interrumpted")) 258 | 259 | (.set setter (WebSocketListenerWrapper. on-message 260 | on-ping 261 | on-pong 262 | on-error 263 | on-close)) 264 | (.resumeReceives ^WebSocketChannel channel)))))) 265 | 266 | 267 | (defn upgrade-response 268 | [^HttpServerExchange exchange listener options] 269 | 270 | (assert (satisfies? rwp/Listener listener) 271 | "listener should satisfy Listener protocol") 272 | 273 | (let [^WebSocketProtocolHandshakeHandler handler (listener->handler listener)] 274 | (.addExtension handler (PerMessageDeflateHandshake. false 6)) 275 | (.handleRequest handler exchange))) 276 | -------------------------------------------------------------------------------- /src/yetti/adapter.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright © Andrey Antukh 6 | 7 | (ns yetti.adapter 8 | (:require 9 | [clojure.stacktrace :as ctr] 10 | [ring.websocket :as-alias rws] 11 | [yetti.request :as yrq] 12 | [yetti.response :as yrs] 13 | [yetti.util :as yu] 14 | [yetti.websocket :as yws]) 15 | (:import 16 | io.undertow.Undertow 17 | io.undertow.UndertowOptions 18 | io.undertow.server.DefaultByteBufferPool 19 | io.undertow.server.HttpHandler 20 | io.undertow.server.HttpServerExchange 21 | io.undertow.util.HeaderMap 22 | io.undertow.util.HttpString 23 | io.undertow.util.SameThreadExecutor 24 | java.util.concurrent.Executor 25 | java.util.concurrent.Executors 26 | java.util.concurrent.atomic.AtomicBoolean 27 | java.util.function.BiConsumer)) 28 | 29 | (set! *warn-on-reflection* true) 30 | 31 | (def defaults 32 | {:http/headers-cache-size 64 33 | :http/max-cookies 32 34 | :http/max-headers 64 35 | :http/max-headers-size (* 1024 1024) ; 1 MiB 36 | :http/max-body-size (* 1024 1024 6) ; 6 MiB 37 | :http/max-multipart-body-size (* 1024 1024 12) ; 12 MiB 38 | :xnio/buffer-size (- (* 1024 16) 20) 39 | :http/port 11010 40 | :http/host "localhost" 41 | :http/idle-timeout 300000 42 | :http/parse-timeout 30000 43 | :xnio/direct-buffers true 44 | :xnio/worker-thread-keep-alive 30000 45 | :ring/compat :ring2 46 | :socket/tcp-nodelay true 47 | :socket/backlog 1024 48 | :socket/reuse-address true 49 | :socket/read-timeout 300000 50 | :socket/write-timeout 300000 51 | :websocket/idle-timeout 500000}) 52 | 53 | (defn dispatch! 54 | ([^HttpServerExchange exchange ^Runnable f] 55 | (.dispatch exchange f)) 56 | ([^HttpServerExchange exchange ^Executor executor ^Runnable f] 57 | (.dispatch exchange executor f))) 58 | 59 | (defn- handle-error 60 | [request cause {:keys [:http/on-error] :as options}] 61 | (let [trace (with-out-str (ctr/print-cause-trace cause))] 62 | (if (fn? on-error) 63 | (on-error cause request) 64 | (println trace)) 65 | {::yrs/status 500 66 | ::yrs/body trace 67 | ::yrs/headers {"content-type" "text/plain"}})) 68 | 69 | (defn- handle-response 70 | [exchange request response {:keys [:http/on-error] :as options}] 71 | (try 72 | (if-let [listener (or (::yws/listener response) 73 | (::rws/listener response))] 74 | (yws/upgrade-response exchange listener options) 75 | (yrs/write-response! exchange response)) 76 | (catch Throwable cause 77 | (if (fn? on-error) 78 | (on-error cause request) 79 | (ctr/print-cause-trace cause))) 80 | (finally 81 | (.endExchange ^HttpServerExchange exchange)))) 82 | 83 | (defn- dispatch-fn 84 | [handler {:keys [:http/on-error :ring/compat] :as options}] 85 | (let [exchange->request (case compat 86 | :ring1 yrq/exchange->ring1-request 87 | :ring2 yrq/exchange->ring2-request)] 88 | 89 | (fn [^HttpServerExchange exchange] 90 | (let [request (exchange->request exchange) 91 | response (try 92 | (handler request) 93 | (catch Throwable cause 94 | (handle-error request cause options)))] 95 | (handle-response exchange request response options))))) 96 | 97 | (defn- create-handler 98 | "Creates an instance of the final handler that will be attached to 99 | Server." 100 | [handler-fn {:keys [:xnio/dispatch :events/on-dispatch] :as options}] 101 | (let [dispatch-fn (dispatch-fn handler-fn options)] 102 | (cond 103 | (instance? Executor dispatch) 104 | (reify HttpHandler 105 | (^void handleRequest [_ ^HttpServerExchange exchange] 106 | (let [spoint (System/nanoTime)] 107 | (.dispatch exchange 108 | ^Executor dispatch 109 | ^Runnable #(do 110 | (when (fn? on-dispatch) 111 | (on-dispatch exchange spoint)) 112 | (.startBlocking exchange) 113 | (dispatch-fn exchange)))))) 114 | 115 | (= :virtual dispatch) 116 | (let [executor (Executors/newVirtualThreadPerTaskExecutor)] 117 | (reify HttpHandler 118 | (^void handleRequest [_ ^HttpServerExchange exchange] 119 | (let [spoint (System/nanoTime)] 120 | (.dispatch exchange 121 | ^Executor executor 122 | ^Runnable #(do 123 | (when (fn? on-dispatch) 124 | (on-dispatch exchange spoint)) 125 | (.startBlocking exchange) 126 | (dispatch-fn exchange))))))) 127 | 128 | (false? dispatch) 129 | (reify HttpHandler 130 | (^void handleRequest [_ ^HttpServerExchange exchange] 131 | (let [spoint (System/nanoTime)] 132 | (.dispatch exchange 133 | ^Executor SameThreadExecutor/INSTANCE 134 | ^Runnable #(do 135 | (when (fn? on-dispatch) 136 | (on-dispatch exchange spoint)) 137 | (dispatch-fn exchange)))))) 138 | 139 | :else 140 | (reify HttpHandler 141 | (^void handleRequest [_ ^HttpServerExchange exchange] 142 | (let [spoint (System/nanoTime)] 143 | (.dispatch exchange 144 | ^Runnable #(do 145 | (when (fn? on-dispatch) 146 | (on-dispatch exchange spoint)) 147 | (.startBlocking exchange) 148 | (dispatch-fn exchange))))))))) 149 | 150 | (defn- create-server 151 | "Construct a Server instance." 152 | [handler {:keys [:http/port 153 | :http/host 154 | :http/idle-timeout 155 | :http/headers-cache-size 156 | :http/max-body-size 157 | :http/max-multipart-body-size 158 | :http/max-headers-size 159 | :http/max-cookies 160 | :http/max-headers 161 | :xnio/direct-buffers 162 | :xnio/buffer-size 163 | :xnio/io-threads 164 | :xnio/worker-thread-keep-alive 165 | :xnio/min-worker-threads 166 | :xnio/max-worker-threads 167 | :socket/send-buffer 168 | :socket/receive-buffer 169 | :socket/write-timeout 170 | :socket/read-timeout 171 | :socket/reuse-address 172 | :socket/tcp-nodelay 173 | :socket/backlog] 174 | :as options}] 175 | 176 | (let [num-processors 177 | (.availableProcessors (Runtime/getRuntime)) 178 | 179 | io-threads 180 | (or io-threads 181 | (max 2 num-processors)) 182 | 183 | min-worker-threads 184 | (or min-worker-threads 185 | (* io-threads 2)) 186 | 187 | max-worker-threads 188 | (or max-worker-threads 189 | (* io-threads 32))] 190 | 191 | (-> (Undertow/builder) 192 | (.addHttpListener port host) 193 | 194 | (cond-> (int? buffer-size) (.setBufferSize buffer-size)) 195 | (cond-> (some? direct-buffers) (.setDirectBuffers direct-buffers)) 196 | 197 | (cond-> (some? backlog) (.setSocketOption org.xnio.Options/BACKLOG (int backlog))) 198 | (cond-> (some? read-timeout) (.setSocketOption org.xnio.Options/READ_TIMEOUT (int read-timeout))) 199 | (cond-> (some? write-timeout) (.setSocketOption org.xnio.Options/WRITE_TIMEOUT (int write-timeout))) 200 | (cond-> (some? tcp-nodelay) (.setSocketOption org.xnio.Options/TCP_NODELAY ^Boolean tcp-nodelay)) 201 | (cond-> (some? reuse-address) (.setSocketOption org.xnio.Options/REUSE_ADDRESSES ^Boolean reuse-address)) 202 | (cond-> (some? send-buffer) (.setSocketOption org.xnio.Options/SEND_BUFFER (int send-buffer))) 203 | (cond-> (some? receive-buffer) (.setSocketOption org.xnio.Options/RECEIVE_BUFFER (int receive-buffer))) 204 | 205 | (.setWorkerOption org.xnio.Options/WORKER_IO_THREADS (int io-threads)) 206 | (.setWorkerOption org.xnio.Options/WORKER_TASK_CORE_THREADS (int min-worker-threads)) 207 | (.setWorkerOption org.xnio.Options/WORKER_TASK_MAX_THREADS (int max-worker-threads)) 208 | 209 | (cond-> (int? worker-thread-keep-alive) 210 | (.setWorkerOption org.xnio.Options/WORKER_TASK_KEEPALIVE (int worker-thread-keep-alive))) 211 | 212 | (.setServerOption UndertowOptions/MAX_COOKIES (int max-cookies)) 213 | (.setServerOption UndertowOptions/MAX_HEADERS (int max-headers)) 214 | (.setServerOption UndertowOptions/MAX_HEADER_SIZE (int max-headers-size)) 215 | (.setServerOption UndertowOptions/ALWAYS_SET_KEEP_ALIVE, false) 216 | (.setServerOption UndertowOptions/BUFFER_PIPELINED_DATA false) 217 | (.setServerOption UndertowOptions/IDLE_TIMEOUT (int idle-timeout)) 218 | (.setServerOption UndertowOptions/ENABLE_HTTP2 true) 219 | (.setServerOption UndertowOptions/HTTP_HEADERS_CACHE_SIZE (int headers-cache-size)) 220 | (.setServerOption UndertowOptions/MULTIPART_MAX_ENTITY_SIZE max-multipart-body-size) 221 | (.setServerOption UndertowOptions/MAX_ENTITY_SIZE max-body-size) 222 | (.setServerOption UndertowOptions/HTTP2_SETTINGS_ENABLE_PUSH false) 223 | (.setHandler ^HttpHandler handler) 224 | (.build)))) 225 | 226 | (defn ^Undertow server 227 | " 228 | Creates and confgures an instance of the server. This is a list of options 229 | that you can provide: 230 | 231 | :http/port - the port to listen on (defaults to 11010) 232 | :http/host - the hostname to listen on, defaults to 'localhost' 233 | :http/idle-timeout - the max idle time in ms for a connection (default to 200000) 234 | :http/parse-timeout - max time spend in parsing request (defaults to 30000) 235 | :http/max-headers-size - max headers (all) size (defaults to 1 MiB) 236 | :http/max-body-size - max body size (defaults to 6 MiB) 237 | :http/max-multipart-body-size - max size for multipart uploads (defaults to 12 MiB) 238 | :http/max-cookies - max number of allowed cookies in the request (defaults to 32) 239 | :http/max-headers - max number of allowed headers in the request (defaults to 64) 240 | :ring/compat - ring compatibility mode: :ring2 (default), :ring2-map, :ring1 241 | 242 | :xnio/buffer-size - default http IO buffe size (default 64 KiB) 243 | :xnio/direct-buffers - use or not direct buffers (default to false) 244 | :xnio/dispatch - dispatch or not the body of the handler to the worker executor 245 | (defaults to :virtual, can be a custom executor instance) 246 | :websocket/idle-timeout - websocket specific idle timeout (defaults to 500000) 247 | " 248 | ([handler-fn] (server handler-fn {})) 249 | ([handler-fn options] 250 | (let [options (merge defaults options) 251 | handler (create-handler handler-fn options)] 252 | (create-server handler options)))) 253 | 254 | (defn start! 255 | "Starts the server. It accepts an optional `options` parameter 256 | that accepts the following attrs: 257 | 258 | :join - blocks the thread until the server is starts (defaults false) 259 | " 260 | [^Undertow server] 261 | (.start server) 262 | server) 263 | 264 | (defn stop! 265 | "Stops the server." 266 | [^Undertow s] 267 | (.stop s) 268 | s) 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------