├── .clj-kondo └── config.edn ├── .github └── workflows │ ├── ci.yml │ └── graalvm-compatibility.yml ├── .gitignore ├── LICENSE ├── README.md ├── codecov.yml ├── doc └── 01-mTLS.md ├── package.json ├── project.clj ├── resources └── META-INF │ └── native-image │ └── unixsocket-http │ └── unixsocket-http │ └── native-image.properties ├── src └── unixsocket_http │ ├── client.clj │ ├── core.clj │ ├── data.clj │ └── impl │ ├── FixedPathTcpSocket.clj │ ├── FixedPathTcpSocketFactory.clj │ ├── FixedPathUnixSocket.clj │ ├── FixedPathUnixSocketFactory.clj │ ├── ResponseSocket.clj │ ├── SingletonSocketFactory.clj │ ├── StreamingBody.clj │ └── delegate.clj ├── test-graalvm ├── resources │ └── META-INF │ │ └── native-image │ │ └── uhttp │ │ └── uhttp │ │ └── native-image.properties └── src │ └── compat │ └── main.clj ├── test └── unixsocket_http │ ├── client_test.clj │ ├── core_test.clj │ ├── data_test.clj │ └── test │ └── http_server.clj ├── tests.edn └── yarn.lock /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {unixsocket-http.core/defrequest clojure.core/declare 2 | unixsocket-http.impl.delegate/delegate clojure.core/quote 3 | clojure.test.check.properties/for-all clojure.core/let 4 | clojure.test.check.clojure-test/defspec clojure.test/deftest}} 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["**"] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 2 16 | - uses: DeLaGuardo/clojure-lint-action@master 17 | with: 18 | clj-kondo-args: --lint src test 19 | check-name: clj-kondo 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install dependencies 27 | run: lein deps 28 | - name: Run tests 29 | run: lein ci 30 | - name: upload-code-coverage-report 31 | uses: codecov/codecov-action@v1 32 | with: 33 | file: target/coverage/codecov.json 34 | -------------------------------------------------------------------------------- /.github/workflows/graalvm-compatibility.yml: -------------------------------------------------------------------------------- 1 | name: graalvm-compatibility 2 | 3 | on: 4 | push: 5 | branches: ["main", "ci/*"] 6 | 7 | jobs: 8 | build-graalvm: 9 | strategy: 10 | matrix: 11 | build: 12 | - "linux-amd64" 13 | - "macos-amd64" 14 | include: 15 | - build: "linux-amd64" 16 | os: "ubuntu-latest" 17 | - build: "macos-amd64" 18 | os: "macos-latest" 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: setup-clojure 24 | uses: DeLaGuardo/setup-clojure@3.5 25 | with: 26 | lein: 2.10.0 27 | - name: setup-graalvm-ce 28 | uses: graalvm/setup-graalvm@v1 29 | with: 30 | distribution: "graalvm-community" 31 | java-version: "21" 32 | 33 | - name: compile-native-image 34 | run: | 35 | lein with-profile +graalvm-compatibility uberjar 36 | native-image -jar target/compat.jar -o compat 37 | chmod +x compat 38 | 39 | - name: verify-result 40 | run: ./compat 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | node_modules 14 | .clj-kondo/.cache 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Yannick Scherer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unixsocket-http 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/unixsocket-http.svg)](https://clojars.org/unixsocket-http) 4 | [![Documentation](https://cljdoc.org/badge/unixsocket-http/unixsocket-http)](https://cljdoc.org/d/unixsocket-http/unixsocket-http/CURRENT) 5 | [![CI](https://github.com/into-docker/unixsocket-http/workflows/CI/badge.svg)](https://github.com/into-docker/unixsocket-http/actions?query=workflow%3ACI) 6 | [![codecov](https://codecov.io/gh/into-docker/unixsocket-http/branch/master/graph/badge.svg?token=GLSK1G95TX)](https://codecov.io/gh/into-docker/unixsocket-http) 7 | [![Compatible with GraalVM](https://img.shields.io/badge/graalvm-compatible-success)](https://www.graalvm.org/docs/reference-manual/native-image) 8 | 9 | **unixsocket-http** is a Clojure library to handle HTTP communication over 10 | UNIX domain sockets. This kind of I/O is notably used by the [Docker][docker] 11 | daemon which was the main driver to create this library. 12 | 13 | [docker]: https://www.docker.com/ 14 | 15 | ## Usage 16 | 17 | Use the `unixsocket-http.core` namespace to access HTTP functionality with 18 | a similar API as [clj-http][]. 19 | 20 | ```clojure 21 | (require '[unixsocket-http.core :as uhttp]) 22 | (def client (uhttp/client "unix:///var/run/docker.sock")) 23 | ``` 24 | 25 | To provide a common API towards TCP sockets, they are also supported: 26 | 27 | ```clojure 28 | (def client (uhttp/client "tcp://127.0.0.1:6537")) 29 | ``` 30 | 31 | Note that this project does not have the ambition to replicate all of clj-http's 32 | functionality. The main use case is communication with Docker which will 33 | naturally restrict the feature set implemented. 34 | 35 | [clj-http]: https://github.com/dakrone/clj-http 36 | 37 | Once you have a client, you can send requests: 38 | 39 | ```clojure 40 | (uhttp/get client "/_ping") 41 | ;; {:status 200, 42 | ;; :headers 43 | ;; {"api-version" "1.40", 44 | ;; "server" "Docker/19.03.2 (linux)", 45 | ;; "content-type" "text/plain; charset=utf-8", 46 | ;; "content-length" "2", 47 | ;; "docker-experimental" "false", 48 | ;; "pragma" "no-cache", 49 | ;; "date" "Thu, 09 Apr 2020 15:20:06 GMT", 50 | ;; "ostype" "linux", 51 | ;; "cache-control" "no-cache, no-store, must-revalidate"}, 52 | ;; :body "OK"} 53 | ``` 54 | 55 | All HTTP functions take an options map as their last parameter that can be used 56 | to supply additional data or alter the request/response behaviour. 57 | 58 | ### Query Parameters & Body 59 | 60 | Query parameters can be passed using `:query-params`, and a body can be 61 | supplied as either `InputStream` or `String` using the `:body` key. 62 | 63 | ```clojure 64 | (uhttp/post 65 | client 66 | "/images/create" 67 | {:query-params {:fromImage "node:alpine", :repo "testnode", :tag "latest"}}) 68 | ;; {:status 200, 69 | ;; :headers 70 | ;; {"api-version" "1.40", 71 | ;; "content-type" "application/json", 72 | ;; "date" "Thu, 09 Apr 2020 15:27:11 GMT", 73 | ;; "docker-experimental" "false", 74 | ;; "ostype" "linux", 75 | ;; "server" "Docker/19.03.2 (linux)", 76 | ;; "transfer-encoding" "chunked"}, 77 | ;; :body 78 | ;; "{\"status\":\"Pulling from library/node\",\"id\":\"latest\"}\r\n..."} 79 | ``` 80 | 81 | As this is supported by some APIs, you can pass a query parameter multiple times 82 | by supplying a collection instead of value. For example, `{:x [1 2 3]}` turns 83 | into `?x=1&x=2&x=3`. 84 | 85 | ### Streaming Responses 86 | 87 | Use `:as :stream` in the options map to make `:body` an `java.io.InputStream` to 88 | consume from. Alternatively, use `:as :socket` to get access to the underlying 89 | `java.net.Socket` for bidirectional communication. 90 | 91 | Always make sure to call `close` on the resources obtained this way, otherwise 92 | you'll run into connection leaks. 93 | 94 | ### Exceptions 95 | 96 | By default, HTTP status codes ≥ 400 will cause an exception 97 | (`clojure.lang.ExceptionInfo`) to be thrown. You can access the underlying 98 | response via `ex-data`. 99 | 100 | Note that the `:body` will not be present if you are expecting a streaming 101 | response, unless you explicitly set `:throw-entire-message?` to `true`. 102 | 103 | If you want to prevent the client from throwing exceptions, and you'd rather get 104 | the response no matter what, you can set `:throw-exceptions?` to `false`. 105 | 106 | ### TLS 107 | 108 | You can create a client with an `https://...` URL, which will prompt it to use 109 | TLS to perform any requests. Usually, the relevant certificates should be 110 | present in your Java keystore and the underlying `OkHttpClient` will pick them 111 | up automatically when verifying the connection. 112 | 113 | For _mutual_ TLS (mTLS) or pointers on how to tackle TLS setup manually, check 114 | out the [respective documentation](./doc/01-mTLS.md). 115 | 116 | ## GraalVM 117 | 118 | This library can be used with GraalVM's [`native-image`][native-image] tool to 119 | create native Clojure executables. The necessary configuration files are already 120 | bundled with this library and should be picked up automatically. 121 | 122 | [native-image]: https://www.graalvm.org/docs/reference-manual/native-image/ 123 | 124 | ## License 125 | 126 | ``` 127 | MIT License 128 | 129 | Copyright (c) 2020-2021 Yannick Scherer 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining a copy 132 | of this software and associated documentation files (the "Software"), to deal 133 | in the Software without restriction, including without limitation the rights 134 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the Software is 136 | furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all 139 | copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 143 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 144 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 145 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 146 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 147 | SOFTWARE. 148 | ``` 149 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /doc/01-mTLS.md: -------------------------------------------------------------------------------- 1 | # mTLS 2 | 3 | While TLS is supported out-of-the-box, as long as the relevant certificates are 4 | part of the Java keystore/truststore, **mutual TLS (mTLS)** requires explicit 5 | configuration. 6 | 7 | Since the underlying client is provided by [`okhttp`][okhttp] we'll rely on 8 | [`okhttp-tls`][okhttp-tls] to set up the communication. 9 | 10 | [okhttp]: https://square.github.io/okhttp/ 11 | [okhttp-tls]: https://square.github.io/okhttp/4.x/okhttp-tls/okhttp3.tls/ 12 | 13 | ## Keys & Certificate 14 | 15 | We need a `java.security.KeyPair` (private key) and 16 | `java.security.cert.X509Certificate` instances (public key and certificate 17 | authority) to create the underlying `SslSocketFactory`. 18 | 19 | Depending on the input formats 20 | available you might be able to use built-in Java facilities to obtain these; 21 | alternatively, [BouncyCastle][bc] or [pem-reader][] provide the desired 22 | functionality. 23 | 24 | [bc]: https://www.bouncycastle.org/docs/pkixdocs1.5on/org/bouncycastle/openssl/PEMParser.html 25 | [pem-reader]: https://github.com/into-docker/pem-reader 26 | 27 | ## Client Creation 28 | 29 | ### Imports 30 | 31 | ```clojure 32 | (import '(okhttp3.tls 33 | HandshakeCertificates 34 | HandshakeCertificates$Builder 35 | HeldCertificate) 36 | '(java.security KeyPair) 37 | '(java.security.cert X509Certificate)) 38 | ``` 39 | 40 | ### HandshakeCertificates 41 | 42 | The class `okhttp3.tls.HandshakeCertificates` encapsulates all the data 43 | exchanged during connection setup. 44 | 45 | ```clojure 46 | (defn handshake-certificates 47 | ^HandshakeCertificates 48 | [^KeyPair key 49 | ^X509Certificate cert 50 | ^X509Certificate ca] 51 | (-> (HandshakeCertificates$Builder.) 52 | (.addTrustedCertificate ca) 53 | (.heldCertificate 54 | (HeldCertificate. key cert) 55 | (into-array X509Certificate [])) 56 | (.build))) 57 | ``` 58 | 59 | ### Create `:builder-fn` 60 | 61 | Once the `HandshakeCertificates` are ready, you can use them to retrieve an 62 | `SslSocketFactory` and `TrustManager` and pass them to every created client: 63 | 64 | ```clojure 65 | (defn mtls-builder-fn 66 | [key cert ca] 67 | (let [hs (handshake-certificates key cert ca)] 68 | (fn [^okhttp3.OkHttpClient$Builder builder] 69 | (doto builder 70 | (.sslSocketFactory 71 | (.sslSocketFactory hs) 72 | (.trustManager hs)))))) 73 | ``` 74 | 75 | ### Create client 76 | 77 | Note that you'll still have to supply a `https://...` URL for the certificates 78 | to actually be picked up. 79 | 80 | ```clojure 81 | (require '[unixsocket-http.core :as uhttp]) 82 | (let [client (uhttp/client 83 | "https://..." 84 | {:builder-fn (mtls-builder-fn key cert ca)})] 85 | (uhttp/get client "/_ping")) 86 | ``` 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "husky": "3.0.2", 4 | "prettier": "1.18.2", 5 | "pretty-quick": "1.11.1" 6 | }, 7 | "husky": { 8 | "hooks": { 9 | "pre-commit": "pretty-quick --staged 'src/**/*.java' --pattern '**/*.json' --pattern '**/*.md' --pattern '**/*.yml'" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject unixsocket-http "1.0.15-SNAPSHOT" 2 | :description "A library to allow HTTP calls over a UNIX socket, e.g. for 3 | communicating with Docker." 4 | :url "https://github.com/into-docker/unixsocket-http" 5 | :license {:name "MIT" 6 | :url "https://choosealicense.com/licenses/mit" 7 | :year 2020 8 | :key "mit" 9 | :comment "MIT License"} 10 | :dependencies [[org.clojure/clojure "1.11.1" :scope "provided"] 11 | [org.clojure/tools.logging "1.2.4"] 12 | [com.kohlschutter.junixsocket/junixsocket-common "2.8.1"] 13 | [com.kohlschutter.junixsocket/junixsocket-native-common "2.8.1"] 14 | [com.squareup.okhttp3/okhttp "4.11.0"] 15 | [org.jetbrains.kotlin/kotlin-stdlib-common "1.9.10"]] 16 | :exclusions [org.clojure/clojure] 17 | :aot [unixsocket-http.impl.FixedPathUnixSocket 18 | unixsocket-http.impl.FixedPathUnixSocketFactory 19 | unixsocket-http.impl.FixedPathTcpSocket 20 | unixsocket-http.impl.FixedPathTcpSocketFactory 21 | unixsocket-http.impl.SingletonSocketFactory 22 | unixsocket-http.impl.ResponseSocket 23 | unixsocket-http.impl.StreamingBody] 24 | :profiles {:dev 25 | {:dependencies [[com.squareup.okhttp3/okhttp-tls "4.11.0"] 26 | [com.squareup.okhttp3/mockwebserver "4.11.0"] 27 | [org.clojure/test.check "1.1.1"] 28 | [com.gfredericks/test.chuck "0.2.14"]] 29 | :global-vars {*warn-on-reflection* true}} 30 | :kaocha 31 | {:dependencies [[lambdaisland/kaocha "1.87.1366" 32 | :exclusions [org.clojure/spec.alpha]] 33 | [lambdaisland/kaocha-cloverage "1.1.89"] 34 | [org.clojure/java.classpath "1.0.0"]]} 35 | :ci 36 | [:kaocha 37 | {:global-vars {*warn-on-reflection* false}}] 38 | :graalvm-compatibility 39 | {:jar-name "compat-base.jar" 40 | :uberjar-name "compat.jar" 41 | :source-paths ["test-graalvm/src"] 42 | :resource-paths ["test-graalvm/resources"] 43 | :global-vars {*assert* false} 44 | :jvm-opts ["-Dclojure.compiler.direct-linking=true" 45 | "-Dclojure.spec.skip-macros=true"] 46 | :dependencies [[com.github.clj-easy/graal-build-time "1.0.5"]] 47 | :main compat.main 48 | :aot [compat.main]}} 49 | :aliases {"kaocha" ["with-profile" "+kaocha" "run" "-m" "kaocha.runner"] 50 | "ci" ["with-profile" "+ci" "run" "-m" "kaocha.runner" 51 | "--reporter" "documentation" 52 | "--plugin" "cloverage" 53 | "--codecov" 54 | "--no-cov-html" 55 | "--cov-ns-exclude-regex" "unixsocket-http\\.impl\\..+"]} 56 | :pedantic? :abort) 57 | -------------------------------------------------------------------------------- /resources/META-INF/native-image/unixsocket-http/unixsocket-http/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = --initialize-at-build-time=okhttp3 \ 2 | --initialize-at-build-time=okio \ 3 | --initialize-at-build-time=kotlin.text \ 4 | --enable-http \ 5 | --enable-https 6 | -------------------------------------------------------------------------------- /src/unixsocket_http/client.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.client 2 | (:require [clojure.string :as string]) 3 | (:import [unixsocket_http.impl 4 | FixedPathTcpSocketFactory 5 | FixedPathUnixSocketFactory 6 | SingletonSocketFactory] 7 | [okhttp3 8 | Dns 9 | OkHttpClient 10 | OkHttpClient$Builder] 11 | [javax.net SocketFactory] 12 | [java.net InetAddress Socket URI] 13 | [java.time Duration])) 14 | 15 | ;; ## No DNS Lookups 16 | ;; 17 | ;; DNS lookups in static builds would be a problem. Plus, since the socket is 18 | ;; already connected to a specific IP, it's kinda pointless. 19 | 20 | (deftype NoDns [] 21 | Dns 22 | (lookup [_ hostname] 23 | [(InetAddress/getByAddress hostname (byte-array 4))])) 24 | 25 | ;; ## SocketFactory 26 | 27 | (defn- adapt-url 28 | "Ensure backwards-compatibility by prefixing filesystem paths with `unix://`" 29 | [^String url] 30 | (-> (if-not (re-matches #"[a-zA-Z]+://.*" url) 31 | (do 32 | (println "Calling unixsocket-http.core/client with a path instead of a URL is DEPRECATED.") 33 | (str "unix://" url)) 34 | url) 35 | (string/replace #"/+$" ""))) 36 | 37 | (defn- get-port 38 | [^URI uri & [default]] 39 | (let [port (.getPort uri)] 40 | (int 41 | (or (if (pos? port) 42 | port 43 | default) 44 | (throw 45 | (IllegalArgumentException. 46 | (str "Port is required in URI: " uri))))))) 47 | 48 | (defn- create-socket-factory-from-uri 49 | [^URI uri] 50 | (case (.getScheme uri) 51 | "unix" (FixedPathUnixSocketFactory. (.getPath uri)) 52 | "tcp" (FixedPathTcpSocketFactory. (.getHost uri) (get-port uri)) 53 | "http" (FixedPathTcpSocketFactory. (.getHost uri) (get-port uri 80)) 54 | "https" (FixedPathTcpSocketFactory. (.getHost uri) (get-port uri 443)) 55 | (throw 56 | (IllegalArgumentException. 57 | (str "Can only handle URI schemes 'unix', 'tcp', 'http' and 'https', " 58 | "given: " 59 | uri))))) 60 | 61 | (defn- create-base-url 62 | [^URI uri] 63 | (if (contains? #{"tcp" "unix"} (.getScheme uri)) 64 | "http://localhost" 65 | (str uri))) 66 | 67 | (defn- create-socket-factory 68 | ^SocketFactory [^String uri-str] 69 | (let [uri (URI. (adapt-url uri-str))] 70 | {:factory (create-socket-factory-from-uri uri) 71 | :base-url (create-base-url uri)})) 72 | 73 | ;; ## OkHttpClient 74 | 75 | (defn- create-client 76 | [^SocketFactory factory 77 | {:keys [builder-fn 78 | timeout-ms 79 | read-timeout-ms 80 | write-timeout-ms 81 | connect-timeout-ms 82 | call-timeout-ms 83 | ;; undocumented options (maybe useful for debugging?) 84 | ::dns?] 85 | :or {builder-fn identity 86 | timeout-ms 0 87 | dns? false}}] 88 | (let [default-timeout (Duration/ofMillis timeout-ms) 89 | to-timeout #(or (some-> % Duration/ofMillis) default-timeout) 90 | builder (doto (OkHttpClient$Builder.) 91 | (.connectTimeout (to-timeout connect-timeout-ms)) 92 | (.callTimeout (to-timeout call-timeout-ms)) 93 | (.readTimeout (to-timeout read-timeout-ms)) 94 | (.writeTimeout (to-timeout write-timeout-ms)) 95 | (cond-> (not dns?) (.dns (NoDns.))) 96 | (builder-fn) 97 | (.socketFactory factory))] 98 | (.build builder))) 99 | 100 | 101 | ;; ## Client Modes 102 | 103 | ;; ### Factories 104 | 105 | (defn- recreating-client-factory 106 | [{:keys [factory]} opts] 107 | (fn [{:keys [as]}] 108 | (let [factory (SingletonSocketFactory. factory)] 109 | (cond-> {::client (create-client factory opts)} 110 | (= as :socket) (assoc ::socket (.getSocket factory)))))) 111 | 112 | (defn- reusable-client-factory 113 | [{:keys [factory]} opts] 114 | (let [client (create-client factory opts)] 115 | (fn [request] 116 | (when (= (:as request) :socket) 117 | (throw 118 | (IllegalArgumentException. 119 | (str "Client mode `:reuse` does not allow direct socket access.\n" 120 | "See documentation of `unixsocket-http.core/client`.")))) 121 | {::client client}))) 122 | 123 | (defn- hybrid-client-factory 124 | [socket-factory opts] 125 | (let [fresh (recreating-client-factory socket-factory opts) 126 | reusable (reusable-client-factory socket-factory opts)] 127 | (fn [{:keys [as] :as request}] 128 | (if (= as :socket) 129 | (fresh request) 130 | (reusable request))))) 131 | 132 | ;; ### Clients 133 | 134 | (defn- make-client 135 | "Create client map with `::factory` being the client creator function and 136 | `::base-url` being the URL to use to prefix relative paths." 137 | [client-factory-fn url opts] 138 | (let [socket-factory (create-socket-factory url)] 139 | {::factory (client-factory-fn socket-factory opts) 140 | ::base-url (or (:base-url opts) (:base-url socket-factory))})) 141 | 142 | (defn- recreating-client 143 | "Client that will always initiate a new connection." 144 | [url opts] 145 | (make-client recreating-client-factory url opts)) 146 | 147 | (defn- reusable-client 148 | "Client that will always reuse connections. Note taht this one cannot use 149 | the `:as :socket` mode." 150 | [url opts] 151 | (make-client reusable-client-factory url opts)) 152 | 153 | (defn- hybrid-client 154 | "Client that will create a fresh connection when the raw socket is requested." 155 | [url opts] 156 | (make-client hybrid-client-factory url opts)) 157 | 158 | ;; ## API 159 | 160 | (defn create 161 | [url {:keys [mode] :or {mode :default} :as opts}] 162 | (case mode 163 | :reuse (reusable-client url opts) 164 | :recreate (recreating-client url opts) 165 | :default (hybrid-client url opts))) 166 | 167 | (defn connection 168 | [{:keys [client] :as request}] 169 | {:post [(some? %)]} 170 | ((::factory client) request)) 171 | 172 | (defn base-url 173 | [{:keys [client]}] 174 | (::base-url client)) 175 | 176 | (defn get-client 177 | ^OkHttpClient [connection] 178 | (::client connection)) 179 | 180 | (defn get-socket 181 | ^Socket [connection] 182 | (::socket connection)) 183 | -------------------------------------------------------------------------------- /src/unixsocket_http/core.clj: -------------------------------------------------------------------------------- 1 | (ns unixsocket-http.core 2 | (:require [unixsocket-http.client :as client] 3 | [unixsocket-http.data :as data] 4 | [clojure.tools.logging :as log]) 5 | (:refer-clojure :exclude [get])) 6 | 7 | ;; ## Client 8 | 9 | (defn client 10 | "Create a new HTTP client that utilises the given TCP or UNIX domain socket to 11 | perform HTTP communication. Options are: 12 | 13 | - `:base-url`: A string containing the base URL to use for HTTP calls; by 14 | default this will be the URL given to create the client. 15 | - `:timeout-ms`: A timeout that will be used for connect/call/read/write 16 | timeout settings, unless they are explicitly specified using the 17 | following options: 18 | - `:connect-timeout-ms` 19 | - `:call-timeout-ms` 20 | - `:read-timeout-ms` 21 | - `:write-timeout-ms` 22 | - `:mode`: A keyword describing the connection behaviour: 23 | - `:default`: A reusable client will be created, except for raw socket 24 | access where a new connection will be established. 25 | - `:reuse`: A reusable client will be created, and raw socket access will 26 | not be possible. 27 | - `:recreate`: For every request a new client will be created; this might 28 | be useful if you encounter problems with sharing the client across 29 | threads. 30 | - `:builder-fn`: A function that will be called on the underlying 31 | `OkHttpClient$Builder` and can be used to perform arbitrary adjustments 32 | to the HTTP client (with exception of the socket factory). 33 | 34 | Examples: 35 | 36 | ``` 37 | (client \"unix:///var/run/docker.sock\") 38 | (client \"tcp://127.0.0.1:6537\") 39 | ``` 40 | 41 | " 42 | ([url] 43 | (client url {})) 44 | ([url opts] 45 | (client/create url opts))) 46 | 47 | ;; ## I/O 48 | 49 | (defn- throw? 50 | [{:keys [status]} 51 | {:keys [throw-exceptions throw-exceptions?] 52 | :or {throw-exceptions true 53 | throw-exceptions? true}}] 54 | (and (>= status 400) 55 | throw-exceptions 56 | throw-exceptions?)) 57 | 58 | (letfn [(without-body [{:keys [^java.io.Closeable body] :as response}] 59 | (.close body) 60 | (dissoc response :body))] 61 | (defn- prepare-ex-data! 62 | [response {:keys [throw-entire-message? as]}] 63 | (if-not throw-entire-message? 64 | (case as 65 | (:stream :socket) (without-body response) 66 | response) 67 | response))) 68 | 69 | (defn- throw-http-exception! 70 | [{:keys [status] :as response} opts] 71 | (when (throw? response opts) 72 | (let [edata (prepare-ex-data! response opts)] 73 | (throw 74 | (ex-info 75 | (format "HTTP Error: %d" status) 76 | edata))))) 77 | 78 | (defn- handle-response 79 | [response opts] 80 | (log/tracef "[unixsocket-http] <--- %s" (pr-str response)) 81 | (when (throw? response opts) 82 | (throw-http-exception! response opts)) 83 | response) 84 | 85 | (defn request 86 | "Perform an HTTP request. Options are: 87 | 88 | - `:client` (required): the `client` to use for the request. 89 | - `:method` (required): a keyword indicating the HTTP method to use. 90 | - `:url` (required): the URL/path to send the request to. 91 | - `:body`: request body, if supported by the HTTP method. 92 | - `:headers`: a map of string/string pairs that will be sent as headers. 93 | - `:query-params`: a map of string/string pairs that will be sent as the query string. 94 | - `:as`: if set to `:stream` the response's body will be an `InputStream` value (that needs to be closed after consuming). 95 | - `:throw-exceptions?`: if set to `false` all responses will be returned and no exception is thrown on HTTP error codes. 96 | - `:throw-entire-message?`: if set to `true` HTTP exceptions will contain the full response as `ex-data`; streams and sockets will not be closed automatically! 97 | " 98 | [request] 99 | (let [req (data/build-request request) 100 | connection (client/connection request)] 101 | (log/tracef "[unixsocket-http] ---> %s" (pr-str (dissoc request :client))) 102 | (-> (client/get-client connection) 103 | (.newCall req) 104 | (.execute) 105 | (data/parse-response request connection) 106 | (handle-response request)))) 107 | 108 | ;; ## Convenience 109 | 110 | (defmacro ^:private defrequest 111 | [method] 112 | `(defn ~method 113 | ~(format 114 | (str "Perform a %s request against the given client. Options are:%n%n" 115 | " - `:headers`: a map of string/string pairs that will be sent as headers.%n" 116 | " - `:query-params`: a map of string/string pairs that will be sent as the query string.%n" 117 | " - `:as`: if set to `:stream` the response's body will be an `InputStream` value (that needs to be closed after consuming).%n" 118 | " - `:throw-exceptions?`: if set to `false` all responses will be returned and no exception is thrown on HTTP error codes.%n" 119 | " - `:throw-entire-message?`: if set to `true` HTTP exceptions will contain the full response as `ex-data`; streams and sockets will not be closed automatically!%n") 120 | (.toUpperCase (name method))) 121 | ([~'client ~'url] 122 | (~method ~'client ~'url {})) 123 | ([~'client ~'url ~'opts] 124 | (request (merge ~'opts {:client ~'client, :method ~(keyword (name method)) , :url ~'url}))))) 125 | 126 | (defrequest get) 127 | (defrequest head) 128 | (defrequest post) 129 | (defrequest put) 130 | (defrequest delete) 131 | (defrequest patch) 132 | -------------------------------------------------------------------------------- /src/unixsocket_http/data.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.data 2 | (:require [unixsocket-http.client :as client] 3 | [clojure.java.io :as io]) 4 | (:import [unixsocket_http.impl 5 | ResponseSocket 6 | StreamingBody] 7 | [okhttp3 8 | HttpUrl 9 | HttpUrl$Builder 10 | Request 11 | Request$Builder 12 | RequestBody 13 | Response 14 | ResponseBody] 15 | [java.io InputStream])) 16 | 17 | ;; ## Helpers 18 | 19 | (defn- normalize-headers 20 | "Use lowercase string headers." 21 | [headers] 22 | (->> (for [[k v] headers] 23 | [(-> k (name) (.toLowerCase)) v]) 24 | (into {}))) 25 | 26 | ;; ## Request 27 | 28 | (defn- add-query-params! 29 | [^HttpUrl$Builder builder {:keys [query-params]}] 30 | (doseq [[k value-or-collection] query-params 31 | v (if (coll? value-or-collection) 32 | value-or-collection 33 | [value-or-collection])] 34 | (.addQueryParameter builder (name k) (str v)))) 35 | 36 | (defn- build-url 37 | "Create URL to use for request. This will read the base URL from the 38 | client contained in the request in case the given url does not include 39 | a scheme and host." 40 | ^HttpUrl 41 | [{:keys [url] :as request}] 42 | (let [^HttpUrl$Builder builder (or (some-> (HttpUrl/parse url) 43 | (.newBuilder)) 44 | (-> (str (client/base-url request) 45 | url) 46 | (HttpUrl/parse) 47 | (.newBuilder)))] 48 | (add-query-params! builder request) 49 | (.build builder))) 50 | 51 | (defn- build-body 52 | [{:keys [method headers body]}] 53 | (cond (instance? InputStream body) 54 | (StreamingBody. body (get headers "content-type")) 55 | 56 | (string? body) 57 | (StreamingBody. 58 | (io/input-stream 59 | (.getBytes ^String body)) 60 | (get headers "content-type")) 61 | 62 | (#{:post :put :patch} method) 63 | (RequestBody/create nil "") 64 | 65 | :else nil)) 66 | 67 | (defn build-request 68 | ^Request 69 | [{:keys [method headers] :as request}] 70 | (when-not (#{:get :post :put :delete :patch :head} method) 71 | (throw 72 | (IllegalArgumentException. 73 | (str "Invalid HTTP method keyword supplied: " method)))) 74 | (let [^Request$Builder builder (doto (Request$Builder.) 75 | (.method 76 | (.toUpperCase (name method)) 77 | (build-body request)) 78 | (.url (build-url request)))] 79 | (doseq [[k v] (normalize-headers headers)] 80 | (.addHeader builder (name k) (str v))) 81 | (.build builder))) 82 | 83 | ;; ## Response 84 | 85 | (defn parse-response 86 | [^Response response 87 | {:keys [as] :or {as :string}} 88 | connection] 89 | {:status (.code response) 90 | :headers (let [it (.. response headers iterator)] 91 | (loop [headers {}] 92 | (if (.hasNext it) 93 | (let [^kotlin.Pair pair (.next it)] 94 | (recur 95 | (assoc headers (.getFirst pair) (.getSecond pair)))) 96 | (normalize-headers headers)))) 97 | :body (let [^ResponseBody body (.body response)] 98 | (case as 99 | :string (.string body) 100 | :stream (.byteStream body) 101 | :socket (ResponseSocket. 102 | (client/get-socket connection) 103 | response)))}) 104 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/FixedPathTcpSocket.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.FixedPathTcpSocket 2 | "Wrapper around AFUNIXSocket that is bound to a fixed address and will 3 | ignore the argument passed to 'connect'. This is necessary since an HTTP 4 | client using this socket will pass its own socket address." 5 | (:gen-class 6 | :name unixsocket_http.impl.FixedPathTcpSocket 7 | :extends java.net.Socket 8 | :init init 9 | :state state 10 | :constructors {[java.net.SocketAddress] []} 11 | :methods [[connect [] void]]) 12 | (:require [unixsocket-http.impl.delegate :refer [delegate]]) 13 | (:import [java.net Socket SocketAddress])) 14 | 15 | ;; ## State 16 | 17 | (defn- get-socket 18 | ^Socket [^unixsocket_http.impl.FixedPathTcpSocket this] 19 | (-> this (.-state) (deref) (:socket))) 20 | 21 | (defn- get-address 22 | ^SocketAddress [^unixsocket_http.impl.FixedPathTcpSocket this] 23 | (-> this (.-state) (deref) (:address))) 24 | 25 | ;; ## Constructor 26 | 27 | (defn -init 28 | [^SocketAddress addr] 29 | [[] (atom {:socket (Socket.) 30 | :address addr})]) 31 | 32 | ;; ## Methods 33 | 34 | (delegate 35 | {:class java.net.Socket 36 | :via get-socket 37 | :except [connect]}) 38 | 39 | (defn -connect 40 | ([this] 41 | (.connect (get-socket this) (get-address this))) 42 | ([this _] 43 | (.connect (get-socket this) (get-address this))) 44 | ([this _ ^Integer timeout] 45 | (.connect (get-socket this) (get-address this) timeout))) 46 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/FixedPathTcpSocketFactory.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.FixedPathTcpSocketFactory 2 | "SocketFactory that produces `FixedPathTcpSocket` objects bound to a fixed path." 3 | (:gen-class 4 | :name unixsocket_http.impl.FixedPathTcpSocketFactory 5 | :extends javax.net.SocketFactory 6 | :init init 7 | :state state 8 | :constructors {[String Integer] []}) 9 | (:import [unixsocket_http.impl FixedPathTcpSocket] 10 | [java.net InetAddress] 11 | [java.net InetSocketAddress SocketAddress])) 12 | 13 | ;; ## Constructor 14 | 15 | (defn -init 16 | [^String host ^Integer port] 17 | [[] (InetSocketAddress. host port)]) 18 | 19 | ;; ## Methods 20 | 21 | (defn- create-socket! 22 | ^FixedPathTcpSocket [^unixsocket_http.impl.FixedPathTcpSocketFactory this] 23 | (FixedPathTcpSocket. ^SocketAddress (.-state this))) 24 | 25 | (defn -createSocket 26 | ([this] 27 | (create-socket! this)) 28 | ([this ^InetAddress _ ^Integer _] 29 | (doto (create-socket! this) 30 | (.connect))) 31 | ([this ^InetAddress _ ^Integer _ ^InetAddress _ ^Integer _] 32 | (doto (create-socket! this) 33 | (.connect)))) 34 | 35 | (defn -toString 36 | [^unixsocket_http.impl.FixedPathTcpSocketFactory this] 37 | (str (.-state this))) 38 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/FixedPathUnixSocket.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.FixedPathUnixSocket 2 | "Wrapper around AFUNIXSocket that is bound to a fixed address and will 3 | ignore the argument passed to 'connect'. This is necessary since an HTTP 4 | client using this socket will pass its own socket address." 5 | (:gen-class 6 | :name unixsocket_http.impl.FixedPathUnixSocket 7 | :extends java.net.Socket 8 | :init init 9 | :state state 10 | :constructors {[String] []} 11 | :methods [[connect [] void]]) 12 | (:require [unixsocket-http.impl.delegate :refer [delegate]] 13 | [clojure.java.io :as io]) 14 | (:import [org.newsclub.net.unix 15 | AFUNIXSelectorProvider 16 | AFUNIXSocketAddress 17 | AFUNIXSocket])) 18 | 19 | ;; ## State 20 | 21 | #_:clj-kondo/ignore 22 | (defn- get-socket 23 | ^AFUNIXSocket [^unixsocket_http.impl.FixedPathUnixSocket this] 24 | (-> this (.-state) (deref) (:socket))) 25 | 26 | (defn- get-address 27 | ^AFUNIXSocketAddress [^unixsocket_http.impl.FixedPathUnixSocket this] 28 | (-> this (.-state) (deref) (:address))) 29 | 30 | (defn- set-socket! 31 | [^unixsocket_http.impl.FixedPathUnixSocket this socket] 32 | (swap! (.-state this) assoc :socket socket)) 33 | 34 | ;; ## Constructor 35 | 36 | (defn -init 37 | [^String path] 38 | [[] (atom {:socket (AFUNIXSocket/newInstance) 39 | :address (AFUNIXSocketAddress/of (io/file path))})]) 40 | 41 | ;; ## Methods 42 | 43 | (delegate 44 | {:class java.net.Socket 45 | :via get-socket 46 | :except [connect]}) 47 | 48 | (defn- connect-socket! 49 | ^AFUNIXSocket 50 | [^AFUNIXSocketAddress address] 51 | (let [provider (AFUNIXSelectorProvider/provider) 52 | channel (.openSocketChannel provider)] 53 | (.connect channel address) 54 | (.socket channel))) 55 | 56 | (defn- connect-and-store-socket! 57 | [^unixsocket_http.impl.FixedPathUnixSocket this] 58 | (->> (get-address this) 59 | (connect-socket!) 60 | (set-socket! this))) 61 | 62 | (defn -connect 63 | ([this] 64 | (connect-and-store-socket! this)) 65 | ([this _] 66 | (connect-and-store-socket! this)) 67 | ([this _ _] 68 | (connect-and-store-socket! this))) 69 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/FixedPathUnixSocketFactory.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.FixedPathUnixSocketFactory 2 | "SocketFactory that produces `FixedPathUnixSocket` objects bound to a 3 | given path." 4 | (:gen-class 5 | :name unixsocket_http.impl.FixedPathUnixSocketFactory 6 | :extends javax.net.SocketFactory 7 | :init init 8 | :state state 9 | :constructors {[String] []}) 10 | (:import [unixsocket_http.impl FixedPathUnixSocket] 11 | [java.net InetAddress])) 12 | 13 | ;; ## Constructor 14 | 15 | (defn -init 16 | [^String path] 17 | [[] path]) 18 | 19 | ;; ## Methods 20 | 21 | (defn- create-socket! 22 | ^FixedPathUnixSocket [^unixsocket_http.impl.FixedPathUnixSocketFactory this] 23 | (FixedPathUnixSocket. ^String (.-state this))) 24 | 25 | (defn -createSocket 26 | ([this] 27 | (doto (create-socket! this) 28 | (.connect))) 29 | ([this ^InetAddress _ ^Integer _] 30 | (doto (create-socket! this) 31 | (.connect))) 32 | ([this ^InetAddress _ ^Integer _ ^InetAddress _ ^Integer _] 33 | (doto (create-socket! this) 34 | (.connect)))) 35 | 36 | (defn -toString 37 | [^unixsocket_http.impl.FixedPathUnixSocketFactory this] 38 | (str (.-state this))) 39 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/ResponseSocket.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.ResponseSocket 2 | "Wrapper around an existing socket that will make sure that the originating 3 | OkHttp Response is closed when the socket is closed. Without this, we 4 | run into connection leaks." 5 | (:gen-class 6 | :name unixsocket_http.impl.ResponseSocket 7 | :extends java.net.Socket 8 | :init init 9 | :state state 10 | :constructors {[java.net.Socket okhttp3.Response] []}) 11 | (:require [unixsocket-http.impl.delegate :refer [delegate]]) 12 | (:import [okhttp3 Response] 13 | [java.net Socket])) 14 | 15 | ;; ## State 16 | 17 | (defn- get-socket 18 | ^Socket [^unixsocket_http.impl.ResponseSocket this] 19 | (-> this (.-state) (:socket))) 20 | 21 | (defn- get-response 22 | ^Response [^unixsocket_http.impl.ResponseSocket this] 23 | (-> this (.-state) (:response))) 24 | 25 | ;; ## Constructor 26 | 27 | (defn -init 28 | [^Socket socket ^Response response] 29 | [[] {:socket socket 30 | :response response}]) 31 | 32 | ;; ## Methods 33 | 34 | (delegate 35 | {:class java.net.Socket 36 | :via get-socket 37 | :except [connect close]}) 38 | 39 | (defn -connect 40 | ([this addr] 41 | (.connect (get-socket this) addr)) 42 | ([this addr ^Integer timeout] 43 | (.connect (get-socket this) addr timeout))) 44 | 45 | (defn -close 46 | [this] 47 | (.close (get-response this)) 48 | (.close (get-socket this))) 49 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/SingletonSocketFactory.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.SingletonSocketFactory 2 | "Wrapper around a `SocketFactory` that will cache the created socket." 3 | (:gen-class 4 | :name unixsocket_http.impl.SingletonSocketFactory 5 | :extends javax.net.SocketFactory 6 | :init init 7 | :state socket 8 | :constructors {[javax.net.SocketFactory] []} 9 | :methods [[getSocket [] java.net.Socket]]) 10 | (:import [java.net InetAddress InetSocketAddress Socket] 11 | [javax.net SocketFactory])) 12 | 13 | ;; ## Constructor 14 | 15 | (defn -init 16 | [^SocketFactory factory] 17 | [[] (delay (.createSocket factory))]) 18 | 19 | ;; ## Methods 20 | 21 | (defn- get-socket 22 | ^Socket [^unixsocket_http.impl.SingletonSocketFactory this] 23 | @(.-socket this)) 24 | 25 | (defn -createSocket 26 | ([this] 27 | (get-socket this)) 28 | ([this ^InetAddress _ ^Integer _] 29 | (doto (get-socket this) 30 | (.connect (InetSocketAddress. 0)))) 31 | ([this ^InetAddress _ ^Integer _ ^InetAddress _ ^Integer _] 32 | (doto (get-socket this) 33 | (.connect (InetSocketAddress. 0))))) 34 | 35 | (defn -getSocket 36 | [this] 37 | (get-socket this)) 38 | 39 | (defn -toString 40 | [^unixsocket_http.impl.SingletonSocketFactory this] 41 | (str (.-socket this))) 42 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/StreamingBody.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.StreamingBody 2 | "Implementation of OkHttp's RequestBody that streams some data." 3 | (:gen-class 4 | :name unixsocket_http.impl.StreamingBody 5 | :extends okhttp3.RequestBody 6 | :init init 7 | :state state 8 | :constructors {[java.io.InputStream] [] 9 | [java.io.InputStream String] []}) 10 | (:import [okhttp3 MediaType] 11 | [okio BufferedSink Okio] 12 | [java.io InputStream])) 13 | 14 | ;; ## Constructor 15 | 16 | (def ^:private media-type-octetstream 17 | (MediaType/get "application/octet-stream")) 18 | 19 | (defn -init 20 | ([^InputStream stream] 21 | [[] {:stream stream 22 | :media-type media-type-octetstream}]) 23 | ([^InputStream stream ^String content-type] 24 | [[] {:stream stream 25 | :media-type (if content-type 26 | (MediaType/get content-type) 27 | media-type-octetstream)}])) 28 | 29 | (defn- stream 30 | ^InputStream [^unixsocket_http.impl.StreamingBody this] 31 | (:stream (.-state this))) 32 | 33 | (defn- media-type 34 | ^InputStream [^unixsocket_http.impl.StreamingBody this] 35 | (:media-type (.-state this))) 36 | 37 | ;; ## Methods 38 | 39 | (defn -contentType 40 | [this] 41 | (media-type this)) 42 | 43 | (defn -contentLength 44 | [_] 45 | ;; This will always use chunked transfer encoding - which makes sense since 46 | ;; it's not possible to reliable get the total length from a stream. 47 | -1) 48 | 49 | (defn -writeTo 50 | [this ^BufferedSink sink] 51 | (with-open [source (Okio/source (stream this))] 52 | (.writeAll sink source))) 53 | -------------------------------------------------------------------------------- /src/unixsocket_http/impl/delegate.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc unixsocket-http.impl.delegate 2 | (:import [java.lang.reflect Method Modifier])) 3 | 4 | (defn is-public-instance-method? 5 | [^Method method] 6 | (let [modifiers (.getModifiers method)] 7 | (and (Modifier/isPublic modifiers) 8 | (not (Modifier/isStatic modifiers))))) 9 | 10 | (defmacro delegate 11 | "Used inside a `:gen-class` namespace this macro will generate all 12 | method definitions of `class`, delegating calls to the value 13 | returned by `via`. 14 | 15 | You can set up exclusions using `except`." 16 | [{:keys [class via except]}] 17 | (let [include? (complement (set except)) 18 | class (Class/forName (str class))] 19 | `(do 20 | ~@(for [^Method method (.getDeclaredMethods class) 21 | :when (is-public-instance-method? method) 22 | :let [method-name (.getName method) 23 | method-sym (symbol method-name) 24 | method-fn-sym (symbol (str "-" method-name)) 25 | args (->> (.getParameterTypes method) 26 | (map 27 | (fn [^Class c] 28 | (with-meta 29 | (gensym (.getSimpleName c)) 30 | {:type (symbol (.getName c))}))))] 31 | :when (include? method-sym)] 32 | `(defn ~method-fn-sym 33 | [~'this ~@args] 34 | (. (~via ~'this) ~method-sym ~@args)))))) 35 | -------------------------------------------------------------------------------- /test-graalvm/resources/META-INF/native-image/uhttp/uhttp/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = --no-fallback \ 2 | --report-unsupported-elements-at-runtime \ 3 | --features=clj_easy.graal_build_time.InitClojureClasses \ 4 | -H:+ReportExceptionStackTraces \ 5 | -J-Dclojure.spec.skip-macros=true \ 6 | -J-Dclojure.compiler.direct-linking=true \ 7 | -J-Xmx3g 8 | -------------------------------------------------------------------------------- /test-graalvm/src/compat/main.clj: -------------------------------------------------------------------------------- 1 | (ns compat.main 2 | (:gen-class) 3 | (:require [unixsocket-http.core :as uhttp] 4 | [clojure.java.io :as io]) 5 | (:import [org.newsclub.lib.junixsocket.common NarMetadata] 6 | [org.newsclub.net.unix AFUNIXSocket] 7 | [java.io File] 8 | [java.util Properties])) 9 | 10 | (defn- junixsocket-version 11 | [] 12 | (let [path "/META-INF/maven/com.kohlschutter.junixsocket/junixsocket-common/pom.properties"] 13 | (with-open [in (.getResourceAsStream AFUNIXSocket path)] 14 | (-> (doto (Properties.) 15 | (.load in)) 16 | (.getProperty "version"))))) 17 | 18 | (defn- arch-and-os 19 | [] 20 | (let [arch (System/getProperty "os.arch") 21 | os (-> (System/getProperty "os.name") (.replaceAll " " ""))] 22 | ;; Look, this is a fix for Github Actions' MacOS runner. I know this is not 23 | ;; pretty. 24 | (case [arch os] 25 | ["amd64" "MacOSX"] ["x86_64" "MacOSX"] 26 | [arch os]))) 27 | 28 | (defn run-selftest 29 | "Attempt to load the native library." 30 | [] 31 | (let [library (System/mapLibraryName 32 | (str "junixsocket-native-" (junixsocket-version))) 33 | [arch os] (arch-and-os) 34 | path (format "/lib/%s-%s-clang/jni/%s" arch os library) 35 | tmp-dir (System/getProperty "java.io.tmpdir")] 36 | (println "Native Library: " library) 37 | (println " Architecture: " arch) 38 | (println " OS: " os) 39 | (println " Path: " path) 40 | (println " Temp Dir: " tmp-dir) 41 | (println "Loading native library ...") 42 | (let [tmp-file (File/createTempFile "libtmp" library) 43 | tmp-path (.getAbsolutePath tmp-file) 44 | stream (.getResourceAsStream NarMetadata path)] 45 | (or (when stream 46 | (println " Copying to" tmp-path "...") 47 | (with-open [stream stream 48 | out (io/output-stream tmp-file)] 49 | (io/copy stream out)) 50 | (println " Loading" tmp-path "...") 51 | (System/load (.getAbsolutePath tmp-file)) 52 | (println " OK!") 53 | :ok) 54 | (do 55 | (println "The native library could not be found.") 56 | :fail))))) 57 | 58 | (defn -main 59 | [& [socket-addr path]] 60 | (if (and socket-addr path) 61 | (let [client (uhttp/client socket-addr)] 62 | (prn (uhttp/get client path))) 63 | (when (= :fail (run-selftest)) 64 | (System/exit 1)))) 65 | -------------------------------------------------------------------------------- /test/unixsocket_http/client_test.clj: -------------------------------------------------------------------------------- 1 | (ns unixsocket-http.client-test 2 | (:require [unixsocket-http.client :as client] 3 | [clojure.test :refer [deftest testing is]]) 4 | (:import [okhttp3 OkHttpClient] 5 | [java.net Socket])) 6 | 7 | (def ^:private url 8 | "http://areiotnaiotrsanitosrna:12345") 9 | 10 | (deftest t-client-and-connection 11 | (testing "default client" 12 | (let [client {:client (client/create url {})}] 13 | (is (= url (client/base-url client))) 14 | (testing "- default request" 15 | (let [connection (client/connection client)] 16 | (is (instance? okhttp3.OkHttpClient (client/get-client connection))) 17 | (is (nil? (client/get-socket connection))))) 18 | (testing "- socket request" 19 | (let [connection (client/connection (merge {:as :socket} client))] 20 | (is (instance? OkHttpClient (client/get-client connection))) 21 | (is (instance? Socket (client/get-socket connection))))))) 22 | 23 | (testing "recreating client" 24 | (let [client {:client (client/create url {:mode :recreate})}] 25 | (is (= url (client/base-url client))) 26 | (testing "- default request" 27 | (let [connection (client/connection client)] 28 | (is (instance? okhttp3.OkHttpClient (client/get-client connection))) 29 | (is (nil? (client/get-socket connection))))) 30 | (testing "- socket request" 31 | (let [connection (client/connection (merge {:as :socket} client))] 32 | (is (instance? OkHttpClient (client/get-client connection))) 33 | (is (instance? Socket (client/get-socket connection))))))) 34 | 35 | (testing "reusable client" 36 | (let [client {:client (client/create url {:mode :reuse})}] 37 | (is (= url (client/base-url client))) 38 | (testing "- default request" 39 | (let [connection (client/connection client)] 40 | (is (instance? okhttp3.OkHttpClient (client/get-client connection))) 41 | (is (nil? (client/get-socket connection))))) 42 | (testing "- socket request" 43 | (is (thrown-with-msg? 44 | IllegalArgumentException 45 | #"Client mode `:reuse` does not allow direct socket access" 46 | (client/connection (merge {:as :socket} client)))))))) 47 | 48 | (deftest t-client-with-base-url 49 | (testing "uses custom base URL" 50 | (doseq [mode [:default :reuse :recreate]] 51 | (let [base-url (str url "/base") 52 | client {:client (client/create url {:mode mode, :base-url base-url})}] 53 | (is (= base-url (client/base-url client)))))) 54 | (testing "removes trailing slash" 55 | (let [url' (str url "/") 56 | client {:client (client/create url' {})}] 57 | (is (= url (client/base-url client))))) 58 | (testing "introduces 'unix://' prefix (deprecated behaviour)" 59 | (let [path "/var/sock/docker.sock" 60 | client {:client (client/create path {})}] 61 | (is (= "http://localhost" 62 | (client/base-url client)))))) 63 | 64 | (deftest t-client-with-timeouts 65 | (testing "default timeout" 66 | (let [client {:client (client/create url {})} 67 | oclient (-> (client/connection client) (client/get-client))] 68 | (is (= 0 (.callTimeoutMillis oclient))) 69 | (is (= 0 (.connectTimeoutMillis oclient))) 70 | (is (= 0 (.readTimeoutMillis oclient))))) 71 | (testing "general timeout" 72 | (let [timeout-ms 100 73 | client {:client (client/create url {:timeout-ms timeout-ms})} 74 | oclient (-> (client/connection client) (client/get-client))] 75 | (is (= timeout-ms (.callTimeoutMillis oclient))) 76 | (is (= timeout-ms (.connectTimeoutMillis oclient))) 77 | (is (= timeout-ms (.readTimeoutMillis oclient))))) 78 | (testing "single timeouts" 79 | (let [client {:client (client/create url {:call-timeout-ms 100 80 | :connect-timeout-ms 101 81 | :read-timeout-ms 102})} 82 | oclient (-> (client/connection client) (client/get-client))] 83 | (is (= 100 (.callTimeoutMillis oclient))) 84 | (is (= 101 (.connectTimeoutMillis oclient))) 85 | (is (= 102 (.readTimeoutMillis oclient)))))) 86 | 87 | (deftest t-client-with-invalid-mode 88 | (is (thrown? 89 | IllegalArgumentException 90 | (client/create url {:mode :unknown})))) 91 | 92 | (deftest t-client-with-invalid-url 93 | (testing "invalid scheme" 94 | (is (thrown-with-msg? 95 | IllegalArgumentException 96 | #"Can only handle URI schemes .+, given: .+" 97 | (client/create "unknown://host" {})))) 98 | (testing "TCP URL without port" 99 | (is (thrown-with-msg? 100 | IllegalArgumentException 101 | #"Port is required in URI: tcp://.+" 102 | (client/create "tcp://12.1.1.1" {}))))) 103 | -------------------------------------------------------------------------------- /test/unixsocket_http/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns unixsocket-http.core-test 2 | (:require [clojure.test.check 3 | [clojure-test :refer [defspec]] 4 | [generators :as gen] 5 | [properties :as prop]] 6 | [com.gfredericks.test.chuck :refer [times]] 7 | [clojure.test :refer [is use-fixtures]] 8 | [clojure.java.io :as io] 9 | [unixsocket-http.test.http-server :as httpd] 10 | [unixsocket-http.core :as http])) 11 | 12 | ;; ## Test Setup 13 | ;; 14 | ;; Runs all test cases automatically against UNIX/TCP/... endpoints. 15 | 16 | (def ^:dynamic make-client nil) 17 | (def ^:dynamic adjust-url identity) 18 | 19 | (use-fixtures 20 | :each 21 | (fn [f] 22 | (doseq [server-fn [httpd/create-unix-socket-server 23 | httpd/create-tcp-socket-server 24 | httpd/create-http-socket-server 25 | httpd/create-https-socket-server] 26 | :let [{:keys [^String url stop opts]} (server-fn)]] 27 | (try 28 | (binding [make-client (if opts 29 | #(http/client url opts) 30 | #(http/client url)) 31 | adjust-url (if (.startsWith url "http") 32 | #(str url %) 33 | #(str "http://localhost" %))] 34 | (f)) 35 | (finally 36 | (stop)))))) 37 | 38 | ;; ## Request Generators 39 | 40 | (defn- gen-request-fn 41 | "This allows us to easily test e.g. `:as` specifiers in requests." 42 | [] 43 | (->> (gen/elements 44 | [;; response as stream 45 | {:pre #(assoc % :as :stream) 46 | :post #(update % :body slurp)} 47 | 48 | ;; request as stream 49 | {:pre #(update % 50 | :body 51 | (fn [data] 52 | (some-> ^String data 53 | (.getBytes "UTF-8") 54 | (java.io.ByteArrayInputStream.)))) 55 | :post identity} 56 | 57 | ;; as-is 58 | {:pre identity 59 | :post identity} 60 | 61 | ;; use full url 62 | {:pre #(update % :url adjust-url) 63 | :post identity}]) 64 | (gen/fmap 65 | (fn [{:keys [pre post]}] 66 | (comp post http/request pre))))) 67 | 68 | (defn- gen-socket-request-fn 69 | [] 70 | (->> (gen/elements 71 | [;; response as socket 72 | {:pre #(assoc % :as :socket) 73 | :post identity}]) 74 | (gen/fmap 75 | (fn [{:keys [pre post]}] 76 | (comp post http/request pre))))) 77 | 78 | (defn- gen-any-request-fn 79 | [] 80 | (gen/one-of [(gen-socket-request-fn) 81 | (gen-request-fn)])) 82 | 83 | (defn- gen-request 84 | [gen] 85 | (gen/fmap 86 | (fn [[req headers query-params]] 87 | (-> req 88 | (update :headers #(merge headers %)) 89 | (update :query-params #(merge query-params %)))) 90 | (gen/tuple 91 | gen 92 | (gen/map 93 | (gen/elements ["x-header-one" "x-header-two"]) 94 | gen/string-ascii 95 | {:min-elements 0, :max-elements 2}) 96 | (gen/map 97 | (gen/fmap str gen/char-alpha) 98 | gen/string-ascii 99 | {:min-elements 0, :max-elements 3})))) 100 | 101 | (defn- gen-stream-request 102 | [] 103 | (->> (gen/tuple 104 | (gen/elements [:post :put :patch :delete]) 105 | gen/string-ascii) 106 | (gen/fmap 107 | (fn [[method data]] 108 | {:client (make-client) 109 | :method method 110 | :url "/stream" 111 | :data data 112 | :as :socket})) 113 | (gen-request))) 114 | 115 | (defn- gen-echo-request 116 | [] 117 | (->> (gen/tuple 118 | (gen/elements [:post :put :patch :delete]) 119 | (gen/such-that seq gen/string-ascii)) 120 | (gen/fmap 121 | (fn [[method body]] 122 | {:client (make-client) 123 | :method method 124 | :url "/echo" 125 | :body body})) 126 | (gen-request))) 127 | 128 | (defn- gen-ok-request 129 | [] 130 | (->> (gen/elements [:get :post :put :patch :delete]) 131 | (gen/fmap 132 | (fn [method] 133 | {:client (make-client) 134 | :method method 135 | :url "/ok"})) 136 | (gen-request))) 137 | 138 | (defn- gen-head-request 139 | [] 140 | (->> (gen/elements [:head]) 141 | (gen/fmap 142 | (fn [method] 143 | {:client (make-client) 144 | :method method 145 | :url "/head"})) 146 | (gen-request))) 147 | 148 | (defn- gen-fail-request 149 | [& [opts]] 150 | (->> (gen/elements [:get :post :put :patch :delete]) 151 | (gen/fmap 152 | (fn [method] 153 | (merge 154 | {:client (make-client) 155 | :method method 156 | :url "/fail"} 157 | opts))) 158 | (gen-request))) 159 | 160 | ;; ## Tests 161 | 162 | (defspec t-request-with-body (times 50) 163 | (prop/for-all 164 | [request (gen-echo-request) 165 | send! (gen-request-fn)] 166 | (let [{:keys [status body]} (send! request)] 167 | (and (= 200 status) (= (:body request) body))))) 168 | 169 | (defspec t-request-without-body (times 50) 170 | (prop/for-all 171 | [request (gen-ok-request) 172 | send! (gen-request-fn)] 173 | (let [{:keys [status body]} (send! request)] 174 | (and (= 200 status) (= "OK" body))))) 175 | 176 | (defspec t-head-request (times 5) 177 | (prop/for-all 178 | [request (gen-head-request) 179 | send! (gen-request-fn)] 180 | (let [{:keys [status body]} (send! request)] 181 | (and (= 200 status) (= "" body))))) 182 | 183 | (defspec t-request-with-socket (times 10) 184 | (prop/for-all 185 | [request (gen-ok-request) 186 | send! (gen-socket-request-fn)] 187 | (let [{:keys [status body]} (send! request)] 188 | (with-open [^java.net.Socket socket body] 189 | (and (instance? java.net.Socket socket) 190 | (= 200 status)))))) 191 | 192 | (defspec t-failing-request (times 50) 193 | (prop/for-all 194 | [request (gen-fail-request) 195 | send! (gen-any-request-fn)] 196 | (and (is (thrown-with-msg? 197 | Exception 198 | #"HTTP Error: 500" 199 | (send! request))) 200 | true))) 201 | 202 | (defspec t-failing-request-without-body (times 10) 203 | (prop/for-all 204 | [request (gen-fail-request {:as :stream}) 205 | send! (gen-request-fn)] 206 | (try 207 | (send! request) 208 | false 209 | (catch clojure.lang.ExceptionInfo e 210 | (let [{:keys [status body]} (ex-data e)] 211 | (and (= 500 status) 212 | (nil? body))))))) 213 | 214 | (defspec t-failing-request-with-body (times 10) 215 | (prop/for-all 216 | [request (gen-fail-request 217 | {:as :stream 218 | :throw-entire-message? true}) 219 | send! (gen-request-fn)] 220 | (try 221 | (send! request) 222 | false 223 | (catch clojure.lang.ExceptionInfo e 224 | (let [{:keys [status body]} (ex-data e)] 225 | (and (= 500 status) 226 | (= "FAIL" (slurp body)))))))) 227 | 228 | (defspec t-failing-request-without-exception (times 50) 229 | (prop/for-all 230 | [request (gen/let [req (gen-fail-request) 231 | k (gen/elements [:throw-exceptions :throw-exceptions?]) 232 | v (gen/elements [nil false])] 233 | (assoc req k v)) 234 | send! (gen-request-fn)] 235 | (let [{:keys [status body]} (send! request)] 236 | (and (= 500 status) (= "FAIL" body))))) 237 | 238 | (defspec t-invalid-method (times 5) 239 | (prop/for-all 240 | [request (->> (gen-fail-request) 241 | (gen/fmap #(assoc % :method :unknown))) 242 | send! (gen-any-request-fn)] 243 | (and (is (thrown-with-msg? 244 | Exception 245 | #"Invalid HTTP method keyword supplied: :unknown" 246 | (send! request))) 247 | true))) 248 | 249 | (defspec t-invalid-as-statements (times 5) 250 | (prop/for-all 251 | [request (->> (gen-ok-request) 252 | (gen/fmap #(assoc % :as :unknown)))] 253 | (and (is (thrown? 254 | IllegalArgumentException 255 | (http/request request))) 256 | true))) 257 | 258 | (defspec t-http-wrappers (times 50) 259 | (prop/for-all 260 | [request (gen-ok-request)] 261 | (let [{:keys [client method url]} request 262 | opts (dissoc request :client :method :url) 263 | http-fn (case method 264 | :get http/get 265 | :post http/post 266 | :put http/put 267 | :delete http/delete 268 | :patch http/patch)] 269 | (if (empty? opts) 270 | (http-fn client url) 271 | (http-fn client url opts))))) 272 | 273 | (comment 274 | ;; This cannot be verified using the MockWebServer, unfortunately. 275 | (defspec t-bidirectional-request (times 50) 276 | (prop/for-all 277 | [{:keys [data] :as request} (gen-stream-request)] 278 | (let [{:keys [status body]} (http/request request)] 279 | (with-open [^java.net.Socket socket body 280 | out (.getOutputStream socket) 281 | in (.getInputStream socket)] 282 | (io/copy data out) 283 | (and (= 200 status) 284 | (= data (slurp in)))))))) 285 | -------------------------------------------------------------------------------- /test/unixsocket_http/data_test.clj: -------------------------------------------------------------------------------- 1 | (ns unixsocket-http.data-test 2 | (:require [unixsocket-http.client :as client] 3 | [unixsocket-http.data :as data] 4 | [clojure.test :refer [deftest testing is]])) 5 | 6 | (def ^:private url 7 | "http://areiotnaiotrsanitosrna:12345") 8 | 9 | (def ^:private client 10 | {::client/factory (constantly nil) 11 | ::client/base-url url}) 12 | 13 | (deftest t-build-request 14 | (testing "request without scheme/hostname" 15 | (let [request (data/build-request 16 | {:client client 17 | :method :get 18 | :url "/"})] 19 | (is (= "GET" (.method request))) 20 | (is (= (str url "/") (str (.url request)))) 21 | (is (nil? (.body request))))) 22 | 23 | (testing "request with scheme/hostname" 24 | (let [url' "http://oneiontgsreionnehbfei:4344/test" 25 | request (data/build-request 26 | {:client client 27 | :method :get 28 | :url url'})] 29 | (is (= "GET" (.method request))) 30 | (is (= url' (str (.url request)))) 31 | (is (nil? (.body request))))) 32 | 33 | (testing "request with query parameters" 34 | (let [request (data/build-request 35 | {:client client 36 | :method :get 37 | :query-params {:x 1, :y "2"} 38 | :url url})] 39 | (is (= "GET" (.method request))) 40 | (is (= (str url "/?x=1&y=2") (str (.url request)))) 41 | (is (nil? (.body request))))) 42 | 43 | (testing "request with repeating query parameters" 44 | (let [request (data/build-request 45 | {:client client 46 | :method :get 47 | :query-params {:x [1 2 3]} 48 | :url url})] 49 | (is (= "GET" (.method request))) 50 | (is (= (str url "/?x=1&x=2&x=3") (str (.url request)))) 51 | (is (nil? (.body request))))) 52 | 53 | (testing "request with headers" 54 | (let [request (data/build-request 55 | {:client client 56 | :method :get 57 | :headers {"X-Custom" "1"} 58 | :url url})] 59 | (is (= "GET" (.method request))) 60 | (is (= (str url "/") (str (.url request)))) 61 | (is (= {"x-custom" ["1"]} (.toMultimap (.headers request)))) 62 | (is (nil? (.body request)))))) 63 | -------------------------------------------------------------------------------- /test/unixsocket_http/test/http_server.clj: -------------------------------------------------------------------------------- 1 | (ns unixsocket-http.test.http-server 2 | (:require [clojure.string :as string]) 3 | (:import [org.newsclub.net.unix 4 | AFUNIXServerSocket 5 | AFUNIXSocketAddress] 6 | [okhttp3.mockwebserver 7 | Dispatcher 8 | MockResponse 9 | MockWebServer 10 | RecordedRequest] 11 | [okhttp3.tls 12 | HandshakeCertificates$Builder 13 | HeldCertificate$Builder 14 | HeldCertificate] 15 | [java.io File] 16 | [javax.net 17 | ServerSocketFactory])) 18 | 19 | ;; ## Mock Server 20 | 21 | (defn- mock-response 22 | [status ^String body] 23 | (.. (MockResponse.) 24 | (setResponseCode status) 25 | (setHeader "Content-Type" "text/plain") 26 | (setBody body))) 27 | 28 | (defn- create-mock-server! 29 | ^MockWebServer [& [builder-fn]] 30 | ;; Don't log warnings that, for some reason, occur when using 31 | ;; the UNIX socket. 32 | (doto (java.util.logging.LogManager/getLogManager) 33 | (.reset)) 34 | 35 | ;; Mock Server 36 | (doto (MockWebServer.) 37 | (cond-> builder-fn builder-fn) 38 | (.setDispatcher 39 | (proxy [Dispatcher] [] 40 | (dispatch [^RecordedRequest request] 41 | (with-open [body (.getBody request)] 42 | (case (string/replace (.getPath request) #"\?.*$" "") 43 | "/ok" (mock-response 200 "OK") 44 | "/head" (mock-response 200 "") 45 | "/echo" (mock-response 200 (.readUtf8 body)) 46 | "/fail" (mock-response 500 "FAIL") 47 | (mock-response 404 "NOT_FOUND")))))) 48 | (.start))) 49 | 50 | (defn- as-url 51 | [^MockWebServer server] 52 | (-> (str (.url server "")) 53 | (string/replace #"/+$" ""))) 54 | 55 | ;; ## Servers 56 | 57 | (defn create-unix-socket-server 58 | [] 59 | (let [socket-file (doto (File/createTempFile "http" ".sock") 60 | (.delete)) 61 | address (AFUNIXSocketAddress. socket-file) 62 | factory (proxy [ServerSocketFactory] [] 63 | (createServerSocket [] 64 | (AFUNIXServerSocket/forceBindOn address))) 65 | server (create-mock-server! 66 | (fn [^MockWebServer server] 67 | (.setServerSocketFactory server factory)))] 68 | {:url (str "unix://" (.getCanonicalPath socket-file)) 69 | :stop #(.shutdown server)})) 70 | 71 | ;; ### TCP 72 | 73 | (defn create-tcp-socket-server 74 | [] 75 | (let [server (create-mock-server!)] 76 | {:url (-> (as-url server) 77 | (string/replace #"^http:" "tcp:")) 78 | :stop #(.shutdown server)})) 79 | 80 | ;; ### HTTP 81 | 82 | (defn create-http-socket-server 83 | [] 84 | (let [server (create-mock-server!)] 85 | {:url (as-url server) 86 | :stop #(.shutdown server)})) 87 | 88 | ;; ### HTTPS 89 | 90 | (defn- create-certificate 91 | ^HeldCertificate [^String hostname] 92 | (-> (HeldCertificate$Builder.) 93 | (.addSubjectAlternativeName hostname) 94 | (.build))) 95 | 96 | (defn- create-server-builder-fn 97 | [^HeldCertificate certificate] 98 | (fn [^MockWebServer server] 99 | (let [socket-factory (.. (HandshakeCertificates$Builder.) 100 | (heldCertificate 101 | certificate 102 | (into-array java.security.cert.X509Certificate [])) 103 | (build) 104 | (sslSocketFactory))] 105 | (.useHttps server socket-factory false)))) 106 | 107 | (defn- create-client-builder-fn 108 | [^HeldCertificate certificate] 109 | (let [certs (-> (HandshakeCertificates$Builder.) 110 | (.addTrustedCertificate (.certificate certificate)) 111 | (.build))] 112 | (fn [^okhttp3.OkHttpClient$Builder builder] 113 | (.sslSocketFactory builder 114 | (.sslSocketFactory certs) 115 | (.trustManager certs))))) 116 | 117 | (defn create-https-socket-server 118 | [] 119 | (let [certificate (create-certificate "localhost") 120 | builder-fn (create-server-builder-fn certificate) 121 | server (create-mock-server! builder-fn)] 122 | {:url (as-url server) 123 | :opts {:builder-fn (create-client-builder-fn certificate)} 124 | :stop #(.shutdown server)})) 125 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 {} 2 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0": 6 | version "7.8.3" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" 8 | integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== 9 | dependencies: 10 | "@babel/highlight" "^7.8.3" 11 | 12 | "@babel/helper-validator-identifier@^7.9.0": 13 | version "7.9.5" 14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" 15 | integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== 16 | 17 | "@babel/highlight@^7.8.3": 18 | version "7.9.0" 19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" 20 | integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== 21 | dependencies: 22 | "@babel/helper-validator-identifier" "^7.9.0" 23 | chalk "^2.0.0" 24 | js-tokens "^4.0.0" 25 | 26 | "@types/normalize-package-data@^2.4.0": 27 | version "2.4.0" 28 | resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" 29 | integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== 30 | 31 | ansi-styles@^3.2.1: 32 | version "3.2.1" 33 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 34 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 35 | dependencies: 36 | color-convert "^1.9.0" 37 | 38 | argparse@^1.0.7: 39 | version "1.0.10" 40 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 41 | integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== 42 | dependencies: 43 | sprintf-js "~1.0.2" 44 | 45 | array-differ@^2.0.3: 46 | version "2.1.0" 47 | resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" 48 | integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== 49 | 50 | array-union@^1.0.2: 51 | version "1.0.2" 52 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" 53 | integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= 54 | dependencies: 55 | array-uniq "^1.0.1" 56 | 57 | array-uniq@^1.0.1: 58 | version "1.0.3" 59 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 60 | integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= 61 | 62 | arrify@^1.0.1: 63 | version "1.0.1" 64 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 65 | integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= 66 | 67 | balanced-match@^1.0.0: 68 | version "1.0.0" 69 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 70 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 71 | 72 | brace-expansion@^1.1.7: 73 | version "1.1.11" 74 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 75 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 76 | dependencies: 77 | balanced-match "^1.0.0" 78 | concat-map "0.0.1" 79 | 80 | caller-callsite@^2.0.0: 81 | version "2.0.0" 82 | resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" 83 | integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= 84 | dependencies: 85 | callsites "^2.0.0" 86 | 87 | caller-path@^2.0.0: 88 | version "2.0.0" 89 | resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" 90 | integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= 91 | dependencies: 92 | caller-callsite "^2.0.0" 93 | 94 | callsites@^2.0.0: 95 | version "2.0.0" 96 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" 97 | integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= 98 | 99 | chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: 100 | version "2.4.2" 101 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 102 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 103 | dependencies: 104 | ansi-styles "^3.2.1" 105 | escape-string-regexp "^1.0.5" 106 | supports-color "^5.3.0" 107 | 108 | ci-info@^2.0.0: 109 | version "2.0.0" 110 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" 111 | integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== 112 | 113 | color-convert@^1.9.0: 114 | version "1.9.3" 115 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 116 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 117 | dependencies: 118 | color-name "1.1.3" 119 | 120 | color-name@1.1.3: 121 | version "1.1.3" 122 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 123 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 124 | 125 | concat-map@0.0.1: 126 | version "0.0.1" 127 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 128 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 129 | 130 | cosmiconfig@^5.2.1: 131 | version "5.2.1" 132 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" 133 | integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== 134 | dependencies: 135 | import-fresh "^2.0.0" 136 | is-directory "^0.3.1" 137 | js-yaml "^3.13.1" 138 | parse-json "^4.0.0" 139 | 140 | cross-spawn@^5.0.1: 141 | version "5.1.0" 142 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" 143 | integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= 144 | dependencies: 145 | lru-cache "^4.0.1" 146 | shebang-command "^1.2.0" 147 | which "^1.2.9" 148 | 149 | cross-spawn@^6.0.0: 150 | version "6.0.5" 151 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 152 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 153 | dependencies: 154 | nice-try "^1.0.4" 155 | path-key "^2.0.1" 156 | semver "^5.5.0" 157 | shebang-command "^1.2.0" 158 | which "^1.2.9" 159 | 160 | end-of-stream@^1.1.0: 161 | version "1.4.4" 162 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 163 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 164 | dependencies: 165 | once "^1.4.0" 166 | 167 | error-ex@^1.3.1: 168 | version "1.3.2" 169 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 170 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== 171 | dependencies: 172 | is-arrayish "^0.2.1" 173 | 174 | escape-string-regexp@^1.0.5: 175 | version "1.0.5" 176 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 177 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 178 | 179 | esprima@^4.0.0: 180 | version "4.0.1" 181 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 182 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 183 | 184 | execa@^0.8.0: 185 | version "0.8.0" 186 | resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" 187 | integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo= 188 | dependencies: 189 | cross-spawn "^5.0.1" 190 | get-stream "^3.0.0" 191 | is-stream "^1.1.0" 192 | npm-run-path "^2.0.0" 193 | p-finally "^1.0.0" 194 | signal-exit "^3.0.0" 195 | strip-eof "^1.0.0" 196 | 197 | execa@^1.0.0: 198 | version "1.0.0" 199 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" 200 | integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== 201 | dependencies: 202 | cross-spawn "^6.0.0" 203 | get-stream "^4.0.0" 204 | is-stream "^1.1.0" 205 | npm-run-path "^2.0.0" 206 | p-finally "^1.0.0" 207 | signal-exit "^3.0.0" 208 | strip-eof "^1.0.0" 209 | 210 | find-up@^2.1.0: 211 | version "2.1.0" 212 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" 213 | integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= 214 | dependencies: 215 | locate-path "^2.0.0" 216 | 217 | find-up@^4.0.0: 218 | version "4.1.0" 219 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 220 | integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== 221 | dependencies: 222 | locate-path "^5.0.0" 223 | path-exists "^4.0.0" 224 | 225 | get-stdin@^7.0.0: 226 | version "7.0.0" 227 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" 228 | integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== 229 | 230 | get-stream@^3.0.0: 231 | version "3.0.0" 232 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" 233 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= 234 | 235 | get-stream@^4.0.0: 236 | version "4.1.0" 237 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 238 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== 239 | dependencies: 240 | pump "^3.0.0" 241 | 242 | has-flag@^3.0.0: 243 | version "3.0.0" 244 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 245 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 246 | 247 | hosted-git-info@^2.1.4: 248 | version "2.8.9" 249 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" 250 | integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== 251 | 252 | husky@3.0.2: 253 | version "3.0.2" 254 | resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.2.tgz#e78fd2ae16edca59fc88e56aeb8d70acdcc1c082" 255 | integrity sha512-WXCtaME2x0o4PJlKY4ap8BzLA+D0zlvefqAvLCPriOOu+x0dpO5uc5tlB7CY6/0SE2EESmoZsj4jW5D09KrJoA== 256 | dependencies: 257 | chalk "^2.4.2" 258 | cosmiconfig "^5.2.1" 259 | execa "^1.0.0" 260 | get-stdin "^7.0.0" 261 | is-ci "^2.0.0" 262 | opencollective-postinstall "^2.0.2" 263 | pkg-dir "^4.2.0" 264 | please-upgrade-node "^3.1.1" 265 | read-pkg "^5.1.1" 266 | run-node "^1.0.0" 267 | slash "^3.0.0" 268 | 269 | ignore@^3.3.7: 270 | version "3.3.10" 271 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" 272 | integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== 273 | 274 | import-fresh@^2.0.0: 275 | version "2.0.0" 276 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" 277 | integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= 278 | dependencies: 279 | caller-path "^2.0.0" 280 | resolve-from "^3.0.0" 281 | 282 | is-arrayish@^0.2.1: 283 | version "0.2.1" 284 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 285 | integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= 286 | 287 | is-ci@^2.0.0: 288 | version "2.0.0" 289 | resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" 290 | integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== 291 | dependencies: 292 | ci-info "^2.0.0" 293 | 294 | is-directory@^0.3.1: 295 | version "0.3.1" 296 | resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" 297 | integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= 298 | 299 | is-stream@^1.1.0: 300 | version "1.1.0" 301 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 302 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 303 | 304 | isexe@^2.0.0: 305 | version "2.0.0" 306 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 307 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 308 | 309 | js-tokens@^4.0.0: 310 | version "4.0.0" 311 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 312 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 313 | 314 | js-yaml@^3.13.1: 315 | version "3.13.1" 316 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" 317 | integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== 318 | dependencies: 319 | argparse "^1.0.7" 320 | esprima "^4.0.0" 321 | 322 | json-parse-better-errors@^1.0.1: 323 | version "1.0.2" 324 | resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" 325 | integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== 326 | 327 | lines-and-columns@^1.1.6: 328 | version "1.1.6" 329 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" 330 | integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= 331 | 332 | locate-path@^2.0.0: 333 | version "2.0.0" 334 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 335 | integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= 336 | dependencies: 337 | p-locate "^2.0.0" 338 | path-exists "^3.0.0" 339 | 340 | locate-path@^5.0.0: 341 | version "5.0.0" 342 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" 343 | integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== 344 | dependencies: 345 | p-locate "^4.1.0" 346 | 347 | lru-cache@^4.0.1: 348 | version "4.1.5" 349 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 350 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 351 | dependencies: 352 | pseudomap "^1.0.2" 353 | yallist "^2.1.2" 354 | 355 | minimatch@^3.0.4: 356 | version "3.0.4" 357 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 358 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 359 | dependencies: 360 | brace-expansion "^1.1.7" 361 | 362 | mri@^1.1.0: 363 | version "1.1.5" 364 | resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.5.tgz#ce21dba2c69f74a9b7cf8a1ec62307e089e223e0" 365 | integrity sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg== 366 | 367 | multimatch@^3.0.0: 368 | version "3.0.0" 369 | resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" 370 | integrity sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA== 371 | dependencies: 372 | array-differ "^2.0.3" 373 | array-union "^1.0.2" 374 | arrify "^1.0.1" 375 | minimatch "^3.0.4" 376 | 377 | nice-try@^1.0.4: 378 | version "1.0.5" 379 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 380 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 381 | 382 | normalize-package-data@^2.5.0: 383 | version "2.5.0" 384 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" 385 | integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== 386 | dependencies: 387 | hosted-git-info "^2.1.4" 388 | resolve "^1.10.0" 389 | semver "2 || 3 || 4 || 5" 390 | validate-npm-package-license "^3.0.1" 391 | 392 | npm-run-path@^2.0.0: 393 | version "2.0.2" 394 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 395 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= 396 | dependencies: 397 | path-key "^2.0.0" 398 | 399 | once@^1.3.1, once@^1.4.0: 400 | version "1.4.0" 401 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 402 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 403 | dependencies: 404 | wrappy "1" 405 | 406 | opencollective-postinstall@^2.0.2: 407 | version "2.0.2" 408 | resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" 409 | integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== 410 | 411 | p-finally@^1.0.0: 412 | version "1.0.0" 413 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 414 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= 415 | 416 | p-limit@^1.1.0: 417 | version "1.3.0" 418 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" 419 | integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== 420 | dependencies: 421 | p-try "^1.0.0" 422 | 423 | p-limit@^2.2.0: 424 | version "2.3.0" 425 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 426 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 427 | dependencies: 428 | p-try "^2.0.0" 429 | 430 | p-locate@^2.0.0: 431 | version "2.0.0" 432 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" 433 | integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= 434 | dependencies: 435 | p-limit "^1.1.0" 436 | 437 | p-locate@^4.1.0: 438 | version "4.1.0" 439 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" 440 | integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== 441 | dependencies: 442 | p-limit "^2.2.0" 443 | 444 | p-try@^1.0.0: 445 | version "1.0.0" 446 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" 447 | integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= 448 | 449 | p-try@^2.0.0: 450 | version "2.2.0" 451 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 452 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 453 | 454 | parse-json@^4.0.0: 455 | version "4.0.0" 456 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" 457 | integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= 458 | dependencies: 459 | error-ex "^1.3.1" 460 | json-parse-better-errors "^1.0.1" 461 | 462 | parse-json@^5.0.0: 463 | version "5.0.0" 464 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" 465 | integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== 466 | dependencies: 467 | "@babel/code-frame" "^7.0.0" 468 | error-ex "^1.3.1" 469 | json-parse-better-errors "^1.0.1" 470 | lines-and-columns "^1.1.6" 471 | 472 | path-exists@^3.0.0: 473 | version "3.0.0" 474 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 475 | integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= 476 | 477 | path-exists@^4.0.0: 478 | version "4.0.0" 479 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 480 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 481 | 482 | path-key@^2.0.0, path-key@^2.0.1: 483 | version "2.0.1" 484 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 485 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 486 | 487 | path-parse@^1.0.6: 488 | version "1.0.7" 489 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 490 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 491 | 492 | pkg-dir@^4.2.0: 493 | version "4.2.0" 494 | resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" 495 | integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== 496 | dependencies: 497 | find-up "^4.0.0" 498 | 499 | please-upgrade-node@^3.1.1: 500 | version "3.2.0" 501 | resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" 502 | integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== 503 | dependencies: 504 | semver-compare "^1.0.0" 505 | 506 | prettier@1.18.2: 507 | version "1.18.2" 508 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" 509 | integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== 510 | 511 | pretty-quick@1.11.1: 512 | version "1.11.1" 513 | resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-1.11.1.tgz#462ffa2b93d24c05b7a0c3a001e08601a0c55ee4" 514 | integrity sha512-kSXCkcETfak7EQXz6WOkCeCqpbC4GIzrN/vaneTGMP/fAtD8NerA9bPhCUqHAks1geo7biZNl5uEMPceeneLuA== 515 | dependencies: 516 | chalk "^2.3.0" 517 | execa "^0.8.0" 518 | find-up "^2.1.0" 519 | ignore "^3.3.7" 520 | mri "^1.1.0" 521 | multimatch "^3.0.0" 522 | 523 | pseudomap@^1.0.2: 524 | version "1.0.2" 525 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 526 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= 527 | 528 | pump@^3.0.0: 529 | version "3.0.0" 530 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 531 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 532 | dependencies: 533 | end-of-stream "^1.1.0" 534 | once "^1.3.1" 535 | 536 | read-pkg@^5.1.1: 537 | version "5.2.0" 538 | resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" 539 | integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== 540 | dependencies: 541 | "@types/normalize-package-data" "^2.4.0" 542 | normalize-package-data "^2.5.0" 543 | parse-json "^5.0.0" 544 | type-fest "^0.6.0" 545 | 546 | resolve-from@^3.0.0: 547 | version "3.0.0" 548 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" 549 | integrity sha1-six699nWiBvItuZTM17rywoYh0g= 550 | 551 | resolve@^1.10.0: 552 | version "1.15.1" 553 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" 554 | integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== 555 | dependencies: 556 | path-parse "^1.0.6" 557 | 558 | run-node@^1.0.0: 559 | version "1.0.0" 560 | resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" 561 | integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== 562 | 563 | semver-compare@^1.0.0: 564 | version "1.0.0" 565 | resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" 566 | integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= 567 | 568 | "semver@2 || 3 || 4 || 5", semver@^5.5.0: 569 | version "5.7.1" 570 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 571 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 572 | 573 | shebang-command@^1.2.0: 574 | version "1.2.0" 575 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 576 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= 577 | dependencies: 578 | shebang-regex "^1.0.0" 579 | 580 | shebang-regex@^1.0.0: 581 | version "1.0.0" 582 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 583 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 584 | 585 | signal-exit@^3.0.0: 586 | version "3.0.3" 587 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 588 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== 589 | 590 | slash@^3.0.0: 591 | version "3.0.0" 592 | resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" 593 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 594 | 595 | spdx-correct@^3.0.0: 596 | version "3.1.0" 597 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" 598 | integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== 599 | dependencies: 600 | spdx-expression-parse "^3.0.0" 601 | spdx-license-ids "^3.0.0" 602 | 603 | spdx-exceptions@^2.1.0: 604 | version "2.2.0" 605 | resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" 606 | integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== 607 | 608 | spdx-expression-parse@^3.0.0: 609 | version "3.0.0" 610 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 611 | integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== 612 | dependencies: 613 | spdx-exceptions "^2.1.0" 614 | spdx-license-ids "^3.0.0" 615 | 616 | spdx-license-ids@^3.0.0: 617 | version "3.0.5" 618 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" 619 | integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== 620 | 621 | sprintf-js@~1.0.2: 622 | version "1.0.3" 623 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 624 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 625 | 626 | strip-eof@^1.0.0: 627 | version "1.0.0" 628 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 629 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 630 | 631 | supports-color@^5.3.0: 632 | version "5.5.0" 633 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 634 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 635 | dependencies: 636 | has-flag "^3.0.0" 637 | 638 | type-fest@^0.6.0: 639 | version "0.6.0" 640 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" 641 | integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== 642 | 643 | validate-npm-package-license@^3.0.1: 644 | version "3.0.4" 645 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" 646 | integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== 647 | dependencies: 648 | spdx-correct "^3.0.0" 649 | spdx-expression-parse "^3.0.0" 650 | 651 | which@^1.2.9: 652 | version "1.3.1" 653 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 654 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 655 | dependencies: 656 | isexe "^2.0.0" 657 | 658 | wrappy@1: 659 | version "1.0.2" 660 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 661 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 662 | 663 | yallist@^2.1.2: 664 | version "2.1.2" 665 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 666 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 667 | --------------------------------------------------------------------------------