From 1e539d908d7cbc8bf3e60e6313bae7a56913cd47 Mon Sep 17 00:00:00 2001 From: CJ_Clippy Date: Sat, 1 Feb 2025 20:00:49 -0800 Subject: [PATCH] add bittorrent url encoding --- docker-compose.yml | 3 +- services/bright/config/runtime.exs | 3 +- .../lib/bright/bittorrent_url_encode.ex | 39 +++++++ .../lib/bright/oban_workers/create_torrent.ex | 18 +-- services/bright/lib/bright/torrent.ex | 44 ------- services/bright/lib/bright/torrentfile.ex | 22 ++++ services/bright/lib/bright/torrents.ex | 42 ++++++- services/bright/lib/bright/tracker.ex | 108 ++++++++++++++++-- .../controllers/torrent_controller.ex | 62 ++++++++++ .../bright_web/controllers/torrent_html.ex | 13 +++ .../controllers/torrent_html/edit.html.heex | 8 ++ .../controllers/torrent_html/index.html.heex | 26 +++++ .../controllers/torrent_html/new.html.heex | 8 ++ .../controllers/torrent_html/show.html.heex | 18 +++ .../torrent_html/torrent_form.html.heex | 12 ++ .../bright_web/controllers/torrent_json.ex | 26 +++++ services/bright/lib/bright_web/router.ex | 2 +- services/bright/mix.exs | 1 + services/bright/mix.lock | 1 + .../test/bright/bittorrent_url_encode_test.ex | 55 +++++++++ services/bright/test/bright/cache_test.ex | 12 +- .../oban_workers/create_torrent_test.exs | 2 +- services/bright/test/bright/torrent_test.exs | 49 -------- .../bright/test/bright/torrentfile_test.ex | 13 ++- services/bright/test/bright/torrents_test.exs | 37 +++++- services/bright/test/bright/tracker_test.exs | 55 ++++++--- .../controllers/torrent_controller_test.exs | 84 ++++++++++++++ 27 files changed, 613 insertions(+), 150 deletions(-) create mode 100644 services/bright/lib/bright/bittorrent_url_encode.ex delete mode 100644 services/bright/lib/bright/torrent.ex create mode 100644 services/bright/lib/bright_web/controllers/torrent_controller.ex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html.ex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html/edit.html.heex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html/index.html.heex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html/new.html.heex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html/show.html.heex create mode 100644 services/bright/lib/bright_web/controllers/torrent_html/torrent_form.html.heex create mode 100644 services/bright/lib/bright_web/controllers/torrent_json.ex create mode 100644 services/bright/test/bright/bittorrent_url_encode_test.ex delete mode 100644 services/bright/test/bright/torrent_test.exs create mode 100644 services/bright/test/bright_web/controllers/torrent_controller_test.exs diff --git a/docker-compose.yml b/docker-compose.yml index f4249fb..4c247cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: dockerfile: dockerfiles/opentracker.dockerfile container_name: opentracker environment: - - WHITELIST_FEED_URL=http://bright:4000/torrents/whitelist?type=json + - WHITELIST_FEED_URL=http://bright:4000/torrents ports: - "6969:6969/tcp" - "6969:6969/udp" @@ -63,6 +63,7 @@ services: SUPERSTREAMER_URL: http://superstreamer-api:52001 PUBLIC_S3_ENDPOINT: https://fp-dev.b-cdn.net BT_TRACKER_URL: https://tracker.futureporn.net/announce + BT_TRACKER_ACCESSLIST_URL: http://opentracker:8666 SITE_URL: https://futureporn.net env_file: - .kamal/secrets.development diff --git a/services/bright/config/runtime.exs b/services/bright/config/runtime.exs index c9382a1..7c5ef23 100644 --- a/services/bright/config/runtime.exs +++ b/services/bright/config/runtime.exs @@ -30,7 +30,8 @@ config :bright, superstreamer_auth_token: System.get_env("SUPERSTREAMER_AUTH_TOKEN"), public_s3_endpoint: System.get_env("PUBLIC_S3_ENDPOINT"), s3_cdn_endpoint: System.get_env("PUBLIC_S3_ENDPOINT"), - bittorrent_tracker_url: System.get_env("BT_TRACKER_URL"), + bt_tracker_url: System.get_env("BT_TRACKER_URL"), + bt_tracker_accesslist_url: System.get_env("BT_TRACKER_ACCESSLIST_URL"), site_url: System.get_env("SITE_URL") config :bright, :buckets, diff --git a/services/bright/lib/bright/bittorrent_url_encode.ex b/services/bright/lib/bright/bittorrent_url_encode.ex new file mode 100644 index 0000000..207399d --- /dev/null +++ b/services/bright/lib/bright/bittorrent_url_encode.ex @@ -0,0 +1,39 @@ +defmodule Bright.BittorrentUrlEncode do + @moduledoc """ + URL encoding for Bittorrent Info hash v1. https://www.bittorrent.org/beps/bep_0003.html + Contains deviations from Elixir's URI.encode(). https://github.com/elixir-lang/elixir/blob/78f63d08313677a680868685701ae79a2459dcc1/lib/elixir/lib/uri.ex#L395 + Contains deviations from RFC3986 Section 2.2. https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + Designed to be compatible with qBittorrent's percent encoding. + """ + import Bitwise + + + @spec encode(binary()) :: binary() + def encode(string) when is_binary(string) do + string = Base.decode16!(string, case: :lower) + enc(string, &char_allowed?/1) + end + + @spec enc(binary, (byte -> as_boolean(term))) :: binary + defp enc(string, predicate) + when is_binary(string) and is_function(predicate, 1) do + for <>, into: "", do: percent(byte, predicate) + end + + defp char_allowed?(character) do + character in ?0..?9 or character in ?a..?z or character in ?A..?Z or character in ~c"~_-.!" + end + + defp percent(char, predicate) do + if predicate.(char) do + <> + else + <<"%", hex(bsr(char, 4)), hex(band(char, 15))>> + end + end + + defp hex(n) when n <= 9, do: n + ?0 + defp hex(n), do: n + ?a - 10 + + +end diff --git a/services/bright/lib/bright/oban_workers/create_torrent.ex b/services/bright/lib/bright/oban_workers/create_torrent.ex index f9c30b7..4e5a532 100644 --- a/services/bright/lib/bright/oban_workers/create_torrent.ex +++ b/services/bright/lib/bright/oban_workers/create_torrent.ex @@ -9,8 +9,9 @@ defmodule Bright.ObanWorkers.CreateTorrent do B2, Images, Cache, - Torrent, - Tracker + Torrents, + Tracker, + Torrentfile } require Logger import Ecto.Query, warn: false @@ -18,13 +19,16 @@ defmodule Bright.ObanWorkers.CreateTorrent do def perform(%Oban.Job{args: %{"vod_id" => vod_id}}) do + IO.puts "hello this is Oban.Job and we are testing to see if we can get env vars here. Application.fetch_env!(:bright, :bt_tracker_accesslist_url)=#{Application.fetch_env!(:bright, :bt_tracker_accesslist_url)} System.get_env('BT_TRACKER_ACCESSLIST_URL')=#{System.get_env("BT_TRACKER_ACCESSLIST_URL")}" vod = Streams.get_vod!(vod_id) with {:ok, filename} <- B2.get(vod), - {:ok, torrent} <- Torrent.create_torrent(vod), - {:ok, %{cdn_url: cdn_url}} <- B2.put(torrent.local_path, torrent.basename), - :ok <- Tracker.whitelist_info_hash(torrent.info_hash), - :ok <- Tracker.announce_torrent(torrent.magnet_link), - {:ok, updated_vod} <- Streams.update_vod(vod, %{torrent: cdn_url, magnet_link: torrent.magnet_link}) do + {:ok, tf} <- Torrentfile.create(vod, filename), + {:ok, %{cdn_url: cdn_url}} <- B2.put(tf.save_path, Path.basename(tf.save_path)), + {:ok, torrent} <- Torrents.create_torrent(%{info_hash_v1: tf.btih, info_hash_v2: tf.btmh, cdn_url: cdn_url, magnet: tf.magnet}), + :ok <- Tracker.whitelist_info_hash(torrent.info_hash_v1), + :ok <- Tracker.whitelist_info_hash(torrent.info_hash_v2), + :ok <- Tracker.announce_torrent(torrent.magnet), + {:ok, updated_vod} <- Streams.update_vod(vod, %{torrent_id: torrent.id}) do {:ok, updated_vod} end end diff --git a/services/bright/lib/bright/torrent.ex b/services/bright/lib/bright/torrent.ex deleted file mode 100644 index 8e5beba..0000000 --- a/services/bright/lib/bright/torrent.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Bright.Torrent do - - - alias Bright.Streams.Vod - alias Bright.{Cache,Torrentfile,B2} - - - def bittorrent_tracker_url do - Application.fetch_env!(:bright, :bittorrent_tracker_url) - end - - def site_url do - Application.fetch_env!(:bright, :site_url) - end - - - - def create_torrent(input_path, output_path, web_seed_url, vod_id) do - IO.puts "site_url=#{site_url()}" - IO.puts "bittorrent_tracker_url=#{bittorrent_tracker_url()}" - tracker_url = bittorrent_tracker_url() - source_url = URI.append_path(URI.parse(site_url()), "/vods/#{vod_id}") |> URI.to_string() - comment = site_url() - meta_version = 3 # hybrid BT v1 & v2 - - {:ok, %{btih: btih, btmh: btmh, magnet: magnet, save_path: save_path} = torrentfile} = Torrentfile.create(input_path, output_path, tracker_url, source_url, comment, web_seed_url, meta_version) - - - # upload to s3 - basename = Path.basename(save_path) - {:ok, asset} = B2.put(save_path, basename) - - {:ok, %{basename: basename, local_path: save_path, magnet_link: magnet, info_hash_v1: btih, info_hash_v2: btmh}} - - - end - - - - - - - -end diff --git a/services/bright/lib/bright/torrentfile.ex b/services/bright/lib/bright/torrentfile.ex index 04248d0..4424853 100644 --- a/services/bright/lib/bright/torrentfile.ex +++ b/services/bright/lib/bright/torrentfile.ex @@ -4,6 +4,16 @@ defmodule Bright.Torrentfile do """ alias Bright.Cache + alias Bright.Streams.Vod + + + def bittorrent_tracker_url do + Application.fetch_env!(:bright, :bt_tracker_url) + end + + def site_url do + Application.fetch_env!(:bright, :site_url) + end # @spec execute(command :: Command.t) :: {:ok, binary()} | {:error, {Collectable.t, exit_status :: non_neg_integer}} # def execute(%Command{} = command) do @@ -66,7 +76,19 @@ defmodule Bright.Torrentfile do defp extract_last(nil), do: nil defp extract_last(list) when is_list(list), do: List.last(list) + def create(%Vod{} = vod, input_path) do + output_path = Cache.generate_filename("vod-#{vod.id}", "torrent") + tracker_url = bittorrent_tracker_url() + site_url = site_url() + comment = site_url + source_url = URI.parse(site_url) |> URI.append_path("/vods/#{vod.id}") |> URI.to_string() + web_seed_url = vod.s3_cdn_url + meta_version = 3 + create(input_path, output_path, tracker_url, comment, source_url, web_seed_url, meta_version) + end + def create(input_path, output_path, tracker_url, source_url, comment, web_seed_url, meta_version) do + IO.puts "Torrentfile.create called with args input_path=#{input_path}, output_path=#{output_path}, tracker_url=#{tracker_url}, source_url=#{source_url}, comment=#{comment}, web_seed_url=#{web_seed_url}, meta_version=#{meta_version}" case Rambo.run(torrentfile_path(), [ "--magnet", "--prog", "0", diff --git a/services/bright/lib/bright/torrents.ex b/services/bright/lib/bright/torrents.ex index 5b5bb91..5be5a5b 100644 --- a/services/bright/lib/bright/torrents.ex +++ b/services/bright/lib/bright/torrents.ex @@ -4,20 +4,24 @@ defmodule Bright.Torrents do """ import Ecto.Query, warn: false - alias Bright.Repo + alias Bright.Streams.Vod + alias Bright.{Repo,Cache,Torrentfile,B2} alias Bright.Torrents.Torrent + + + @doc """ Returns the list of torrent. ## Examples - iex> list_torrent() + iex> list_torrents() [%Torrent{}, ...] """ - def list_torrent do + def list_torrents do Repo.all(Torrent) end @@ -37,6 +41,7 @@ defmodule Bright.Torrents do """ def get_torrent!(id), do: Repo.get!(Torrent, id) + @doc """ Creates a torrent. @@ -55,6 +60,10 @@ defmodule Bright.Torrents do |> Repo.insert() end + + + + @doc """ Updates a torrent. @@ -101,4 +110,31 @@ defmodule Bright.Torrents do def change_torrent(%Torrent{} = torrent, attrs \\ %{}) do Torrent.changeset(torrent, attrs) end + + + + # @doc """ + # Generates a torrent file on disk, then uploads it to S3 + # """ + # def generate_torrent_file(input_path, output_path, web_seed_url, vod_id) do + # s3_asset_key = Cache.deterministic_filename("vod", attrs.vod_id, "mp4") + # output_path = Cache.generate_filename(s3_asset_key, "torrent") + # input_path = Cache.get(s3_asset_key).local_path + # tracker_url = bittorrent_tracker_url() + # source_url = URI.append_path(URI.parse(site_url()), "/vods/#{vod_id}") |> URI.to_string() + # comment = site_url() + # meta_version = 3 # hybrid BT v1 & v2 + + # {:ok, %{btih: btih, btmh: btmh, magnet: magnet, save_path: save_path} = torrentfile} = Torrentfile.create(input_path, output_path, tracker_url, source_url, comment, web_seed_url, meta_version) + + + # # upload to s3 + # {:ok, asset} = B2.put(save_path, s3_asset_key) + + # # {:ok, %{basename: basename, local_path: save_path, magnet_link: magnet, info_hash_v1: btih, info_hash_v2: btmh}} + # end + + + + end diff --git a/services/bright/lib/bright/tracker.ex b/services/bright/lib/bright/tracker.ex index 79ea707..6b663bc 100644 --- a/services/bright/lib/bright/tracker.ex +++ b/services/bright/lib/bright/tracker.ex @@ -1,25 +1,117 @@ defmodule Bright.Tracker do + @unreserved_chars 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~' alias Bright.Streams.Vod alias Bright.{Cache,Torrentfile,B2} - def tracker_url do - Application.get_env!(:bright, :bt_tracker_url) + def bt_tracker_url do + Application.fetch_env!(:bright, :bt_tracker_url) end + def bt_tracker_accesslist_url do + Application.fetch_env!(:bright, :bt_tracker_accesslist_url) + end + + + + # We start with the `Info hash v1` of the torrent, as copied from qBittorrent. + info_hash_v1 = "acc3b2e433d7c7475abb5941b5681cb7a1ea26e2" + + # Next we decode into binary. + binary = Base.decode16!(info_hash_v1, case: :lower) + + # Next we + + + + + @doc """ + Encodes `string` as BEP3's weird URL encoded string. + + ## Example + + iex> bep3_encode("a88fda5954e89178c372716a6a78b8180ed4dad3") + "%A8%8F%DAYT%E8%91x%C3rqjjx%B8%18%0E%D4%DA%D3" + + """ + @spec bep3_encode(binary) :: binary + def bep3_encode(string) when is_binary(string) do + string = Base.decode16!(string, case: :lower) + URI.encode(string, &URI.char_unreserved?/1) + end + + + + + + + # @spec bep3_encode(binary) :: binary + # def bep3_encode(string) when is_binary(string) do + # string = Base.decode16!(string, case: :lower) + # IO.puts inspect(string) + # for <>, into: "" do + # percent(byte, &URI.char_unreserved?/1) + # end + # end + + + + # defp percent(char, predicate) do + # if predicate.(char) do + # <> + # else + # <<"%", hex(Bitwise.bsr(char, 4)), hex(Bitwise.band(char, 15))>> + # end + # end + + # defp hex(n) when n <= 9, do: n + ?0 + # defp hex(n), do: n + ?A - 10 + + # def url_encode_info_hash(info_hash) do + # info_hash |> Base.decode16!(case: :lower) |> URI.encode_www_form() |> String.replace("+", "%20") + # end def announce_torrent(info_hash) do + encoded_info_hash = bep3_encode(info_hash) + url = bt_tracker_url() |> URI.parse() |> URI.append_query("info_hash=#{encoded_info_hash}") |> URI.to_string() + body = [] + headers = [] + + case HTTPoison.get(url, body, headers) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + IO.puts inspect(Bento.decode(body)) + {:ok, body} + + {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + {:error, %{status: status, body: body}} + + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} + + failed -> + Logger.error("Failed to POST. #{inspect(failed)}") + {:error, :failed} + end end + def whitelist_info_hash(info_hash) do - server = "tcp://ncat:8666" - port = 8666 + IO.puts "bt_tracker_accesslist_url=#{bt_tracker_accesslist_url()}" + {host, port} = case URI.parse(bt_tracker_accesslist_url()) do + %URI{host: host, port: port} when not is_nil(host) and not is_nil(port) -> + {host, port} + + _ -> + raise "Invalid bt_tracker_accesslist_url: #{bt_tracker_accesslist_url()}" + end + + host = String.to_charlist(host) # Open a TCP connection - {:ok, socket} = :gen_tcp.connect(server, port, [:binary, packet: :raw, active: false]) + {:ok, socket} = :gen_tcp.connect(host, port, [:binary, packet: :raw, active: false]) # Send the "hello world" data to the server :gen_tcp.send(socket, "#{info_hash}\n") @@ -28,12 +120,6 @@ defmodule Bright.Tracker do :gen_tcp.close(socket) - # url = "http://ncat:6868" - # body = [ - - # ] - # headers = [] - # HTTPoison.post(url, body, headers) end end diff --git a/services/bright/lib/bright_web/controllers/torrent_controller.ex b/services/bright/lib/bright_web/controllers/torrent_controller.ex new file mode 100644 index 0000000..96f1ab2 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_controller.ex @@ -0,0 +1,62 @@ +defmodule BrightWeb.TorrentController do + use BrightWeb, :controller + + alias Bright.Torrents + alias Bright.Torrents.Torrent + + def index(conn, _params) do + torrent = Torrents.list_torrents() + render(conn, :index, torrents: torrent) + end + + def new(conn, _params) do + changeset = Torrents.change_torrent(%Torrent{}) + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"torrent" => torrent_params}) do + case Torrents.create_torrent(torrent_params) do + {:ok, torrent} -> + conn + |> put_flash(:info, "Torrent created successfully.") + |> redirect(to: ~p"/torrent/#{torrent}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + torrent = Torrents.get_torrent!(id) + render(conn, :show, torrent: torrent) + end + + def edit(conn, %{"id" => id}) do + torrent = Torrents.get_torrent!(id) + changeset = Torrents.change_torrent(torrent) + render(conn, :edit, torrent: torrent, changeset: changeset) + end + + def update(conn, %{"id" => id, "torrent" => torrent_params}) do + torrent = Torrents.get_torrent!(id) + + case Torrents.update_torrent(torrent, torrent_params) do + {:ok, torrent} -> + conn + |> put_flash(:info, "Torrent updated successfully.") + |> redirect(to: ~p"/torrent/#{torrent}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, torrent: torrent, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + torrent = Torrents.get_torrent!(id) + {:ok, _torrent} = Torrents.delete_torrent(torrent) + + conn + |> put_flash(:info, "Torrent deleted successfully.") + |> redirect(to: ~p"/torrent") + end +end diff --git a/services/bright/lib/bright_web/controllers/torrent_html.ex b/services/bright/lib/bright_web/controllers/torrent_html.ex new file mode 100644 index 0000000..815f92c --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html.ex @@ -0,0 +1,13 @@ +defmodule BrightWeb.TorrentHTML do + use BrightWeb, :html + + embed_templates "torrent_html/*" + + @doc """ + Renders a torrent form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def torrent_form(assigns) +end diff --git a/services/bright/lib/bright_web/controllers/torrent_html/edit.html.heex b/services/bright/lib/bright_web/controllers/torrent_html/edit.html.heex new file mode 100644 index 0000000..664da01 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit Torrent {@torrent.id} + <:subtitle>Use this form to manage torrent records in the database. + + +<.torrent_form changeset={@changeset} action={~p"/torrent/#{@torrent}"} /> + +<.back navigate={~p"/torrent"}>Back to torrent diff --git a/services/bright/lib/bright_web/controllers/torrent_html/index.html.heex b/services/bright/lib/bright_web/controllers/torrent_html/index.html.heex new file mode 100644 index 0000000..cee9bc9 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html/index.html.heex @@ -0,0 +1,26 @@ +<.header> + Listing Torrent + <:actions> + <.link href={~p"/torrent/new"}> + <.button>New Torrent + + + + +<.table id="torrent" rows={@torrents} row_click={&JS.navigate(~p"/torrent/#{&1}")}> + <:col :let={torrent} label="Info hash v1">{torrent.info_hash_v1} + <:col :let={torrent} label="Info hash v2">{torrent.info_hash_v2} + <:col :let={torrent} label="Cdn url">{torrent.cdn_url} + <:col :let={torrent} label="Magnet">{torrent.magnet} + <:action :let={torrent}> +
+ <.link navigate={~p"/torrent/#{torrent}"}>Show +
+ <.link navigate={~p"/torrent/#{torrent}/edit"}>Edit + + <:action :let={torrent}> + <.link href={~p"/torrent/#{torrent}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/services/bright/lib/bright_web/controllers/torrent_html/new.html.heex b/services/bright/lib/bright_web/controllers/torrent_html/new.html.heex new file mode 100644 index 0000000..0232970 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New Torrent + <:subtitle>Use this form to manage torrent records in the database. + + +<.torrent_form changeset={@changeset} action={~p"/torrent"} /> + +<.back navigate={~p"/torrent"}>Back to torrent diff --git a/services/bright/lib/bright_web/controllers/torrent_html/show.html.heex b/services/bright/lib/bright_web/controllers/torrent_html/show.html.heex new file mode 100644 index 0000000..b82b89a --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html/show.html.heex @@ -0,0 +1,18 @@ +<.header> + Torrent {@torrent.id} + <:subtitle>This is a torrent record from the database. + <:actions> + <.link href={~p"/torrent/#{@torrent}/edit"}> + <.button>Edit torrent + + + + +<.list> + <:item title="Info hash v1">{@torrent.info_hash_v1} + <:item title="Info hash v2">{@torrent.info_hash_v2} + <:item title="Cdn url">{@torrent.cdn_url} + <:item title="Magnet">{@torrent.magnet} + + +<.back navigate={~p"/torrent"}>Back to torrent diff --git a/services/bright/lib/bright_web/controllers/torrent_html/torrent_form.html.heex b/services/bright/lib/bright_web/controllers/torrent_html/torrent_form.html.heex new file mode 100644 index 0000000..dfd34d5 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_html/torrent_form.html.heex @@ -0,0 +1,12 @@ +<.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:info_hash_v1]} type="text" label="Info hash v1" /> + <.input field={f[:info_hash_v2]} type="text" label="Info hash v2" /> + <.input field={f[:cdn_url]} type="text" label="Cdn url" /> + <.input field={f[:magnet]} type="text" label="Magnet" /> + <:actions> + <.button>Save Torrent + + diff --git a/services/bright/lib/bright_web/controllers/torrent_json.ex b/services/bright/lib/bright_web/controllers/torrent_json.ex new file mode 100644 index 0000000..bb8d378 --- /dev/null +++ b/services/bright/lib/bright_web/controllers/torrent_json.ex @@ -0,0 +1,26 @@ +defmodule BrightWeb.TorrentJSON do + alias Bright.Torrents.Torrent + @doc """ + Renders a list of torrents. + """ + def index(%{torrents: torrents}) do + %{data: for(torrent <- torrents, do: data(torrent))} + end + + @doc """ + Renders a single torrent. + """ + def show(%{torrent: torrent}) do + %{data: data(torrent)} + end + + defp data(%Torrent{} = torrent) do + %{ + id: torrent.id, + info_hash_v1: torrent.info_hash_v1, + info_hash_v2: torrent.info_hash_v2, + cdn_url: torrent.cdn_url, + magnet: torrent.magnet + } + end +end diff --git a/services/bright/lib/bright_web/router.ex b/services/bright/lib/bright_web/router.ex index bd2e741..8fc2976 100644 --- a/services/bright/lib/bright_web/router.ex +++ b/services/bright/lib/bright_web/router.ex @@ -88,7 +88,7 @@ defmodule BrightWeb.Router do post("/join", UserController, :join) post("/join", UserController, :join) - resources("/orders", OrderController, only: [:create, :show]) + resources("/torrents", TorrentController, only: [:index, :show]) get("/streams", StreamController, :index) get("/streams/:id", StreamController, :show) diff --git a/services/bright/mix.exs b/services/bright/mix.exs index e65c091..67e82b0 100644 --- a/services/bright/mix.exs +++ b/services/bright/mix.exs @@ -67,6 +67,7 @@ defmodule Bright.MixProject do {:sweet_xml, "~> 0.6"}, {:ex_m3u8, "~> 0.14.2"}, {:atomex, "~> 0.3.0"}, + {:bento, "~> 1.0"}, # {:membrane_core, "~> 1.0"}, # {:membrane_mpeg_ts_plugin, "~> 1.0.3"}, # {:membrane_file_plugin, "~> 0.17.2"}, diff --git a/services/bright/mix.lock b/services/bright/mix.lock index 1751585..c817220 100644 --- a/services/bright/mix.lock +++ b/services/bright/mix.lock @@ -2,6 +2,7 @@ "atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "025dbc3a3e99380894791a093019f535d0ef6cf1916f6ec1b778ac107fcfc3e4"}, "bandit": {:hex, :bandit, "1.6.6", "f2019a95261d400579075df5bc15641ba8e446cc4777ede6b4ec19e434c3340d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "ceb19bf154bc2c07ee0c9addf407d817c48107e36a66351500846fc325451bf9"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, + "bento": {:hex, :bento, "1.0.0", "5097e6f02e4980b72d08bf0270026f6f5c9bf5d8ca606b43d41321b549d49de8", [:mix], [], "hexpm", "b921b335a555f7570adfac0cc41864c76377f6852617cf1f25fbb88578b993c8"}, "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, "bulma": {:hex, :bulma, "1.0.2", "50dfffe8d28b0bd527418560223b407f9e80e990e187e1653b17eff818f8fcbe", [:mix], [], "hexpm", "27745727ff7f451d140a2438c0ca4448bc8ca73e0a6d2d4f24e1b5b9ced8a774"}, "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, diff --git a/services/bright/test/bright/bittorrent_url_encode_test.ex b/services/bright/test/bright/bittorrent_url_encode_test.ex new file mode 100644 index 0000000..df14502 --- /dev/null +++ b/services/bright/test/bright/bittorrent_url_encode_test.ex @@ -0,0 +1,55 @@ +defmodule Bright.BittorrentUrlEncodeTest do + use ExUnit.Case + doctest Bright.BittorrentUrlEncode + alias Bright.BittorrentUrlEncode + + test "The WIRED CD - Rip. Sample. Mash. Share a8 8f da 59 54 e8 91 78 c3 72 71 6a 6a 78 b8 18 0e d4 da d3" do + actual = BittorrentUrlEncode.encode("a88fda5954e89178c372716a6a78b8180ed4dad3") + expected = "%a8%8f%daYT%e8%91x%c3rqjjx%b8%18%0e%d4%da%d3" + assert actual === expected + end + + test "tails-amd64-6.10-img 07 b4 51 63 36 e4 af e9 23 2c 73 bc 31 26 42 59 0a 7d 7e 95" do + actual = BittorrentUrlEncode.encode("07b4516336e4afe9232c73bc312642590a7d7e95") + expected = "%07%b4Qc6%e4%af%e9%23%2cs%bc1%26BY%0a%7d~%95" + assert actual === expected + end + + test "linuxmint-22-mate-64bit.iso e0 a4 05 8e 40 7d dd ad 1c ac f8 c9 ce db 0b 27 21 c0 7f 92" do + actual = BittorrentUrlEncode.encode("e0a4058e407dddad1cacf8c9cedb0b2721c07f92") + expected = "%e0%a4%05%8e%40%7d%dd%ad%1c%ac%f8%c9%ce%db%0b%27!%c0%7f%92" + assert actual === expected + end + + test "debian-12.8.0-amd64-DVD-1.iso 56 3e 72 81 c0 00 e1 80 91 e5 c0 d3 9d 09 8c ff 13 5d ab 26" do + actual = BittorrentUrlEncode.encode("563e7281c000e18091e5c0d39d098cff135dab26") + expected = "V%3er%81%c0%00%e1%80%91%e5%c0%d3%9d%09%8c%ff%13%5d%ab%26" + assert actual === expected + end + + test "KNOPPIX_V9.1DVD-2021-01-25-EN c0 3b b7 09 bd 7e fe 79 68 87 75 c4 fc 92 51 41 e4 1d b2 87" do + actual = BittorrentUrlEncode.encode("c03bb709bd7efe79688775c4fc925141e41db287") + expected = "%c0%3b%b7%09%bd~%feyh%87u%c4%fc%92QA%e4%1d%b2%87" + assert actual === expected + end + + test "Peppermint-7-20160616-i386.iso 72 c3 7d bf e4 39 0f 1b 74 b9 b4 25 07 e8 97 01 b4 9e c7 a3" do + actual = BittorrentUrlEncode.encode("72c37dbfe4390f1b74b9b42507e89701b49ec7a3") + expected = "r%c3%7d%bf%e49%0f%1bt%b9%b4%25%07%e8%97%01%b4%9e%c7%a3" + assert actual === expected + end + + test "crunchbang-11-20130506-i686.iso 88 fa 85 16 ca 2b 3d 3d 41 86 75 20 84 f9 3a 59 91 8f a3 51" do + actual = BittorrentUrlEncode.encode("88fa8516ca2b3d3d4186752084f93a59918fa351") + expected = "%88%fa%85%16%ca%2b%3d%3dA%86u%20%84%f9%3aY%91%8f%a3Q" + assert actual === expected + end + + test "kali-linux-2024.3-installer-amd64.iso 7b 14 90 47 4e 51 53 6e 1a 7a c0 df ec 24 67 e8 6d a2 32 a4" do + actual = BittorrentUrlEncode.encode("7b1490474e51536e1a7ac0dfec2467e86da232a4") + expected = "%7b%14%90GNQSn%1az%c0%df%ec%24g%e8m%a22%a4" + assert actual === expected + end + + +end diff --git a/services/bright/test/bright/cache_test.ex b/services/bright/test/bright/cache_test.ex index ace02c2..e170b32 100644 --- a/services/bright/test/bright/cache_test.ex +++ b/services/bright/test/bright/cache_test.ex @@ -14,19 +14,11 @@ defmodule Bright.CacheTest do ## To implement this cache before the system works is pre-mature optimization! # describe "cache k/v" do - # test "get/1 with string cache key" do + # test "get/1 with string cache key" do - # end + # end - # test "get/1 with %Vod{}" do - # stream = stream_fixture() - # vod = vod_fixture(%{stream_id: stream.id}) - # Cache.get(vod) - # end - # test "put/2" do - - # end # end describe "cache" do diff --git a/services/bright/test/bright/oban_workers/create_torrent_test.exs b/services/bright/test/bright/oban_workers/create_torrent_test.exs index 8adc4fc..d713ee9 100644 --- a/services/bright/test/bright/oban_workers/create_torrent_test.exs +++ b/services/bright/test/bright/oban_workers/create_torrent_test.exs @@ -20,7 +20,7 @@ defmodule Bright.CreateTorrentTest do @tag :integration test "torrent creation" do stream = stream_fixture() - vod = vod_fixture(%{torrent: nil, stream_id: stream.id, origin_temp_input_url: @test_video_url}) + vod = vod_fixture(%{torrent: nil, stream_id: stream.id, s3_cdn_url: @test_video_url}) {:ok, %Vod{torrent: torrent, magnet_link: magnet_link, info_hash_v1: info_hash_v1, info_hash_v2: info_hash_v2}} = perform_job(Bright.ObanWorkers.CreateTorrent, %{vod_id: vod.id}) assert Regex.match?(~r/^https:\/\/.*\.torrent$/, torrent) diff --git a/services/bright/test/bright/torrent_test.exs b/services/bright/test/bright/torrent_test.exs deleted file mode 100644 index 6da7361..0000000 --- a/services/bright/test/bright/torrent_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -defmodule Bright.TorrentTest do - use Bright.DataCase - - alias Bright.Torrent - - - describe "torrent" do - - import Bright.StreamsFixtures - alias Bright.{Downloader,Cache} - - @test_fixture "https://futureporn-b2.b-cdn.net/test-fixture.ts" - - # @tag :integration - # test "create_torrent/1" do - # stream = stream_fixture() - # vod = vod_fixture(%{stream_id: stream.id, s3_cdn_url: "https://futureporn-b2.b-cdn.net/test-fixture.ts"}) - # {:ok, _} = Torrent.create_torrent(vod) - # assert :ok - # end - - @tag :integration - test "create_torrent/7" do - stream = stream_fixture() - vod = vod_fixture(%{stream_id: stream.id, s3_cdn_url: "https://futureporn-b2.b-cdn.net/test-fixture.ts"}) - input_path = Path.absname("./test/fixtures/test-fixture.ts") - output_path = Cache.generate_filename("test", "torrent") - tracker_url = "https://tracker.futureporn.net/announce" - source_url = "https://futureporn.net/vods/69" - comment = "https://futureporn.net" - web_seed_url = @test_fixture - meta_version = 3 - IO.puts "input_path=#{input_path} output_path=#{output_path} tracker_url=#{tracker_url} source_url=#{source_url}" - {:ok, %{local_path: local_path, magnet_link: magnet_link, basename: basename, info_hash_v1: info_hash_v1, info_hash_v2: info_hash_v2}} - = Torrent.create_torrent(input_path, output_path, web_seed_url, vod.id) - assert :ok - assert local_path === output_path - assert File.exists?(output_path) - assert String.starts_with?(magnet_link, "magnet:") - assert String.ends_with?(basename, ".torrent") - assert is_binary(info_hash_v1) - assert is_binary(info_hash_v2) - end - - - - - end -end diff --git a/services/bright/test/bright/torrentfile_test.ex b/services/bright/test/bright/torrentfile_test.ex index 6077bd0..e4029b0 100644 --- a/services/bright/test/bright/torrentfile_test.ex +++ b/services/bright/test/bright/torrentfile_test.ex @@ -23,6 +23,15 @@ defmodule Bright.TorrentfileTest do assert Regex.match?(~r"\/torrentfile", Torrentfile.torrentfile_path()) end + test "create/2" do + {:ok, output} = Torrentfile.create(vod, input_path) + assert :ok + assert is_binary(output.save_path) + assert output.save_path === output_path + assert is_binary(output.btih) + assert is_binary(output.btmh) + assert File.exists?(output_path) + end test "create/7" do input_path = @test_ts_fixture @@ -30,9 +39,9 @@ defmodule Bright.TorrentfileTest do tracker_url = @test_tracker_url comment = @test_comment source_url = @test_source_url - web_Seed_url = @test_web_seed_url + web_seed_url = @test_web_seed_url meta_version = 3 - {:ok, output} = Torrentfile.create(input_path, output_path, tracker_url, comment, source_url, web_Seed_url, meta_version) + {:ok, output} = Torrentfile.create(input_path, output_path, tracker_url, comment, source_url, web_seed_url, meta_version) assert :ok assert is_binary(output.save_path) diff --git a/services/bright/test/bright/torrents_test.exs b/services/bright/test/bright/torrents_test.exs index 23ce85c..4d62ea5 100644 --- a/services/bright/test/bright/torrents_test.exs +++ b/services/bright/test/bright/torrents_test.exs @@ -2,17 +2,21 @@ defmodule Bright.TorrentsTest do use Bright.DataCase alias Bright.Torrents + alias Bright.{Downloader,Cache} - describe "torrent" do + @test_fixture "https://futureporn-b2.b-cdn.net/test-fixture.ts" + + + describe "torrents" do alias Bright.Torrents.Torrent import Bright.TorrentsFixtures @invalid_attrs %{info_hash_v1: nil, info_hash_v2: nil, cdn_url: nil, magnet: nil} - test "list_torrent/0 returns all torrent" do + test "list_torrents/0 returns all torrent" do torrent = torrent_fixture() - assert Torrents.list_torrent() == [torrent] + assert Torrents.list_torrents() == [torrent] end test "get_torrent!/1 returns the torrent with given id" do @@ -61,5 +65,32 @@ defmodule Bright.TorrentsTest do torrent = torrent_fixture() assert %Ecto.Changeset{} = Torrents.change_torrent(torrent) end + + + + @tag :integration + test "generate_torrent_file/7" do + stream = stream_fixture() + vod = vod_fixture(%{stream_id: stream.id, s3_cdn_url: "https://futureporn-b2.b-cdn.net/test-fixture.ts"}) + input_path = Path.absname("./test/fixtures/test-fixture.ts") + output_path = Cache.generate_filename("test", "torrent") + tracker_url = "https://tracker.futureporn.net/announce" + source_url = "https://futureporn.net/vods/69" + comment = "https://futureporn.net" + web_seed_url = @test_fixture + meta_version = 3 + IO.puts "input_path=#{input_path} output_path=#{output_path} tracker_url=#{tracker_url} source_url=#{source_url}" + {:ok, %{local_path: local_path, magnet_link: magnet_link, basename: basename, info_hash_v1: info_hash_v1, info_hash_v2: info_hash_v2}} + = Torrent.create_torrent(input_path, output_path, web_seed_url, vod.id) + assert :ok + assert local_path === output_path + assert File.exists?(output_path) + assert String.starts_with?(magnet_link, "magnet:") + assert String.ends_with?(basename, ".torrent") + assert is_binary(info_hash_v1) + assert is_binary(info_hash_v2) + end + + end end diff --git a/services/bright/test/bright/tracker_test.exs b/services/bright/test/bright/tracker_test.exs index 062f3bd..b533a18 100644 --- a/services/bright/test/bright/tracker_test.exs +++ b/services/bright/test/bright/tracker_test.exs @@ -2,39 +2,60 @@ defmodule Bright.TrackerTest do use Bright.DataCase alias Bright.Tracker + alias Bright.URLEncoder describe "tracker" do import Bright.StreamsFixtures - @info_hash_fixture "723886c0b0d9d41bfaa5276a9b2552d84ba09dd8a77d9ddcab5c9fa16cdb9770" # test-fixture.ts (BT info_hash v2) + @info_hash_v2_fixture "723886c0b0d9d41bfaa5276a9b2552d84ba09dd8a77d9ddcab5c9fa16cdb9770" # test-fixture.ts (BT info_hash v2) + @info_hash_v1_fixture "157835a64d398fd63d83b5fd6dac5612bd60b6c6" # test-fixture.ts (BT info_hash v1) + @tag :integration test "whitelist_info_hash/1 using a string info_hash" do - :ok = Tracker.whitelist_info_hash(@info_hash_fixture) - assert :ok - end - - @tag :integration - test "whitelist_info_hash/1 using a %Vod{}" do - stream = stream_fixture() - vod = vod_fixture(%{stream_id: stream.id}) - :ok = Tracker.whitelist_info_hash(vod) + :ok = Tracker.whitelist_info_hash(@info_hash_v2_fixture) assert :ok end @tag :integration test "announce_torrent/1 using a string info_hash" do - :ok = Tracker.announce_torrent(@info_hash_fixture) + {:ok, body} = Tracker.announce_torrent(@info_hash_v1_fixture) assert :ok end - @tag :integration - test "announce_torrent/1 using a %Vod{}" do - stream = stream_fixture() - vod = vod_fixture(%{stream_id: stream.id, info_hash: @info_hash_fixture}) - :ok = Tracker.announce_torrent(vod) - assert :ok + @tag :unit + @tag :bep3 + test "bep3_encode/1 projekt-melody.jpg" do + + ## projekt-melody.jpg + info_hash_v1 = "4a7620a29789ca70c6e461a11da810bd1d253fc6" + expected_output = "Jv%20%a2%97%89%cap%c6%e4a%a1%1d%a8%10%bd%1d%25%3f%c6" + assert Tracker.bep3_encode(info_hash_v1) === expected_output + + end + + @tag :unit + @tag :bep3 + test "bep3_encode/1 StackOverflow" do + + ## @see https://stackoverflow.com/a/40000337/1004931 + info_hash_v1 = "acc3b2e433d7c7475abb5941b5681cb7a1ea26e2" + expected_output = "%ac%c3%b2%e43%d7%c7GZ%bbYA%b5h%1c%b7%a1%ea%26%e2" + assert Tracker.bep3_encode(info_hash_v1) === expected_output + + end + + @tag :unit + @tag :bep3 + test "bep3_encode/1 WIRED CD" do + + info_hash_v1 = "a88fda5954e89178c372716a6a78b8180ed4dad3" + expected_output = "%A8%8F%DAYT%E8%91x%C3rqjjx%B8%18%0E%D4%DA%D3" + actual = Tracker.bep3_encode(info_hash_v1) + IO.puts actual + assert actual === expected_output + end end diff --git a/services/bright/test/bright_web/controllers/torrent_controller_test.exs b/services/bright/test/bright_web/controllers/torrent_controller_test.exs new file mode 100644 index 0000000..053d31e --- /dev/null +++ b/services/bright/test/bright_web/controllers/torrent_controller_test.exs @@ -0,0 +1,84 @@ +defmodule BrightWeb.TorrentControllerTest do + use BrightWeb.ConnCase + + import Bright.TorrentsFixtures + + @create_attrs %{info_hash_v1: "some info_hash_v1", info_hash_v2: "some info_hash_v2", cdn_url: "some cdn_url", magnet: "some magnet"} + @update_attrs %{info_hash_v1: "some updated info_hash_v1", info_hash_v2: "some updated info_hash_v2", cdn_url: "some updated cdn_url", magnet: "some updated magnet"} + @invalid_attrs %{info_hash_v1: nil, info_hash_v2: nil, cdn_url: nil, magnet: nil} + + describe "index" do + test "lists all torrent", %{conn: conn} do + conn = get(conn, ~p"/torrent") + assert html_response(conn, 200) =~ "Listing Torrent" + end + end + + describe "new torrent" do + test "renders form", %{conn: conn} do + conn = get(conn, ~p"/torrent/new") + assert html_response(conn, 200) =~ "New Torrent" + end + end + + describe "create torrent" do + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, ~p"/torrent", torrent: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == ~p"/torrent/#{id}" + + conn = get(conn, ~p"/torrent/#{id}") + assert html_response(conn, 200) =~ "Torrent #{id}" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/torrent", torrent: @invalid_attrs) + assert html_response(conn, 200) =~ "New Torrent" + end + end + + describe "edit torrent" do + setup [:create_torrent] + + test "renders form for editing chosen torrent", %{conn: conn, torrent: torrent} do + conn = get(conn, ~p"/torrent/#{torrent}/edit") + assert html_response(conn, 200) =~ "Edit Torrent" + end + end + + describe "update torrent" do + setup [:create_torrent] + + test "redirects when data is valid", %{conn: conn, torrent: torrent} do + conn = put(conn, ~p"/torrent/#{torrent}", torrent: @update_attrs) + assert redirected_to(conn) == ~p"/torrent/#{torrent}" + + conn = get(conn, ~p"/torrent/#{torrent}") + assert html_response(conn, 200) =~ "some updated info_hash_v1" + end + + test "renders errors when data is invalid", %{conn: conn, torrent: torrent} do + conn = put(conn, ~p"/torrent/#{torrent}", torrent: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit Torrent" + end + end + + describe "delete torrent" do + setup [:create_torrent] + + test "deletes chosen torrent", %{conn: conn, torrent: torrent} do + conn = delete(conn, ~p"/torrent/#{torrent}") + assert redirected_to(conn) == ~p"/torrent" + + assert_error_sent 404, fn -> + get(conn, ~p"/torrent/#{torrent}") + end + end + end + + defp create_torrent(_) do + torrent = torrent_fixture() + %{torrent: torrent} + end +end