Repository: derekkraan/curl_req (elixir)

Issue:
Support Proxy
Inspired by this [Elixir Forum Thread](https://elixirforum.com/t/making-a-req-request-with-proxy/65771/2)

These are the proposed flags:

```
-x, --proxy [protocol://]host[:port] Use this proxy
--proxy-basic   Use Basic authentication on the proxy
 -U, --proxy-user <user:password> Proxy user and password
```

Pull request description:
Proxy Flags
Closes #21 

For the first iteration i aimed for the basics and defaulted to basic authentication and only parsing the `proxy` and `proxy-user` flag

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c32cc77..4331ede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 0.98.7
+
+- Add new supported flags: `--proxy` and `--proxy-user`
+
 ## 0.98.6
 - Handle `--data-raw` and `--data-ascii` ([#16](https://github.com/derekkraan/curl_req/pull/16))
 - Strip `$` as necessary
diff --git a/lib/curl_req.ex b/lib/curl_req.ex
index e9d53c1..6bf83be 100644
--- a/lib/curl_req.ex
+++ b/lib/curl_req.ex
@@ -58,6 +58,8 @@ defmodule CurlReq do
   * `-I`/`--head`
   * `-d`/`--data`/`--data-ascii`
   * `--data-raw`
+  * `-x`/`--proxy`
+  * `-U`/`--proxy-user`
 
   Options:
 
@@ -127,6 +129,24 @@ defmodule CurlReq do
         %{auth: {:basic, credentials}} ->
           [user_flag(flag_style), credentials] ++ [basic_auth_flag()]
 
+        %{connect_options: connect_options} ->
+          proxy =
+            case Keyword.get(connect_options, :proxy) do
+              nil ->
+                []
+
+              {scheme, host, port, _} ->
+                [proxy_flag(flag_style), "#{scheme}://#{host}:#{port}"]
+            end
+
+          case Keyword.get(connect_options, :proxy_headers) do
+            [{"proxy-authorization", "Basic " <> encoded_creds}] ->
+              proxy ++ [proxy_user_flag(flag_style), Base.decode64!(encoded_creds)]
+
+            _ ->
+              proxy
+          end
+
         _ ->
           []
       end
@@ -188,6 +208,12 @@ defmodule CurlReq do
 
   defp compressed_flag(), do: "--compressed"
 
+  defp proxy_flag(:short), do: "-x"
+  defp proxy_flag(:long), do: "--proxy"
+
+  defp proxy_user_flag(:short), do: "-U"
+  defp proxy_user_flag(:long), do: "--proxy-user"
+
   @doc """
   Transforms a curl command into a Req request.
 
@@ -201,6 +227,8 @@ defmodule CurlReq do
   * `-F`/`--form`
   * `-L`/`--location`
   * `-u`/`--user`
+  * `-x`/`--proxy`
+  * `-U`/`--proxy-user`
   * `--compressed`
 
   The `curl` command prefix is optional
diff --git a/lib/curl_req/macro.ex b/lib/curl_req/macro.ex
index 4abe676..4e2f92f 100644
--- a/lib/curl_req/macro.ex
+++ b/lib/curl_req/macro.ex
@@ -25,7 +25,9 @@ defmodule CurlReq.Macro do
           form: :keep,
           location: :boolean,
           user: :string,
-          compressed: :boolean
+          compressed: :boolean,
+          proxy: :string,
+          proxy_user: :string
         ],
         aliases: [
           H: :header,
@@ -35,7 +37,9 @@ defmodule CurlReq.Macro do
           I: :head,
           F: :form,
           L: :location,
-          u: :user
+          u: :user,
+          x: :proxy,
+          U: :proxy_user
         ]
       )
 
@@ -50,7 +54,7 @@ defmodule CurlReq.Macro do
 
     %Req.Request{}
     # Req would accept an URI struct but here we use to_string/1 because Req uses URI.parse/1 which sets the deprecated `authority` field which upsets the test assertions.
-    # Can be removed the Req uses URI.new/1
+    # Can be removed when Req uses URI.new/1
     |> Req.merge(url: URI.to_string(url))
     |> add_header(options)
     |> add_method(options)
@@ -59,6 +63,7 @@ defmodule CurlReq.Macro do
     |> add_form(options)
     |> add_auth(options)
     |> add_compression(options)
+    |> add_proxy(options)
     |> configure_redirects(options)
   end
 
@@ -169,6 +174,67 @@ defmodule CurlReq.Macro do
     end
   end
 
+  defp add_proxy(req, options) do
+    with proxy when is_binary(proxy) <- Keyword.get(options, :proxy),
+         %URI{scheme: scheme, port: port, host: host, userinfo: userinfo}
+         when scheme in ["http", "https"] <-
+           validate_proxy_uri(proxy) do
+      connect_options =
+        [
+          proxy: {String.to_existing_atom(scheme), host, port, []}
+        ]
+        |> maybe_add_proxy_auth(options, userinfo)
+
+      req
+      |> Req.Request.register_options([
+        :connect_options
+      ])
+      |> Req.merge(connect_options: connect_options)
+    else
+      _ -> req
+    end
+  end
+
+  defp validate_proxy_uri("http://" <> _rest = uri), do: URI.parse(uri)
+  defp validate_proxy_uri("https://" <> _rest = uri), do: URI.parse(uri)
+
+  defp validate_proxy_uri(uri) do
+    case String.split(uri, "://") do
+      [scheme, _uri] ->
+        raise ArgumentError, "Unsupported scheme #{scheme} for proxy in #{uri}"
+
+      [uri] ->
+        URI.parse("http://" <> uri)
+    end
+  end
+
+  defp maybe_add_proxy_auth(connect_options, options, nil) do
+    proxy_headers =
+      case Keyword.get(options, :proxy_user) do
+        nil ->
+          []
+
+        credentials ->
+          [
+            proxy_headers: [
+              {"proxy-authorization", "Basic " <> Base.encode64(credentials)}
+            ]
+          ]
+      end
+
+    Keyword.merge(connect_options, proxy_headers)
+  end
+
+  defp maybe_add_proxy_auth(connect_options, _options, userinfo) do
+    proxy_headers = [
+      proxy_headers: [
+        {"proxy-authorization", "Basic " <> Base.encode64(userinfo)}
+      ]
+    ]
+
+    Keyword.merge(connect_options, proxy_headers)
+  end
+
   defp configure_redirects(req, options) do
     if Keyword.get(options, :location, false) do
       req


Test patch:
diff --git a/test/curl_req/macro_test.exs b/test/curl_req/macro_test.exs
index c792555..800ec64 100644
--- a/test/curl_req/macro_test.exs
+++ b/test/curl_req/macro_test.exs
@@ -205,6 +205,59 @@ defmodule CurlReq.MacroTest do
                  response_steps: [redirect: &Req.Steps.redirect/1]
                }
     end
+
+    test "proxy" do
+      assert ~CURL(curl --proxy my.proxy.com:22225 http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [proxy: {:http, "my.proxy.com", 22225, []}]
+                 }
+               }
+    end
+
+    test "proxy with basic auth" do
+      assert ~CURL(curl --proxy https://my.proxy.com:22225 --proxy-user foo:bar http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [
+                     proxy: {:https, "my.proxy.com", 22225, []},
+                     proxy_headers: [
+                       {"proxy-authorization", "Basic " <> Base.encode64("foo:bar")}
+                     ]
+                   ]
+                 }
+               }
+    end
+
+    test "proxy with inline basic auth" do
+      assert ~CURL(curl --proxy https://foo:bar@my.proxy.com:22225 http://example.com) ==
+               %Req.Request{
+                 url: URI.parse("http://example.com"),
+                 registered_options: MapSet.new([:connect_options]),
+                 options: %{
+                   connect_options: [
+                     proxy: {:https, "my.proxy.com", 22225, []},
+                     proxy_headers: [
+                       {"proxy-authorization", "Basic " <> Base.encode64("foo:bar")}
+                     ]
+                   ]
+                 }
+               }
+    end
+
+    test "proxy raises on non http scheme uri" do
+      assert_raise(
+        ArgumentError,
+        "Unsupported scheme ssh for proxy in ssh://my.proxy.com:22225",
+        fn ->
+          CurlReq.Macro.parse("curl --proxy ssh://my.proxy.com:22225 http://example.com")
+        end
+      )
+    end
   end
 
   describe "newlines" do
diff --git a/test/curl_req_test.exs b/test/curl_req_test.exs
index 83e5b6a..ce34e26 100644
--- a/test/curl_req_test.exs
+++ b/test/curl_req_test.exs
@@ -90,5 +90,28 @@ defmodule CurlReqTest do
                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(
+                 url: "https://example.com",
+                 connect_options: [proxy: {:http, "my.proxy.com", 80, []}]
+               )
+               |> CurlReq.to_curl()
+    end
+
+    test "proxy user" do
+      assert ~S(curl --compressed -x "http://my.proxy.com:80" -U foo:bar -X GET https://example.com) ==
+               Req.new(
+                 url: "https://example.com",
+                 connect_options: [
+                   proxy: {:http, "my.proxy.com", 80, []},
+                   proxy_headers: [
+                     {"proxy-authorization", "Basic " <> Base.encode64("foo:bar")}
+                   ]
+                 ]
+               )
+               |> CurlReq.to_curl()
+    end
   end
 end