Commit 0f65bdd09c79237aa23fae7111ef18a519a6b199

Stephen Moloney 2017-02-16T11:07:30

enable shell input of TOTP code when 2FA is activated.

diff --git a/lib/mix/tasks/ovh.ex b/lib/mix/tasks/ovh.ex
index bbee71e..6ed7472 100644
--- a/lib/mix/tasks/ovh.ex
+++ b/lib/mix/tasks/ovh.ex
@@ -351,17 +351,93 @@ defmodule Mix.Tasks.Ovh do
     headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
     options = @default_options
     resp = HTTPoison.request!(method, uri, body, headers, options)
+    |> Og.log_return()
+
+    case check_for_successful_binding(resp, validation_url, ck) do
+      {:ok, ck} -> ck
+      {:ok, :handle_2fa} -> handle_2fa(resp.body, validation_url, ck)
+      {:error, msg} -> raise msg
+    end
+  end
+
+  def check_for_successful_binding(resp, validation_url, ck) do
+    Og.context(__ENV__, :debug)
 
     error_msg1 = "Failed to bind the consumer token to the application. Please try to validate the consumer token manually at #{validation_url}"
     error_msg2 = "Invalid validity period entered for the consumer token. Please try to validate the consumer token manually at #{validation_url}"
     cond do
-      String.contains?(resp.body, "Invalid validity") -> raise error_msg2
-      String.contains?(resp.body, "The token is now valid, it can be used in the application") -> ck
-      String.contains?(resp.body, "Your token is now valid, you can use it in your application") -> ck
-      String.contains?(resp.body, "token is now valid") -> ck
+      String.contains?(resp.body, "Invalid validity") -> {:error, error_msg2}
+      String.contains?(resp.body, "The token is now valid, it can be used in the application") -> {:ok, ck}
+      String.contains?(resp.body, "Your token is now valid, you can use it in your application") -> {:ok, ck}
+      String.contains?(resp.body, "token is now valid") -> {:ok, ck}
+      String.contains?(resp.body, "You have activated the double factor authentication") -> {:ok, :handle_2fa}
       # presume the validation was successful if redirected to redirect uri
-      resp.status_code == 302 && (resp.headers |> Enum.into(%{}) |> Map.has_key?("Location")) -> ck
-      true -> raise error_msg1
+      resp.status_code == 302 && (resp.headers |> Enum.into(%{}) |> Map.has_key?("Location")) -> {:ok, ck}
+      true -> {:error, "Unexpected error " <> error_msg1}
+    end
+  end
+
+
+  defp build_2fa_request(resp_body) do
+    Og.context(__ENV__, :debug)
+
+    Mix.Shell.IO.info("You have activated 2FA on your OVH account, you need to verify your account via 2FA")
+
+    Floki.find(resp_body, "form input")
+    |> Enum.reduce("", fn({type, input, _options}, acc) ->
+      {name_val, value} =
+     cond do
+        type == "input" && {"name", "sessionId"} in input ->
+          name_val = :proplists.get_value("name", input)
+          value = :proplists.get_value("value", input)
+          {name_val, value}
+        type == "input" && {"name", "credentialToken"} in input ->
+          name_val = :proplists.get_value("name", input)
+          value = :proplists.get_value("value", input)
+          {name_val, value}
+        type == "input" && {"name", "duration"} in input ->
+          name_val = :proplists.get_value("name", input)
+          value = "0"
+          {name_val, value}
+        type == "input" && {"type", "number"} in input && {"placeholder", "Code"} in input ->
+          name_val = :proplists.get_value("name", input)
+          # Get value from shell asking user for 2FA code.
+          value = Mix.Shell.IO.prompt("Please enter *promptly* the 2FA (2 Factor Authentication) code generated by your mobile application:")
+          Mix.Shell.IO.info("The code #{value} will be sent as the 2FA code")
+          {name_val, value}
+        true ->
+          # raise "Unexpected input"
+          Og.log("Ignoring unexpected input " <> inspect(input), __ENV__, :warn)
+          {:no_name, :no_val}
+      end
+      case {name_val, value} do
+        {:no_name, :no_val} -> acc
+        {name_val, value} -> acc <> name_val <> "=" <> value  <> "&"
+      end
+    end)
+    |> String.trim_trailing("&")
+  end
+
+
+  defp handle_2fa(resp_body, validation_url, ck) do
+    Og.context(__ENV__, :debug)
+
+    method = :post
+    uri = validation_url
+    |> Og.log_return()
+    body = build_2fa_request(resp_body)
+    |> Og.log_return(:error)
+    headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
+    options = @default_options
+    resp = HTTPoison.request!(method, uri, body, headers, options)
+
+    resp.body |> Og.log_return()
+
+    error_msg = "Function check_for_successful_binding seems to be entering an error loop"
+    case check_for_successful_binding(resp, validation_url, ck) do
+      {:ok, ck} -> ck
+      {:ok, :handle_2fa} -> raise error_msg
+      {:error, msg} -> raise "#{error_msg} - #{msg}"
     end
   end