3
votes

I have the following string which is the value from WWW-Authenticate from a http request:

"Digest realm=\"Web Service Realm via Digest Authentication\", qop=\"auth\", nonce=\"MTU3MjchuUVEIHEUnVNV==\""

I need to convert this into a map so I can easily reference the values for realm and nonce. I have some working code which is very brittle, e.g. to extract the realm:

    headers
    |> String.split(",")
    |> List.first()
    |> String.split("=")
    |> List.last()
    |> String.replace("\"", "")

However, this isn't great because it relies on the realm being the first in the list. Whats the most optimal way of converting this data?

4

4 Answers

4
votes

If you have :cowlib as a dependency (:phoenix depends on it through :plug_cowboy => :cowboy => :cowlib), it includes functions for parsing various headers, including "WWW-Authenticate".

iex> www_authenticate = "Digest realm=\"Web Service Realm via Digest Authentication\", qop=\"auth\", nonce=\"MTU3MjchuUVEIHEUnVNV==\""
"Digest realm=\"Web Service Realm via Digest Authentication\", qop=\"auth\", nonce=\"MTU3MjchuUVEIHEUnVNV==\""
iex> [digest: params] = :cow_http_hd.parse_www_authenticate(www_authenticate)
[
  digest: [
    {"realm", "Web Service Realm via Digest Authentication"},
    {"qop", "auth"},
    {"nonce", "MTU3MjchuUVEIHEUnVNV=="}
  ]
]
iex> Map.new(params)
%{
  "nonce" => "MTU3MjchuUVEIHEUnVNV==",
  "qop" => "auth",
  "realm" => "Web Service Realm via Digest Authentication"
}
4
votes

I would post the recursive solution, just for educational purpose. This, of course, should not probably be used in production.

defmodule Splitter do
  def parse(input), do: do_parse(input, %{}, :none)

  defp do_parse("realm=\"" <> rest, acc, :none),
    do: do_parse(rest, acc, :realm)
  defp do_parse("qop=\"" <> rest, acc, :none),
    do: do_parse(rest, acc, :qop)
  defp do_parse("nonce=\"" <> rest, acc, :none),
    do: do_parse(rest, acc, :nonce)
  defp do_parse("\"" <> rest, acc, key),
    do: do_parse(rest, acc, :none)
  defp do_parse(<<c :: binary-size(1), rest :: binary>>, acc, :none),
    do: do_parse(rest, acc, :none)
  defp do_parse(<<c :: binary-size(1), rest :: binary>>, acc, key),
    do: do_parse(rest, Map.update(acc, key, c, & &1 <> c), key)
  defp do_parse("", acc, _), do: acc
end

Splitter.parse(input)
#⇒ %{
#   nonce: "jefsFENEFJWIfejkshfshfhu332bfesf==",
#   qop: "auth",
#   realm: "Web Something Realm via Digest Authentication"
# }
2
votes

Regex to the rescue? This does not include any validation or error handling, but it works for your sample case.

~r/([^\s=]+)="([^"]*)"/
|> Regex.scan(headers)
|> Enum.map(fn [_, k, v] -> {k, v} end)
|> Map.new()

results in the map

%{
  "nonce" => "jefsFENEFJWIfejkshfshfhu332bfesf==",
  "qop" => "auth",
  "realm" => "Web Something Realm via Digest Authentication"
}
0
votes

You can use String.split() in a more generic way:

str = ~s(Digest realm="Web Something Realm via Digest Authentication", qop="auth", nonce="jefsFENEFJWIfejkshfshfhu332bfesf==")
[type, rest] = String.split(str, " ", [parts: 2])
key_vals = String.split(rest, ", ")

for key_val <- key_vals, into: %{} do
  [key, val] = String.split(key_val, "=", [parts: 2]) 
  {key, String.trim(val, ~s{"})}  #return a tuple for each key/val in map
end
|> Map.put_new("type", type) 

=>

%{
  "nonce" => "jefsFENEFJWIfejkshfshfhu332bfesf==",
  "qop" => "auth",
  "realm" => "Web Something Realm via Digest Authentication",
  "type" => "Digest"
}