Skip to content
Open
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
60 changes: 56 additions & 4 deletions src/gleam/httpc.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ pub type ConnectError {
TlsAlert(code: String, detail: String)
}

/// Use with `tls_versions` to restrict which TLS versions the client will use.
/// This can be needed when interfacing with old or buggy TLS implementations.
pub type TlsVersion {
Tls12
Tls13
}

@external(erlang, "gleam_httpc_ffi", "default_user_agent")
fn default_user_agent() -> #(Charlist, Charlist)

Expand Down Expand Up @@ -53,12 +60,21 @@ type Inet6fb4 {

type ErlSslOption {
Verify(ErlVerifyOption)
Versions(List(ErlTlsVersion))
}

type ErlVerifyOption {
VerifyNone
}

type ErlTlsVersion

@external(erlang, "gleam_httpc_ffi", "tlsv12")
fn erl_tlsv12() -> ErlTlsVersion

@external(erlang, "gleam_httpc_ffi", "tlsv13")
fn erl_tlsv13() -> ErlTlsVersion

@external(erlang, "httpc", "request")
fn erl_request(
a: Method,
Expand Down Expand Up @@ -86,6 +102,13 @@ fn string_header(header: #(Charlist, Charlist)) -> #(String, String) {
#(charlist.to_string(k), charlist.to_string(v))
}

fn tls_version(version: TlsVersion) -> ErlTlsVersion {
case version {
Tls12 -> erl_tlsv12()
Tls13 -> erl_tlsv13()
}
}

// TODO: refine error type
/// Send a HTTP request of binary data using the default configuration.
///
Expand Down Expand Up @@ -115,10 +138,21 @@ pub fn dispatch_bits(
Autoredirect(config.follow_redirects),
Timeout(config.timeout),
]
let erl_http_options = case config.verify_tls {
True -> erl_http_options
False -> [Ssl([Verify(VerifyNone)]), ..erl_http_options]

// Build SSL options based on verify_tls and tls_versions settings
let ssl_opts = case config.verify_tls {
True -> []
False -> [Verify(VerifyNone)]
}
let ssl_opts = case config.tls_versions {
[] -> ssl_opts
versions -> [Versions(list.map(versions, tls_version)), ..ssl_opts]
}
let erl_http_options = case ssl_opts {
[] -> erl_http_options
_ -> [Ssl(ssl_opts), ..erl_http_options]
}

let erl_options = [BodyFormat(Binary), SocketOpts([Ipfamily(Inet6fb4)])]

use response <- result.try(
Expand Down Expand Up @@ -166,6 +200,9 @@ pub opaque type Configuration {
/// Timeout for the request in milliseconds.
///
timeout: Int,
/// Which TLS versions to allow. If empty, uses system defaults.
///
tls_versions: List(TlsVersion),
)
}

Expand All @@ -177,9 +214,15 @@ pub opaque type Configuration {
/// - Redirects are not followed.
/// - The timeout for the response to be received is 30 seconds from when the
/// request is sent.
/// - All TLS versions supported by the system are allowed.
///
pub fn configure() -> Configuration {
Builder(verify_tls: True, follow_redirects: False, timeout: 30_000)
Builder(
verify_tls: True,
follow_redirects: False,
timeout: 30_000,
tls_versions: [],
)
}

/// Set whether to verify the TLS certificate of the server.
Expand Down Expand Up @@ -209,6 +252,15 @@ pub fn timeout(config: Configuration, timeout: Int) -> Configuration {
Builder(..config, timeout:)
}

/// Set which TLS versions to allow for HTTPS connections.
///
pub fn tls_versions(
config: Configuration,
versions: List(TlsVersion),
) -> Configuration {
Builder(..config, tls_versions: versions)
}

/// Send a HTTP request of unicode data.
///
pub fn dispatch(
Expand Down
6 changes: 5 additions & 1 deletion src/gleam_httpc_ffi.erl
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
-module(gleam_httpc_ffi).
-export([default_user_agent/0, normalise_error/1]).
-export([default_user_agent/0, normalise_error/1, tlsv12/0, tlsv13/0]).

%% TLS version atoms for SSL options
tlsv12() -> 'tlsv1.2'.
tlsv13() -> 'tlsv1.3'.

normalise_error(Error = {failed_connect, Opts}) ->
Ipv6 = case lists:keyfind(inet6, 1, Opts) of
Expand Down
27 changes: 27 additions & 0 deletions test/gleam_httpc_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,30 @@ pub fn timeout_error_test() {
|> httpc.dispatch(req)
== Error(httpc.ResponseTimeout)
}

pub fn tls_versions_tls12_enforced_test() {
// Verify TLS 1.2 is actually used when configured
// check.ja3.zone returns JSON with "protocol" field showing TLS version
let assert Ok(req) = request.to("https://check.ja3.zone/")

let assert Ok(resp) =
httpc.configure()
|> httpc.verify_tls(False)
|> httpc.tls_versions([httpc.Tls12])
|> httpc.dispatch(req)

assert string.contains(resp.body, "\"protocol\":\"TLSv1.2\"")
}

pub fn tls_versions_tls13_enforced_test() {
// Verify TLS 1.3 is actually used when configured
let assert Ok(req) = request.to("https://check.ja3.zone/")

let assert Ok(resp) =
httpc.configure()
|> httpc.verify_tls(False)
|> httpc.tls_versions([httpc.Tls13])
|> httpc.dispatch(req)

assert string.contains(resp.body, "\"protocol\":\"TLSv1.3\"")
}