Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/web/plugs/static_no_content_type.ex b/lib/pleroma/web/plugs/static_no_content_type.ex
new file mode 100644
index 000000000..285e4ff30
--- /dev/null
+++ b/lib/pleroma/web/plugs/static_no_content_type.ex
@@ -0,0 +1,451 @@
+# This is almost identical to Plug.Static from Plug 1.15.3 (2024-01-16)
+# It being copied is a temporary measure to fix an urgent bug without
+# needing to wait for merge of a suitable patch upstream
+# The differences are:
+# - this leading comment
+# - renaming of the module from 'Plug.Static' to 'Pleroma.Web.Plugs.StaticNoCT'
+
+defmodule Pleroma.Web.Plugs.StaticNoCT do
+ @moduledoc """
+ A plug for serving static assets.
+
+ It requires two options:
+
+ * `:at` - the request path to reach for static assets.
+ It must be a string.
+
+ * `:from` - the file system path to read static assets from.
+ It can be either: a string containing a file system path, an
+ atom representing the application name (where assets will
+ be served from `priv/static`), a tuple containing the
+ application name and the directory to serve assets from (besides
+ `priv/static`), or an MFA tuple.
+
+ The preferred form is to use `:from` with an atom or tuple, since
+ it will make your application independent from the starting directory.
+ For example, if you pass:
+
+ plug Plug.Static, from: "priv/app/path"
+
+ Plug.Static will be unable to serve assets if you build releases
+ or if you change the current directory. Instead do:
+
+ plug Plug.Static, from: {:app_name, "priv/app/path"}
+
+ If a static asset cannot be found, `Plug.Static` simply forwards
+ the connection to the rest of the pipeline.
+
+ ## Cache mechanisms
+
+ `Plug.Static` uses etags for HTTP caching. This means browsers/clients
+ should cache assets on the first request and validate the cache on
+ following requests, not downloading the static asset once again if it
+ has not changed. The cache-control for etags is specified by the
+ `cache_control_for_etags` option and defaults to `"public"`.
+
+ However, `Plug.Static` also supports direct cache control by using
+ versioned query strings. If the request query string starts with
+ "?vsn=", `Plug.Static` assumes the application is versioning assets
+ and does not set the `ETag` header, meaning the cache behaviour will
+ be specified solely by the `cache_control_for_vsn_requests` config,
+ which defaults to `"public, max-age=31536000"`.
+
+ ## Options
+
+ * `:encodings` - list of 2-ary tuples where first value is value of
+ the `Accept-Encoding` header and second is extension of the file to
+ be served if given encoding is accepted by client. Entries will be tested
+ in order in list, so entries higher in list will be preferred. Defaults
+ to: `[]`.
+
+ In addition to setting this value directly it supports 2 additional
+ options for compatibility reasons:
+
+ + `:brotli` - will append `{"br", ".br"}` to the encodings list.
+ + `:gzip` - will append `{"gzip", ".gz"}` to the encodings list.
+
+ Additional options will be added in the above order (Brotli takes
+ preference over Gzip) to reflect older behaviour which was set due
+ to fact that Brotli in general provides better compression ratio than
+ Gzip.
+
+ * `:cache_control_for_etags` - sets the cache header for requests
+ that use etags. Defaults to `"public"`.
+
+ * `:etag_generation` - specify a `{module, function, args}` to be used
+ to generate an etag. The `path` of the resource will be passed to
+ the function, as well as the `args`. If this option is not supplied,
+ etags will be generated based off of file size and modification time.
+ Note it is [recommended for the etag value to be quoted](https://tools.ietf.org/html/rfc7232#section-2.3),
+ which Plug won't do automatically.
+
+ * `:cache_control_for_vsn_requests` - sets the cache header for
+ requests starting with "?vsn=" in the query string. Defaults to
+ `"public, max-age=31536000"`.
+
+ * `:only` - filters which requests to serve. This is useful to avoid
+ file system access on every request when this plug is mounted
+ at `"/"`. For example, if `only: ["images", "favicon.ico"]` is
+ specified, only files in the "images" directory and the
+ "favicon.ico" file will be served by `Plug.Static`.
+ Note that `Plug.Static` matches these filters against request
+ uri and not against the filesystem. When requesting
+ a file with name containing non-ascii or special characters,
+ you should use urlencoded form. For example, you should write
+ `only: ["file%20name"]` instead of `only: ["file name"]`.
+ Defaults to `nil` (no filtering).
+
+ * `:only_matching` - a relaxed version of `:only` that will
+ serve any request as long as one of the given values matches the
+ given path. For example, `only_matching: ["images", "favicon"]`
+ will match any request that starts at "images" or "favicon",
+ be it "/images/foo.png", "/images-high/foo.png", "/favicon.ico"
+ or "/favicon-high.ico". Such matches are useful when serving
+ digested files at the root. Defaults to `nil` (no filtering).
+
+ * `:headers` - other headers to be set when serving static assets. Specify either
+ an enum of key-value pairs or a `{module, function, args}` to return an enum. The
+ `conn` will be passed to the function, as well as the `args`.
+
+ * `:content_types` - custom MIME type mapping. As a map with filename as key
+ and content type as value. For example:
+ `content_types: %{"apple-app-site-association" => "application/json"}`.
+
+ ## Examples
+
+ This plug can be mounted in a `Plug.Builder` pipeline as follows:
+
+ defmodule MyPlug do
+ use Plug.Builder
+
+ plug Plug.Static,
+ at: "/public",
+ from: :my_app,
+ only: ~w(images robots.txt)
+ plug :not_found
+
+ def not_found(conn, _) do
+ send_resp(conn, 404, "not found")
+ end
+ end
+
+ """
+
+ @behaviour Plug
+ @allowed_methods ~w(GET HEAD)
+
+ import Plug.Conn
+ alias Plug.Conn
+
+ # In this module, the `:prim_file` Erlang module along with the `:file_info`
+ # record are used instead of the more common and Elixir-y `File` module and
+ # `File.Stat` struct, respectively. The reason behind this is performance: all
+ # the `File` operations pass through a single process in order to support node
+ # operations that we simply don't need when serving assets.
+
+ require Record
+ Record.defrecordp(:file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl"))
+
+ defmodule InvalidPathError do
+ defexception message: "invalid path for static asset", plug_status: 400
+ end
+
+ @impl true
+ def init(opts) do
+ from =
+ case Keyword.fetch!(opts, :from) do
+ {_, _} = from -> from
+ {_, _, _} = from -> from
+ from when is_atom(from) -> {from, "priv/static"}
+ from when is_binary(from) -> from
+ _ -> raise ArgumentError, ":from must be an atom, a binary or a tuple"
+ end
+
+ encodings =
+ opts
+ |> Keyword.get(:encodings, [])
+ |> maybe_add("br", ".br", Keyword.get(opts, :brotli, false))
+ |> maybe_add("gzip", ".gz", Keyword.get(opts, :gzip, false))
+
+ %{
+ encodings: encodings,
+ only_rules: {Keyword.get(opts, :only, []), Keyword.get(opts, :only_matching, [])},
+ qs_cache: Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000"),
+ et_cache: Keyword.get(opts, :cache_control_for_etags, "public"),
+ et_generation: Keyword.get(opts, :etag_generation, nil),
+ headers: Keyword.get(opts, :headers, %{}),
+ content_types: Keyword.get(opts, :content_types, %{}),
+ from: from,
+ at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split()
+ }
+ end
+
+ @impl true
+ def call(
+ conn = %Conn{method: meth},
+ %{at: at, only_rules: only_rules, from: from, encodings: encodings} = options
+ )
+ when meth in @allowed_methods do
+ segments = subset(at, conn.path_info)
+
+ if allowed?(only_rules, segments) do
+ segments = Enum.map(segments, &uri_decode/1)
+
+ if invalid_path?(segments) do
+ raise InvalidPathError, "invalid path for static asset: #{conn.request_path}"
+ end
+
+ path = path(from, segments)
+ range = get_req_header(conn, "range")
+ encoding = file_encoding(conn, path, range, encodings)
+ serve_static(encoding, conn, segments, range, options)
+ else
+ conn
+ end
+ end
+
+ def call(conn, _options) do
+ conn
+ end
+
+ defp uri_decode(path) do
+ # TODO: Remove rescue as this can't fail from Elixir v1.13
+ try do
+ URI.decode(path)
+ rescue
+ ArgumentError ->
+ raise InvalidPathError
+ end
+ end
+
+ defp allowed?(_only_rules, []), do: false
+ defp allowed?({[], []}, _list), do: true
+
+ defp allowed?({full, prefix}, [h | _]) do
+ h in full or (prefix != [] and match?({0, _}, :binary.match(h, prefix)))
+ end
+
+ defp serve_static({content_encoding, file_info, path}, conn, segments, range, options) do
+ %{
+ qs_cache: qs_cache,
+ et_cache: et_cache,
+ et_generation: et_generation,
+ headers: headers,
+ content_types: types
+ } = options
+
+ case put_cache_header(conn, qs_cache, et_cache, et_generation, file_info, path) do
+ {:stale, conn} ->
+ filename = List.last(segments)
+ content_type = Map.get(types, filename) || MIME.from_path(filename)
+
+ conn
+ |> put_resp_header("content-type", content_type)
+ |> put_resp_header("accept-ranges", "bytes")
+ |> maybe_add_encoding(content_encoding)
+ |> merge_headers(headers)
+ |> serve_range(file_info, path, range, options)
+
+ {:fresh, conn} ->
+ conn
+ |> maybe_add_vary(options)
+ |> send_resp(304, "")
+ |> halt()
+ end
+ end
+
+ defp serve_static(:error, conn, _segments, _range, _options) do
+ conn
+ end
+
+ defp serve_range(conn, file_info, path, [range], options) do
+ file_info(size: file_size) = file_info
+
+ with %{"bytes" => bytes} <- Plug.Conn.Utils.params(range),
+ {range_start, range_end} <- start_and_end(bytes, file_size) do
+ send_range(conn, path, range_start, range_end, file_size, options)
+ else
+ _ -> send_entire_file(conn, path, options)
+ end
+ end
+
+ defp serve_range(conn, _file_info, path, _range, options) do
+ send_entire_file(conn, path, options)
+ end
+
+ defp start_and_end("-" <> rest, file_size) do
+ case Integer.parse(rest) do
+ {last, ""} when last > 0 and last <= file_size -> {file_size - last, file_size - 1}
+ _ -> :error
+ end
+ end
+
+ defp start_and_end(range, file_size) do
+ case Integer.parse(range) do
+ {first, "-"} when first >= 0 ->
+ {first, file_size - 1}
+
+ {first, "-" <> rest} when first >= 0 ->
+ case Integer.parse(rest) do
+ {last, ""} when last >= first -> {first, min(last, file_size - 1)}
+ _ -> :error
+ end
+
+ _ ->
+ :error
+ end
+ end
+
+ defp send_range(conn, path, 0, range_end, file_size, options) when range_end == file_size - 1 do
+ send_entire_file(conn, path, options)
+ end
+
+ defp send_range(conn, path, range_start, range_end, file_size, _options) do
+ length = range_end - range_start + 1
+
+ conn
+ |> put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file_size}")
+ |> send_file(206, path, range_start, length)
+ |> halt()
+ end
+
+ defp send_entire_file(conn, path, options) do
+ conn
+ |> maybe_add_vary(options)
+ |> send_file(200, path)
+ |> halt()
+ end
+
+ defp maybe_add_encoding(conn, nil), do: conn
+ defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce)
+
+ defp maybe_add_vary(conn, %{encodings: encodings}) do
+ # If we serve gzip or brotli at any moment, we need to set the proper vary
+ # header regardless of whether we are serving gzip content right now.
+ # See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
+ if encodings != [] do
+ update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1])
+ else
+ conn
+ end
+ end
+
+ defp put_cache_header(
+ %Conn{query_string: "vsn=" <> _} = conn,
+ qs_cache,
+ _et_cache,
+ _et_generation,
+ _file_info,
+ _path
+ )
+ when is_binary(qs_cache) do
+ {:stale, put_resp_header(conn, "cache-control", qs_cache)}
+ end
+
+ defp put_cache_header(conn, _qs_cache, et_cache, et_generation, file_info, path)
+ when is_binary(et_cache) do
+ etag = etag_for_path(file_info, et_generation, path)
+
+ conn =
+ conn
+ |> put_resp_header("cache-control", et_cache)
+ |> put_resp_header("etag", etag)
+
+ if etag in get_req_header(conn, "if-none-match") do
+ {:fresh, conn}
+ else
+ {:stale, conn}
+ end
+ end
+
+ defp put_cache_header(conn, _, _, _, _, _) do
+ {:stale, conn}
+ end
+
+ defp etag_for_path(file_info, et_generation, path) do
+ case et_generation do
+ {module, function, args} ->
+ apply(module, function, [path | args])
+
+ nil ->
+ file_info(size: size, mtime: mtime) = file_info
+ <<?", {size, mtime} |> :erlang.phash2() |> Integer.to_string(16)::binary, ?">>
+ end
+ end
+
+ defp file_encoding(conn, path, [_range], _encodings) do
+ # We do not support compression for range queries.
+ file_encoding(conn, path, nil, [])
+ end
+
+ defp file_encoding(conn, path, _range, encodings) do
+ encoded =
+ Enum.find_value(encodings, fn {encoding, ext} ->
+ if file_info = accept_encoding?(conn, encoding) && regular_file_info(path <> ext) do
+ {encoding, file_info, path <> ext}
+ end
+ end)
+
+ cond do
+ not is_nil(encoded) ->
+ encoded
+
+ file_info = regular_file_info(path) ->
+ {nil, file_info, path}
+
+ true ->
+ :error
+ end
+ end
+
+ defp regular_file_info(path) do
+ case :prim_file.read_file_info(path) do
+ {:ok, file_info(type: :regular) = file_info} ->
+ file_info
+
+ _ ->
+ nil
+ end
+ end
+
+ defp accept_encoding?(conn, encoding) do
+ encoding? = &String.contains?(&1, [encoding, "*"])
+
+ Enum.any?(get_req_header(conn, "accept-encoding"), fn accept ->
+ accept |> Plug.Conn.Utils.list() |> Enum.any?(encoding?)
+ end)
+ end
+
+ defp maybe_add(list, key, value, true), do: list ++ [{key, value}]
+ defp maybe_add(list, _key, _value, false), do: list
+
+ defp path({module, function, arguments}, segments)
+ when is_atom(module) and is_atom(function) and is_list(arguments),
+ do: Enum.join([apply(module, function, arguments) | segments], "/")
+
+ defp path({app, from}, segments) when is_atom(app) and is_binary(from),
+ do: Enum.join([Application.app_dir(app), from | segments], "/")
+
+ defp path(from, segments),
+ do: Enum.join([from | segments], "/")
+
+ defp subset([h | expected], [h | actual]), do: subset(expected, actual)
+ defp subset([], actual), do: actual
+ defp subset(_, _), do: []
+
+ defp invalid_path?(list) do
+ invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
+ end
+
+ defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
+ defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
+ defp invalid_path?([], _match), do: false
+
+ defp merge_headers(conn, {module, function, args}) do
+ merge_headers(conn, apply(module, function, [conn | args]))
+ end
+
+ defp merge_headers(conn, headers) do
+ merge_resp_headers(conn, headers)
+ end
+end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Aug 9, 4:00 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724184
Default Alt Text
(15 KB)

Event Timeline