Skip to content

Commit

Permalink
move fetch_body!/1 to its own module
Browse files Browse the repository at this point in the history
  • Loading branch information
sheerlox committed Nov 24, 2023
1 parent 1ca0153 commit ca9dd4d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 111 deletions.
10 changes: 5 additions & 5 deletions lib/mix/tasks/nodelix.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ defmodule Mix.Tasks.Nodelix.Install do
def run(args) do
valid_options = [runtime_config: :boolean, if_missing: :boolean, assets: :boolean]

{opts, base_url} =
{opts, archive_url} =
case OptionParser.parse_head!(args, strict: valid_options) do
{opts, []} ->
{opts, NodeDownloader.default_base_url()}
{opts, NodeDownloader.default_archive_url()}

{opts, [base_url]} ->
{opts, base_url}
{opts, [archive_url]} ->
{opts, archive_url}

{_, _} ->
Mix.raise("""
Expand All @@ -60,7 +60,7 @@ defmodule Mix.Tasks.Nodelix.Install do
end

Mix.Task.run("loadpaths")
NodeDownloader.install(base_url)
NodeDownloader.install(archive_url)
end
end

Expand Down
96 changes: 96 additions & 0 deletions lib/nodelix/http_utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule Nodelix.HttpUtils do
@moduledoc false

require Logger

@spec fetch_body!(String.t()) :: any()
def fetch_body!(url) do
scheme = URI.parse(url).scheme
url = String.to_charlist(url)

{:ok, _} = Application.ensure_all_started(:inets)
{:ok, _} = Application.ensure_all_started(:ssl)

if proxy = proxy_for_scheme(scheme) do
%{host: host, port: port} = URI.parse(proxy)
Logger.debug("Using #{String.upcase(scheme)}_PROXY: #{proxy}")
set_option = if "https" == scheme, do: :https_proxy, else: :proxy
:httpc.set_options([{set_option, {{String.to_charlist(host), port}, []}}])
end

# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
cacertfile = cacertfile() |> String.to_charlist()

http_options =
[
ssl: [
verify: :verify_peer,
cacertfile: cacertfile,
depth: 2,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
],
versions: protocol_versions()
]
]
|> maybe_add_proxy_auth(scheme)

options = [body_format: :binary]

case :httpc.request(:get, {url, []}, http_options, options) do
{:ok, {{_, 200, _}, _headers, body}} ->
body

other ->
raise """
Couldn't fetch #{url}: #{inspect(other)}
This typically means we cannot reach the source or you are behind a proxy.
You can try again later and, if that does not work, you might:
1. If behind a proxy, ensure your proxy is configured and that
your certificates are set via the cacerts_path configuration
2. Manually download the file from the URL above and
place it inside "_build/"
"""
end
end

defp proxy_for_scheme("http") do
System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
end

defp proxy_for_scheme("https") do
System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
end

defp maybe_add_proxy_auth(http_options, scheme) do
case proxy_auth(scheme) do
nil -> http_options
auth -> [{:proxy_auth, auth} | http_options]
end
end

defp proxy_auth(scheme) do
with proxy when is_binary(proxy) <- proxy_for_scheme(scheme),
%{userinfo: userinfo} when is_binary(userinfo) <- URI.parse(proxy),
[username, password] <- String.split(userinfo, ":") do
{String.to_charlist(username), String.to_charlist(password)}
else
_ -> nil
end
end

defp cacertfile() do
Application.get_env(:nodelix, :cacerts_path) || CAStore.file_path()
end

defp protocol_versions do
if otp_version() < 25, do: [:"tlsv1.2"], else: [:"tlsv1.2", :"tlsv1.3"]
end

defp otp_version do
:erlang.system_info(:otp_release) |> List.to_integer()
end
end
124 changes: 18 additions & 106 deletions lib/nodelix/node_downloader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Nodelix.NodeDownloader do

require Logger

alias Nodelix.HttpUtils

@moduledoc false

@doc """
Expand Down Expand Up @@ -51,12 +53,7 @@ defmodule Nodelix.NodeDownloader do
end
end

@doc """
Returns the path to the archive.
The archive may not be available if it was not yet installed.
"""
def archive_path do
defp archive_path do
name = "nodejs-#{target()}"

if Code.ensure_loaded?(Mix.Project) do
Expand All @@ -67,19 +64,17 @@ defmodule Nodelix.NodeDownloader do
end

@doc """
The default URL to fetch the Node.js archive from.
Installs Node.js with `configured_version/0`.
"""
def default_base_url do
"https://nodejs.org/dist/v$version/node-v$version-$target"
def install(archive_url \\ default_archive_url()) do
fetch_archive(archive_url)
end

@doc """
Installs Node.js with `configured_version/0`.
"""
def install(base_url \\ default_base_url()) do
url = get_url(base_url)
defp fetch_archive(archive_url) do
url = get_url(archive_url)
archive_path = archive_path()
binary = fetch_body!(url)
Logger.debug("Downloading Node.js from #{url}")
binary = HttpUtils.fetch_body!(url)
File.mkdir_p!(Path.dirname(archive_path))

# MacOS doesn't recompute code signing information if a binary
Expand All @@ -91,6 +86,14 @@ defmodule Nodelix.NodeDownloader do
File.write!(archive_path, binary, [:binary])
end

@spec default_archive_url() :: String.t()
@doc """
The default URL to fetch the Node.js archive from.
"""
def default_archive_url do
"https://nodejs.org/dist/v$version/node-v$version-$target"
end

# Available targets:
# aix-ppc64.tar.gz
# darwin-arm64.tar.gz
Expand Down Expand Up @@ -123,97 +126,6 @@ defmodule Nodelix.NodeDownloader do
end
end

defp fetch_body!(url) do
scheme = URI.parse(url).scheme
url = String.to_charlist(url)
Logger.debug("Downloading Node.js from #{url}")

{:ok, _} = Application.ensure_all_started(:inets)
{:ok, _} = Application.ensure_all_started(:ssl)

if proxy = proxy_for_scheme(scheme) do
%{host: host, port: port} = URI.parse(proxy)
Logger.debug("Using #{String.upcase(scheme)}_PROXY: #{proxy}")
set_option = if "https" == scheme, do: :https_proxy, else: :proxy
:httpc.set_options([{set_option, {{String.to_charlist(host), port}, []}}])
end

# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
cacertfile = cacertfile() |> String.to_charlist()

http_options =
[
ssl: [
verify: :verify_peer,
cacertfile: cacertfile,
depth: 2,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
],
versions: protocol_versions()
]
]
|> maybe_add_proxy_auth(scheme)

options = [body_format: :binary]

case :httpc.request(:get, {url, []}, http_options, options) do
{:ok, {{_, 200, _}, _headers, body}} ->
body

other ->
raise """
Couldn't fetch #{url}: #{inspect(other)}
This typically means we cannot reach the source or you are behind a proxy.
You can try again later and, if that does not work, you might:
1. If behind a proxy, ensure your proxy is configured and that
your certificates are set via the cacerts_path configuration
2. Manually download the executable from the URL above and
place it inside "_build/node-#{target()}"
"""
end
end

defp proxy_for_scheme("http") do
System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
end

defp proxy_for_scheme("https") do
System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
end

defp maybe_add_proxy_auth(http_options, scheme) do
case proxy_auth(scheme) do
nil -> http_options
auth -> [{:proxy_auth, auth} | http_options]
end
end

defp proxy_auth(scheme) do
with proxy when is_binary(proxy) <- proxy_for_scheme(scheme),
%{userinfo: userinfo} when is_binary(userinfo) <- URI.parse(proxy),
[username, password] <- String.split(userinfo, ":") do
{String.to_charlist(username), String.to_charlist(password)}
else
_ -> nil
end
end

defp cacertfile() do
Application.get_env(:nodelix, :cacerts_path) || CAStore.file_path()
end

defp protocol_versions do
if otp_version() < 25, do: [:"tlsv1.2"], else: [:"tlsv1.2", :"tlsv1.3"]
end

defp otp_version do
:erlang.system_info(:otp_release) |> List.to_integer()
end

defp get_url(base_url) do
base_url
|> String.replace("$version", Nodelix.configured_version())
Expand Down

0 comments on commit ca9dd4d

Please sign in to comment.