├── .github └── workflows │ └── test.yml ├── .gitignore ├── FUNDING.yml ├── LICENSE ├── README.md ├── VERSION ├── deps.edn ├── dev └── playground.clj ├── resources └── headers.txt ├── src └── s_exp │ ├── hirundo.clj │ └── hirundo │ ├── http │ ├── request.clj │ ├── response.clj │ └── routing.clj │ ├── options.clj │ ├── utils.clj │ ├── websocket.clj │ └── websocket │ ├── listener.clj │ └── routing.clj └── test ├── keystore.jceks ├── keystore.jks ├── s_exp ├── hirundo_test.clj └── hirundo_test_runner.clj ├── server.crt └── server.key /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Prepare java 13 | uses: actions/setup-java@v3 14 | with: 15 | distribution: 'zulu' 16 | java-version: '21' 17 | 18 | - name: Install clojure tools 19 | uses: DeLaGuardo/setup-clojure@10.0 20 | with: 21 | cli: 1.11.1.1182 22 | 23 | - name: Run tests 24 | run: clojure -X:test s-exp.hirundo-test-runner/run 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .calva/output-window/ 2 | .classpath 3 | .clj-kondo/.cache 4 | .cpcache 5 | .eastwood 6 | .factorypath 7 | .hg/ 8 | .hgignore 9 | .java-version 10 | .lein-* 11 | .lsp/.cache 12 | .lsp/sqlite.db 13 | .nrepl-history 14 | .nrepl-port 15 | .project 16 | .rebel_readline_history 17 | .settings 18 | .socket-repl-port 19 | .sw* 20 | .vscode 21 | *.class 22 | *.jar 23 | *.swp 24 | *~ 25 | /checkouts 26 | /classes 27 | /target 28 | .DS_Store 29 | /pipeline.lua 30 | .clj-kondo 31 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mpenet 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hirundo [![Clojars Project](https://img.shields.io/clojars/v/com.s-exp/hirundo.svg)](https://clojars.org/com.s-exp/hirundo) 2 | 3 | 4 | 5 | 6 | 7 | [Helidon/Nima](https://helidon.io/nima) 8 | [RING](https://github.com/ring-clojure/ring/blob/master/SPEC) compliant adapter 9 | for clojure, loom based 10 | 11 | ## Usage 12 | 13 | ```clojure 14 | (require '[s-exp.hirundo :as hirundo]) 15 | (require '[s-exp.hirundo.websocket :as ws]) 16 | 17 | (def server 18 | (hirundo/start! {;; regular ring handler 19 | :http-handler (fn [{:as request :keys [body headers ...]}] 20 | {:status 200 21 | :body "Hello world" 22 | :headers {"Something" "Interesting"}}) 23 | 24 | ;; websocket endpoints 25 | :websocket-endpoints {"/ws" {:message (fn [session data _last-msg] 26 | ;; echo back data 27 | (ws/send! session data true)) 28 | :open (fn [session] (prn :opening-session)) 29 | :close (fn [_session status reason] 30 | (prn :closed-session status reason)) 31 | :error (fn [session error] 32 | (prn :error error)) 33 | ;; :subprotocols ["chat"] 34 | ;; :extensions ["foobar"] 35 | ;; :http-upgrade (fn [headers] ...) 36 | }} 37 | :port 8080})) 38 | ;; ... 39 | 40 | (hirundo/stop! server) 41 | 42 | ``` 43 | 44 | 45 | There is nothing special to its API, you use hirundo as you would use any blocking 46 | http adapter like jetty; it is RING compliant so compatible with most/all 47 | middlewares out there. 48 | 49 | ## Supported options 50 | 51 | * `:host` - host of the default socket, defaults to 127.0.0.1 52 | 53 | * `:port` - port the server listens to, defaults to random free port 54 | 55 | * `:http-handler` - ring handler function 56 | 57 | * `:websocket-endpoints` - /!\ subject to changes - (map-of string-endpoint handler-fns-map), where handler if can be of `:message`, `:ping`, `:pong`, `:close`, `:error`, `:open`, `:http-upgrade`. `handler-fns-map` can also contain 2 extra keys, `:extensions`, `:subprotocols`, which are sets of subprotocols and protocol extensions acceptable by the server 58 | 59 | * `:write-queue-length` 60 | 61 | * `:backlog` 62 | 63 | * `:max-payload-size` 64 | 65 | * `:write-queue-length` 66 | 67 | * `:receive-buffer-size` 68 | 69 | * `:connection-options`(map-of `:socket-receive-buffer-size` `:socket-send-buffer-size` `:socket-reuse-address` `:socket-keep-alive` `:tcp-no-delay` `:read-timeout` `:connect-timeout`) 70 | 71 | * `:tls` - A `io.helidon.nima.common.tls.Tls` instance 72 | 73 | 74 | You can hook into the server builder via `s-exp.hirundo.options/set-server-option!` 75 | multimethod at runtime and add/modify whatever you want if you need anything 76 | extra we don't provide (yet). 77 | 78 | http2 (h2 & h2c) is supported out of the box, iif a client connects with http2 79 | it will do the protocol switch automatically. 80 | 81 | ## Installation 82 | 83 | Note: You need to use java **21** 84 | 85 | https://clojars.org/com.s-exp/hirundo 86 | 87 | ## Running the tests 88 | 89 | ``` 90 | clj -X:test 91 | ``` 92 | 93 | ## Implemented 94 | 95 | - [x] HTTP (1.1 & 2) server/handlers 96 | - [x] WebSocket handlers (initial implementation) 97 | - [ ] Grpc handlers 98 | 99 | ## Building Uberjars with hirundo 100 | 101 | Because of the way helidon handles service configuration we need to carefuly 102 | craft the uberjar with merged resources for some entries. 103 | 104 | You will need to provide `:conflict-handlers` for the uberjar task that 105 | concatenates some of the files from resources found in helidon module 106 | dependencies. 107 | 108 | 109 | Pay attention to the `b/uber` call here: 110 | 111 | ```clj 112 | (ns build 113 | (:refer-clojure :exclude [test]) 114 | (:require [clojure.data.json :as json] 115 | [clojure.java.io :as io] 116 | [clojure.tools.build.api :as b] 117 | [clojure.tools.build.tasks.uber :as uber])) 118 | 119 | (def lib 'foo/bar) 120 | (def version "0.1.0-SNAPSHOT") 121 | (def main 'foo.bar.baz) 122 | (def class-dir "target/classes") 123 | (defn- uber-opts [opts] 124 | (assoc opts 125 | :lib lib :main main 126 | :uber-file (format "target/%s-%s.jar" lib version) 127 | :basis (b/create-basis {}) 128 | :class-dir class-dir 129 | :src-dirs ["src"] 130 | :ns-compile [main])) 131 | 132 | (defn append-json 133 | [{:keys [path in existing state]}] 134 | {:write 135 | {path 136 | {:append false 137 | :string 138 | (json/write-str 139 | (concat (json/read-str (slurp existing)) 140 | (json/read-str (#'uber/stream->string in))))}}}) 141 | 142 | (defn ci "Run the CI pipeline of tests (and build the uberjar)." [opts] 143 | (b/delete {:path "target"}) 144 | (let [opts (uber-opts opts)] 145 | (println "\nCopying source...") 146 | (b/copy-dir {:src-dirs ["src"] :target-dir class-dir}) 147 | (println (str "\nCompiling " main "...")) 148 | (b/compile-clj opts) 149 | (println "\nBuilding JAR...") 150 | 151 | ;; HERE is the important part 152 | (b/uber (assoc opts :conflict-handlers 153 | {"META-INF/helidon/service.loader" :append-dedupe 154 | "META-INF/helidon/feature-metadata.properties" :append-dedupe 155 | "META-INF/helidon/config-metadata.json" append-json 156 | "META-INF/helidon/service-registry.json" append-json}))) 157 | 158 | opts) 159 | ``` 160 | 161 | ## License 162 | 163 | Copyright © 2023 Max Penet 164 | 165 | Distributed under the Eclipse Public License version 1. 166 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.43-SNAPSHOT -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:exoscale.project/lib com.s-exp/hirundo 2 | :exoscale.project/version-file "VERSION" 3 | :exoscale.project/deploy? true 4 | :exoscale.project/pom-data 5 | [[:licenses 6 | [:license 7 | [:name "Eclipse Public License - v 2.0"] 8 | [:url "https://www.eclipse.org/legal/epl-2.0/"] 9 | [:distribution "repo"]]]] 10 | 11 | :slipset.deps-deploy/exec-args {:installer :remote 12 | :sign-releases? false 13 | :repository "clojars"} 14 | 15 | :paths ["src" "resources"] 16 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 17 | io.helidon.http/helidon-http {:mvn/version "4.2.0"} 18 | io.helidon.webserver/helidon-webserver {:mvn/version "4.2.0"} 19 | io.helidon.webserver/helidon-webserver-websocket {:mvn/version "4.2.0"} 20 | io.helidon.webserver/helidon-webserver-http2 {:mvn/version "4.2.0"} 21 | org.ring-clojure/ring-core-protocols {:mvn/version "1.13.0"}} 22 | 23 | :aliases 24 | {:test {:extra-paths ["test"] 25 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} 26 | less-awful-ssl/less-awful-ssl {:mvn/version "1.0.6"} 27 | eftest/eftest {:mvn/version "0.6.0"} 28 | stylefruits/gniazdo {:mvn/version "1.2.2"} 29 | clj-http/clj-http {:mvn/version "3.12.0"}} 30 | :exec-fn s-exp.hirundo-test-runner/run} 31 | 32 | :project 33 | {:extra-deps {io.github.exoscale/tools.project {:git/sha "ce87a95e95abfde832043aff5af7ecb06ad79fe7"}} 34 | :ns-default exoscale.tools.project 35 | :jvm-opts ["-Dclojure.main.report=stderr"]}}} 36 | -------------------------------------------------------------------------------- /dev/playground.clj: -------------------------------------------------------------------------------- 1 | (ns dev.playground 2 | (:require 3 | [gniazdo.core :as ws] 4 | [s-exp.hirundo :as m] 5 | [s-exp.hirundo.websocket :as mws])) 6 | 7 | (def r {:status 200}) 8 | (def h (fn [req] r)) 9 | 10 | (def s (m/start! 11 | {:host "0.0.0.0" :port 8080 12 | :http-hander #'h 13 | :websocket-endpoints {"/foo" 14 | {;; :subprotocols ["chat"] 15 | :error (fn [session e] 16 | (prn :err e)) 17 | :open (fn [session] 18 | ;; (prn :open) 19 | ) 20 | :close (fn [session status data] 21 | (prn :close status)) 22 | :message (fn [session data last] 23 | (prn :message data last) 24 | (mws/send! session data true))}} 25 | :write-queue-length 10240 26 | :connection-options {:socket-send-buffer-size 1024}})) 27 | 28 | (def socket 29 | (ws/connect 30 | "ws://localhost:8080/foo" 31 | :on-receive #(prn 'received %) 32 | ;; :headers {"foo" "bar"} 33 | ;; :subprotocols ["chat, foo"] 34 | )) 35 | (ws/send-msg socket "hello") 36 | 37 | (ws/close socket) 38 | 39 | -------------------------------------------------------------------------------- /resources/headers.txt: -------------------------------------------------------------------------------- 1 | A-IM 2 | Accept 3 | Accept-Additions 4 | Accept-CH 5 | Accept-Charset 6 | Accept-Datetime 7 | Accept-Encoding 8 | Accept-Features 9 | Accept-Language 10 | Accept-Patch 11 | Accept-Post 12 | Accept-Ranges 13 | Access-Control 14 | Access-Control-Allow-Credentials 15 | Access-Control-Allow-Headers 16 | Access-Control-Allow-Methods 17 | Access-Control-Allow-Origin 18 | Access-Control-Expose-Headers 19 | Access-Control-Max-Age 20 | Access-Control-Request-Headers 21 | Access-Control-Request-Method 22 | Age 23 | Allow 24 | ALPN 25 | Alt-Svc 26 | Alt-Used 27 | Alternates 28 | AMP-Cache-Transform 29 | Apply-To-Redirect-Ref 30 | Authentication-Control 31 | Authentication-Info 32 | Authorization 33 | C-Ext 34 | C-Man 35 | C-Opt 36 | C-PEP 37 | C-PEP-Info 38 | Cache-Control 39 | Cache-Status 40 | Cal-Managed-ID 41 | CalDAV-Timezones 42 | Capsule-Protocol 43 | CDN-Cache-Control 44 | CDN-Loop 45 | Cert-Not-After 46 | Cert-Not-Before 47 | Clear-Site-Data 48 | Close 49 | Configuration-Context 50 | Connection 51 | Content-Base 52 | Content-Disposition 53 | Content-Encoding 54 | Content-ID 55 | Content-Language 56 | Content-Length 57 | Content-Location 58 | Content-MD5 59 | Content-Range 60 | Content-Script-Type 61 | Content-Security-Policy 62 | Content-Security-Policy-Report-Only 63 | Content-Style-Type 64 | Content-Type 65 | Content-Version 66 | Cookie 67 | Cookie2 68 | Cross-Origin-Embedder-Policy 69 | Cross-Origin-Embedder-Policy-Report-Only 70 | Cross-Origin-Opener-Policy 71 | Cross-Origin-Opener-Policy-Report-Only 72 | Cross-Origin-Resource-Policy 73 | DASL 74 | Date 75 | DAV 76 | Default-Style 77 | Delta-Base 78 | Depth 79 | Derived-From 80 | Destination 81 | Differential-ID 82 | Digest 83 | Early-Data 84 | EDIINT-Features 85 | ETag 86 | Expect 87 | Expect-CT 88 | Expires 89 | Ext 90 | Forwarded 91 | From 92 | GetProfile 93 | Hobareg 94 | Host 95 | HTTP2-Settings 96 | If 97 | If-Match 98 | If-Modified-Since 99 | If-None-Match 100 | If-Range 101 | If-Schedule-Tag-Match 102 | If-Unmodified-Since 103 | IM 104 | Include-Referred-Token-Binding-ID 105 | Isolation 106 | Keep-Alive 107 | Label 108 | Last-Event-ID 109 | Last-Modified 110 | Link 111 | Location 112 | Lock-Token 113 | Man 114 | Max-Forwards 115 | Memento-Datetime 116 | Meter 117 | Method-Check 118 | Method-Check-Expires 119 | MIME-Version 120 | Negotiate 121 | OData-EntityId 122 | OData-Isolation 123 | OData-MaxVersion 124 | OData-Version 125 | Opt 126 | Optional-WWW-Authenticate 127 | Ordering-Type 128 | Origin 129 | Origin-Agent-Cluster 130 | OSCORE 131 | OSLC-Core-Version 132 | Overwrite 133 | P3P 134 | PEP 135 | Pep-Info 136 | PICS-Label 137 | Ping-From 138 | Ping-To 139 | Position 140 | Pragma 141 | Prefer 142 | Preference-Applied 143 | Priority 144 | ProfileObject 145 | Protocol 146 | Protocol-Info 147 | Protocol-Query 148 | Protocol-Request 149 | Proxy-Authenticate 150 | Proxy-Authentication-Info 151 | Proxy-Authorization 152 | Proxy-Features 153 | Proxy-Instruction 154 | Proxy-Status 155 | Public 156 | Public-Key-Pins 157 | Public-Key-Pins-Report-Only 158 | Range 159 | Redirect-Ref 160 | Referer 161 | Referer-Root 162 | Refresh 163 | Repeatability-Client-ID 164 | Repeatability-First-Sent 165 | Repeatability-Request-ID 166 | Repeatability-Result 167 | Replay-Nonce 168 | Retry-After 169 | Safe 170 | Schedule-Reply 171 | Schedule-Tag 172 | Sec-GPC 173 | Sec-Token-Binding 174 | Sec-WebSocket-Accept 175 | Sec-WebSocket-Extensions 176 | Sec-WebSocket-Key 177 | Sec-WebSocket-Protocol 178 | Sec-WebSocket-Version 179 | Security-Scheme 180 | Server 181 | Server-Timing 182 | Set-Cookie 183 | Set-Cookie2 184 | SetProfile 185 | SLUG 186 | SoapAction 187 | Status-URI 188 | Strict-Transport-Security 189 | Sunset 190 | Surrogate-Capability 191 | Surrogate-Control 192 | TCN 193 | TE 194 | Timeout 195 | Timing-Allow-Origin 196 | Topic 197 | Traceparent 198 | Tracestate 199 | Trailer 200 | Transfer-Encoding 201 | TTL 202 | Upgrade 203 | Urgency 204 | URI 205 | User-Agent 206 | Variant-Vary 207 | Vary 208 | Via 209 | Want-Digest 210 | Warning 211 | WWW-Authenticate 212 | X-Content-Type-Options 213 | X-Frame-Options 214 | * -------------------------------------------------------------------------------- /src/s_exp/hirundo.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo 2 | (:require [s-exp.hirundo.http.routing] 3 | [s-exp.hirundo.options :as options] 4 | [s-exp.hirundo.websocket] 5 | [s-exp.hirundo.websocket.routing]) 6 | (:import (io.helidon.webserver WebServer WebServerConfig WebServerConfig$Builder))) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | (def default-options {:connection-provider false}) 11 | 12 | (defn- server-builder 13 | ^WebServerConfig$Builder 14 | [options] 15 | (reduce (fn [builder [k v]] 16 | (options/set-server-option! builder k v options)) 17 | (WebServerConfig/builder) 18 | options)) 19 | 20 | (defn start! 21 | "Starts a new server. 22 | 23 | `options` can contain: 24 | 25 | * `:http-handler` - ring http handler function 26 | 27 | * `:websocket-endpoints` - websocket endpoints (map-of string-endpoint handler-fns-map), where handler if can be of `:message`, `:ping`, `:pong`, `:close`, `:error`, `:open`, `:http-upgrade`. `handler-fns-map` can also contain 2 extra keys, `:extensions`, `:subprotocols`, which are sets of exts/subprotos acceptable by the server. 28 | 29 | * `:host` - host of the default socket 30 | 31 | * `:port` - port the server listens to, default to 8080 32 | 33 | * `:default-socket` - map-of :write-queue-length :backlog :max-payload-size :receive-buffer-size `:connection-options`(map-of `:socket-receive-buffer-size` `:socket-send-buffer-size` `:socket-reuse-address` `:socket-keep-alive` `:tcp-no-delay` `:read-timeout` `:connect-timeout`) 34 | 35 | * `:tls` - a `io.helidon.nima.common.tls.Tls` instance" 36 | ([http-handler options] 37 | (start! (assoc options :http-handler http-handler))) 38 | ([options] 39 | (-> (server-builder (merge default-options options)) 40 | .build 41 | (.start)))) 42 | 43 | (defn stop! 44 | "Stops server, noop if already stopped" 45 | [^WebServer server] 46 | (.stop server)) 47 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/http/request.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.http.request 2 | (:require [clojure.string :as str]) 3 | (:import (clojure.lang PersistentHashMap) 4 | (io.helidon.common.uri UriQuery UriPath) 5 | (io.helidon.http HttpPrologue Headers Header) 6 | (io.helidon.webserver.http ServerRequest ServerResponse))) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | (defn ring-headers 11 | [^Headers headers] 12 | (-> (reduce (fn [m ^Header h] 13 | (assoc! m 14 | (.lowerCase (.headerName h)) 15 | (.value h))) 16 | (transient {}) 17 | headers) 18 | persistent!)) 19 | 20 | (defn ring-method [^HttpPrologue prologue] 21 | (let [method (-> prologue .method .text)] 22 | (case method 23 | "GET" :get 24 | "POST" :post 25 | "PUT" :put 26 | "DELETE" :delete 27 | "HEAD" :head 28 | "OPTIONS" :options 29 | "TRACE" :trace 30 | "PATCH" :patch 31 | (keyword (str/lower-case method))))) 32 | 33 | (defn ring-protocol 34 | [^HttpPrologue prologue] 35 | (case (.protocolVersion prologue) 36 | "1.0" "HTTP/1.0" 37 | "1.1" "HTTP/1.1" 38 | "2.0" "HTTP/2")) 39 | 40 | (defn ring-query 41 | [^UriQuery query] 42 | (let [query (.rawValue query)] 43 | (when (not= "" query) query))) 44 | 45 | (defn ring-path [^UriPath path] 46 | (.rawPath path)) 47 | 48 | (defn ring-request 49 | [^ServerRequest server-request 50 | ^ServerResponse server-response] 51 | (let [qs (ring-query (.query server-request)) 52 | body (let [content (.content server-request)] 53 | (when-not (.consumed content) (.inputStream content))) 54 | 55 | ring-request (transient 56 | {:server-port (.port (.localPeer server-request)) 57 | :server-name (.host (.localPeer server-request)) 58 | :remote-addr (let [address ^java.net.InetSocketAddress (.address (.remotePeer server-request))] 59 | (-> address .getAddress .getHostAddress)) 60 | :ssl-client-cert (some-> server-request .remotePeer .tlsCertificates (.orElse nil) first) 61 | :uri (ring-path (.path server-request)) 62 | :scheme (if (.isSecure server-request) :https :http) 63 | :protocol (ring-protocol (.prologue server-request)) 64 | :request-method (ring-method (.prologue server-request)) 65 | :headers (ring-headers (.headers server-request)) 66 | :authority (.authority server-request) 67 | ::server-request server-request 68 | ::server-response server-response}) 69 | 70 | ring-request (cond-> ring-request 71 | qs (assoc! :query-string (ring-query (.query server-request))) 72 | body (assoc! :body body))] 73 | 74 | (persistent! ring-request))) 75 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/http/response.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.http.response 2 | (:require [ring.core.protocols :as rp]) 3 | (:import (io.helidon.http HeaderNames 4 | HeaderName 5 | Status) 6 | (io.helidon.webserver.http ServerResponse) 7 | (java.io FileInputStream InputStream OutputStream))) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defprotocol BodyWriter 12 | (write-body! [x server-response])) 13 | 14 | (extend-protocol BodyWriter 15 | clojure.lang.Sequential 16 | (write-body! [xs ^ServerResponse server-response] 17 | (with-open [os ^OutputStream (.outputStream server-response)] 18 | (run! (fn [^String chunk] (.write os (.getBytes chunk))) 19 | xs))) 20 | 21 | java.io.InputStream 22 | (write-body! [is ^ServerResponse server-response] 23 | (with-open [^InputStream is is 24 | os ^OutputStream (.outputStream server-response)] 25 | (.transferTo is os))) 26 | 27 | java.io.File 28 | (write-body! [file ^ServerResponse server-response] 29 | (with-open [os ^OutputStream (.outputStream server-response) 30 | is (FileInputStream. file)] 31 | (.transferTo is os))) 32 | 33 | String 34 | (write-body! [s ^ServerResponse server-response] 35 | (.send ^ServerResponse server-response 36 | ^String s)) 37 | 38 | nil 39 | (write-body! [_ server-response] 40 | (.send ^ServerResponse server-response)) 41 | 42 | Object 43 | (write-body! [o ^ServerResponse server-response] 44 | (if (satisfies? rp/StreamableResponseBody o) 45 | (rp/write-body-to-stream o nil (.outputStream server-response)) 46 | (.send server-response o)))) 47 | 48 | (defn header-name ^HeaderName [ring-header-name] 49 | (HeaderNames/create (name ring-header-name))) 50 | 51 | (defn set-headers! 52 | [^ServerResponse server-response headers] 53 | (when headers 54 | (run! (fn [[k v]] 55 | (let [values-seq (if (sequential? v) v [v]) 56 | headers ^"[Ljava.lang.String;" (into-array String values-seq)] 57 | (.header server-response (header-name k) headers))) 58 | headers))) 59 | 60 | (defn- set-status! 61 | [^ServerResponse server-response status] 62 | (when status 63 | (.status server-response (Status/create status)))) 64 | 65 | (defn set-response! 66 | [^ServerResponse server-response {:keys [body headers status]}] 67 | (set-headers! server-response headers) 68 | (set-status! server-response status) 69 | (write-body! body server-response)) 70 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/http/routing.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.http.routing 2 | (:require [s-exp.hirundo.http.request :as request] 3 | [s-exp.hirundo.http.response :as response] 4 | [s-exp.hirundo.options :as options]) 5 | (:import (io.helidon.webserver WebServerConfig$Builder) 6 | (io.helidon.webserver.http Handler 7 | HttpRouting))) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn set-ring1-handler! ^WebServerConfig$Builder 12 | [^WebServerConfig$Builder builder handler _options] 13 | (doto builder 14 | (.addRouting 15 | (doto (HttpRouting/builder) 16 | (.any 17 | ^"[Lio.helidon.webserver.http.Handler;" 18 | (into-array Handler 19 | [(reify Handler 20 | (handle [_ server-request server-response] 21 | (->> (request/ring-request server-request server-response) 22 | handler 23 | (response/set-response! server-response))))])))))) 24 | 25 | (defmethod options/set-server-option! :http-handler 26 | [^WebServerConfig$Builder builder _ handler options] 27 | (set-ring1-handler! builder handler options)) 28 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/options.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.options 2 | (:import (io.helidon.common.concurrency.limits Limit) 3 | (io.helidon.common.socket SocketOptions$Builder) 4 | (io.helidon.common.tls Tls) 5 | (io.helidon.webserver WebServerConfig$Builder) 6 | (java.time Duration))) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | (defmulti set-server-option! (fn [_builder k _v _options] k)) 11 | 12 | (defmethod set-server-option! :default [builder _ _ _] 13 | builder) 14 | 15 | (defmethod set-server-option! :host 16 | [^WebServerConfig$Builder builder _ host _] 17 | (.host builder host)) 18 | 19 | (defmethod set-server-option! :port 20 | [^WebServerConfig$Builder builder _ port _] 21 | (.port builder (int port))) 22 | 23 | (defmethod set-server-option! :backlog 24 | [^WebServerConfig$Builder builder _ backlog _] 25 | (.backlog builder (int backlog))) 26 | 27 | (defmethod set-server-option! :idle-connection-timeout 28 | [^WebServerConfig$Builder builder _ idle-connection-timeout-ms _] 29 | (.idleConnectionTimeout builder (Duration/ofMillis idle-connection-timeout-ms))) 30 | 31 | (defmethod set-server-option! :idle-connection-period 32 | [^WebServerConfig$Builder builder _ idle-connection-period-ms _] 33 | (.idleConnectionPeriod builder (Duration/ofMillis idle-connection-period-ms))) 34 | 35 | (defmethod set-server-option! :max-payload-size 36 | [^WebServerConfig$Builder builder _ max-payload-size _] 37 | (.maxPayloadSize builder (long max-payload-size))) 38 | 39 | (defmethod set-server-option! :max-tcp-connections 40 | [^WebServerConfig$Builder builder _ max-tcp-connections _] 41 | (.maxTcpConnections builder (int max-tcp-connections))) 42 | 43 | (defmethod set-server-option! :concurrency-limit 44 | [^WebServerConfig$Builder builder _ ^Limit concurrency-limit _] 45 | (.concurrencyLimit builder concurrency-limit)) 46 | 47 | (defmethod set-server-option! :max-concurrent-requests 48 | [^WebServerConfig$Builder builder _ max-concurrent-requests _] 49 | (.maxConcurrentRequests builder (long max-concurrent-requests))) 50 | 51 | (defmethod set-server-option! :max-in-memory-entity 52 | [^WebServerConfig$Builder builder _ max-in-memory-entity _] 53 | (.maxInMemoryEntity builder (int max-in-memory-entity))) 54 | 55 | (defmethod set-server-option! :smart-async-writes 56 | [^WebServerConfig$Builder builder _ smart-async-writes _] 57 | (.smartAsyncWrites builder (boolean smart-async-writes))) 58 | 59 | (defmethod set-server-option! :write-queue-length 60 | [^WebServerConfig$Builder builder _ write-queue-length _] 61 | (.writeQueueLength builder (long write-queue-length))) 62 | 63 | (defmethod set-server-option! :write-buffer-size 64 | [^WebServerConfig$Builder builder _ write-buffer-size _] 65 | (.writeBufferSize builder (int write-buffer-size))) 66 | 67 | (defn- set-connection-options! 68 | [^SocketOptions$Builder socket-options-builder 69 | {:keys [socket-receive-buffer-size socket-send-buffer-size 70 | socket-reuse-address socket-keep-alive tcp-no-delay 71 | read-timeout connect-timeout]}] 72 | (when socket-receive-buffer-size 73 | (.socketReceiveBufferSize socket-options-builder 74 | (int socket-receive-buffer-size))) 75 | 76 | (when socket-send-buffer-size 77 | (.socketSendBufferSize socket-options-builder 78 | (int socket-send-buffer-size))) 79 | 80 | (when (some? socket-reuse-address) 81 | (.socketReuseAddress socket-options-builder 82 | (boolean socket-reuse-address))) 83 | 84 | (when (some? socket-keep-alive) 85 | (.socketKeepAlive socket-options-builder 86 | (boolean socket-keep-alive))) 87 | (when (some? tcp-no-delay) 88 | (.tcpNoDelay socket-options-builder 89 | (boolean tcp-no-delay))) 90 | 91 | (when read-timeout 92 | (.readTimeout socket-options-builder 93 | (Duration/ofMillis read-timeout))) 94 | (when connect-timeout 95 | (.connectTimeout socket-options-builder 96 | (Duration/ofMillis connect-timeout)))) 97 | 98 | (defmethod set-server-option! :connection-options 99 | [^WebServerConfig$Builder builder _ connection-options _] 100 | (.connectionOptions builder 101 | (reify java.util.function.Consumer 102 | (accept [_ socket-options-builder] 103 | (set-connection-options! socket-options-builder 104 | connection-options))))) 105 | 106 | (defmethod set-server-option! :tls 107 | [^WebServerConfig$Builder builder _ tls-config _] 108 | (doto builder (.tls ^Tls tls-config))) 109 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/utils.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.utils 2 | (:require [clojure.string :as str])) 3 | 4 | (set! *warn-on-reflection* true) 5 | 6 | (defn camel->dashed 7 | [s] 8 | (-> s 9 | (str/replace #"^[A-Z]+" str/lower-case) 10 | (str/replace #"_?([A-Z]+)" 11 | (comp (partial str "-") 12 | str/lower-case second)) 13 | (str/replace #"-|_" "-"))) 14 | 15 | (defn format-key 16 | [k ns] 17 | (->> k 18 | camel->dashed 19 | (keyword (some-> ns name)))) 20 | 21 | (defn enum->map 22 | ([enum ns] 23 | (reduce (fn [m hd] 24 | (assoc m (format-key (.name ^Enum hd) 25 | ns) 26 | hd)) 27 | {} 28 | (java.util.EnumSet/allOf enum))) 29 | ([enum] 30 | (enum->map enum nil))) 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/websocket.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.websocket 2 | (:import (io.helidon.common.buffers BufferData) 3 | (io.helidon.websocket WsSession))) 4 | 5 | (set! *warn-on-reflection* true) 6 | 7 | (defprotocol ToBufferData 8 | (buffer-data [x])) 9 | 10 | (extend-protocol ToBufferData 11 | 12 | (Class/forName "[B") 13 | (buffer-data [ba] 14 | (BufferData/create ^"[B" ba)) 15 | 16 | clojure.lang.Sequential 17 | (buffer-data [s] 18 | (BufferData/create ^java.util.List s)) 19 | 20 | BufferData 21 | (buffer-data [bd] 22 | bd) 23 | 24 | nil 25 | (buffer-data [x] 26 | (BufferData/empty))) 27 | 28 | (defn send! 29 | "Sends `msg` fragment over socket, if `last` is true this will be considered 30 | the last fragment" 31 | [^WsSession ws-session msg last] 32 | (if (string? msg) 33 | (.send ws-session ^String msg 34 | (boolean last)) 35 | (.send ws-session 36 | ^BufferData (buffer-data msg) 37 | (boolean last)))) 38 | 39 | (defn ping! 40 | "Sends PING response with optional `data` to client" 41 | [^WsSession ws-session data] 42 | (.ping ws-session (buffer-data data))) 43 | 44 | (defn pong! 45 | "Sends PONG response with optional `data` to client" 46 | [^WsSession ws-session data] 47 | (.pong ws-session (buffer-data data))) 48 | 49 | (defn close! 50 | "Closes socket with int `code` and `reason`" 51 | [^WsSession ws-session code reason] 52 | (.close ws-session (int code) (str reason))) 53 | 54 | (defn terminate! 55 | "Terminates connection" 56 | [^WsSession ws-session] 57 | (.terminate ws-session)) 58 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/websocket/listener.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.websocket.listener 2 | (:require [clojure.string :as str] 3 | [s-exp.hirundo.http.request :as r]) 4 | (:import (io.helidon.common.buffers BufferData) 5 | (io.helidon.http HeaderValues Headers) 6 | (io.helidon.http HttpPrologue) 7 | (io.helidon.http WritableHeaders) 8 | (io.helidon.websocket WsListener WsSession WsUpgradeException) 9 | (java.util Optional))) 10 | 11 | (set! *warn-on-reflection* true) 12 | 13 | ;; Client must send Sec-WebSocket-Version and Sec-WebSocket-Key. 14 | ;; Server must confirm the protocol by returning Sec-WebSocket-Accept. 15 | ;; Client may send a list of application subprotocols via Sec-WebSocket-Protocol. 16 | ;; Server must select one of the advertised subprotocols and return it via 17 | ;; Sec-WebSocket-Protocol. If the server does not support any, then the 18 | ;; connection is aborted. 19 | ;; Client may send a list of protocol extensions in Sec-WebSocket-Extensions. 20 | ;; Server may confirm one or more selected extensions via 21 | ;; Sec-WebSocket-Extensions. If no extensions are provided, then the connection 22 | ;; proceeds without them. 23 | ;; Finally, once the preceding handshake is complete, and if the handshake is 24 | ;; successful, the connection can now be used as a two-way communication channel 25 | ;; for exchanging WebSocket messages. From here on, there is no other explicit 26 | ;; HTTP communication between the client and server, and the WebSocket protocol 27 | ;; takes over. 28 | 29 | (defn- split-header-value 30 | [header-value] 31 | (->> (str/split header-value #",") 32 | (map str/trim))) 33 | 34 | (defn- header-negotiate 35 | [headers allowed-values header-name] 36 | (when (seq allowed-values) 37 | (if-let [selected-value (reduce (fn [_ x] 38 | (when (contains? allowed-values x) 39 | (reduced x))) 40 | nil 41 | (some-> (get headers header-name) 42 | split-header-value))] 43 | {header-name selected-value} 44 | (throw (WsUpgradeException. (format "Failed negotiation for %s" 45 | header-name)))))) 46 | 47 | (defn negotiate-subprotocols! 48 | [headers allowed-sub-protocols] 49 | (header-negotiate headers 50 | allowed-sub-protocols 51 | "sec-websocket-protocol")) 52 | 53 | (defn negotiate-extensions! 54 | [headers allowed-extensions] 55 | (header-negotiate headers 56 | allowed-extensions 57 | "sec-websocket-extensions")) 58 | 59 | (defn http-upgrade-default 60 | [{:as ring-request 61 | ::keys [allowed-subprotocols 62 | allowed-extensions]}] 63 | (merge (negotiate-subprotocols! (:headers ring-request) 64 | allowed-subprotocols) 65 | (negotiate-extensions! (:headers ring-request) 66 | allowed-extensions))) 67 | 68 | (defn headers-response [headers-map] 69 | (let [wh (WritableHeaders/create)] 70 | (run! (fn [[k v]] 71 | (.set wh 72 | (if (sequential? v) 73 | (HeaderValues/create (name k) 74 | ^java.util.Collection v) 75 | (HeaderValues/create (name k) 76 | (str v))))) 77 | headers-map) 78 | (Optional/of wh))) 79 | 80 | (defn make-listener 81 | ^WsListener [{:as _listener 82 | :keys [message ping pong close error open http-upgrade 83 | subprotocols extensions] 84 | :or {message (constantly nil) 85 | ping (constantly nil) 86 | pong (constantly nil) 87 | close (constantly nil) 88 | error (constantly nil) 89 | open (constantly nil)}}] 90 | (let [subprotocols (-> subprotocols not-empty set) 91 | extensions (-> extensions not-empty set)] 92 | (reify WsListener 93 | (^void onMessage [_ ^WsSession session ^String data ^boolean last] 94 | (message session data last)) 95 | (^void onMessage [_ ^WsSession session ^BufferData data ^boolean last] 96 | (message session data last)) 97 | (^void onPing [_ ^WsSession session ^BufferData data] 98 | (ping session data)) 99 | (^void onPong [_ ^WsSession session ^BufferData data] 100 | (pong session data)) 101 | (^void onClose [_ ^WsSession session ^int status ^String reason] 102 | (close session status reason)) 103 | (^void onError [_ ^WsSession session ^Throwable e] 104 | (error session e)) 105 | (^void onOpen [_ ^WsSession session] 106 | (open session)) 107 | (^Optional onHttpUpgrade [_ ^HttpPrologue http-prologue ^Headers headers] 108 | (let [ring-request {:method (r/ring-method http-prologue) 109 | :protocol (r/ring-protocol http-prologue) 110 | :headers (r/ring-headers headers) 111 | ::allowed-subprotocols subprotocols 112 | ::allowed-extensions extensions 113 | ::http-prologue http-prologue 114 | ::headers headers}] 115 | (headers-response 116 | (if http-upgrade 117 | (http-upgrade ring-request) 118 | (http-upgrade-default ring-request)))))))) 119 | -------------------------------------------------------------------------------- /src/s_exp/hirundo/websocket/routing.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo.websocket.routing 2 | (:require [s-exp.hirundo.options :as options] 3 | [s-exp.hirundo.websocket.listener :as l]) 4 | (:import (io.helidon.http HttpPrologue) 5 | (io.helidon.http PathMatchers) 6 | (io.helidon.webserver WebServerConfig$Builder Routing Route) 7 | (io.helidon.webserver.websocket WsRouting WsRoute WsRouting$Builder) 8 | (java.util.function Supplier))) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defn set-websocket-endpoints! ^WebServerConfig$Builder 13 | [^WebServerConfig$Builder builder endpoints _options] 14 | (doto builder 15 | (.addRouting 16 | ^WsRouting$Builder 17 | (reduce (fn [^WsRouting$Builder builder [path listener]] 18 | (.endpoint builder ^String path 19 | (reify Supplier 20 | (get [_] 21 | (l/make-listener listener))))) 22 | (WsRouting/builder) 23 | endpoints)))) 24 | 25 | (defmethod options/set-server-option! :websocket-endpoints 26 | [^WebServerConfig$Builder builder _ endpoints options] 27 | (set-websocket-endpoints! builder endpoints options)) 28 | -------------------------------------------------------------------------------- /test/keystore.jceks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpenet/hirundo/3426e6cbf924d7b32281bb1a282a0c56afcd0945/test/keystore.jceks -------------------------------------------------------------------------------- /test/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpenet/hirundo/3426e6cbf924d7b32281bb1a282a0c56afcd0945/test/keystore.jks -------------------------------------------------------------------------------- /test/s_exp/hirundo_test.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo-test 2 | (:require [clj-http.client :as client] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [clojure.test :refer [deftest is]] 6 | [gniazdo.core :as wsc] 7 | [less.awful.ssl :as ls] 8 | [ring.core.protocols :as p] 9 | [s-exp.hirundo :as m] 10 | [s-exp.hirundo.websocket :as ws]) 11 | (:import (io.helidon.common.tls Tls TlsClientAuth) 12 | (io.helidon.common.tls TlsConfig))) 13 | 14 | (def ^:dynamic *endpoint* nil) 15 | (def ^:dynamic *client* nil) 16 | 17 | (defn status-ok? [response] 18 | (some-> response :status (= 200))) 19 | 20 | (defmacro with-server [options & body] 21 | `(let [server# (m/start! ~options)] 22 | (binding [*endpoint* (format "http://localhost:%s/" (.port server#))] 23 | (try 24 | ~@body 25 | (finally (m/stop! server#)))))) 26 | 27 | (deftest test-send-headers 28 | (with-server {:http-handler (fn [req] {:headers {:foo "bar"}})} 29 | (is (-> (client/get *endpoint*) :headers :foo (= "bar")))) 30 | (with-server {:http-handler (fn [req] {:headers {:foo ["bar" "baz"]}})} 31 | (is (-> (client/get *endpoint*) :headers :foo (= ["bar" "baz"]))))) 32 | 33 | (deftest test-status 34 | (with-server {:http-handler (fn [req] {:status 201})} 35 | (is (-> (client/get *endpoint*) :status (= 201))))) 36 | 37 | (deftest test-headers 38 | (with-server {:http-handler (fn [req] 39 | {:body (str (count (:headers req)))})} 40 | (is (-> (client/get *endpoint*) :body (= "4")))) 41 | (with-server {:http-handler (fn [req] 42 | {:body (str (:headers req))})} 43 | (is (-> (client/get *endpoint*) :status (= 200))))) 44 | 45 | (deftest test-query-string 46 | (with-server {:http-handler (fn [req] {:body (:query-string req)})} 47 | (is (-> (client/get (str *endpoint* "?foo=bar")) :body (= "foo=bar")))) 48 | 49 | (with-server {:http-handler (fn [req] {:body (:query-string req)})} 50 | (is (-> (client/get (str *endpoint* "?")) :body (= "")))) 51 | 52 | (with-server {:http-handler (fn [req] {:body (:query-string req)})} 53 | (is (-> (client/get (str *endpoint* "")) :body (= ""))))) 54 | 55 | (deftest test-method 56 | (with-server {:http-handler (fn [req] {:body (str (:request-method req))})} 57 | (is (-> (client/post *endpoint*) :body (= ":post")))) 58 | 59 | (with-server {:http-handler (fn [req] {:body (str (:request-method req))})} 60 | (is (-> (client/put *endpoint*) :body (= ":put")))) 61 | 62 | (with-server {:http-handler (fn [req] {:body (str (:request-method req))})} 63 | (is (-> (client/delete *endpoint*) :body (= ":delete"))))) 64 | 65 | (deftest test-uri 66 | (with-server {:http-handler (fn [req] {:body (:uri req)})} 67 | (is (-> (client/delete (str *endpoint* "foo/bar")) :body (= "/foo/bar"))))) 68 | 69 | (deftest test-scheme 70 | (with-server {:http-handler (fn [req] {:body (str (:scheme req))})} 71 | (is (-> (client/get *endpoint*) :body (= ":http"))))) 72 | 73 | (deftest test-body 74 | (with-server {:http-handler (fn [req] {})} 75 | (is (-> (client/get *endpoint*) :body (= "")))) 76 | 77 | (with-server {:http-handler (fn [req] {:body "yes"})} 78 | (is (-> (client/get *endpoint*) :body (= "yes")))) 79 | 80 | (with-server {:http-handler (fn [req] {:body ["yes" "no"]})} 81 | (is (-> (client/get *endpoint*) :body (= "yesno")))) 82 | 83 | (with-server {:http-handler (fn [req] {:body (.getBytes "yes")})} 84 | (is (-> (client/get *endpoint*) :body (= "yes")))) 85 | 86 | (with-server {:http-handler (fn [req] {:body (java.io.ByteArrayInputStream. (.getBytes "yes"))})} 87 | (is (-> (client/get *endpoint*) :body (= "yes"))))) 88 | 89 | (deftest resp-map-decoding 90 | (with-server {:http-handler (fn [req] 91 | {:body (str (select-keys req [:something]))})} 92 | (is (status-ok? (client/get (str *endpoint* "")))))) 93 | 94 | (defn tls [] 95 | (let [b (doto (TlsConfig/builder) 96 | (.sslContext (ls/ssl-context "test/server.key" 97 | "test/server.crt" 98 | "test/server.crt")) 99 | (.clientAuth TlsClientAuth/REQUIRED) 100 | (.trustAll true) 101 | (.endpointIdentificationAlgorithm (Tls/ENDPOINT_IDENTIFICATION_NONE)))] 102 | (.build b))) 103 | 104 | (deftest test-ssl-context 105 | (with-server {:http-handler (fn [req] {}) :tls (tls)} 106 | (let [endpoint (str/replace *endpoint* "http://" "https://")] 107 | (is (thrown? Exception (client/get endpoint))) 108 | (is (status-ok? (client/get endpoint 109 | {:insecure? true 110 | :keystore "test/keystore.jks" 111 | :keystore-pass "password" 112 | :trust-store "test/keystore.jks" 113 | :trust-store-pass "password"})))))) 114 | 115 | (deftest test-streamable-body 116 | (with-server {:http-handler (fn [_req] 117 | {:status 200 118 | :headers {"content-type" "text/event-stream" 119 | "transfer-encoding" "chunked"} 120 | :body (reify p/StreamableResponseBody 121 | (write-body-to-stream [_ _ output-stream] 122 | (with-open [w (io/writer output-stream)] 123 | (doseq [n (range 1 6)] 124 | (doto w 125 | (.write (str "data: " n "\n\n")) 126 | (.flush))))))})} 127 | (let [resp (client/get *endpoint*)] 128 | (is (status-ok? resp)) 129 | (is (= "data: 1\n\ndata: 2\n\ndata: 3\n\ndata: 4\n\ndata: 5\n\n" 130 | (:body resp)))))) 131 | 132 | (defmacro with-ws-client 133 | [options & body] 134 | `(binding [*client* (wsc/connect (str (str/replace *endpoint* "http" "ws") "/ws") 135 | ~@(into [] cat options))] 136 | 137 | (try 138 | ~@body 139 | (finally 140 | (wsc/close *client*))))) 141 | 142 | (deftest test-websocket 143 | (with-server {:websocket-endpoints {"/ws" 144 | {:message (fn [session data _last] 145 | (s-exp.hirundo.websocket/send! session data true))}}} 146 | (let [client-recv (promise)] 147 | (with-ws-client {:on-receive (fn [msg] (deliver client-recv msg))} 148 | (wsc/send-msg *client* "bar") 149 | (is (= "bar" @client-recv) "echo test")))) 150 | 151 | (with-server {:websocket-endpoints {"/ws" 152 | {:message (fn [session data _last] 153 | (s-exp.hirundo.websocket/send! session data false) 154 | (s-exp.hirundo.websocket/send! session data true))}}} 155 | (let [client-recv (promise)] 156 | (with-ws-client {:on-receive (fn [msg] (deliver client-recv msg))} 157 | (wsc/send-msg *client* "bar") 158 | (is (= "barbar" @client-recv) "double echo test")))) 159 | 160 | (with-server {:websocket-endpoints {"/ws" 161 | {:subprotocols ["chat"] 162 | :message (fn [session data _last] 163 | (s-exp.hirundo.websocket/send! session data true))}}} 164 | (let [client-recv (promise)] 165 | (with-ws-client {:subprotocols ["chat"] 166 | :on-receive (fn [msg] (deliver client-recv msg))} 167 | (wsc/send-msg *client* "bar") 168 | (is (= "bar" @client-recv) "echo with correct subprotocols")) 169 | 170 | (is (thrown-with-msg? Exception 171 | #"Not Found" 172 | (with-ws-client {})) 173 | "Missing subprotocols") 174 | 175 | (is (thrown-with-msg? Exception 176 | #"Not Found" 177 | (with-ws-client {:subprotocols ["foo"]})) 178 | "Incorrect subprotocols")))) 179 | -------------------------------------------------------------------------------- /test/s_exp/hirundo_test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.hirundo-test-runner 2 | (:gen-class) 3 | (:require [clojure.test] 4 | eftest.report.pretty 5 | [eftest.runner :as ef])) 6 | 7 | (def default-options 8 | {:dir "test" 9 | :capture-output? false 10 | :fail-fast? true 11 | :multithread? :vars 12 | :reporters [eftest.report.pretty/report]}) 13 | 14 | (defn- ret->exit-code 15 | [{:as _ret :keys [error fail]}] 16 | (System/exit 17 | (cond 18 | (and (pos? fail) (pos? error)) 30 19 | (pos? fail) 20 20 | (pos? error) 10 21 | :else 0))) 22 | 23 | (defn combined-reporter 24 | "Combines the reporters by running first one directly, 25 | and others with clojure.test/*report-counters* bound to nil." 26 | [[report & rst]] 27 | (fn [m] 28 | (report m) 29 | (doseq [report rst] 30 | (binding [clojure.test/*report-counters* nil] 31 | (report m))))) 32 | 33 | (defn run 34 | [options] 35 | (let [options (merge default-options options)] 36 | (-> (ef/find-tests (:dir options)) 37 | (ef/run-tests options) 38 | ret->exit-code))) 39 | -------------------------------------------------------------------------------- /test/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDdzCCAl+gAwIBAgIEfylh0TANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV 3 | bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD 4 | VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du 5 | MB4XDTE5MDgwOTIyMzU0MloXDTE5MTEwNzIyMzU0MlowbDEQMA4GA1UEBhMHVW5r 6 | bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE 7 | ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC 8 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOSfL7tphuizvHWzvyOmQseV 9 | 2MJojA/kIAWVEIgxintwJqDGGiA3n+YxB9p301ooJo32fhv4SKWf0AHEHoBw4EVr 10 | YjS09nHLhQkovNIIf3PxjJo9Puc4qnAkO21vECJeTuS0zAzZWNpjVbCvSMrkEWvn 11 | aKtRO8SGj5HBPUezvZvfkpMi69woT8W/DKrQubHiSgKXgyzSRKFOcqKqrS08Ee5j 12 | nQaMu3gyDCzoM1U7QSZ1krdweBgDJTXiQ1GQpuZT3VR9bXAHhxLI5NKjhSgkb1Ck 13 | oQ1t3MZ1g3s3H4t/AqGYK0SLriPBLVWRFY7MxMvQe9V3w5uHGyO8IJ7pQI6ReQcC 14 | AwEAAaMhMB8wHQYDVR0OBBYEFIs/uUIw7IhVYzYOn2Qm1+XeyG6vMA0GCSqGSIb3 15 | DQEBCwUAA4IBAQAN8FfmPC8wJ8OVHi9XCf4vd+J6Gmb4ASIVzbSn6fYs77G+/Q5y 16 | YhpD685BWGvb/As0hj//va9GGvQWdFsP2IyGc8qQJ/YflmqCSBAwt6X2QkgFqghK 17 | oAPWxfQGEsn1H6pePc1LrFqjc0MYv/hIEKLvXfptTvKGlYL32ve+DPnHmKUSlf/L 18 | oQDeJwEjw3PSWmGlJLCq7JqK6H0K8B3JH/cne/4CbCmb1KArvynoF1b26vke1T6k 19 | bW4zYG/BsobZpLlo802XErMcc96fX/b+wo2vbcxOVbST6w9E6DdcHGFdzE0Zytre 20 | K1FiY0bMSPha1u0R/fkS9hVSMyKBrv2lHhh5 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDkny+7aYbos7x1 3 | s78jpkLHldjCaIwP5CAFlRCIMYp7cCagxhogN5/mMQfad9NaKCaN9n4b+Eiln9AB 4 | xB6AcOBFa2I0tPZxy4UJKLzSCH9z8YyaPT7nOKpwJDttbxAiXk7ktMwM2VjaY1Ww 5 | r0jK5BFr52irUTvEho+RwT1Hs72b35KTIuvcKE/Fvwyq0Lmx4koCl4Ms0kShTnKi 6 | qq0tPBHuY50GjLt4Mgws6DNVO0EmdZK3cHgYAyU14kNRkKbmU91UfW1wB4cSyOTS 7 | o4UoJG9QpKENbdzGdYN7Nx+LfwKhmCtEi64jwS1VkRWOzMTL0HvVd8ObhxsjvCCe 8 | 6UCOkXkHAgMBAAECggEAf2loBJ5vHUjuAvrFz2xdUzXjs5ToWgLdBb9pkz1o3BpI 9 | rNk6vjHB31v78HlPRLOTqRJyfcwFUKicGB/gj9xYYNfTx2QC5LBz2ALoGHW78jXw 10 | EhWH9PzTn+PRas+2wjCk7zkDbzXCGxO5Mdq+SrSIMQaqgq+nC4Oih6yWJlu5lCAb 11 | gxhfndfhBuxDDgA+ikg1EbHsTHsG9DuM4OrjodqImXUuy7JKTaXE1bp3BV7etWpB 12 | KXsYxTo41r28Zq9jw/COZSRvcvnT31ZVFBJ+tm+db8goPrDaTGDlKH2MDEpAMLSb 13 | dU/0GCfOU+i0/plDKJXmTYZ6iO2nvCVMuuYs7g+OQQKBgQD7/msMZmPGsBCIztss 14 | w5fPzTao+tVHwznmkDSOWxgjQVgaarFaHXOmUEEooF0M2veHrHkxH5KjHihQWS6a 15 | piRN6C3R9nq4UC8p5eKZyU04GDwSO77zgh4mF/AS/0lpg8G5K29uv/qMCscwpYfj 16 | FE40TmcBSbKkfQa66cQu3iozaQKBgQDoQaW24Dmt56MP+BWl+kVvdQ5TzNZ4Qr3E 17 | T01LrIAZQ2Q3KCSubUZ4D6Ck74jqyBRFnNZYxXLK7KT74gLubLu5xiz6cCYeWONS 18 | nNpbXoQWuv2ZVz2jKtyjjZfuHYijcPW7rhmEqrVl+B2+nk/jPvH0vYGwaJgYt36/ 19 | kA90SpJq7wKBgC7+jXwshCMDprG80gcRGvi37p+o16mgcOXcstBxkJjSkNmsWBLU 20 | N0Mm6F/pODEzjMISmi9dEf21nBMP7q8MmOymhhUIAG7rFUCdDJtdfTce3eUQm1XS 21 | SEkWYcFJNqmk1rXvscJXryP6xpb958+jc8oxbOhClDw0zNKC0gFjkT8xAoGAFNEd 22 | e0Cpqs6iLOI9fnzqEE1blWM8rda9j4TUFr5FVP5XUyJD92ZP9J7h9SaLDRAV0Q93 23 | nslDurBvMSxWuuv//2ylx2SZ5gpthZANejcEe80SwdWx7pf+4qHkRjFbd9q+C65V 24 | FzD9xT/x2o3x9xpDMX5yy070ovJ/Z8gPuoqciP0CgYEAkeutDtbPrqE6aDDcyeCj 25 | BJdJles+SM1eAYLu8qjWGJyoin73GwUjcYDHBptC9IwCKQZRC8TosunpofZ0LA5A 26 | 4CP59msacbIQ/CS7GFZ9I/X/KW7Q2Hq4zeMZsxo5B91coNogGSx1KgCo3l4J1m2/ 27 | g8diJCDNmp5vNncOnGP4bXc= 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------