-
Notifications
You must be signed in to change notification settings - Fork 22
/
client_hints.ex
104 lines (83 loc) · 3.05 KB
/
client_hints.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
defmodule UAInspector.ClientHints do
@moduledoc """
Parse and store client hint headers for usage in device detection.
"""
@type t :: %__MODULE__{
application: String.t() | :unknown,
architecture: String.t() | :unknown,
bitness: String.t() | :unknown,
full_version: String.t() | :unknown,
full_version_list: [{String.t(), String.t()}],
mobile: boolean,
model: String.t() | :unknown,
platform: String.t() | :unknown,
platform_version: String.t() | :unknown
}
defstruct application: :unknown,
architecture: :unknown,
bitness: :unknown,
full_version: :unknown,
full_version_list: [],
mobile: false,
model: :unknown,
platform: :unknown,
platform_version: :unknown
@regex_version ~r/^"([^"]+)"; ?v="([^"]+)"(?:, )?/
@doc """
Parse headers into a new client hint struct.
All headers are expected in dash-case (lowercase with dashes) format.
Last header (if multiple possible) will be used for the result.
"""
@spec new([{String.t(), String.t()}]) :: t()
def new([]), do: %__MODULE__{}
def new(headers) do
Enum.reduce(headers, %__MODULE__{}, fn
{"sec-ch-ua", _}, %{full_version_list: [_ | _]} = hints ->
# ignore header if "sec-ch-ua-full-version-list" is already parsed
hints
{"sec-ch-ua", version_list}, hints ->
%{hints | full_version_list: parse_version_list(version_list)}
{"sec-ch-ua-arch", architecture}, hints ->
%{hints | architecture: cleanup(architecture)}
{"sec-ch-ua-bitness", bitness}, hints ->
%{hints | bitness: cleanup(bitness)}
{"sec-ch-ua-full-version", full_version}, hints ->
%{hints | full_version: cleanup(full_version)}
{"sec-ch-ua-full-version-list", version_list}, hints ->
%{hints | full_version_list: parse_version_list(version_list)}
{"sec-ch-ua-mobile", mobile}, hints ->
%{hints | mobile: mobile == "?1"}
{"sec-ch-ua-model", model}, hints ->
%{hints | model: cleanup(model)}
{"sec-ch-ua-platform", platform}, hints ->
%{hints | platform: cleanup(platform)}
{"sec-ch-ua-platform-version", platform_version}, hints ->
%{hints | platform_version: cleanup(platform_version)}
{"x-requested-with", application_header}, hints ->
application =
application_header
|> cleanup()
|> String.downcase()
case application do
"xmlhttprequest" -> hints
_ -> %{hints | application: application}
end
_, hints ->
hints
end)
end
defp cleanup(value) do
value
|> String.trim()
|> String.trim(~s("))
|> String.trim()
end
defp parse_version_list(version_list) do
version_list
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.map(&Regex.run(@regex_version, &1, capture: :all_but_first))
|> Enum.reject(&is_nil/1)
|> Enum.map(fn [brand, version] -> {brand, version} end)
end
end