diff --git a/README.md b/README.md index f57c059..c78bd08 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ NOTE: it's recommended to not run the container as root. Doing so can create per HTTP basic authentication is optionally supported. To use it, set the `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables when starting the container. No authentication will be required unless you set _both_ of these. +### Important note: + +The media streaming endpoint is not protected by basic auth. To help protect your media, these endpoints work with UUIDs instead of sequential IDs but this is still essentially security through obscurity. If you're concerned about the security of your media, consider using a reverse proxy with authentication or a VPN. + ## EFF donations A portion of all donations to Pinchflat will be donated to the [Electronic Frontier Foundation](https://www.eff.org/). The EFF defends your online liberties and [backed](https://github.com/github/dmca/blob/9a85e0f021f7967af80e186b890776a50443f06c/2020/11/2020-11-16-RIAA-reversal-effletter.pdf) `youtube-dl` when Google took them down. [See here](https://github.com/kieraneglin/pinchflat/wiki/EFF-Donation-Receipts) for a list of donation receipts. diff --git a/lib/pinchflat_web/controllers/media_items/media_item_controller.ex b/lib/pinchflat_web/controllers/media_items/media_item_controller.ex index 56f8f7d..bf44c77 100644 --- a/lib/pinchflat_web/controllers/media_items/media_item_controller.ex +++ b/lib/pinchflat_web/controllers/media_items/media_item_controller.ex @@ -3,6 +3,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do alias Pinchflat.Repo alias Pinchflat.Media + alias Pinchflat.Media.MediaItem def show(conn, %{"id" => id}) do media_item = @@ -32,13 +33,11 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do # See here for details on streaming files and range requests: # https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/ - def stream(conn, %{"id" => id}) do - media_item = Media.get_media_item!(id) - - # TODO: show audio vs. video element in UI depending on media type - # TODO: consider how a podcast RSS feed would interact with HTTP basic auth - # TODO: reconsider the sobelow changes I made - # TODO: UUID stuff + # + # Uses the UUID instead of the ID to avoid enumeration attacks + # since streaming is a public endpoint (ie: no auth required) + def stream(conn, %{"id" => uuid}) do + media_item = Repo.get_by!(MediaItem, uuid: uuid) if File.exists?(media_item.media_filepath) do file_size = File.stat!(media_item.media_filepath).size diff --git a/lib/pinchflat_web/controllers/media_items/media_item_html.ex b/lib/pinchflat_web/controllers/media_items/media_item_html.ex index ffae16a..12d7ffa 100644 --- a/lib/pinchflat_web/controllers/media_items/media_item_html.ex +++ b/lib/pinchflat_web/controllers/media_items/media_item_html.ex @@ -2,4 +2,16 @@ defmodule PinchflatWeb.MediaItems.MediaItemHTML do use PinchflatWeb, :html embed_templates "media_item_html/*" + + def media_file_exists?(media_item) do + !!media_item.media_filepath and File.exists?(media_item.media_filepath) + end + + def media_type(media_item) do + case Path.extname(media_item.media_filepath) do + ext when ext in [".mp4", ".webm", ".mkv"] -> :video + ext when ext in [".mp3", ".m4a"] -> :audio + _ -> :unknown + end + end end diff --git a/lib/pinchflat_web/controllers/media_items/media_item_html/media_preview.heex b/lib/pinchflat_web/controllers/media_items/media_item_html/media_preview.heex new file mode 100644 index 0000000..2146a98 --- /dev/null +++ b/lib/pinchflat_web/controllers/media_items/media_item_html/media_preview.heex @@ -0,0 +1,13 @@ +<%= if media_type(@media_item) == :video do %> + +<% end %> + +<%= if media_type(@media_item) == :audio do %> + +<% end %> diff --git a/lib/pinchflat_web/controllers/media_items/media_item_html/show.html.heex b/lib/pinchflat_web/controllers/media_items/media_item_html/show.html.heex index 3d379e5..7d56c51 100644 --- a/lib/pinchflat_web/controllers/media_items/media_item_html/show.html.heex +++ b/lib/pinchflat_web/controllers/media_items/media_item_html/show.html.heex @@ -8,19 +8,16 @@ -
+
<.tabbed_layout> <:tab title="Attributes">
- <%!-- --%> - + <%= if media_file_exists?(@media_item) do %> +

Preview

+ <.media_preview media_item={@media_item} /> + <% end %> +

Attributes

Source: diff --git a/lib/pinchflat_web/router.ex b/lib/pinchflat_web/router.ex index a45efa2..eae7109 100644 --- a/lib/pinchflat_web/router.ex +++ b/lib/pinchflat_web/router.ex @@ -30,6 +30,9 @@ defmodule PinchflatWeb.Router do # Routes in here are NOT protected by basic auth. This is necessary for # media streaming to work for RSS podcast feeds. + # + # TODO: consider putting the basic auth here behind a config flag + # so people that want RSS feeds to work can enable it. scope "/", PinchflatWeb do get "/media/:id/stream", MediaItems.MediaItemController, :stream end diff --git a/test/pinchflat_web/controllers/media_item_controller_test.exs b/test/pinchflat_web/controllers/media_item_controller_test.exs index 71992a4..428d337 100644 --- a/test/pinchflat_web/controllers/media_item_controller_test.exs +++ b/test/pinchflat_web/controllers/media_item_controller_test.exs @@ -69,14 +69,14 @@ defmodule PinchflatWeb.MediaItemControllerTest do describe "streaming media" do test "returns 404 if the media isn't found", %{conn: conn} do media_item = media_item_fixture() - conn = get(conn, ~p"/media/#{media_item.id}/stream") + conn = get(conn, ~p"/media/#{media_item.uuid}/stream") assert conn.status == 404 end test "automatically sets the content type", %{conn: conn} do media_item = media_item_with_attachments() - conn = get(conn, ~p"/media/#{media_item.id}/stream") + conn = get(conn, ~p"/media/#{media_item.uuid}/stream") assert {"content-type", "video/mp4; charset=utf-8"} in conn.resp_headers end @@ -85,7 +85,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do media_item = media_item_with_attachments() filesize = File.stat!(media_item.media_filepath).size - conn = get(conn, ~p"/media/#{media_item.id}/stream") + conn = get(conn, ~p"/media/#{media_item.uuid}/stream") assert {"content-length", to_string(filesize)} in conn.resp_headers end @@ -104,7 +104,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=0-100") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.status == 206 assert {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers @@ -115,7 +115,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=0-100") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert byte_size(conn.resp_body) == 101 end @@ -127,7 +127,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=100-200") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.resp_body == expected end @@ -139,7 +139,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=0-#{filesize * 10}") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.resp_body == contents assert {"content-range", "bytes 0-#{filesize - 1}/#{filesize}"} in conn.resp_headers @@ -152,7 +152,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=0-") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.resp_body == contents end @@ -164,7 +164,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=100-") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.resp_body == expected end @@ -180,7 +180,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do test "sets the correct status and headers", %{conn: conn, media_item: media_item} do filesize = File.stat!(media_item.media_filepath).size - conn = get(conn, ~p"/media/#{media_item.id}/stream") + conn = get(conn, ~p"/media/#{media_item.uuid}/stream") assert conn.status == 200 assert {"content-length", to_string(filesize)} in conn.resp_headers @@ -189,7 +189,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do test "streams the entire file", %{conn: conn, media_item: media_item} do contents = File.read!(media_item.media_filepath) - conn = get(conn, ~p"/media/#{media_item.id}/stream") + conn = get(conn, ~p"/media/#{media_item.uuid}/stream") assert conn.resp_body == contents end @@ -200,7 +200,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do conn = conn |> put_req_header("range", "bytes=-") - |> get(~p"/media/#{media_item.id}/stream") + |> get(~p"/media/#{media_item.uuid}/stream") assert conn.status == 200 assert conn.resp_body == contents