• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supavisor / e5e7ebfe80dbec4965226225050d4ef5c8216e88-PR-605

21 Feb 2025 02:35PM UTC coverage: 45.973% (-0.03%) from 46.003%
e5e7ebfe80dbec4965226225050d4ef5c8216e88-PR-605

Pull #605

github

hauleth
fix: remaining SSL connections that need to set `verify_none` option
Pull Request #605: fix: remaining SSL connections that need to set `verify_none` option

2 of 9 new or added lines in 3 files covered. (22.22%)

267 existing lines in 26 files now uncovered.

959 of 2086 relevant lines covered (45.97%)

635.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.03
/lib/supavisor/helpers.ex
1
defmodule Supavisor.Helpers do
2
  @moduledoc false
3
  require Logger
4

5
  @spec check_creds_get_ver(map) :: {:ok, String.t() | nil} | {:error, String.t()}
6

7
  def check_creds_get_ver(%{"require_user" => false} = params) do
8
    cond do
×
9
      length(params["users"]) != 1 ->
×
10
        {:error, "Can't use 'require_user' and 'auth_query' with multiple users"}
11

12
      !hd(params["users"])["is_manager"] ->
×
13
        {:error, "Can't use 'require_user' and 'auth_query' with non-manager user"}
14

15
      true ->
×
16
        do_check_creds_get_ver(params)
×
17
    end
18
  end
19

20
  def check_creds_get_ver(%{"users" => _} = params) do
21
    do_check_creds_get_ver(params)
×
22
  end
23

24
  def check_creds_get_ver(_), do: {:ok, nil}
×
25

26
  def do_check_creds_get_ver(params) do
27
    Enum.reduce_while(params["users"], {nil, nil}, fn user, _ ->
28
      upstream_ssl? = !!params["upstream_ssl"]
×
29

30
      ssl_opts =
×
31
        if upstream_ssl? and params["upstream_verify"] == "peer" do
×
32
          [
33
            verify: :verify_peer,
34
            cacerts: [upstream_cert(params["upstream_tls_ca"])],
35
            server_name_indication: String.to_charlist(params["db_host"]),
NEW
36
            customize_hostname_check: [{:match_fun, fn _, _ -> true end}]
×
37
          ]
38
        else
39
          [
40
            verify: :verify_none
41
          ]
42
        end
43

44
      {:ok, conn} =
×
45
        Postgrex.start_link(
46
          hostname: params["db_host"],
47
          port: params["db_port"],
48
          database: params["db_database"],
49
          password: user["db_password"],
50
          username: user["db_user"],
51
          ssl: upstream_ssl?,
52
          socket_options: [
53
            ip_version(params["ip_version"], params["db_host"])
54
          ],
55
          queue_target: 1_000,
56
          queue_interval: 5_000,
57
          ssl_opts: ssl_opts
58
        )
59

60
      check =
×
61
        Postgrex.query(conn, "select version()", [])
62
        |> case do
63
          {:ok, %{rows: [[version]]}} ->
64
            if params["require_user"] do
×
65
              {:cont, {:ok, version}}
66
            else
67
              case get_user_secret(conn, params["auth_query"], user["db_user"]) do
×
68
                {:ok, _} ->
×
69
                  {:halt, {:ok, version}}
70

71
                {:error, reason} ->
×
72
                  {:halt, {:error, reason}}
73
              end
74
            end
75

76
          {:error, reason} ->
×
77
            {:halt, {:error, "Can't connect the user #{user["db_user"]}: #{inspect(reason)}"}}
×
78
        end
79

80
      GenServer.stop(conn)
×
81
      check
×
82
    end)
83
    |> case do
×
84
      {:ok, version} ->
85
        parse_pg_version(version)
×
86

87
      other ->
88
        other
×
89
    end
90
  end
91

92
  @spec get_user_secret(pid(), String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
93
  def get_user_secret(conn, auth_query, user) do
94
    try do
95
      Postgrex.query!(conn, auth_query, [user])
4✔
96
    catch
97
      _error, reason ->
×
98
        {:error, "Authentication query failed: #{inspect(reason)}"}
99
    end
100
    |> case do
4✔
101
      %{columns: [_, _], rows: [[^user, secret]]} ->
102
        parse_secret(secret, user)
4✔
103

104
      %{columns: [_, _], rows: []} ->
×
105
        {:error,
106
         "There is no user '#{user}' in the database. Please create it or change the user in the config"}
×
107

108
      %{columns: columns} ->
×
109
        {:error,
110
         "Authentication query returned wrong format. Should be two columns: user and secret, but got: #{inspect(columns)}"}
111

112
      {:error, reason} ->
×
113
        {:error, reason}
114
    end
115
  end
116

117
  @spec parse_secret(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
118
  def parse_secret("SCRAM-SHA-256" <> _ = secret, user) do
119
    # <digest>$<iteration>:<salt>$<stored_key>:<server_key>
120
    case Regex.run(~r/^(.+)\$(\d+):(.+)\$(.+):(.+)$/, secret) do
4✔
121
      [_, digest, iterations, salt, stored_key, server_key] ->
4✔
122
        {:ok,
123
         %{
124
           digest: digest,
125
           iterations: String.to_integer(iterations),
126
           salt: salt,
127
           stored_key: Base.decode64!(stored_key),
128
           server_key: Base.decode64!(server_key),
129
           user: user
130
         }}
131

132
      _ ->
×
133
        {:error, "Can't parse secret"}
134
    end
135
  end
136

137
  def parse_secret("md5" <> secret, user) do
×
138
    {:ok, %{digest: :md5, secret: secret, user: user}}
139
  end
140

141
  def parse_secret(_secret, _user) do
×
142
    {:error, "Unsupported or invalid secret format"}
143
  end
144

145
  def parse_postgres_secret(_), do: {:error, "Digest not supported"}
×
146

147
  ## Internal functions
148

149
  @doc """
150
  Parses a PostgreSQL version string and returns the version number and platform.
151

152
  ## Examples
153

154
      iex> Supavisor.Helpers.parse_pg_version("PostgreSQL 14.6 (Debian 14.6-1.pgdg110+1) some string")
155
      {:ok, "14.6 (Debian 14.6-1.pgdg110+1)"}
156

157
      iex> Supavisor.Helpers.parse_pg_version("PostgreSQL 15.1 on aarch64-unknown-linux-gnu, compiled by gcc (Ubuntu 10.3.0-1ubuntu1~20.04) 10.3.0, 64-bit")
158
      {:ok, "15.1"}
159

160
      iex> Supavisor.Helpers.parse_pg_version("PostgreSQL on x86_64-pc-linux-gnu")
161
      {:error, "Can't parse version in PostgreSQL on x86_64-pc-linux-gnu"}
162
  """
163
  def parse_pg_version(version) do
164
    case Regex.run(~r/PostgreSQL\s(\d+\.\d+)(?:\s\(([^)]+)\))?.*/, version) do
×
165
      [_, version, platform] ->
×
166
        {:ok, "#{version} (#{platform})"}
×
167

168
      [_, version] ->
×
169
        {:ok, version}
170

171
      _ ->
×
172
        {:error, "Can't parse version in #{version}"}
×
173
    end
174
  end
175

176
  @doc """
177
  Returns the IP version for a given host.
178

179
  ## Examples
180

181
      iex> Supavisor.Helpers.ip_version(:v4, "example.com")
182
      :inet
183
      iex> Supavisor.Helpers.ip_version(:v6, "example.com")
184
      :inet6
185
      iex> Supavisor.Helpers.ip_version(nil, "example.com")
186
      :inet
187
  """
188
  @spec ip_version(any(), String.t()) :: :inet | :inet6
189
  def ip_version(:v4, _), do: :inet
×
190
  def ip_version(:v6, _), do: :inet6
×
191

192
  def ip_version(_, host) do
193
    detect_ip_version(host)
4✔
194
  end
195

196
  @doc """
197
  Detects the IP version for a given host.
198

199
  ## Examples
200

201
      iex> Supavisor.Helpers.detect_ip_version("example.com")
202
      :inet
203
      iex> Supavisor.Helpers.detect_ip_version("ipv6.example.com")
204
      :inet6
205
  """
206
  @spec detect_ip_version(String.t()) :: :inet | :inet6
207
  def detect_ip_version(host) when is_binary(host) do
208
    Logger.info("Detecting IP version for #{host}")
4✔
209
    host = String.to_charlist(host)
4✔
210

211
    case :inet.gethostbyname(host) do
4✔
212
      {:ok, _} -> :inet
4✔
213
      _ -> :inet6
×
214
    end
215
  end
216

217
  @spec cert_to_bin(binary()) :: {:ok, binary()} | {:error, atom()}
218
  def cert_to_bin(cert) do
219
    case :public_key.pem_decode(cert) do
×
220
      [] ->
×
221
        {:error, :cant_decode_certificate}
222

223
      pem_entries ->
224
        cert = for {:Certificate, cert, :not_encrypted} <- pem_entries, do: cert
×
225

226
        case cert do
×
227
          [cert] -> {:ok, cert}
×
228
          _ -> {:error, :invalid_certificate}
×
229
        end
230
    end
231
  end
232

233
  @spec upstream_cert(binary() | nil) :: binary() | nil
234
  def upstream_cert(default) do
235
    Application.get_env(:supavisor, :global_upstream_ca) || default
2✔
236
  end
237

238
  @spec downstream_cert() :: Path.t() | nil
239
  def downstream_cert do
240
    Application.get_env(:supavisor, :global_downstream_cert)
×
241
  end
242

243
  @spec downstream_key() :: Path.t() | nil
244
  def downstream_key do
245
    Application.get_env(:supavisor, :global_downstream_key)
×
246
  end
247

248
  @spec get_client_final(:password | :auth_query, map(), map(), binary(), binary(), binary()) ::
249
          {iolist(), binary()}
250
  def get_client_final(
251
        :password,
252
        secrets,
253
        srv_first,
254
        client_nonce,
255
        user_name,
256
        channel
257
      ) do
258
    channel_binding = "c=#{channel}"
×
259
    nonce = ["r=", srv_first.nonce]
×
260

261
    salt = srv_first.salt
×
262
    i = srv_first.i
×
263

264
    salted_password = :pgo_scram.hi(:pgo_sasl_prep_profile.validate(secrets), salt, i)
×
265
    client_key = :pgo_scram.hmac(salted_password, "Client Key")
×
266
    stored_key = :pgo_scram.h(client_key)
×
267
    client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce]
×
268
    server_first = srv_first.raw
×
269
    client_final_without_proof = [channel_binding, ",", nonce]
×
270
    auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof]
×
271
    client_signature = :pgo_scram.hmac(stored_key, auth_message)
×
272
    client_proof = :pgo_scram.bin_xor(client_key, client_signature)
×
273

274
    server_key = :pgo_scram.hmac(salted_password, "Server Key")
×
275
    server_signature = :pgo_scram.hmac(server_key, auth_message)
×
276

277
    {[client_final_without_proof, ",p=", Base.encode64(client_proof)], server_signature}
278
  end
279

280
  def get_client_final(
281
        :auth_query,
282
        secrets,
283
        srv_first,
284
        client_nonce,
285
        user_name,
286
        channel
287
      ) do
288
    channel_binding = "c=#{channel}"
136✔
289
    nonce = ["r=", srv_first.nonce]
136✔
290

291
    client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce]
136✔
292
    server_first = srv_first.raw
136✔
293
    client_final_without_proof = [channel_binding, ",", nonce]
136✔
294
    auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof]
136✔
295
    client_signature = :pgo_scram.hmac(secrets.stored_key, auth_message)
136✔
296
    client_proof = :pgo_scram.bin_xor(secrets.client_key, client_signature)
136✔
297

298
    server_signature = :pgo_scram.hmac(secrets.server_key, auth_message)
136✔
299

300
    {[client_final_without_proof, ",p=", Base.encode64(client_proof)], server_signature}
301
  end
302

303
  def signatures(stored_key, server_key, srv_first, client_nonce, user_name, channel) do
304
    channel_binding = "c=#{channel}"
137✔
305
    nonce = ["r=", srv_first.nonce]
137✔
306
    client_first_bare = [<<"n=">>, user_name, <<",r=">>, client_nonce]
137✔
307
    server_first = srv_first.raw
137✔
308
    client_final_without_proof = [channel_binding, ",", nonce]
137✔
309
    auth_message = [client_first_bare, ",", server_first, ",", client_final_without_proof]
137✔
310

311
    %{
137✔
312
      client: :pgo_scram.hmac(stored_key, auth_message),
313
      server: :pgo_scram.hmac(server_key, auth_message)
314
    }
315
  end
316

317
  def hash(bin) do
318
    :crypto.hash(:sha256, bin)
135✔
319
  end
320

321
  @spec parse_server_first(binary(), binary()) :: map()
322
  def parse_server_first(message, nonce) do
323
    :pgo_scram.parse_server_first(message, nonce) |> Map.new()
273✔
324
  end
325

326
  @spec md5([String.t()]) :: String.t()
327
  def md5(strings) do
328
    strings
329
    |> :erlang.md5()
330
    |> Base.encode16(case: :lower)
×
331
  end
332

333
  @spec rpc(Node.t(), module(), atom(), [any()], non_neg_integer()) :: {:error, any()} | any()
334
  def rpc(node, module, function, args, timeout \\ 15_000) do
335
    :erpc.call(node, module, function, args, timeout)
×
336
  catch
337
    kind, reason -> {:error, {:badrpc, {kind, reason}}}
×
338
  else
339
    {:EXIT, _} = badrpc -> {:error, {:badrpc, badrpc}}
×
340
    result -> result
×
341
  end
342

343
  @doc """
344
  Sets the maximum heap size for the current process. The `max_heap_size` parameter is in megabytes.
345

346
  ## Parameters
347

348
  - `max_heap_size`: The maximum heap size in megabytes.
349
  """
350
  @spec set_max_heap_size(pos_integer()) :: map()
351
  def set_max_heap_size(max_heap_size) do
352
    max_heap_words = div(max_heap_size * 1024 * 1024, :erlang.system_info(:wordsize))
282✔
353
    Process.flag(:max_heap_size, %{size: max_heap_words})
282✔
354
  end
355

356
  @spec set_log_level(atom()) :: :ok | nil
357
  def set_log_level(level) when level in [:debug, :info, :notice, :warning, :error] do
358
    Logger.notice("Setting log level to #{inspect(level)}")
×
359
    Logger.put_process_level(self(), level)
×
360
  end
361

362
  def set_log_level(_), do: nil
139✔
363

364
  @spec peer_ip(:gen_tcp.socket()) :: String.t()
365
  def peer_ip(socket) do
366
    case :inet.peername(socket) do
145✔
367
      {:ok, {ip, _port}} -> List.to_string(:inet.ntoa(ip))
144✔
368
      _error -> "undefined"
1✔
369
    end
370
  end
371

372
  @spec controlling_process(Supavisor.sock(), pid) :: :ok | {:error, any()}
373
  def controlling_process({mod, socket}, pid),
374
    do: mod.controlling_process(socket, pid)
×
375

376
  # This is the value of `NAMEDATALEN` set when compiling PostgreSQL. By default
377
  # we use default Postgres value of `64`
378
  @max_length Application.compile_env(:supabase, :namedatalen, 64) - 1
379

380
  @spec validate_name(String.t()) :: boolean()
381
  def validate_name(name) do
382
    byte_size(name) in 1..@max_length and String.printable?(name)
274✔
383
  end
384
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc