Packages
nerves
1.8.0
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.0
1.11.3
1.11.2
1.11.1
1.11.0
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.3
1.9.2
retired
1.9.1
1.9.0
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
retired
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
retired
1.7.1
retired
1.7.0
retired
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.11.0
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-rc.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
Craft and deploy bulletproof embedded software
Current section
Files
Jump to
Current section
Files
lib/nerves/utils/http_client.ex
defmodule Nerves.Utils.HTTPClient do
@moduledoc false
use GenServer
@progress_steps 50
# See https://www.erlang.org/doc/man/httpc.html#request-5
@type http_opts ::
{:timeout, timeout()}
| {:connect_timeout, timeout()}
| {:ssl, [:ssl.tls_option()]}
| {:essl, [:ssl.tls_option()]}
| {:autoredirect, boolean()}
| {:proxy_auth, {charlist(), charlist()}}
| {:relaxed, boolean()}
@type opts :: [
progress?: boolean(),
headers: [{String.t() | charlist(), String.t() | charlist()}],
http_opts: http_opts()
]
@spec start_link() :: GenServer.on_start()
def start_link() do
{:ok, _} = Application.ensure_all_started(:nerves)
_ = start_httpc()
GenServer.start_link(__MODULE__, [])
end
@spec stop(GenServer.server()) :: :ok
def stop(pid) do
GenServer.stop(pid)
end
@spec get(GenServer.server(), URI.t() | String.t(), opts()) ::
{:ok, String.t()} | {:error, String.t() | :too_many_redirects | atom()}
def get(_, _, _ \\ [])
def get(_pid, %URI{host: nil, path: path}, _opts) do
path
|> Path.expand()
|> File.read()
end
def get(pid, %URI{} = uri, opts) do
url = URI.to_string(uri)
get(pid, url, opts)
end
def get(pid, url, opts), do: GenServer.call(pid, {:get, url, opts}, :infinity)
@impl GenServer
def init([]) do
{:ok,
%{
url: nil,
content_length: 0,
buffer: "",
buffer_size: 0,
filename: "",
caller: nil,
number_of_redirects: 0,
progress?: true,
get_opts: []
}}
end
@impl GenServer
def handle_call({:get, _url, _opts}, _from, %{number_of_redirects: n} = s) when n > 5 do
GenServer.reply(s.caller, {:error, :too_many_redirects})
{:noreply, %{s | url: nil, number_of_redirects: 0, caller: nil}}
end
def handle_call({:get, url, opts}, from, s) do
progress? = Keyword.get(opts, :progress?, true)
user_headers = Keyword.get(opts, :headers, []) |> Enum.map(&tuple_to_charlist/1)
headers = [
{'User-Agent', 'Nerves/#{Nerves.version()}'},
{'Content-Type', 'application/octet-stream'} | user_headers
]
http_opts =
[
timeout: :infinity,
autoredirect: false,
ssl: [
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 3,
customize_hostname_check: [
{:match_fun, :public_key.pkix_verify_hostname_match_fun(:https)}
]
]
]
|> Keyword.merge(Nerves.Utils.Proxy.config(url))
|> Keyword.merge(Keyword.get(opts, :http_opts, []))
:httpc.request(
:get,
{String.to_charlist(url), headers},
http_opts,
[stream: :self, receiver: self(), sync: false],
:nerves
)
{:noreply, %{s | url: url, caller: from, get_opts: opts, progress?: progress?}}
end
@impl GenServer
def handle_info({:http, {_ref, {:error, {:failed_connect, _}} = err}}, s) do
GenServer.reply(s.caller, err)
end
def handle_info({:http, {_, :stream_start, headers}}, s) do
content_length =
case Enum.find(headers, fn {key, _} -> key == 'content-length' end) do
nil ->
0
{_, content_length} ->
{content_length, _} =
content_length
|> to_string()
|> Integer.parse()
content_length
end
filename =
case Enum.find(headers, fn {key, _} -> key == 'content-disposition' end) do
nil ->
Path.basename(s.url)
{_, filename} ->
filename
|> to_string
|> String.split(";")
|> List.last()
|> String.trim()
|> String.trim("filename=")
end
{:noreply, %{s | content_length: content_length, filename: filename}}
end
def handle_info({:http, {_, :stream, data}}, s) do
size = byte_size(data) + s.buffer_size
buffer = s.buffer <> data
if progress?(s) do
put_progress(size, s.content_length)
end
{:noreply, %{s | buffer_size: size, buffer: buffer}}
end
def handle_info({:http, {_, :stream_end, _headers}}, s) do
if progress?(s) do
IO.write(:stderr, "\n")
end
GenServer.reply(s.caller, {:ok, s.buffer})
{:noreply, %{s | filename: "", content_length: 0, buffer: "", buffer_size: 0, url: nil}}
end
def handle_info({:http, {_ref, {{_, status_code, reason}, headers, _body}}}, s)
when div(status_code, 100) == 3 do
case Enum.find(headers, fn {key, _} -> key == 'location' end) do
{'location', next_location} ->
next_get_opts = Keyword.drop(s.get_opts, [:headers])
handle_call({:get, List.to_string(next_location), next_get_opts}, s.caller, %{
s
| buffer: "",
buffer_size: 0,
number_of_redirects: s.number_of_redirects + 1
})
_ ->
GenServer.reply(s.caller, {:error, format_error(status_code, reason)})
end
end
def handle_info({:http, {_ref, {{_, status_code, reason}, _headers, _body}}}, s) do
GenServer.reply(s.caller, {:error, format_error(status_code, reason)})
{:noreply, s}
end
defp put_progress(size, max) do
fraction = size / max
completed = trunc(fraction * @progress_steps)
percent = trunc(fraction * 100)
unfilled = @progress_steps - completed
IO.write(
:stderr,
"\r|#{String.duplicate("=", completed)}#{String.duplicate(" ", unfilled)}| #{percent}% (#{bytes_to_mb(size)} / #{bytes_to_mb(max)}) MB"
)
end
defp format_error(status_code, reason) do
"Status #{to_string(status_code)} #{to_string(reason)}"
end
defp start_httpc() do
:inets.start(:httpc, profile: :nerves)
opts = [
max_sessions: 8,
max_keep_alive_length: 4,
max_pipeline_length: 4,
keep_alive_timeout: 120_000,
pipeline_timeout: 60_000
]
:httpc.set_options(opts, :nerves)
end
defp bytes_to_mb(bytes) do
trunc(bytes / 1024 / 1024)
end
defp progress?(%{progress?: progress?}) do
System.get_env("NERVES_LOG_DISABLE_PROGRESS_BAR") == nil and progress?
end
defp tuple_to_charlist({k, v}) do
{to_charlist(k), to_charlist(v)}
end
end