/
configuration.ex
234 lines (188 loc) · 7.17 KB
/
configuration.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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
defmodule Spear.Connection.Configuration do
@default_mint_opts [protocols: [:http2], mode: :active]
@moduledoc """
Configuration for `Spear.Connection`s
## Options
* `:name` - the name of the GenServer. See `t:GenServer.name/0` for more
information. When not provided, the spawned process is not aliased to a
name and is only addressable through its PID.
* `:connection_string` - the connection string to parse
containing all connection information. Other options like `:host` or
`:port` will be parsed from the connection string. If options parsed from
the connection string are passed, they will be treated as overrides to the
value found in the connection string. Consult the EventStoreDB
documentation for formulating a valid connection string.
* `:mint_opts` - (default: `#{inspect(@default_mint_opts)}`) a keyword
list of options to pass to mint. The default values cannot be overridden.
This can be useful for configuring TLS. See the
[security guide](guides/security.md) for more information.
* `:host` - (default: `"localhost"`) the host address of the EventStoreDB
* `:port` - (default: `2113`) the external gRPC port of the EventStoreDB
* `:tls?` - (default: `false`) whether or not to use TLS to secure the
connection to the EventStoreDB
* `:username` - (default: `"admin"`) the user to connect as
* `:password` - (default: `"changeit"`) the user's password
* `:keep_alive_interval` - (default: `10_000`ms - 10s) the period to send
keep-alive pings to the EventStoreDB. Set `-1` to disable keep-alive
checks. Should be any integer value `>= 10_000`. This option can be used
in conjunction with `:keep_alive_timeout` to properly disconnect if the
EventStoreDB is not responding to network traffic.
* `:keep_alive_timeout` - (default: `10_000`ms - 10s) the time after sending
a keep-alive ping when the ping will be considered unacknowledged. Used
in conjunction with `:keep_alive_interval`. Set to `-1` to disable
keep-alive checks. Should be any integer value `>= 10_000`.
* `:read_only?` - (default: `false`) controls whether the connection should
be limited to read-only functionality. The list of read-only APIs can be
queried with `Spear.Connection.read_apis/0`
* `:on_connect` - (default: `nil`) a 0-arity fun or MFA to be called when the
connection is established.
* `:on_disconnect` - (default: `nil`) a 0-arity fun or MFA to be called when the
connection is lost.
See the `Spear.Connection` module docs for more information about keep-alive.
"""
@moduledoc since: "0.2.0"
require Logger
defmacrop warn(msg) do
if Version.match?(System.version(), ">= 1.11.0") do
quote do: Logger.warning(unquote(msg))
else
quote do: Logger.warn(unquote(msg))
end
end
# ms
@default_keepalive 10_000
@typedoc """
Configuration for a `Spear.Connection`.
"""
@typedoc since: "0.2.0"
@type t :: %__MODULE__{
scheme: :http | :https,
host: Mint.Types.address(),
port: :inet.port_number(),
tls?: boolean(),
username: String.t() | nil,
password: String.t() | nil,
keep_alive_interval: pos_integer() | false,
keep_alive_timeout: pos_integer() | false,
mint_opts: Keyword.t(),
valid?: boolean(),
errors: Keyword.t(),
read_only?: boolean(),
on_connect: fun() | {module(), atom(), [any()]} | nil,
on_disconnect: fun() | {module(), atom(), [any()]} | nil
}
defstruct scheme: :http,
host: "localhost",
port: 2113,
tls?: false,
username: "admin",
password: "changeit",
keep_alive_interval: 10_000,
keep_alive_timeout: 10_000,
mint_opts: [],
valid?: true,
errors: [],
read_only?: false,
on_connect: nil,
on_disconnect: nil
@doc false
def credentials(%__MODULE__{username: username, password: password}) do
{username, password}
end
@doc """
Parses configuration from a keyword list
This function is used internally by `Spear.Connection` when connecting.
"""
@doc since: "0.2.0"
@spec new(Keyword.t()) :: t()
def new(opts) when is_list(opts) do
config =
opts
|> Keyword.get(:connection_string)
|> from_connection_string()
|> Keyword.merge(opts)
|> override_mint_opts()
|> set_scheme()
struct(__MODULE__, config)
|> validate()
end
defp from_connection_string(connection_string) when is_binary(connection_string) do
uri = parse_uri(connection_string)
tls? = tls?(uri)
{username, password} = parse_credentials(uri)
[
host: uri.host,
port: uri.port,
tls?: tls?,
username: username,
password: password,
keep_alive_interval: keep_alive_interval(uri),
keep_alive_timeout: keep_alive_timeout(uri)
]
end
defp from_connection_string(_), do: []
defp parse_uri(connection_string) do
uri = URI.parse(connection_string)
%URI{uri | query: URI.decode_query(uri.query || "")}
end
defp tls?(%URI{query: %{"tls" => "true"}}), do: true
defp tls?(_), do: false
defp keep_alive_interval(uri), do: keep_alive_value(uri, "keepAliveInterval")
defp keep_alive_timeout(uri), do: keep_alive_value(uri, "keepAliveTimeout")
defp keep_alive_value(uri, key) do
with {:ok, value_str} <- Map.fetch(uri.query, key),
{value, ""} <- Integer.parse(value_str),
value when value >= @default_keepalive <- value do
value
else
-1 ->
false
value when value in 0..@default_keepalive ->
warn("Specified #{key} of #{value} is less than recommended 10_000ms")
value
value when is_integer(value) and value < -1 ->
# will get picked up by validation
value
_ ->
@default_keepalive
end
end
defp parse_credentials(uri) do
with userinfo when is_binary(userinfo) <- uri.userinfo,
[username, password] <- String.split(userinfo, ":") do
{username, password}
else
_ -> {nil, nil}
end
end
defp override_mint_opts(opts) do
mint_opts =
opts
|> Keyword.get(:mint_opts, [])
|> Keyword.merge(@default_mint_opts)
Keyword.merge(opts, mint_opts: mint_opts)
end
defp set_scheme(opts) do
Keyword.put(opts, :scheme, if(opts[:tls?], do: :https, else: :http))
end
defp validate(%__MODULE__{} = config) do
errors =
config
|> Map.from_struct()
|> Enum.reduce([], &validate/2)
%__MODULE__{config | errors: errors, valid?: errors == []}
end
defp validate({:keep_alive_interval = key, value}, errors)
when is_integer(value) and value <= 0 do
[{key, "keepAliveInterval must be greater than 1"} | errors]
end
defp validate({:keep_alive_timeout = key, value}, errors)
when is_integer(value) and value <= 0 do
[{key, "keepAliveTimeout must be greater than 1"} | errors]
end
defp validate({:port = key, value}, errors)
when not is_integer(value) or value not in 1..65_535 do
[{key, "#{inspect(value)} is not a valid port number"} | errors]
end
defp validate({_k, _v}, errors), do: errors
end