Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/puppetlabs/trapperkeeper/services/webserver/jetty_core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,20 @@
(proxy [jakarta.servlet.http.HttpServlet] []
(service [^jakarta.servlet.http.HttpServletRequest request
^jakarta.servlet.http.HttpServletResponse response]
;; Force chunked transfer encoding. In ring-core 1.15.x, strings
;; are written directly to the underlying stream and teh stream is
;; closed without flushing, which lets Jetty use Content-Length framing.
;; With TLS 1.3, a NewSessionTicket may be sent immediately after the
;; response data. Ruby's net-http sees the pending TLS record and blocks
;; on a keep-alive check until timeout. Chunked encoding avoids this
;; because the ticket is consumed during body reading rather than
;; lingering in the TCP buffer.
;;
;; See https://github.com/OpenVoxProject/openvox-server/issues/197 for
;; more details. A fix in net-http is https://github.com/ruby/ruby/pull/6423
;; but even if that gets merged into the version of Ruby we use in OpenVox,
;; older agents will be vulnerable to this issue.
(.setHeader response "Transfer-Encoding" "chunked")
(let [request-map (-> (servlet/build-request-map request)
(assoc :response response))
response-map (handler request-map)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,48 @@
access-log-config"
(validate-gzip-encoding-when-gzip-requested body port)))))))

(deftest response-uses-chunked-encoding
(with-test-logging
(testing "responses use chunked transfer encoding instead of content-length"
(let [body (apply str (repeat 1000 "f"))
app (fn [req]
(-> body
(rr/response)
(rr/status 200)
(rr/content-type "text/plain")
(rr/charset "UTF-8")))]
(with-test-webserver app port
(let [resp (http-sync/get (format "http://localhost:%d/" port)
{:decompress-body false
:as :text})]
(is (= 200 (:status resp)))
(is (= "chunked" (get-in resp [:headers "transfer-encoding"]))
"response should use chunked encoding to avoid TLS 1.3 NewSessionTicket hang")
(is (nil? (get-in resp [:headers "content-length"]))
"response should not have content-length when using chunked encoding")))

(with-test-webserver-and-config app port {:gzip-enable true}
(let [resp (http-sync/get (format "http://localhost:%d/" port))]
(is (= 200 (:status resp)))
(is (= body (slurp (:body resp))))
(is (= "gzip" (get-in resp [:orig-content-encoding]))
"gzip should still work with chunked encoding")))

(testing "small responses also use chunked encoding"
(let [small-app (fn [req]
(-> "hello"
(rr/response)
(rr/status 200)
(rr/content-type "text/plain")))]
(with-test-webserver small-app port
(let [resp (http-sync/get (format "http://localhost:%d/" port)
{:decompress-body false
:as :text})]
(is (= 200 (:status resp)))
(is (= "chunked" (get-in resp [:headers "transfer-encoding"])))
(is (nil? (get-in resp [:headers "content-length"])))
(is (= "hello" (:body resp)))))))))))

(deftest jmx
(with-test-logging
(testing "by default Jetty JMX support is enabled"
Expand Down
Loading