2.5 KiB · text History b868907
defmodule GitGudWeb.Plugs.PackageUploadParser do
  @moduledoc """
  Decides who owns the request body for a package-registry upload,
  ahead of the endpoint's general `Plug.Parsers`.

  Two shapes arrive:

    * **multipart** — twine's framing. Parsed here rather than
      downstream so it gets its own, much larger, size limit; 8 MB
      suits an ordinary form post and not a wheel with a bundled CUDA
      runtime.

    * **everything else** — Cargo's length-prefixed publish body and
      Hex's raw tarball. These are claimed for the controller to read
      off the socket. Claiming them matters: Cargo labels its binary
      publish body `Content-Type: application/json`, and without this
      the endpoint's JSON parser would try to decode a tarball and
      fail the request before it ever reached an adapter.

  `Plug.Parsers` treats a fetched `body_params` as "someone else
  handled this" and no-ops, which is how the claim is made. It does
  *not* set `body_params` itself when a body merely passes its `pass`
  list, so setting it here is the whole mechanism rather than a
  belt-and-braces extra.
  """

  @behaviour Plug

  # Comfortably past a manylinux wheel with a bundled CUDA runtime.
  # Anything genuinely larger belongs in LFS or the OCI registry.
  @max_upload_bytes 200_000_000

  # Each socket read (1 MB) may take this long. Plug's default, 15s, is a
  # 66 KB/s floor: since Bandit 1.14 stopped letting HTTP/2 control frames
  # extend a body read, a CI runner pushing a 16 MB APK over a slow link
  # got a 408 "Body read timeout" midway. Two minutes is an 8 KB/s floor.
  @read_timeout 120_000

  @parser_opts Plug.Parsers.init(
                 parsers: [:multipart],
                 pass: ["*/*"],
                 length: @max_upload_bytes,
                 read_timeout: @read_timeout
               )

  @doc "The per-read timeout for package upload bodies, for controllers reading them raw."
  def read_timeout, do: @read_timeout

  @impl true
  def init(opts), do: opts

  @impl true
  def call(%Plug.Conn{path_info: ["pkg" | _], method: method} = conn, _opts)
      when method in ["POST", "PUT", "PATCH"] do
    if multipart?(conn),
      do: Plug.Parsers.call(conn, @parser_opts),
      else: %{conn | body_params: %{}}
  end

  def call(conn, _opts), do: conn

  defp multipart?(conn) do
    with [value | _] <- Plug.Conn.get_req_header(conn, "content-type"),
         {:ok, "multipart", _subtype, _params} <- Plug.Conn.Utils.content_type(value) do
      true
    else
      _ -> false
    end
  end
end