Skip to content

Commit

Permalink
Merge pull request derekkraan#19 from derekkraan/feature/more-auth
Browse files Browse the repository at this point in the history
Auth and run steps
  • Loading branch information
derekkraan authored Dec 22, 2024
2 parents 68e4549 + b84e067 commit 9cb678e
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 0.98.7

- Add new supported flags: `--proxy` and `--proxy-user`
- Add more supported auth steps: `netrc` and `netrc_file`
- Add option to exclude `req` steps to run when generating the cURL command

## 0.98.6
- Handle `--data-raw` and `--data-ascii` ([#16](https://github.com/derekkraan/curl_req/pull/16))
Expand Down
94 changes: 74 additions & 20 deletions lib/curl_req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,30 @@ defmodule CurlReq do
req
end

defp run_steps(req) do
Enum.reduce(req.request_steps, req, fn {step_name, step}, req ->
@spec step_names(Req.Request.t(), boolean()) :: [atom()]
defp step_names(%Req.Request{} = _req, false), do: []
defp step_names(%Req.Request{} = req, true), do: req.request_steps |> Keyword.keys()

@spec step_names(Req.Request.t(), [atom()]) :: [atom()]
defp step_names(%Req.Request{} = req, except: excludes) do
for {name, _} <- req.request_steps, name not in excludes do
name
end
end

defp step_names(%Req.Request{} = req, only: includes) do
for {name, _} <- req.request_steps, name in includes do
name
end
end

@spec run_steps(Req.Request.t(), [atom()]) :: Req.Request.t()
defp run_steps(req, steps) do
req.request_steps
|> Enum.filter(fn {step, _} ->
step in steps
end)
|> Enum.reduce(req, fn {step_name, step}, req ->
case step.(req) do
{_req, _response_or_error} ->
raise "The request was stopped by #{step_name} request_step."
Expand All @@ -60,10 +82,17 @@ defmodule CurlReq do
* `--data-raw`
* `-x`/`--proxy`
* `-U`/`--proxy-user`
* `-u`/`--user`
* `-n`/`--netrc`
* `--netrc-file`
Options:
- `run_steps`: Run the Req.Steps before generating the curl command. Default: `true`. This option is semi-private, introduced to support CurlReq.Plugin.
- `run_steps`: Run the Req.Steps before generating the curl command to have fine-tuned control over the Req.Request. Default: `true`.
* `true`: Run all steps
* `false`: Run no steps
* `only: [atom()]`: A list of step names as atoms and only they will be executed
* `except: [atom()]`: A list of step names as atoms and these steps will be excluded from the executed steps
- `flags`: Specify the style the argument flags are constructed. Can either be `:short` or `:long`, Default: `:short`
- `flavor` or `flavour`: With the `:curl` flavor (the default) it will try to use native curl representations for compression, auth and will use the native user agent.
If flavor is set to `:req` the headers will not be modified and the curl command is constructed to stay as true as possible to the original `Req.Request`
Expand All @@ -78,27 +107,27 @@ defmodule CurlReq do
...> |> CurlReq.to_curl(flags: :long, flavor: :req)
~S(curl --header "accept-encoding: gzip" --header "user-agent: req/#{@req_version}" --request GET https://www.example.com)
iex> Req.new(url: "https://www.example.com")
...> |> CurlReq.to_curl(run_steps: [except: [:compressed]])
~S(curl -X GET https://www.example.com)
"""
@type flags :: :short | :long
@type flavor :: :curl | :req
@type to_curl_opts :: [
flags: flags(),
flavor: flavor(),
flavour: flavor(),
run_steps: boolean()
run_steps: boolean() | [only: [atom()]] | [except: [atom()]]
]
@spec to_curl(Req.Request.t(), to_curl_opts()) :: String.t()
def to_curl(req, options \\ []) do
flag_style = Keyword.get(options, :flags, :short)
flavor = Keyword.get(options, :flavor, nil) || Keyword.get(options, :flavour, :curl)
run_steps? = Keyword.get(options, :run_steps, true)
opts = Keyword.validate!(options, flags: :short, run_steps: true, flavor: nil, flavour: :curl)
flavor = opts[:flavor] || opts[:flavour]
flag_style = opts[:flags]
run_steps = opts[:run_steps]

req =
if run_steps? do
run_steps(req)
else
req
end
available_steps = step_names(req, run_steps)
req = run_steps(req, available_steps)

cookies =
case Map.get(req.headers, "cookie") do
Expand All @@ -124,10 +153,7 @@ defmodule CurlReq do

# avoids duplicate compression argument
%{compressed: true} ->
if run_steps?, do: [], else: [compressed_flag()]

%{auth: {:basic, credentials}} ->
[user_flag(flag_style), credentials] ++ [basic_auth_flag()]
if :compressed in available_steps, do: [], else: [compressed_flag()]

%{connect_options: connect_options} ->
proxy =
Expand All @@ -151,6 +177,29 @@ defmodule CurlReq do
[]
end

auth =
with %{auth: scheme} <- req.options do
case scheme do
{:bearer, token} ->
[header_flag(flag_style), "authorization: Bearer #{token}"]

{:basic, userinfo} ->
[user_flag(flag_style), userinfo] ++ [basic_auth_flag()]

:netrc ->
[netrc_flag(flag_style)]

{:netrc, filepath} ->
[netrc_file_flag(flag_style), filepath]

_ ->
[]
end
else
_ ->
[]
end

method =
case req.method do
nil -> [request_flag(flag_style), "GET"]
Expand All @@ -162,7 +211,7 @@ defmodule CurlReq do

CurlReq.Shell.cmd_to_string(
"curl",
headers ++ cookies ++ body ++ options ++ method ++ url
auth ++ headers ++ cookies ++ body ++ options ++ method ++ url
)
end

Expand All @@ -173,8 +222,8 @@ defmodule CurlReq do
[compressed_flag()]
end

# filter out basic auth header because we expect it to be set as an auth step option
defp map_header({"authorization", ["Basic " <> _credentials]}, _flag_style, :curl),
# filter out auth header because we expect it to be set as an auth step option
defp map_header({"authorization", _}, _flag_style, :curl),
do: []

# filter out user agent when mode is :curl
Expand Down Expand Up @@ -214,6 +263,11 @@ defmodule CurlReq do
defp proxy_user_flag(:short), do: "-U"
defp proxy_user_flag(:long), do: "--proxy-user"

defp netrc_flag(:short), do: "-n"
defp netrc_flag(:long), do: "--netrc"

defp netrc_file_flag(_), do: "--netrc-file"

@doc """
Transforms a curl command into a Req request.
Expand Down
43 changes: 38 additions & 5 deletions lib/curl_req/macro.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ defmodule CurlReq.Macro do
user: :string,
compressed: :boolean,
proxy: :string,
proxy_user: :string
proxy_user: :string,
netrc: :boolean,
netrc_file: :string
],
aliases: [
H: :header,
Expand All @@ -39,7 +41,8 @@ defmodule CurlReq.Macro do
L: :location,
u: :user,
x: :proxy,
U: :proxy_user
U: :proxy_user,
n: :netrc
]
)

Expand Down Expand Up @@ -103,6 +106,12 @@ defmodule CurlReq.Macro do
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: {:bearer, token})

{"authorization", "Basic " <> token} ->
req
|> Req.Request.register_options([:auth])
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: {:basic, token})

_ ->
Req.Request.put_header(req, key, value)
end
Expand Down Expand Up @@ -168,15 +177,39 @@ defmodule CurlReq.Macro do
end

defp add_auth(req, options) do
case Keyword.get(options, :user) do
req =
case Keyword.get(options, :user) do
nil ->
req

credentials ->
req
|> Req.Request.register_options([:auth])
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: {:basic, credentials})
end

req =
case Keyword.get(options, :netrc) do
nil ->
req

true ->
req
|> Req.Request.register_options([:auth])
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: :netrc)
end

case Keyword.get(options, :netrc_file) do
nil ->
req

credentials ->
path ->
req
|> Req.Request.register_options([:auth])
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: {:basic, credentials})
|> Req.merge(auth: {:netrc, path})
end
end

Expand Down
26 changes: 25 additions & 1 deletion test/curl_req/macro_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ defmodule CurlReq.MacroTest do
}
end

test "auth" do
test "basic auth" do
assert ~CURL(curl http://example.com -u user:pass) ==
%Req.Request{
url: URI.parse("http://example.com"),
Expand Down Expand Up @@ -171,6 +171,30 @@ defmodule CurlReq.MacroTest do
}
end

test "netrc auth" do
assert ~CURL(curl http://example.com -n) ==
%Req.Request{
url: URI.parse("http://example.com"),
body: nil,
registered_options: MapSet.new([:auth]),
options: %{auth: :netrc},
current_request_steps: [:auth],
request_steps: [auth: &Req.Steps.auth/1]
}
end

test "netrc file auth" do
assert ~CURL(curl http://example.com --netrc-file "./mynetrc") ==
%Req.Request{
url: URI.parse("http://example.com"),
body: nil,
registered_options: MapSet.new([:auth]),
options: %{auth: {:netrc, "./mynetrc"}},
current_request_steps: [:auth],
request_steps: [auth: &Req.Steps.auth/1]
}
end

test "compressed" do
assert ~CURL(curl --compressed http://example.com) ==
%Req.Request{
Expand Down
65 changes: 59 additions & 6 deletions test/curl_req_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,6 @@ defmodule CurlReqTest do
|> CurlReq.to_curl(flavor: :req)
end

test "basic auth option" do
assert "curl --compressed -u user:pass --basic -X GET https://example.com" ==
Req.new(url: "https://example.com", auth: {:basic, "user:pass"})
|> CurlReq.to_curl()
end

test "proxy" do
assert ~S(curl --compressed -x "http://my.proxy.com:80" -X GET https://example.com) ==
Req.new(
Expand All @@ -113,5 +107,64 @@ defmodule CurlReqTest do
)
|> CurlReq.to_curl()
end

test "basic auth option" do
assert "curl -u user:pass --basic --compressed -X GET https://example.com" ==
Req.new(url: "https://example.com", auth: {:basic, "user:pass"})
|> CurlReq.to_curl()
end

test "bearer auth option" do
assert ~S(curl -H "authorization: Bearer foo123bar" --compressed -X GET https://example.com) ==
Req.new(url: "https://example.com", auth: {:bearer, "foo123bar"})
|> CurlReq.to_curl()
end

@tag :tmp_dir
test "netrc auth option", %{tmp_dir: tmp_dir} do
credentials =
"""
machine example.com
login foo
password bar
"""

netrc_path = Path.join(tmp_dir, "my_netrc")
File.write(netrc_path, credentials)
System.put_env("NETRC", netrc_path)

assert "curl -n --compressed -X GET https://example.com" ==
Req.new(url: "https://example.com", auth: :netrc)
|> CurlReq.to_curl()
end

@tag :tmp_dir
test "netrc file auth option", %{tmp_dir: tmp_dir} do
credentials =
"""
machine example.com
login foo
password bar
"""

netrc_path = Path.join(tmp_dir, "my_netrc")
File.write(netrc_path, credentials)

assert ~s(curl --netrc-file "#{netrc_path}" --compressed -X GET https://example.com) ==
Req.new(url: "https://example.com", auth: {:netrc, netrc_path})
|> CurlReq.to_curl()
end

test "include `encode_body` does not run `comporessed` or other steps" do
assert ~S(curl -H "accept: application/json" -H "content-type: application/json" -d "{\"key\":\"val\"}" -X GET https://example.com) ==
Req.new(url: "https://example.com", json: %{key: "val"})
|> CurlReq.to_curl(run_steps: [only: [:encode_body]])
end

test "exclude `compressed` and `encode_body` do not run" do
assert "curl -X GET https://example.com" ==
Req.new(url: "https://example.com", json: %{key: "val"})
|> CurlReq.to_curl(run_steps: [except: [:compressed, :encode_body]])
end
end
end

0 comments on commit 9cb678e

Please sign in to comment.