Page MenuHomePhorge

No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex
index d910af0..343be81 100644
--- a/lib/tesla/adapter/httpc.ex
+++ b/lib/tesla/adapter/httpc.ex
@@ -1,94 +1,97 @@
defmodule Tesla.Adapter.Httpc do
@moduledoc """
Adapter for [httpc](http://erlang.org/doc/man/httpc.html)
This is the default adapter.
**NOTE** Tesla overrides default autoredirect value with false to ensure
consistency between adapters
"""
@behaviour Tesla.Adapter
import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1]
alias Tesla.Multipart
@override_defaults autoredirect: false
@http_opts ~w(timeout connect_timeout ssl essl autoredirect proxy_auth version relaxed url_encode)a
def call(env, opts) do
opts = Tesla.Adapter.opts(@override_defaults, env, opts)
with {:ok, {status, headers, body}} <- request(env, opts) do
{:ok, format_response(env, status, headers, body)}
end
end
defp format_response(env, {_, status, _}, headers, body) do
%{env | status: status, headers: format_headers(headers), body: format_body(body)}
end
# from http://erlang.org/doc/man/httpc.html
# headers() = [header()]
# header() = {field(), value()}
# field() = string()
# value() = string()
defp format_headers(headers) do
for {key, value} <- headers do
{String.downcase(to_string(key)), to_string(value)}
end
end
# from http://erlang.org/doc/man/httpc.html
# string() = list of ASCII characters
# Body = string() | binary()
defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
defp format_body(data) when is_binary(data), do: data
defp request(env, opts) do
content_type = to_charlist(Tesla.get_header(env, "content-type") || "")
handle(
request(
env.method,
Tesla.build_url(env.url, env.query) |> to_charlist,
Enum.map(env.headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end),
content_type,
env.body,
Keyword.split(opts, @http_opts)
)
)
end
defp request(method, url, headers, _content_type, nil, {http_opts, opts}) do
:httpc.request(method, {url, headers}, http_opts, opts)
end
defp request(method, url, headers, _content_type, %Multipart{} = mp, opts) do
headers = headers ++ Multipart.headers(mp)
headers = for {key, value} <- headers, do: {to_charlist(key), to_charlist(value)}
- {content_type, headers} = case List.keytake(headers, 'content-type', 0) do
- nil -> {'text/plain', headers}
- {{_, ct}, headers} -> {ct, headers}
- end
+
+ {content_type, headers} =
+ case List.keytake(headers, 'content-type', 0) do
+ nil -> {'text/plain', headers}
+ {{_, ct}, headers} -> {ct, headers}
+ end
+
body = stream_to_fun(Multipart.body(mp))
request(method, url, headers, to_charlist(content_type), body, opts)
end
defp request(method, url, headers, content_type, %Stream{} = body, opts) do
fun = stream_to_fun(body)
request(method, url, headers, content_type, fun, opts)
end
defp request(method, url, headers, content_type, body, opts) when is_function(body) do
body = {:chunkify, &next_chunk/1, body}
request(method, url, headers, content_type, body, opts)
end
defp request(method, url, headers, content_type, body, {http_opts, opts}) do
:httpc.request(method, {url, headers, content_type, body}, http_opts, opts)
end
defp handle({:error, {:failed_connect, _}}), do: {:error, :econnrefused}
defp handle(response), do: response
end
diff --git a/lib/tesla/middleware/digest_auth.ex b/lib/tesla/middleware/digest_auth.ex
index 283630a..8dad848 100644
--- a/lib/tesla/middleware/digest_auth.ex
+++ b/lib/tesla/middleware/digest_auth.ex
@@ -1,131 +1,131 @@
defmodule Tesla.Middleware.DigestAuth do
@behaviour Tesla.Middleware
@moduledoc """
Digest access authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Digest_access_authentication)
**NOTE**: Currently the implementation is incomplete and works only for MD5 algorithm
and auth qop.
### Example
```
defmodule MyClient do
use Tesla
def client(username, password, opts \\ %{}) do
Tesla.build_client [
{Tesla.Middleware.DigestAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
- `:cnonce_fn` - custom function generating client nonce (defaults to `&Tesla.Middleware.DigestAuth.cnonce/0`)
- `:nc` - nonce counter (defaults to `"00000000"`)
"""
def call(env, next, opts) do
if env.opts && Keyword.get(env.opts, :digest_auth_handshake) do
Tesla.run(env, next)
else
opts = opts || %{}
with {:ok, headers} <- authorization_header(env, opts) do
env
|> Tesla.put_headers(headers)
|> Tesla.run(next)
end
end
end
defp authorization_header(env, opts) do
with {:ok, vars} <- authorization_vars(env, opts) do
{:ok,
vars
|> calculated_authorization_values
|> create_header}
end
end
defp authorization_vars(env, opts) do
with {:ok, unauthorized_response} <-
env.__module__.get(
env.__client__,
env.url,
opts: Keyword.put(env.opts || [], :digest_auth_handshake, true)
) do
{:ok,
%{
username: opts[:username] || "",
password: opts[:password] || "",
path: URI.parse(env.url).path,
auth:
Tesla.get_header(unauthorized_response, "www-authenticate")
|> parse_www_authenticate_header,
method: env.method |> to_string |> String.upcase(),
- client_nonce: (opts[:cnonce_fn] || &cnonce/0).(),
+ client_nonce: (opts[:cnonce_fn] || (&cnonce/0)).(),
nc: opts[:nc] || "00000000"
}}
end
end
defp calculated_authorization_values(%{auth: auth}) when auth == %{}, do: []
defp calculated_authorization_values(auth_vars) do
[
{"username", auth_vars.username},
{"realm", auth_vars.auth["realm"]},
{"uri", auth_vars[:path]},
{"nonce", auth_vars.auth["nonce"]},
{"nc", auth_vars.nc},
{"cnonce", auth_vars.client_nonce},
{"response", response(auth_vars)},
# hard-coded, will not work for MD5-sess
{"algorithm", "MD5"},
# hard-coded, will not work for auth-int or unspecified
{"qop", "auth"}
]
end
defp single_header_val({k, v}) when k in ~w(nc qop algorithm), do: "#{k}=#{v}"
defp single_header_val({k, v}), do: "#{k}=\"#{v}\""
defp create_header([]), do: []
defp create_header(calculated_authorization_values) do
vals =
calculated_authorization_values
|> Enum.reduce([], fn val, acc -> [single_header_val(val) | acc] end)
|> Enum.join(", ")
[{"authorization", "Digest #{vals}"}]
end
defp ha1(%{username: username, auth: %{"realm" => realm}, password: password}) do
md5("#{username}:#{realm}:#{password}")
end
defp ha2(%{method: method, path: path}) do
md5("#{method}:#{path}")
end
defp response(%{auth: %{"nonce" => nonce}, nc: nc, client_nonce: client_nonce} = auth_vars) do
md5("#{ha1(auth_vars)}:#{nonce}:#{nc}:#{client_nonce}:auth:#{ha2(auth_vars)}")
end
defp parse_www_authenticate_header(nil), do: %{}
defp parse_www_authenticate_header(header) do
Regex.scan(~r/(\w+?)="(.+?)"/, header)
|> Enum.reduce(%{}, fn [_, key, val], acc -> Map.merge(acc, %{key => val}) end)
end
defp md5(data), do: Base.encode16(:erlang.md5(data), case: :lower)
defp cnonce, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
diff --git a/lib/tesla/migration.ex b/lib/tesla/migration.ex
index 213efe2..cfea904 100644
--- a/lib/tesla/migration.ex
+++ b/lib/tesla/migration.ex
@@ -1,111 +1,113 @@
defmodule Tesla.Migration do
## ALIASES
@breaking_alias "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#dropped-aliases-support-159"
@breaking_headers_map "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#headers-are-now-a-list-160"
@breaking_client_fun "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#dropped-client-as-function-176"
def breaking_alias!(_kind, _name, nil), do: nil
def breaking_alias!(kind, name, caller) do
raise CompileError,
file: caller.file,
line: caller.line,
description: """
#{kind |> to_string |> String.capitalize()} aliases and local functions has been removed.
- Use full #{kind} name or define a middleware module #{name |> to_string() |> String.capitalize()}
+ Use full #{kind} name or define a middleware module #{
+ name |> to_string() |> String.capitalize()
+ }
#{snippet(caller)}
See #{@breaking_alias}
"""
end
def breaking_alias_in_config!(module) do
check_config(
Application.get_env(:tesla, module, [])[:adapter],
"config :tesla, #{inspect(module)}, adapter: "
)
check_config(Application.get_env(:tesla, :adapter), "config :tesla, adapter: ")
end
defp check_config(nil, _label), do: nil
defp check_config({module, _opts}, label) do
check_config(module, label)
end
defp check_config(module, label) do
unless elixir_module?(module) do
raise CompileError,
description: """
Calling
#{label}#{inspect(module)}
with atom as argument has been deprecated
Use
#{label}Tesla.Adapter.Name
instead
See #{@breaking_alias}
"""
end
end
## HEADERS AS LIST
def breaking_headers_map!(
{:__aliases__, _, [:Tesla, :Middleware, :Headers]},
{:%{}, _, _},
caller
) do
raise CompileError,
file: caller.file,
line: caller.line,
description: """
Headers are now a list instead of a map.
#{snippet(caller)}
See #{@breaking_headers_map}
"""
end
def breaking_headers_map!(_middleware, _opts, _caller), do: nil
## CLIENT FUNCTION
def client_function! do
raise RuntimeError,
message: """
Using anonymous function as client has been removed.
Use `Tesla.build_client` instead
See #{@breaking_client_fun}
"""
end
## UTILS
defp elixir_module?(atom) do
atom |> Atom.to_string() |> String.starts_with?("Elixir.")
end
defp snippet(caller) do
caller.file
|> File.read!()
|> String.split("\n")
|> Enum.at(caller.line - 1)
rescue
_ in File.Error -> ""
end
end
diff --git a/test/tesla/0.x_to_1.0_migration_test.exs b/test/tesla/0.x_to_1.0_migration_test.exs
index ce43240..36ceaf7 100644
--- a/test/tesla/0.x_to_1.0_migration_test.exs
+++ b/test/tesla/0.x_to_1.0_migration_test.exs
@@ -1,121 +1,122 @@
defmodule MigrationTest do
use ExUnit.Case
describe "Drop aliases #159" do
test "compile error when using atom as plug" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client1 do
use Tesla
plug :json
end
end
)
end
end
test "compile error when using atom as plug even if there is a local function with that name" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client2 do
use Tesla
plug :json
def json(env, next), do: Tesla.run(env, next)
end
end
)
end
end
test "compile error when using atom as adapter" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client3 do
use Tesla
adapter :hackney
end
end
)
end
end
test "compile error when using atom as adapter with opts" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client4 do
use Tesla
adapter :hackney, recv_timeout: 10_000
end
end
)
end
end
test "compile error when using atom as adapter even if there is a local function with that name" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client5 do
use Tesla
adapter :local
def local(env), do: env
end
end
)
end
end
test "compile error when using atom as adapter in config" do
assert_raise CompileError, fn ->
Application.put_env(:tesla, Client6, adapter: :mock)
Code.compile_quoted(
quote do
defmodule Client6 do
use Tesla
end
end
)
end
end
end
describe "Use keyword list to store headers #160" do
test "compile error when passing a map to Headers middleware" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client7 do
use Tesla
plug Tesla.Middleware.Headers, %{"User-Agent" => "tesla"}
end
end
)
end
end
test "no error when passing a list to Headers middleware" do
Code.compile_quoted(
quote do
defmodule Client8 do
use Tesla
plug Tesla.Middleware.Headers, [{"user-agent", "tesla"}]
end
end
)
end
end
describe "Drop client as function #176" do
test "error when passing a function as client" do
client = fn env, next -> Tesla.run(env, next) end
+
assert_raise RuntimeError, fn ->
Tesla.get(client, "/")
end
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 12:33 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39794
Default Alt Text
(14 KB)

Event Timeline