diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d729c7..28ff510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Babashka [http-client](https://github.com/babashka/http-client): HTTP client for Clojure and babashka built on java.net.http +## Unreleased + +- [#80](https://github.com/babashka/http-client/issues/80): add idiomatic proxy configuration functionality + ## 0.4.23 (2025-06-06) - [#75](https://github.com/babashka/http-client/issues/75): override existing content type header in multipart request diff --git a/src/babashka/http_client.clj b/src/babashka/http_client.clj index 36dbb84..f6ae9be 100644 --- a/src/babashka/http_client.clj +++ b/src/babashka/http_client.clj @@ -7,12 +7,17 @@ i/default-client-opts) (defn ->ProxySelector - "Constructs a `java.net.ProxySelector`. + "Constructs a `java.net.ProxySelector`. Can either take a map of `:host` and `:port` and `type` options or a proxy + creation function that takes a java.net.URI and returns a map of these parameters for creating a proxy dynamically. + Options: * `:host` - string - * `:port` - long" - [opts] - (i/->ProxySelector opts)) + * `:port` - long + * `:type` - `:direct` (don't use a proxy, host and port will be ignored), `:socks` (socks proxy) or `:http` (HTTP proxy, default value)." + [opts-or-fn] + (if (instance? java.net.ProxySelector opts-or-fn) + opts-or-fn + (i/->ProxySelector opts-or-fn))) (defn ->SSLContext "Constructs a `javax.net.ssl.SSLContext`. diff --git a/src/babashka/http_client/internal.clj b/src/babashka/http_client/internal.clj index dea6807..b4cf7f1 100644 --- a/src/babashka/http_client/internal.clj +++ b/src/babashka/http_client/internal.clj @@ -36,6 +36,12 @@ :never HttpClient$Redirect/NEVER :normal HttpClient$Redirect/NORMAL)) +(defn- ->proxy-type [type] + (case type + :direct java.net.Proxy$Type/DIRECT + :socks java.net.Proxy$Type/SOCKS + :http java.net.Proxy$Type/HTTP)) + (defn- version-keyword->version-enum [version] (case version :http1.1 HttpClient$Version/HTTP_1_1 @@ -97,13 +103,41 @@ trust-managers (SecureRandom.)))))) -(defn ->ProxySelector +(defn ->Proxy [opts] - (if (instance? java.net.ProxySelector opts) + (if (instance? java.net.Proxy opts) opts - (let [{:keys [host port]} opts] - (cond (and host port) - (java.net.ProxySelector/of (java.net.InetSocketAddress. ^String host ^long port)))))) + (let [{:keys [host port type] :or {type :http}} opts] + (cond + (= type :direct) + java.net.Proxy/NO_PROXY + (and host port type) + (java.net.Proxy. (->proxy-type type) (java.net.InetSocketAddress. ^String host ^long port)) + :else + (throw (ex-info "Don't know how to create proxy from options." {:opts opts})))))) + +(defn ->ProxySelector + [opts-or-fn] + (cond + (instance? java.net.ProxySelector opts-or-fn) + opts-or-fn + (fn? opts-or-fn) + ;; Create a dynamic proxy selector. + (proxy [java.net.ProxySelector] [] + (connectFailed [_ _ _]) + (select [^URI uri] + ;; Only allow the proxy function to return a single proxy. + ;; Returning multiple is currently not supported. + [(if-let [proxy-values (opts-or-fn uri)] + (->Proxy proxy-values) + java.net.Proxy/NO_PROXY)])) + :else + ;; Return a static proxy configuration that always returns the same proxy. + (let [static-proxy (->Proxy opts-or-fn)] + (proxy [java.net.ProxySelector] [] + (connectFailed [_ _ _]) + (select [^URI _uri] + [static-proxy]))))) (defn ->Authenticator [v] diff --git a/test/babashka/http_client/internal/helpers_test.clj b/test/babashka/http_client/internal/helpers_test.clj index ef03e82..6562c5b 100644 --- a/test/babashka/http_client/internal/helpers_test.clj +++ b/test/babashka/http_client/internal/helpers_test.clj @@ -4,5 +4,5 @@ [clojure.test :as t])) (t/deftest ->uri-tests - (let [uri (h/->uri {:scheme "https" :host "example.com" :path "/foo"})] + (let [^java.net.URI uri (h/->uri {:scheme "https" :host "example.com" :path "/foo"})] (t/is (= (.getPort uri) -1)))) diff --git a/test/babashka/http_client_test.clj b/test/babashka/http_client_test.clj index 9a6a924..6e146c3 100644 --- a/test/babashka/http_client_test.clj +++ b/test/babashka/http_client_test.clj @@ -3,6 +3,7 @@ [babashka.fs :as fs] [babashka.http-client :as http] [babashka.http-client.interceptors :as i] + [babashka.http-client.internal :as internal] [babashka.http-client.internal.version :as iv] [borkdude.deflet :refer [deflet]] [cheshire.core :as json] @@ -473,7 +474,31 @@ (deftest proxy-selector (is (instance? java.net.ProxySelector (http/->ProxySelector {:host "https://clojure.org" - :port 1337})))) + :port 1337}))) + ;; Check passthrough behavior. + (is (instance? java.net.ProxySelector + (http/->ProxySelector + (http/->ProxySelector {:host "https://clojure.org" + :port 1337})))) + (let [^java.net.ProxySelector complex-proxy-selector (http/->ProxySelector (fn [^java.net.URI uri] + (when (= (.getScheme uri) "http") + {:host "http://www.example.org" + :port 128 + :type :http})))] + (is (instance? java.net.ProxySelector complex-proxy-selector)) + (let [proxies-for-http (.select complex-proxy-selector (java.net.URI. "http://www.example.org")) + proxies-for-https (.select complex-proxy-selector (java.net.URI. "https://www.example.org"))] + (is (= (count proxies-for-http) (count proxies-for-https) 1)) + (is (= (.type (get proxies-for-http 0)) java.net.Proxy$Type/HTTP)) + (is (= (.type (get proxies-for-https 0)) java.net.Proxy$Type/DIRECT)))) + (let [^java.net.ProxySelector static-proxy-selector (http/->ProxySelector + {:host "127.0.0.1" + :port 8081 + :type :socks})] + (let [selected-proxies (.select static-proxy-selector (java.net.URI. "http://www.example.org"))] + (is (= (count selected-proxies) 1)) + (is (= (.type (selected-proxies 0)) java.net.Proxy$Type/SOCKS)) + (is (= (.address (selected-proxies 0)) (java.net.InetSocketAddress. "127.0.0.1" 8081)))))) (deftest cookie-handler-test (testing "nil passthrough"