Skip to content

Commit ba2842c

Browse files
Use canonical authority when computing the Host header
Computes and use the normalized URI authority when computing the Host header, and building the request URI. This makes the implementation compliant to the URI and HTTP RFCs, and fixes the signature issue when making requests to non-default ports (#263).
1 parent 92e3963 commit ba2842c

File tree

5 files changed

+84
-24
lines changed

5 files changed

+84
-24
lines changed

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# aws-api
22

3+
## DEV
4+
5+
* Fix invalid signature for nonstandard host port [#263](https://github.com/cognitect-labs/aws-api/issues/263)
6+
37
## 0.8.735 / 2025-02-26
48

59
* This is the official release of the previous `0.8.730-beta01` release, see those release notes

src/cognitect/aws/client/impl.clj

+22-7
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,28 @@
3333
{:cognitect.anomalies/category :cognitect.anomalies/fault
3434
::throwable t})))
3535

36-
(defn ^:private with-endpoint [req {:keys [protocol hostname port path]}]
37-
(cond-> (-> req
38-
(assoc-in [:headers "host"] hostname)
39-
(assoc :server-name hostname))
40-
protocol (assoc :scheme protocol)
41-
port (assoc :server-port port)
42-
path (assoc :uri path)))
36+
(defn ^:private with-endpoint
37+
"Updates the request map `req` with data returned by the endpoint provider.
38+
39+
The request map is created with reasonable defaults by `cognitect.aws.protocols/build-http-request`,
40+
and the `:scheme`, `:server-port` and `:uri` may be overridden here with
41+
data from the endpoint provider.
42+
43+
`:server-name` is always set based on the endpoint provider data.
44+
45+
Also computes and sets the `Host` header with the appropriate authority."
46+
[req {:keys [protocol hostname port path]}]
47+
(let [scheme (or protocol (:scheme req) (throw (IllegalArgumentException. "missing protocol/scheme")))
48+
server-name (or hostname (throw (IllegalArgumentException. "missing hostname")))
49+
server-port (or port (:server-port req) (throw (IllegalArgumentException. "missing port/server-port")))
50+
uri (or path (:uri req) (throw (IllegalArgumentException. "missing path/uri")))
51+
authority (http/uri-authority scheme server-name server-port)]
52+
(-> req
53+
(assoc-in [:headers "host"] authority)
54+
(assoc :scheme scheme
55+
:server-name server-name
56+
:server-port server-port
57+
:uri uri))))
4358

4459
(defn ^:private put-throwable [result-ch t response-meta op-map]
4560
(a/put! result-ch (with-meta

src/cognitect/aws/http.clj

+29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"Impl, don't call directly."
66
(:require [clojure.edn :as edn]
77
[clojure.core.async :as a]
8+
[clojure.string :as str]
89
[cognitect.aws.dynaload :as dynaload]))
910

1011
(set! *warn-on-reflection* true)
@@ -91,3 +92,31 @@
9192
(throw (ex-info "not an http client" {:provided http-client-or-sym
9293
:resolved c})))
9394
c))
95+
96+
(defn uri-authority
97+
"Returns the normalized URI authority (RFC 3986 section 3.2) for the given URI components,
98+
good for building the full request URI, signing the request, and computing the Host header,
99+
according to RFC 9112 section 3.2:
100+
101+
A client MUST send a Host header field in all HTTP/1.1 request messages. If the target URI
102+
includes an authority component, then a client MUST send a field value for Host that is identical
103+
to that authority component, excluding any userinfo subcomponent and its \"@\" delimiter.
104+
105+
`uri-scheme` and `uri-host` are required. `uri-port` may be nil in case the default port
106+
for the scheme is used.
107+
108+
Normalization follows RFC 9110 section 4.2.3 (default port for the scheme is omitted;
109+
scheme and host are normalized to lowercase). The userinfo component of the URI is
110+
always omitted as per RFC 9110 section 4.2.4.
111+
112+
authority = host [ \":\" port ]"
113+
[uri-scheme uri-host uri-port]
114+
(let [scheme (str/lower-case (name uri-scheme))
115+
host (str/lower-case uri-host)
116+
is-default-port (case scheme
117+
"http" (= uri-port 80)
118+
"https" (= uri-port 443)
119+
false)
120+
should-include-port (and (some? uri-port)
121+
(not is-default-port))]
122+
(str host (when should-include-port (str ":" uri-port)))))

src/cognitect/aws/http/java.clj

+4-17
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
[clojure.core.async :as async]
44
[clojure.string :as string])
55
(:import [java.io IOException]
6-
[java.lang.invoke MethodHandle MethodHandles MethodType]
76
[java.net URI]
87
[java.net.http
9-
HttpClient HttpClient$Builder HttpClient$Redirect HttpRequest HttpRequest$Builder
8+
HttpClient HttpClient$Redirect HttpRequest HttpRequest$Builder
109
HttpRequest$BodyPublishers HttpResponse HttpResponse$BodyHandlers]
1110
[java.nio ByteBuffer]
1211
[java.time Duration]
13-
[java.util.concurrent ExecutorService Executors]
1412
[java.util.function Function]))
1513

1614
(set! *warn-on-reflection* true)
@@ -53,22 +51,11 @@
5351
"Builds and returns a java.net.URI from the request map."
5452
[{:keys [scheme server-name server-port uri query-string]
5553
:or {scheme "https"}}]
56-
(let [;; NOTE: we should only include the port if it was explicitly specified to a
57-
;; non-default value. This check is needed to ensure the HttpClient instance
58-
;; generate consistent headers with the `host` header used when signing
59-
;; the request.
60-
;; The relevant restricted headers managed by HttpClient here are the `Host`
61-
;; header for HTTP/1.1, and the `:authority` pseudo-header for HTTP/2.
62-
is-default-port (if (= (name scheme) "http")
63-
(= server-port 80)
64-
(= server-port 443))
65-
should-include-port (and (some? server-port)
66-
(not is-default-port))
67-
;; NOTE: we can't use URI's constructor passing individual components, because sometimes
54+
(let [;; NOTE: we can't use URI's constructor passing individual components, because sometimes
6855
;; the `:uri` part includes query params
6956
;; (e.g. on DeleteObjects op, :uri is `/bucket-name?delete`)
70-
full-uri (str (name scheme) "://" server-name
71-
(when should-include-port (str ":" server-port))
57+
full-uri (str (name scheme) "://"
58+
(aws-http/uri-authority scheme server-name server-port)
7259
uri
7360
(when query-string (str "?" query-string)))]
7461
(URI/create full-uri)))

test/src/cognitect/aws/http_test.clj

+25
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,28 @@
1818
(is (= true
1919
(io/delete-file "test/resources/cognitect_aws_http.edn" :failed))
2020
"Accidentally failed to delete file")))
21+
22+
(deftest uri-authority-test
23+
(testing "http scheme, default port"
24+
(is (= "a-server.example.com" (aws-http/uri-authority :http "a-server.example.com" 80)))
25+
(is (= "a-server.example.com" (aws-http/uri-authority :http "A-sErvEr.example.com" nil)))
26+
(is (= "a-server.example.com" (aws-http/uri-authority "HTTP" "a-server.example.com" 80)))
27+
(is (= "a-server.example.com" (aws-http/uri-authority "http" "a-server.example.com" nil))))
28+
29+
(testing "https scheme, default port"
30+
(is (= "a-server.example.com" (aws-http/uri-authority :https "a-server.example.com" 443)))
31+
(is (= "a-server.example.com" (aws-http/uri-authority :https "A-sErvEr.example.com" nil)))
32+
(is (= "a-server.example.com" (aws-http/uri-authority "https" "a-server.example.com" 443)))
33+
(is (= "a-server.example.com" (aws-http/uri-authority "HTTPs" "a-server.example.com" nil))))
34+
35+
(testing "http scheme, custom port"
36+
(is (= "a-server.example.com:8080" (aws-http/uri-authority :http "a-server.example.com" 8080)))
37+
(is (= "a-server.example.com:4080" (aws-http/uri-authority :http "a-server.example.com" 4080)))
38+
(is (= "a-server.example.com:443" (aws-http/uri-authority :http "a-server.example.com" 443)))
39+
(is (= "a-server.example.com:1" (aws-http/uri-authority :http "a-server.example.com" 1))))
40+
41+
(testing "https scheme, custom port"
42+
(is (= "a-server.example.com:8080" (aws-http/uri-authority :https "a-server.example.com" 8080)))
43+
(is (= "a-server.example.com:4443" (aws-http/uri-authority :https "a-server.example.com" 4443)))
44+
(is (= "a-server.example.com:80" (aws-http/uri-authority :https "a-server.example.com" 80)))
45+
(is (= "a-server.example.com:1" (aws-http/uri-authority :https "a-server.example.com" 1)))))

0 commit comments

Comments
 (0)