From ec1452fd1cdb9cb1db9b8bad872916d3213489e2 Mon Sep 17 00:00:00 2001 From: href Date: Thu, 14 May 2020 21:36:31 +0200 Subject: Pleroma.MIME: use gen_magic --- lib/pleroma/application.ex | 1 + lib/pleroma/mime.ex | 84 ++++++++++++---------------------------------- 2 files changed, 23 insertions(+), 62 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d3d92b38..c74255629 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,6 +80,7 @@ defmodule Pleroma.Application do [ Pleroma.Stats, Pleroma.JobQueueMonitor, + Pleroma.MIME, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex index 6ee055f50..3b406630e 100644 --- a/lib/pleroma/mime.ex +++ b/lib/pleroma/mime.ex @@ -6,8 +6,21 @@ defmodule Pleroma.MIME do @moduledoc """ Returns the mime-type of a binary and optionally a normalized file-name. """ - @default "application/octet-stream" @read_bytes 35 + @pool __MODULE__.GenMagicPool + + def child_spec(_) do + pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) + name = @pool + + %{ + id: __MODULE__, + start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end @spec file_mime_type(String.t(), String.t()) :: {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error @@ -20,9 +33,10 @@ defmodule Pleroma.MIME do @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error def file_mime_type(filename) do - File.open(filename, [:read], fn f -> - check_mime_type(IO.binread(f, @read_bytes)) - end) + case GenMagic.Pool.perform(@pool, filename) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(binary, filename) do @@ -34,13 +48,14 @@ defmodule Pleroma.MIME do @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error def bin_mime_type(<>) do - {:ok, check_mime_type(head)} + case GenMagic.Pool.perform(@pool, {:bytes, head}) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(_), do: :error - def mime_type(<<_::binary>>), do: {:ok, @default} - defp fix_extension(filename, content_type) do parts = String.split(filename, ".") @@ -62,59 +77,4 @@ defmodule Pleroma.MIME do Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") end end - - defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do - "image/png" - end - - defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do - "image/gif" - end - - defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do - "image/jpeg" - end - - defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do - "video/webm" - end - - defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do - "video/mp4" - end - - defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type( - <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65, - 0x6F, 0x72, 0x61, _::binary>> - ) do - "video/ogg" - end - - defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do - "audio/ogg" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do - "audio/wav" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do - "image/webp" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do - "video/avi" - end - - defp check_mime_type(_) do - @default - end end -- cgit v1.2.3 From f124f6820582d50be83ba7a1709b14ce8ee1abcc Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 15:11:45 +0200 Subject: Switch from gen_magic to majic, use Majic.Plug, remove Pleroma.MIME --- lib/pleroma/application.ex | 2 +- lib/pleroma/mime.ex | 80 ---------------------- lib/pleroma/upload.ex | 19 ++--- .../web/activity_pub/activity_pub_controller.ex | 2 + .../mastodon_api/controllers/media_controller.ex | 1 + .../pleroma_api/controllers/account_controller.ex | 5 ++ .../pleroma_api/controllers/mascot_controller.ex | 10 +-- 7 files changed, 25 insertions(+), 94 deletions(-) delete mode 100644 lib/pleroma/mime.ex (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c74255629..9c74fa00e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,7 +80,7 @@ defmodule Pleroma.Application do [ Pleroma.Stats, Pleroma.JobQueueMonitor, - Pleroma.MIME, + {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]}, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex deleted file mode 100644 index 3b406630e..000000000 --- a/lib/pleroma/mime.ex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MIME do - @moduledoc """ - Returns the mime-type of a binary and optionally a normalized file-name. - """ - @read_bytes 35 - @pool __MODULE__.GenMagicPool - - def child_spec(_) do - pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) - name = @pool - - %{ - id: __MODULE__, - start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - - @spec file_mime_type(String.t(), String.t()) :: - {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error - def file_mime_type(path, filename) do - with {:ok, content_type} <- file_mime_type(path), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error - def file_mime_type(filename) do - case GenMagic.Pool.perform(@pool, filename) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(binary, filename) do - with {:ok, content_type} <- bin_mime_type(binary), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error - def bin_mime_type(<>) do - case GenMagic.Pool.perform(@pool, {:bytes, head}) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(_), do: :error - - defp fix_extension(filename, content_type) do - parts = String.split(filename, ".") - - new_filename = - if length(parts) > 1 do - Enum.drop(parts, -1) |> Enum.join(".") - else - Enum.join(parts) - end - - cond do - content_type == "application/octet-stream" -> - filename - - ext = List.first(MIME.extensions(content_type)) -> - new_filename <> "." <> ext - - true -> - Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") - end - end -end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..a0ba2f4c0 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -57,6 +57,7 @@ defmodule Pleroma.Upload do defstruct [:id, :name, :tempfile, :content_type, :path] @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()} + @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct." def store(upload, opts \\ []) do opts = get_opts(opts) @@ -123,14 +124,13 @@ defmodule Pleroma.Upload do end defp prepare_upload(%Plug.Upload{} = file, opts) do - with :ok <- check_file_size(file.path, opts.size_limit), - {:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do + with :ok <- check_file_size(file.path, opts.size_limit) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: file.filename, tempfile: file.path, - content_type: content_type + content_type: file.content_type }} end end @@ -138,16 +138,17 @@ defmodule Pleroma.Upload do defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do parsed = Regex.named_captures(~r/(?jpeg|png|gif);base64,(?.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) - hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data))) + hash = Base.encode16(:crypto.hash(:sha256, data), lower: true) with :ok <- check_binary_size(data, opts.size_limit), tmp_path <- tempfile_for_image(data), - {:ok, content_type, name} <- - Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do + {:ok, %{mime_type: content_type}} <- + Majic.perform({:bytes, data}, pool: Pleroma.MajicPool), + [ext | _] <- MIME.extensions(content_type) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: hash <> "." <> ext, tempfile: tmp_path, content_type: content_type }} @@ -156,7 +157,7 @@ defmodule Pleroma.Upload do # For Mix.Tasks.MigrateLocalUploads defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do - with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do + with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do {:ok, %__MODULE__{upload | content_type: content_type}} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index f0b5c6e93..e2a5fb9e9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -45,6 +45,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:read_inbox, :update_outbox, :whoami, :upload_media] ) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) + plug( Pleroma.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 513de279f..06bb718ef 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..97a6ae60d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -56,6 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index df6c50ca5..4ba4154dd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation @@ -22,14 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do @doc "PUT /api/v1/pleroma/mascot" def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do - with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), - # Reject if not an image - %{type: "image"} = attachment <- render_attachment(object) do + with {:content_type, "image" <> _} <- {:content_type, file.content_type}, + {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do + attachment = render_attachment(object) {:ok, _user} = User.mascot_update(user, attachment) json(conn, attachment) else - %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images") + {:content_type, _} -> + render_error(conn, :unsupported_media_type, "mascots can only be images") end end -- cgit v1.2.3 From 39f7fc5b8ef781c98136d1f9be50a14bff394233 Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 19:00:54 +0200 Subject: Update majic & call plug before OpenApiSpex --- lib/pleroma/web/mastodon_api/controllers/media_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 10 +++++----- lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 06bb718ef..09acea7f4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 97a6ae60d..c76cbfc48 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -18,6 +18,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + plug( OpenApiSpex.Plug.PutApiSpec, [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend @@ -56,11 +61,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) - plug( - Majic.Plug, - [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] - ) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index 4ba4154dd..7e2f6c328 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,10 +9,10 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation -- cgit v1.2.3 From 6c61ef14c3f48910c52e17c68fce175682717962 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 11:18:39 -0500 Subject: Support enabling upload filters during instance gen --- lib/mix/tasks/pleroma/instance.ex | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 91440b453..fc21ae062 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -33,7 +33,10 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: :string, static_dir: :string, listen_ip: :string, - listen_port: :string + listen_port: :string, + strip_uploads: :string, + anonymize_uploads: :string, + dedupe_uploads: :string ], aliases: [ o: :output, @@ -158,6 +161,30 @@ defmodule Mix.Tasks.Pleroma.Instance do ) |> Path.expand() + strip_uploads = + get_option( + options, + :strip_uploads, + "Do you want to strip location (GPS) data from uploaded images? (y/n)", + "y" + ) === "y" + + anonymize_uploads = + get_option( + options, + :anonymize_uploads, + "Do you want to anonymize the filenames of uploads? (y/n)", + "n" + ) === "y" + + dedupe_uploads = + get_option( + options, + :dedupe_uploads, + "Do you want to deduplicate uploaded files? (y/n)", + "n" + ) === "y" + Config.put([:instance, :static_dir], static_dir) secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) @@ -188,7 +215,13 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: uploads_dir, rum_enabled: rum_enabled, listen_ip: listen_ip, - listen_port: listen_port + listen_port: listen_port, + upload_filters: + upload_filters(%{ + strip: strip_uploads, + anonymize: anonymize_uploads, + dedupe: dedupe_uploads + }) ) result_psql = @@ -247,4 +280,31 @@ defmodule Mix.Tasks.Pleroma.Instance do File.write(robots_txt_path, robots_txt) shell_info("Writing #{robots_txt_path}.") end + + defp upload_filters(filters) when is_map(filters) do + enabled_filters = + if filters.strip do + [Pleroma.Upload.Filter.ExifTool] + else + [] + end + + enabled_filters = + if filters.anonymize do + enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename] + else + enabled_filters + end + + enabled_filters = + if filters.dedupe do + enabled_filters ++ [Pleroma.Upload.Filter.Dedupe] + else + enabled_filters + end + + enabled_filters + end + + defp upload_filters(_), do: [] end -- cgit v1.2.3 From 8539e386c3f00537f120487e717ec7b25fe6c572 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 12:00:50 -0500 Subject: Add missing Copyright headers --- lib/mix/tasks/pleroma/count_statuses.ex | 4 ++++ lib/mix/tasks/pleroma/digest.ex | 4 ++++ lib/mix/tasks/pleroma/docs.ex | 4 ++++ lib/mix/tasks/pleroma/email.ex | 4 ++++ lib/mix/tasks/pleroma/notification_settings.ex | 4 ++++ lib/pleroma/config/oban.ex | 4 ++++ lib/pleroma/docs/generator.ex | 4 ++++ lib/pleroma/docs/json.ex | 4 ++++ lib/pleroma/docs/markdown.ex | 4 ++++ lib/pleroma/emoji/pack.ex | 4 ++++ lib/pleroma/gun/connection_pool.ex | 4 ++++ lib/pleroma/gun/connection_pool/reclaimer.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker_supervisor.ex | 4 ++++ lib/pleroma/http/adapter_helper/default.ex | 4 ++++ lib/pleroma/http/adapter_helper/hackney.ex | 4 ++++ lib/pleroma/jwt.ex | 4 ++++ lib/pleroma/moderation_log.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/supervisor.ex | 4 ++++ lib/pleroma/telemetry/logger.ex | 4 ++++ lib/pleroma/web/activity_pub/builder.ex | 4 ++++ lib/pleroma/web/activity_pub/side_effects.ex | 4 ++++ lib/pleroma/web/mailer/subscription_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 4 ++++ lib/pleroma/web/views/email_view.ex | 4 ++++ lib/pleroma/web/views/mailer/subscription_view.ex | 4 ++++ 30 files changed, 120 insertions(+) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/count_statuses.ex b/lib/mix/tasks/pleroma/count_statuses.ex index e1e8195dd..8761d8f17 100644 --- a/lib/mix/tasks/pleroma/count_statuses.ex +++ b/lib/mix/tasks/pleroma/count_statuses.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.CountStatuses do @shortdoc "Re-counts statuses for all users" diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 3595f912d..cac148b88 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Digest do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex index 6088fc71d..ad5c37fc9 100644 --- a/lib/mix/tasks/pleroma/docs.ex +++ b/lib/mix/tasks/pleroma/docs.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Docs do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 9972cb988..bc5facc09 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Email do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 00f5ba7bf..f99275de1 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.NotificationSettings do @shortdoc "Enable&Disable privacy option for push notifications" @moduledoc """ diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index 9f601b1a3..8e0351d52 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Config.Oban do require Logger diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index a671a6278..a70f83b73 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Generator do @callback process(keyword()) :: {:ok, String.t()} diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index feeb4320e..13618b509 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.JSON do @behaviour Pleroma.Docs.Generator @external_resource "config/description.exs" diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex index da3f20f43..eac0789a6 100644 --- a/lib/pleroma/docs/markdown.ex +++ b/lib/pleroma/docs/markdown.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Markdown do @behaviour Pleroma.Docs.Generator diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 8f1989ada..0670f29f1 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Emoji.Pack do @derive {Jason.Encoder, only: [:files, :pack, :files_count]} defstruct files: %{}, diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex index f34602b73..e322f192a 100644 --- a/lib/pleroma/gun/connection_pool.ex +++ b/lib/pleroma/gun/connection_pool.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool do @registry __MODULE__ diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex index cea800882..241e8b04f 100644 --- a/lib/pleroma/gun/connection_pool/reclaimer.ex +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Reclaimer do use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index bf57e9e5f..b71816bed 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Worker do alias Pleroma.Gun use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex index 39615c956..4c23bcbd9 100644 --- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex index e13441316..8567a616b 100644 --- a/lib/pleroma/http/adapter_helper/default.ex +++ b/lib/pleroma/http/adapter_helper/default.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Default do alias Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index ef84553c1..ff60513fd 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Hackney do @behaviour Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex index 10102ff5d..faeb77781 100644 --- a/lib/pleroma/jwt.ex +++ b/lib/pleroma/jwt.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.JWT do use Joken.Config diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 47036a6f6..38a863443 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ModerationLog do use Ecto.Schema diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex index 884268d96..0bf5aadfb 100644 --- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do use DynamicSupervisor diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex index 9672f7876..ce196df52 100644 --- a/lib/pleroma/plugs/rate_limiter/supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.Supervisor do use Supervisor diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 197b1d091..003079cf3 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Telemetry.Logger do @moduledoc "Transforms Pleroma telemetry events to logs" diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 9a7b7d9de..298aff6b7 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.Builder do @moduledoc """ This module builds the objects. Meant to be used for creating local objects. diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b9a83a544..2eec0ce86 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.SideEffects do @moduledoc """ This module looks at an inserted object and executes the side effects that it diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex index 478a83518..ace44afd1 100644 --- a/lib/pleroma/web/mailer/subscription_controller.ex +++ b/lib/pleroma/web/mailer/subscription_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index 71c53df1d..7c0345094 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiFileController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 6696f8b92..a0e5c739a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiPackController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex index c5aaea2d4..15109d28d 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do @behaviour Pleroma.Web.RichMedia.Parser.TTL diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex index 6b3ec6d30..13511888c 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL do @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} end diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex index 6b0fbe61e..bcdee6571 100644 --- a/lib/pleroma/web/views/email_view.ex +++ b/lib/pleroma/web/views/email_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.EmailView do use Pleroma.Web, :view import Phoenix.HTML diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex index fc3d20816..4562a9d6c 100644 --- a/lib/pleroma/web/views/mailer/subscription_view.ex +++ b/lib/pleroma/web/views/mailer/subscription_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionView do use Pleroma.Web, :view end -- cgit v1.2.3 From 83ae45b000261d3e03a4b554064350a5ead172c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 12 Oct 2020 18:49:37 -0500 Subject: Preload `/api/pleroma/frontend_configurations`, fixes #1932 --- lib/pleroma/web/preload/instance.ex | 9 +++++++++ lib/pleroma/web/twitter_api/controllers/util_controller.ex | 6 +----- lib/pleroma/web/twitter_api/views/util_view.ex | 6 ++++++ 3 files changed, 16 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex index 50d1f3382..cc6f8cf99 100644 --- a/lib/pleroma/web/preload/instance.ex +++ b/lib/pleroma/web/preload/instance.ex @@ -7,11 +7,13 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Preload.Providers.Provider + alias Pleroma.Web.TwitterAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" @panel_url "/instance/panel.html" @nodeinfo_url "/nodeinfo/2.0.json" + @fe_config_url "/api/pleroma/frontend_configurations" @impl Provider def generate_terms(_params) do @@ -19,6 +21,7 @@ defmodule Pleroma.Web.Preload.Providers.Instance do |> build_info_tag() |> build_panel_tag() |> build_nodeinfo_tag() + |> build_fe_config_tag() end defp build_info_tag(acc) do @@ -47,4 +50,10 @@ defmodule Pleroma.Web.Preload.Providers.Instance do Map.put(acc, @nodeinfo_url, nodeinfo_data) end end + + defp build_fe_config_tag(acc) do + fe_data = UtilView.render("frontend_configurations.json", %{}) + + Map.put(acc, @fe_config_url, fe_data) + end end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 70b0fbd54..6d827846d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -74,11 +74,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end def frontend_configurations(conn, _params) do - config = - Config.get(:frontend_configurations, %{}) - |> Enum.into(%{}) - - json(conn, config) + render(conn, "frontend_configurations.json") end def emoji(conn, _params) do diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index d3bdb4f62..98eea1d18 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.Config alias Pleroma.Web def status_net_config(instance) do @@ -19,4 +20,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do """ end + + def render("frontend_configurations.json", _) do + Config.get(:frontend_configurations, %{}) + |> Enum.into(%{}) + end end -- cgit v1.2.3 From b573711e9c8200ecdd4a722ce1e02b48d3f74cce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 18:37:44 +0300 Subject: file locations consistency --- lib/credo/check/consistency/file_location.ex | 132 +++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 lib/credo/check/consistency/file_location.ex (limited to 'lib') diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex new file mode 100644 index 000000000..5ef17b894 --- /dev/null +++ b/lib/credo/check/consistency/file_location.ex @@ -0,0 +1,132 @@ +defmodule Credo.Check.Consistency.FileLocation do + @moduledoc false + + # credo:disable-for-this-file Credo.Check.Readability.Specs + + @checkdoc """ + File location should follow the namespace hierarchy of the module it defines. + + Examples: + + - `lib/my_system.ex` should define the `MySystem` module + - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module + """ + @explanation [warning: @checkdoc] + + # `use Credo.Check` required that module attributes are already defined, so we need to place these attributes + # before use/alias expressions. + # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout + use Credo.Check, category: :warning, base_priority: :high + + alias Credo.Code + + def run(source_file, params \\ []) do + case verify(source_file, params) do + :ok -> + [] + + {:error, module, expected_file} -> + error(IssueMeta.for(source_file, params), module, expected_file) + end + end + + defp verify(source_file, params) do + source_file.filename + |> Path.relative_to_cwd() + |> verify(Code.ast(source_file), params) + end + + @doc false + def verify(relative_path, ast, params) do + if verify_path?(relative_path, params), + do: ast |> main_module() |> verify_module(relative_path, params), + else: :ok + end + + defp verify_path?(relative_path, params) do + case Path.split(relative_path) do + ["lib" | _] -> not exclude?(relative_path, params) + ["test", "support" | _] -> false + ["test", "test_helper.exs"] -> false + ["test" | _] -> not exclude?(relative_path, params) + _ -> false + end + end + + defp exclude?(relative_path, params) do + params + |> Keyword.get(:exclude, []) + |> Enum.any?(&String.starts_with?(relative_path, &1)) + end + + defp main_module(ast) do + {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) + Enum.at(modules, -1) + end + + defp traverse({:defmodule, _meta, args}, modules) do + [{:__aliases__, _, name_parts}, _module_body] = args + {args, [Module.concat(name_parts) | modules]} + end + + defp traverse(ast, state), do: {ast, state} + + # empty file - shouldn't really happen, but we'll let it through + defp verify_module(nil, _relative_path, _params), do: :ok + + defp verify_module(main_module, relative_path, params) do + parsed_path = parsed_path(relative_path, params) + + expected_file = + expected_file_base(parsed_path.root, main_module) <> + Path.extname(parsed_path.allowed) + + if expected_file == parsed_path.allowed, + do: :ok, + else: {:error, main_module, expected_file} + end + + defp parsed_path(relative_path, params) do + parts = Path.split(relative_path) + + allowed = + Keyword.get(params, :ignore_folder_namespace, %{}) + |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) + |> Stream.map(&Path.split/1) + |> Enum.find(&List.starts_with?(parts, &1)) + |> case do + nil -> + relative_path + + ignore_parts -> + Stream.drop(ignore_parts, -1) + |> Enum.concat(Stream.drop(parts, length(ignore_parts))) + |> Path.join() + end + + %{root: hd(parts), allowed: allowed} + end + + defp expected_file_base(root_folder, module) do + {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + + relative_path = + if parent_namespace == [], + do: "", + else: parent_namespace |> Module.concat() |> Macro.underscore() + + file_name = module_name |> Module.concat() |> Macro.underscore() + + Path.join([root_folder, relative_path, file_name]) + end + + defp error(issue_meta, module, expected_file) do + format_issue(issue_meta, + message: + "Mismatch between file name and main module #{inspect(module)}. " <> + "Expected file path to be #{expected_file}. " <> + "Either move the file or rename the module.", + line_no: 1 + ) + end +end -- cgit v1.2.3 From 6bf85440b373c9b2fa1e8e7184dcf87518600306 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 19:09:04 +0300 Subject: mix tasks consistency --- lib/mix/tasks/pleroma/ecto.ex | 50 +++++++++++++++++++++++++++++++++++++ lib/mix/tasks/pleroma/ecto/ecto.ex | 50 ------------------------------------- lib/mix/tasks/pleroma/robots_txt.ex | 33 ++++++++++++++++++++++++ lib/mix/tasks/pleroma/robotstxt.ex | 33 ------------------------ 4 files changed, 83 insertions(+), 83 deletions(-) create mode 100644 lib/mix/tasks/pleroma/ecto.ex delete mode 100644 lib/mix/tasks/pleroma/ecto/ecto.ex create mode 100644 lib/mix/tasks/pleroma/robots_txt.ex delete mode 100644 lib/mix/tasks/pleroma/robotstxt.ex (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex new file mode 100644 index 000000000..3363cd45f --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto do + @doc """ + Ensures the given repository's migrations path exists on the file system. + """ + @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() + def ensure_migrations_path(repo, opts) do + path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") + + path = + case Path.type(path) do + :relative -> + Path.join(Application.app_dir(:pleroma), path) + + :absolute -> + path + end + + if not File.dir?(path) do + raise_missing_migrations(Path.relative_to_cwd(path), repo) + end + + path + end + + @doc """ + Returns the private repository path relative to the source. + """ + def source_repo_priv(repo) do + config = repo.config() + priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + Path.join(Application.app_dir(:pleroma), priv) + end + + defp raise_missing_migrations(path, repo) do + raise(""" + Could not find migrations directory #{inspect(path)} + for repo #{inspect(repo)}. + This may be because you are in a new project and the + migration directory has not been created yet. Creating an + empty directory at the path above will fix this error. + If you expected existing migrations to be found, please + make sure your repository has been properly configured + and the configured path exists. + """) + end +end diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex deleted file mode 100644 index 3363cd45f..000000000 --- a/lib/mix/tasks/pleroma/ecto/ecto.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Mix.Tasks.Pleroma.Ecto do - @doc """ - Ensures the given repository's migrations path exists on the file system. - """ - @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() - def ensure_migrations_path(repo, opts) do - path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") - - path = - case Path.type(path) do - :relative -> - Path.join(Application.app_dir(:pleroma), path) - - :absolute -> - path - end - - if not File.dir?(path) do - raise_missing_migrations(Path.relative_to_cwd(path), repo) - end - - path - end - - @doc """ - Returns the private repository path relative to the source. - """ - def source_repo_priv(repo) do - config = repo.config() - priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" - Path.join(Application.app_dir(:pleroma), priv) - end - - defp raise_missing_migrations(path, repo) do - raise(""" - Could not find migrations directory #{inspect(path)} - for repo #{inspect(repo)}. - This may be because you are in a new project and the - migration directory has not been created yet. Creating an - empty directory at the path above will fix this error. - If you expected existing migrations to be found, please - make sure your repository has been properly configured - and the configured path exists. - """) - end -end diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex new file mode 100644 index 000000000..24f08180e --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxt do + use Mix.Task + + @shortdoc "Generate robots.txt" + @moduledoc """ + Generates robots.txt + + ## Overwrite robots.txt to disallow all + + mix pleroma.robots_txt disallow_all + + This will write a robots.txt that will hide all paths on your instance + from search engines and other robots that obey robots.txt + + """ + def run(["disallow_all"]) do + Mix.Pleroma.start_pleroma() + static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + if !File.exists?(static_dir) do + File.mkdir_p!(static_dir) + end + + robots_txt_path = Path.join(static_dir, "robots.txt") + robots_txt_content = "User-Agent: *\nDisallow: /\n" + + File.write!(robots_txt_path, robots_txt_content, [:write]) + end +end diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robotstxt.ex deleted file mode 100644 index 24f08180e..000000000 --- a/lib/mix/tasks/pleroma/robotstxt.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RobotsTxt do - use Mix.Task - - @shortdoc "Generate robots.txt" - @moduledoc """ - Generates robots.txt - - ## Overwrite robots.txt to disallow all - - mix pleroma.robots_txt disallow_all - - This will write a robots.txt that will hide all paths on your instance - from search engines and other robots that obey robots.txt - - """ - def run(["disallow_all"]) do - Mix.Pleroma.start_pleroma() - static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") - - if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) - end - - robots_txt_path = Path.join(static_dir, "robots.txt") - robots_txt_content = "User-Agent: *\nDisallow: /\n" - - File.write!(robots_txt_path, robots_txt_content, [:write]) - end -end -- cgit v1.2.3 From 103f3dcb9ed0a12a11e9cc5c574449439fc2cb0e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 18:33:03 +0300 Subject: rich media parser ttl files consistency --- lib/pleroma/web/rich_media/parser/ttl.ex | 7 +++ .../web/rich_media/parser/ttl/aws_signed_url.ex | 50 ++++++++++++++++++++++ .../web/rich_media/parsers/ttl/aws_signed_url.ex | 50 ---------------------- lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 7 --- 4 files changed, 57 insertions(+), 57 deletions(-) create mode 100644 lib/pleroma/web/rich_media/parser/ttl.ex create mode 100644 lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/ttl.ex (limited to 'lib') diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex new file mode 100644 index 000000000..8353f0fff --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL do + @callback ttl(Map.t(), String.t()) :: Integer.t() | nil +end diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex new file mode 100644 index 000000000..fc4ef79c0 --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do + @behaviour Pleroma.Web.RichMedia.Parser.TTL + + @impl true + def ttl(data, _url) do + image = Map.get(data, :image) + + if is_aws_signed_url(image) do + image + |> parse_query_params() + |> format_query_params() + |> get_expiration_timestamp() + else + {:error, "Not aws signed url #{inspect(image)}"} + end + end + + defp is_aws_signed_url(image) when is_binary(image) and image != "" do + %URI{host: host, query: query} = URI.parse(image) + + String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") + end + + defp is_aws_signed_url(_), do: nil + + defp parse_query_params(image) do + %URI{query: query} = URI.parse(image) + query + end + + defp format_query_params(query) do + query + |> String.split(~r/&|=/) + |> Enum.chunk_every(2) + |> Map.new(fn [k, v] -> {k, v} end) + end + + defp get_expiration_timestamp(params) when is_map(params) do + {:ok, date} = + params + |> Map.get("X-Amz-Date") + |> Timex.parse("{ISO:Basic:Z}") + + {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} + end +end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex deleted file mode 100644 index 15109d28d..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do - @behaviour Pleroma.Web.RichMedia.Parser.TTL - - @impl Pleroma.Web.RichMedia.Parser.TTL - def ttl(data, _url) do - image = Map.get(data, :image) - - if is_aws_signed_url(image) do - image - |> parse_query_params() - |> format_query_params() - |> get_expiration_timestamp() - else - {:error, "Not aws signed url #{inspect(image)}"} - end - end - - defp is_aws_signed_url(image) when is_binary(image) and image != "" do - %URI{host: host, query: query} = URI.parse(image) - - String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") - end - - defp is_aws_signed_url(_), do: nil - - defp parse_query_params(image) do - %URI{query: query} = URI.parse(image) - query - end - - defp format_query_params(query) do - query - |> String.split(~r/&|=/) - |> Enum.chunk_every(2) - |> Map.new(fn [k, v] -> {k, v} end) - end - - defp get_expiration_timestamp(params) when is_map(params) do - {:ok, date} = - params - |> Map.get("X-Amz-Date") - |> Timex.parse("{ISO:Basic:Z}") - - {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} - end -end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex deleted file mode 100644 index 13511888c..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL do - @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} -end -- cgit v1.2.3 From b5b4395e4a7c63e31579475888fa892dcdaeecff Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 19:08:19 +0300 Subject: oauth consistency --- lib/pleroma/plugs/o_auth_plug.ex | 120 ++++ lib/pleroma/plugs/o_auth_scopes_plug.ex | 77 +++ lib/pleroma/plugs/oauth_plug.ex | 120 ---- lib/pleroma/plugs/oauth_scopes_plug.ex | 77 --- .../admin_api/controllers/o_auth_app_controller.ex | 77 +++ .../admin_api/controllers/oauth_app_controller.ex | 77 --- .../operations/admin/o_auth_app_operation.ex | 217 ++++++++ .../operations/admin/oauth_app_operation.ex | 217 -------- lib/pleroma/web/o_auth.ex | 6 + lib/pleroma/web/o_auth/app.ex | 149 +++++ lib/pleroma/web/o_auth/authorization.ex | 95 ++++ lib/pleroma/web/o_auth/fallback_controller.ex | 32 ++ lib/pleroma/web/o_auth/mfa_controller.ex | 98 ++++ lib/pleroma/web/o_auth/mfa_view.ex | 17 + lib/pleroma/web/o_auth/o_auth_controller.ex | 610 +++++++++++++++++++++ lib/pleroma/web/o_auth/o_auth_view.ex | 30 + lib/pleroma/web/o_auth/scopes.ex | 76 +++ lib/pleroma/web/o_auth/token.ex | 135 +++++ lib/pleroma/web/o_auth/token/query.ex | 49 ++ .../web/o_auth/token/strategy/refresh_token.ex | 58 ++ lib/pleroma/web/o_auth/token/strategy/revoke.ex | 26 + lib/pleroma/web/o_auth/token/utils.ex | 72 +++ lib/pleroma/web/oauth.ex | 6 - lib/pleroma/web/oauth/app.ex | 149 ----- lib/pleroma/web/oauth/authorization.ex | 95 ---- lib/pleroma/web/oauth/fallback_controller.ex | 32 -- lib/pleroma/web/oauth/mfa_controller.ex | 98 ---- lib/pleroma/web/oauth/mfa_view.ex | 17 - lib/pleroma/web/oauth/oauth_controller.ex | 610 --------------------- lib/pleroma/web/oauth/oauth_view.ex | 30 - lib/pleroma/web/oauth/scopes.ex | 76 --- lib/pleroma/web/oauth/token.ex | 135 ----- lib/pleroma/web/oauth/token/query.ex | 49 -- .../web/oauth/token/strategy/refresh_token.ex | 58 -- lib/pleroma/web/oauth/token/strategy/revoke.ex | 26 - lib/pleroma/web/oauth/token/utils.ex | 72 --- 36 files changed, 1944 insertions(+), 1944 deletions(-) create mode 100644 lib/pleroma/plugs/o_auth_plug.ex create mode 100644 lib/pleroma/plugs/o_auth_scopes_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_scopes_plug.ex create mode 100644 lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex delete mode 100644 lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex delete mode 100644 lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex create mode 100644 lib/pleroma/web/o_auth.ex create mode 100644 lib/pleroma/web/o_auth/app.ex create mode 100644 lib/pleroma/web/o_auth/authorization.ex create mode 100644 lib/pleroma/web/o_auth/fallback_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_view.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_controller.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_view.ex create mode 100644 lib/pleroma/web/o_auth/scopes.ex create mode 100644 lib/pleroma/web/o_auth/token.ex create mode 100644 lib/pleroma/web/o_auth/token/query.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/refresh_token.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/revoke.ex create mode 100644 lib/pleroma/web/o_auth/token/utils.ex delete mode 100644 lib/pleroma/web/oauth.ex delete mode 100644 lib/pleroma/web/oauth/app.ex delete mode 100644 lib/pleroma/web/oauth/authorization.ex delete mode 100644 lib/pleroma/web/oauth/fallback_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_view.ex delete mode 100644 lib/pleroma/web/oauth/oauth_controller.ex delete mode 100644 lib/pleroma/web/oauth/oauth_view.ex delete mode 100644 lib/pleroma/web/oauth/scopes.ex delete mode 100644 lib/pleroma/web/oauth/token.ex delete mode 100644 lib/pleroma/web/oauth/token/query.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/refresh_token.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/revoke.ex delete mode 100644 lib/pleroma/web/oauth/token/utils.ex (limited to 'lib') diff --git a/lib/pleroma/plugs/o_auth_plug.ex b/lib/pleroma/plugs/o_auth_plug.ex new file mode 100644 index 000000000..6fa71ef47 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_plug.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthPlug do + import Plug.Conn + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn + end + end + + # Gets user by token + # + @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil + defp fetch_user_and_token(token) do + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + with %Token{user: user} = token_record <- Repo.one(query) do + {:ok, user, token_record} + end + end + + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token_str(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str([]), do: :no_token_found + + defp fetch_token_str([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token_str(tail) + end + end +end diff --git a/lib/pleroma/plugs/o_auth_scopes_plug.ex b/lib/pleroma/plugs/o_auth_scopes_plug.ex new file mode 100644 index 000000000..b1a736d78 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_scopes_plug.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthScopesPlug do + import Plug.Conn + import Pleroma.Web.Gettext + + alias Pleroma.Config + + use Pleroma.Web, :plug + + def init(%{scopes: _} = options), do: options + + @impl true + def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do + op = options[:op] || :| + token = assigns[:token] + + scopes = transform_scopes(scopes, options) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] + + cond do + token && op == :| && Enum.any?(matched_scopes) -> + conn + + token && op == :& && matched_scopes == scopes -> + conn + + options[:fallback] == :proceed_unauthenticated -> + drop_auth_info(conn) + + true -> + missing_scopes = scopes -- matched_scopes + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + + conn + |> put_resp_content_type("application/json") + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) + |> halt() + end + end + + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" + def filter_descendants(scopes, supported_scopes) do + Enum.filter( + scopes, + fn scope -> + Enum.find( + supported_scopes, + &(scope == &1 || String.starts_with?(scope, &1 <> ":")) + ) + end + ) + end + + @doc "Transforms scopes by applying supported options (e.g. :admin)" + def transform_scopes(scopes, options) do + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + end +end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex deleted file mode 100644 index 6fa71ef47..000000000 --- a/lib/pleroma/plugs/oauth_plug.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlug do - import Plug.Conn - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") - - def init(options), do: options - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(%{params: %{"access_token" => access_token}} = conn, _) do - with {:ok, user, token_record} <- fetch_user_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - end - - def call(conn, _) do - case fetch_token_str(conn) do - {:ok, token} -> - with {:ok, user, token_record} <- fetch_user_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - - _ -> - conn - end - end - - # Gets user by token - # - @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil - defp fetch_user_and_token(token) do - query = - from(t in Token, - where: t.token == ^token, - join: user in assoc(t, :user), - preload: [user: user] - ) - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: user} = token_record <- Repo.one(query) do - {:ok, user, token_record} - end - end - - @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil - defp fetch_app_and_token(token) do - query = - from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) - - with %Token{app: app} = token_record <- Repo.one(query) do - {:ok, app, token_record} - end - end - - # Gets token from session by :oauth_token key - # - @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_from_session(conn) do - case get_session(conn, :oauth_token) do - nil -> :no_token_found - token -> {:ok, token} - end - end - - # Gets token from headers - # - @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str(%Plug.Conn{} = conn) do - headers = get_req_header(conn, "authorization") - - with :no_token_found <- fetch_token_str(headers), - do: fetch_token_from_session(conn) - end - - @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str([]), do: :no_token_found - - defp fetch_token_str([token | tail]) do - trimmed_token = String.trim(token) - - case Regex.run(@realm_reg, trimmed_token) do - [_, match] -> {:ok, String.trim(match)} - _ -> fetch_token_str(tail) - end - end -end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex deleted file mode 100644 index b1a736d78..000000000 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlug do - import Plug.Conn - import Pleroma.Web.Gettext - - alias Pleroma.Config - - use Pleroma.Web, :plug - - def init(%{scopes: _} = options), do: options - - @impl true - def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do - op = options[:op] || :| - token = assigns[:token] - - scopes = transform_scopes(scopes, options) - matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] - - cond do - token && op == :| && Enum.any?(matched_scopes) -> - conn - - token && op == :& && matched_scopes == scopes -> - conn - - options[:fallback] == :proceed_unauthenticated -> - drop_auth_info(conn) - - true -> - missing_scopes = scopes -- matched_scopes - permissions = Enum.join(missing_scopes, " #{op} ") - - error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) - - conn - |> put_resp_content_type("application/json") - |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) - |> halt() - end - end - - @doc "Drops authentication info from connection" - def drop_auth_info(conn) do - # To simplify debugging, setting a private variable on `conn` if auth info is dropped - conn - |> put_private(:authentication_ignored, true) - |> assign(:user, nil) - |> assign(:token, nil) - end - - @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" - def filter_descendants(scopes, supported_scopes) do - Enum.filter( - scopes, - fn scope -> - Enum.find( - supported_scopes, - &(scope == &1 || String.starts_with?(scope, &1 <> ":")) - ) - end - ) - end - - @doc "Transforms scopes by applying supported options (e.g. :admin)" - def transform_scopes(scopes, options) do - if options[:admin] do - Config.oauth_admin_scopes(scopes) - else - scopes - end - end -end diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex new file mode 100644 index 000000000..dca23ea73 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.OAuth.App + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex deleted file mode 100644 index dca23ea73..000000000 --- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, only: [json_response: 3] - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.OAuth.App - - require Logger - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.AppView) - - plug( - OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [:create, :index, :update, :delete] - ) - - action_fallback(Pleroma.Web.AdminAPI.FallbackController) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation - - def index(conn, params) do - search_params = - params - |> Map.take([:client_id, :page, :page_size, :trusted]) - |> Map.put(:client_name, params[:name]) - - with {:ok, apps, count} <- App.search(search_params) do - render(conn, "index.json", - apps: apps, - count: count, - page_size: params.page_size, - admin: true - ) - end - end - - def create(%{body_params: params} = conn, _) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - case App.create(params) do - {:ok, app} -> - render(conn, "show.json", app: app, admin: true) - - {:error, changeset} -> - json(conn, App.errors(changeset)) - end - end - - def update(%{body_params: params} = conn, %{id: id}) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - with {:ok, app} <- App.update(id, params) do - render(conn, "show.json", app: app, admin: true) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def delete(conn, params) do - with {:ok, _app} <- App.destroy(params.id) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end -end diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex new file mode 100644 index 000000000..a75f3e622 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -0,0 +1,217 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + summary: "List OAuth apps", + tags: ["Admin", "oAuth Apps"], + operationId: "AdminAPI.OAuthAppController.index", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), + Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), + Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), + Operation.parameter( + :trusted, + :query, + %Schema{type: :boolean, default: false}, + "Trusted apps" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of apps to return" + ) + | admin_api_params() + ], + responses: %{ + 200 => + Operation.response("List of apps", "application/json", %Schema{ + type: :object, + properties: %{ + apps: %Schema{type: :array, items: oauth_app()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + }, + example: %{ + "apps" => [ + %{ + "id" => 1, + "name" => "App name", + "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", + "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", + "redirect_uri" => "https://example.com/oauth-callback", + "website" => "https://example.com", + "trusted" => true + } + ], + "count" => 1, + "page_size" => 50 + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Create OAuth App", + operationId: "AdminAPI.OAuthAppController.create", + requestBody: request_body("Parameters", create_request()), + parameters: admin_api_params(), + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Update OAuth App", + operationId: "AdminAPI.OAuthAppController.update", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + oneOf: [ApiError, %Schema{type: :string}] + }) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Delete OAuth App", + operationId: "AdminAPI.OAuthAppController.delete", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + responses: %{ + 204 => no_content_response(), + 400 => no_content_response() + } + } + end + + defp create_request do + %Schema{ + title: "oAuthAppCreateRequest", + type: :object, + required: [:name, :redirect_uris], + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp update_request do + %Schema{ + title: "oAuthAppUpdateRequest", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp oauth_app do + %Schema{ + title: "oAuthApp", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true}, + trusted: %Schema{type: :boolean} + }, + example: %{ + "id" => 123, + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "redirect_uri" => "https://myapp.com/oauth-callback", + "website" => "https://myapp.com/", + "trusted" => false + } + } + end + + def id_param do + Operation.parameter(:id, :path, :integer, "App ID", + example: 1337, + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex deleted file mode 100644 index a75f3e622..000000000 --- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex +++ /dev/null @@ -1,217 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do - alias OpenApiSpex.Operation - alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Schemas.ApiError - - import Pleroma.Web.ApiSpec.Helpers - - def open_api_operation(action) do - operation = String.to_existing_atom("#{action}_operation") - apply(__MODULE__, operation, []) - end - - def index_operation do - %Operation{ - summary: "List OAuth apps", - tags: ["Admin", "oAuth Apps"], - operationId: "AdminAPI.OAuthAppController.index", - security: [%{"oAuth" => ["write"]}], - parameters: [ - Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), - Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), - Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), - Operation.parameter( - :trusted, - :query, - %Schema{type: :boolean, default: false}, - "Trusted apps" - ), - Operation.parameter( - :page_size, - :query, - %Schema{type: :integer, default: 50}, - "Number of apps to return" - ) - | admin_api_params() - ], - responses: %{ - 200 => - Operation.response("List of apps", "application/json", %Schema{ - type: :object, - properties: %{ - apps: %Schema{type: :array, items: oauth_app()}, - count: %Schema{type: :integer}, - page_size: %Schema{type: :integer} - }, - example: %{ - "apps" => [ - %{ - "id" => 1, - "name" => "App name", - "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", - "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", - "redirect_uri" => "https://example.com/oauth-callback", - "website" => "https://example.com", - "trusted" => true - } - ], - "count" => 1, - "page_size" => 50 - } - }) - } - } - end - - def create_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Create OAuth App", - operationId: "AdminAPI.OAuthAppController.create", - requestBody: request_body("Parameters", create_request()), - parameters: admin_api_params(), - security: [%{"oAuth" => ["write"]}], - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => Operation.response("Bad Request", "application/json", ApiError) - } - } - end - - def update_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Update OAuth App", - operationId: "AdminAPI.OAuthAppController.update", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - requestBody: request_body("Parameters", update_request()), - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => - Operation.response("Bad Request", "application/json", %Schema{ - oneOf: [ApiError, %Schema{type: :string}] - }) - } - } - end - - def delete_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Delete OAuth App", - operationId: "AdminAPI.OAuthAppController.delete", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - responses: %{ - 204 => no_content_response(), - 400 => no_content_response() - } - } - end - - defp create_request do - %Schema{ - title: "oAuthAppCreateRequest", - type: :object, - required: [:name, :redirect_uris], - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - redirect_uris: %Schema{ - type: :string, - description: - "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp update_request do - %Schema{ - title: "oAuthAppUpdateRequest", - type: :object, - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - redirect_uris: %Schema{ - type: :string, - description: - "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp oauth_app do - %Schema{ - title: "oAuthApp", - type: :object, - properties: %{ - id: %Schema{type: :integer}, - name: %Schema{type: :string}, - client_id: %Schema{type: :string}, - client_secret: %Schema{type: :string}, - redirect_uri: %Schema{type: :string}, - website: %Schema{type: :string, nullable: true}, - trusted: %Schema{type: :boolean} - }, - example: %{ - "id" => 123, - "name" => "My App", - "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", - "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", - "redirect_uri" => "https://myapp.com/oauth-callback", - "website" => "https://myapp.com/", - "trusted" => false - } - } - end - - def id_param do - Operation.parameter(:id, :path, :integer, "App ID", - example: 1337, - required: true - ) - end -end diff --git a/lib/pleroma/web/o_auth.ex b/lib/pleroma/web/o_auth.ex new file mode 100644 index 000000000..2f1b8708d --- /dev/null +++ b/lib/pleroma/web/o_auth.ex @@ -0,0 +1,6 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth do +end diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex new file mode 100644 index 000000000..df99472e1 --- /dev/null +++ b/lib/pleroma/web/o_auth/app.ex @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.App do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "apps" do + field(:client_name, :string) + field(:redirect_uris, :string) + field(:scopes, {:array, :string}, default: []) + field(:website, :string) + field(:client_id, :string) + field(:client_secret, :string) + field(:trusted, :boolean, default: false) + + has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) + has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) + + timestamps() + end + + @spec changeset(t(), map()) :: Ecto.Changeset.t() + def changeset(struct, params) do + cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) + end + + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() + def register_changeset(struct, params \\ %{}) do + changeset = + struct + |> changeset(params) + |> validate_required([:client_name, :redirect_uris, :scopes]) + + if changeset.valid? do + changeset + |> put_change( + :client_id, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + |> put_change( + :client_secret, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + else + changeset + end + end + + @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def create(params) do + %__MODULE__{} + |> register_changeset(params) + |> Repo.insert() + end + + @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def update(id, params) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + app + |> changeset(params) + |> Repo.update() + end + end + + @doc """ + Gets app by attrs or create new with attrs. + And updates the scopes if need. + """ + @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def get_or_make(attrs, scopes) do + with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do + update_scopes(app, scopes) + else + _e -> + %__MODULE__{} + |> register_changeset(Map.put(attrs, :scopes, scopes)) + |> Repo.insert() + end + end + + defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} + defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} + + defp update_scopes(%__MODULE__{} = app, scopes) do + app + |> change(%{scopes: scopes}) + |> Repo.update() + end + + @spec search(map()) :: {:ok, [t()], non_neg_integer()} + def search(params) do + query = from(a in __MODULE__) + + query = + if params[:client_name] do + from(a in query, where: a.client_name == ^params[:client_name]) + else + query + end + + query = + if params[:client_id] do + from(a in query, where: a.client_id == ^params[:client_id]) + else + query + end + + query = + if Map.has_key?(params, :trusted) do + from(a in query, where: a.trusted == ^params[:trusted]) + else + query + end + + query = + from(u in query, + limit: ^params[:page_size], + offset: ^((params[:page] - 1) * params[:page_size]) + ) + + count = Repo.aggregate(__MODULE__, :count, :id) + + {:ok, Repo.all(query), count} + end + + @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def destroy(id) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + Repo.delete(app) + end + end + + @spec errors(Ecto.Changeset.t()) :: map() + def errors(changeset) do + Enum.reduce(changeset.errors, %{}, fn + {:client_name, {error, _}}, acc -> + Map.put(acc, :name, error) + + {key, {error, _}}, acc -> + Map.put(acc, key, error) + end) + end +end diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex new file mode 100644 index 000000000..268ee5b63 --- /dev/null +++ b/lib/pleroma/web/o_auth/authorization.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Authorization do + use Ecto.Schema + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + + import Ecto.Changeset + import Ecto.Query + + @type t :: %__MODULE__{} + + schema "oauth_authorizations" do + field(:token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + field(:used, :boolean, default: false) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} + def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do + %{ + scopes: scopes || app.scopes, + user_id: user.id, + app_id: app.id + } + |> create_changeset() + |> Repo.insert() + end + + @spec create_changeset(map()) :: Changeset.t() + def create_changeset(attrs \\ %{}) do + %Authorization{} + |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) + |> validate_required([:app_id, :scopes]) + |> add_token() + |> add_lifetime() + end + + defp add_token(changeset) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + put_change(changeset, :token, token) + end + + defp add_lifetime(changeset) do + put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) + end + + @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() + def use_changeset(%Authorization{} = auth, params) do + auth + |> cast(params, [:used]) + |> validate_required([:used]) + end + + @spec use_token(Authorization.t()) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} + def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do + if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do + Repo.update(use_changeset(auth, %{used: true})) + else + {:error, "token expired"} + end + end + + def use_token(%Authorization{used: true}), do: {:error, "already used"} + + @spec delete_user_authorizations(User.t()) :: {integer(), any()} + def delete_user_authorizations(%User{} = user) do + user + |> delete_by_user_query + |> Repo.delete_all() + end + + def delete_by_user_query(%User{id: user_id}) do + from(a in __MODULE__, where: a.user_id == ^user_id) + end + + @doc "gets auth for app by token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end +end diff --git a/lib/pleroma/web/o_auth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex new file mode 100644 index 000000000..a89ced886 --- /dev/null +++ b/lib/pleroma/web/o_auth/fallback_controller.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.FallbackController do + use Pleroma.Web, :controller + alias Pleroma.Web.OAuth.OAuthController + + def call(conn, {:register, :generic_error}) do + conn + |> put_status(:internal_server_error) + |> put_flash( + :error, + dgettext("errors", "Unknown error, please check the details and try again.") + ) + |> OAuthController.registration_details(conn.params) + end + + def call(conn, {:register, _error}) do + conn + |> put_status(:unauthorized) + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.registration_details(conn.params) + end + + def call(conn, _error) do + conn + |> put_status(:unauthorized) + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.authorize(conn.params) + end +end diff --git a/lib/pleroma/web/o_auth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex new file mode 100644 index 000000000..f102c93e7 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_controller.ex @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAController do + @moduledoc """ + The model represents api to use Multi Factor authentications. + """ + + use Pleroma.Web, :controller + + alias Pleroma.MFA + alias Pleroma.Web.Auth.TOTPAuthenticator + alias Pleroma.Web.OAuth.MFAView, as: View + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Token + + plug(:fetch_session when action in [:show, :verify]) + plug(:fetch_flash when action in [:show, :verify]) + + @doc """ + Display form to input mfa code or recovery code. + """ + def show(conn, %{"mfa_token" => mfa_token} = params) do + template = Map.get(params, "challenge_type", "totp") + + conn + |> put_view(View) + |> render("#{template}.html", %{ + mfa_token: mfa_token, + redirect_uri: params["redirect_uri"], + state: params["state"] + }) + end + + @doc """ + Verification code and continue authorization. + """ + def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do + with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, mfa_params) do + conn + |> OAuthController.after_create_authorization(auth, %{ + "authorization" => %{ + "redirect_uri" => mfa_params["redirect_uri"], + "state" => mfa_params["state"] + } + }) + else + _ -> + conn + |> put_flash(:error, "Two-factor authentication failed.") + |> put_status(:unauthorized) + |> show(mfa_params) + end + end + + @doc """ + Verification second step of MFA (or recovery) and returns access token. + + ## Endpoint + POST /oauth/mfa/challenge + + params: + `client_id` + `client_secret` + `mfa_token` - access token to check second step of mfa + `challenge_type` - 'totp' or 'recovery' + `code` + + """ + def challenge(conn, %{"mfa_token" => mfa_token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, params), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> + conn + |> put_status(400) + |> json(%{error: "Invalid code"}) + end + end + + # Verify TOTP Code + defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do + TOTPAuthenticator.verify(code, user) + end + + # Verify Recovery Code + defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do + TOTPAuthenticator.verify_recovery_code(user, code) + end + + defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} +end diff --git a/lib/pleroma/web/o_auth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex new file mode 100644 index 000000000..5d87db268 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_view.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end +end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex new file mode 100644 index 000000000..a4152e840 --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -0,0 +1,610 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthController do + use Pleroma.Web, :controller + + alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps + alias Pleroma.MFA + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Registration + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Auth.Authenticator + alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken + alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + + require Logger + + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + + plug(:fetch_session) + plug(:fetch_flash) + + plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + + plug(RateLimiter, [name: :authentication] when action == :create_authorization) + + action_fallback(Pleroma.Web.OAuth.FallbackController) + + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg + def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do + {auth_attrs, params} = Map.pop(params, "authorization") + authorize(conn, Map.merge(params, auth_attrs)) + end + + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do + if ControllerHelper.truthy_param?(params["force_login"]) do + do_authorize(conn, params) + else + handle_existing_authorization(conn, params) + end + end + + # Note: the token is set in oauth_plug, but the token and client do not always go together. + # For example, MastodonFE's token is set if user requests with another client, + # after user already authorized to MastodonFE. + # So we have to check client and token. + def authorize( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"client_id" => client_id} = params + ) do + with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), + ^client_id <- t.app.client_id do + handle_existing_authorization(conn, params) + else + _ -> do_authorize(conn, params) + end + end + + def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) + + defp do_authorize(%Plug.Conn{} = conn, params) do + app = Repo.get_by(App, client_id: params["client_id"]) + available_scopes = (app && app.scopes) || [] + scopes = Scopes.fetch_scopes(params, available_scopes) + + scopes = + if scopes == [] do + available_scopes + else + scopes + end + + # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template + render(conn, Authenticator.auth_template(), %{ + response_type: params["response_type"], + client_id: params["client_id"], + available_scopes: available_scopes, + scopes: scopes, + redirect_uri: params["redirect_uri"], + state: params["state"], + params: params + }) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"redirect_uri" => @oob_token_redirect_uri} + ) do + render(conn, "oob_token_exists.html", %{token: token}) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{} = params + ) do + app = Repo.preload(token, :app).app + + redirect_uri = + if is_binary(params["redirect_uri"]) do + params["redirect_uri"] + else + default_redirect_uri(app) + end + + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{access_token: token.token} + url_params = Maps.put_if_present(url_params, :state, params["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + def create_authorization( + %Plug.Conn{} = conn, + %{"authorization" => _} = params, + opts \\ [] + ) do + with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do + after_create_authorization(conn, auth, params) + else + error -> + handle_create_authorization_error(conn, error, params) + end + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} + }) do + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("oob_authorization_created.html", %{auth: auth}) + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:error, scopes_issue}, + %{"authorization" => _} = params + ) + when scopes_issue in [:unsupported_scopes, :missing_scopes] do + # Per https://github.com/tootsuite/mastodon/blob/ + # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 + conn + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) + |> put_status(:unauthorized) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :confirmation_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:mfa_required, user, auth, _}, + params + ) do + {:ok, token} = MFA.Token.create(user, auth) + + data = %{ + "mfa_token" => token.token, + "redirect_uri" => params["authorization"]["redirect_uri"], + "state" => params["authorization"]["state"] + } + + MFAController.show(conn, data) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Password reset is required")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :deactivated}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do + Authenticator.handle_error(conn, error) + end + + @doc "Renew access_token with refresh_token" + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = _params + ) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), + {:ok, token} <- RefreshToken.grant(token) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> render_invalid_credentials_error(conn) + end + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + fixed_token = Token.Utils.fix_padding(params["code"]), + {:ok, auth} <- Authorization.get_by_token(app, fixed_token), + %User{} = user <- User.get_cached_by_id(auth.user_id), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password"} = params + ) do + with {:ok, %User{} = user} <- Authenticator.get_user(conn), + {:ok, app} <- Token.Utils.fetch_app(conn), + requested_scopes <- Scopes.fetch_scopes(params, app.scopes), + {:ok, token} <- login(user, app, requested_scopes) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password", "name" => name, "password" => _password} = params + ) do + params = + params + |> Map.delete("name") + |> Map.put("username", name) + + token_exchange(conn, params) + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, auth} <- Authorization.create_authorization(app, %User{}), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{token: token})) + else + _error -> + handle_token_exchange_error(conn, :invalid_credentails) + end + end + + # Bad request + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do + conn + |> put_status(:forbidden) + |> json(build_and_response_mfa_token(user, auth)) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do + render_error( + conn, + :forbidden, + "Your account is currently disabled", + %{}, + "account_is_disabled" + ) + end + + defp handle_token_exchange_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending} + ) do + render_error( + conn, + :forbidden, + "Password reset is required", + %{}, + "password_reset_required" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do + render_error( + conn, + :forbidden, + "Your login is missing a confirmed e-mail address", + %{}, + "missing_confirmed_email" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do + render_error( + conn, + :forbidden, + "Your account is awaiting approval.", + %{}, + "awaiting_approval" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do + render_invalid_credentials_error(conn) + end + + def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, _token} <- RevokeToken.revoke(app, params) do + json(conn, %{}) + else + _error -> + # RFC 7009: invalid tokens [in the request] do not cause an error response + json(conn, %{}) + end + end + + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + # Response for bad request + defp bad_request(%Plug.Conn{} = conn, _) do + render_error(conn, :internal_server_error, "Bad request") + end + + @doc "Prepares OAuth request to provider for Ueberauth" + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do + scope = + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() + + state = + auth_attrs + |> Map.delete("scopes") + |> Map.put("scope", scope) + |> Jason.encode!() + + params = + auth_attrs + |> Map.drop(~w(scope scopes client_id redirect_uri)) + |> Map.put("state", state) + + # Handing the request to Ueberauth + redirect(conn, to: o_auth_path(conn, :request, provider, params)) + end + + def request(%Plug.Conn{} = conn, params) do + message = + if params["provider"] do + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) + else + dgettext("errors", "Bad OAuth request.") + end + + conn + |> put_flash(:error, message) + |> redirect(to: "/") + end + + def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do + params = callback_params(params) + messages = for e <- Map.get(failure, :errors, []), do: e.message + message = Enum.join(messages, "; ") + + conn + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + + def callback(%Plug.Conn{} = conn, params) do + params = callback_params(params) + + with {:ok, registration} <- Authenticator.get_registration(conn) do + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) + + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session_registration_id(registration.id) + |> registration_details(%{"authorization" => registration_params}) + end + else + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + + conn + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + end + + defp callback_params(%{"state" => state} = params) do + Map.merge(params, Jason.decode!(state)) + end + + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do + render(conn, "register.html", %{ + client_id: auth_attrs["client_id"], + redirect_uri: auth_attrs["redirect_uri"], + state: auth_attrs["state"], + scopes: Scopes.fetch_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] + }) + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {_, {:ok, auth, _user}} <- + {:create_authorization, do_create_authorization(conn, params)}, + %User{} = user <- Repo.preload(auth, :user).user, + {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do + conn + |> put_session_registration_id(nil) + |> after_create_authorization(auth, params) + else + {:create_authorization, error} -> + {:register, handle_create_authorization_error(conn, error, params)} + + _ -> + {:register, :generic_error} + end + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {:ok, user} <- Authenticator.create_from_registration(conn, registration) do + conn + |> put_session_registration_id(nil) + |> create_authorization( + params, + user: user + ) + else + {:error, changeset} -> + message = + Enum.map(changeset.errors, fn {field, {error, _}} -> + "#{field} #{error}" + end) + |> Enum.join("; ") + + message = + String.replace( + message, + "ap_id has already been taken", + "nickname has already been taken" + ) + + conn + |> put_status(:forbidden) + |> put_flash(:error, "Error: #{message}.") + |> registration_details(params) + + _ -> + {:register, :generic_error} + end + end + + defp do_create_authorization(conn, auth_attrs, user \\ nil) + + defp do_create_authorization( + %Plug.Conn{} = conn, + %{ + "authorization" => + %{ + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = auth_attrs + }, + user + ) do + with {_, {:ok, %User{} = user}} <- + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, + %App{} = app <- Repo.get_by(App, client_id: client_id), + true <- redirect_uri in String.split(app.redirect_uris), + requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), + {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do + {:ok, auth, user} + end + end + + defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) + when is_list(requested_scopes) do + with {:account_status, :active} <- {:account_status, User.account_status(user)}, + {:ok, scopes} <- validate_scopes(app, requested_scopes), + {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do + {:ok, auth} + end + end + + # Note: intended to be a private function but opened for AccountController that logs in on signup + @doc "If checks pass, creates authorization and token for given user, app and requested scopes." + def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do + with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, + {:ok, token} <- Token.exchange_token(app, auth) do + {:ok, token} + end + end + + # Special case: Local MastodonFE + defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) + + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri + + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) + + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), + do: put_session(conn, :registration_id, registration_id) + + defp build_and_response_mfa_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do + MFAView.render("mfa_response.json", %{token: token, user: user}) + end + end + + @spec validate_scopes(App.t(), map() | list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(%App{} = app, params) when is_map(params) do + requested_scopes = Scopes.fetch_scopes(params, app.scopes) + validate_scopes(app, requested_scopes) + end + + defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do + Scopes.validate(requested_scopes, app.scopes) + end + + def default_redirect_uri(%App{} = app) do + app.redirect_uris + |> String.split() + |> Enum.at(0) + end + + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end +end diff --git a/lib/pleroma/web/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex new file mode 100644 index 000000000..f55247ebd --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_view.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) +end diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex new file mode 100644 index 000000000..6f06f1431 --- /dev/null +++ b/lib/pleroma/web/o_auth/scopes.ex @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + alias Pleroma.Plugs.OAuthScopesPlug + + @doc """ + Fetch scopes from request params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map() | struct(), list()) :: list() + + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validate(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} + + def validate(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + ^scopes -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end +end diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex new file mode 100644 index 000000000..de37998f2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token.ex @@ -0,0 +1,135 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token do + use Ecto.Schema + + import Ecto.Changeset + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Query + + @type t :: %__MODULE__{} + + schema "oauth_tokens" do + field(:token, :string) + field(:refresh_token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @doc "Gets token for app by access token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + Query.get_by_app(app_id) + |> Query.get_by_token(token) + |> Repo.find_resource() + end + + @doc "Gets token for app by refresh token" + @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_refresh_token(%App{id: app_id} = _app, token) do + Query.get_by_app(app_id) + |> Query.get_by_refresh_token(token) + |> Query.preload([:user]) + |> Repo.find_resource() + end + + @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} + def exchange_token(app, auth) do + with {:ok, auth} <- Authorization.use_token(auth), + true <- auth.app_id == app.id do + user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} + + create( + app, + user, + %{scopes: auth.scopes} + ) + end + end + + defp put_token(changeset) do + changeset + |> change(%{token: Token.Utils.generate_token()}) + |> validate_required([:token]) + |> unique_constraint(:token) + end + + defp put_refresh_token(changeset, attrs) do + refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) + + changeset + |> change(%{refresh_token: refresh_token}) + |> validate_required([:refresh_token]) + |> unique_constraint(:refresh_token) + end + + defp put_valid_until(changeset, attrs) do + expires_in = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) + + changeset + |> change(%{valid_until: expires_in}) + |> validate_required([:valid_until]) + end + + @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create(%App{} = app, %User{} = user, attrs \\ %{}) do + with {:ok, token} <- do_create(app, user, attrs) do + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ + }) + end + + {:ok, token} + end + end + + defp do_create(app, user, attrs) do + %__MODULE__{user_id: user.id, app_id: app.id} + |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) + |> validate_required([:scopes, :app_id]) + |> put_valid_until(attrs) + |> put_token() + |> put_refresh_token(attrs) + |> Repo.insert() + end + + def delete_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Repo.delete_all() + end + + def delete_user_token(%User{id: user_id}, token_id) do + Query.get_by_user(user_id) + |> Query.get_by_id(token_id) + |> Repo.delete_all() + end + + def get_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Query.preload([:app]) + |> Repo.all() + end + + def is_expired?(%__MODULE__{valid_until: valid_until}) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 + end + + def is_expired?(_), do: false + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) +end diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex new file mode 100644 index 000000000..fd6d9b112 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/query.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Query do + @moduledoc """ + Contains queries for OAuth Token. + """ + + import Ecto.Query, only: [from: 2] + + @type query :: Ecto.Queryable.t() | Token.t() + + alias Pleroma.Web.OAuth.Token + + @spec get_by_refresh_token(query, String.t()) :: query + def get_by_refresh_token(query \\ Token, refresh_token) do + from(q in query, where: q.refresh_token == ^refresh_token) + end + + @spec get_by_token(query, String.t()) :: query + def get_by_token(query \\ Token, token) do + from(q in query, where: q.token == ^token) + end + + @spec get_by_app(query, String.t()) :: query + def get_by_app(query \\ Token, app_id) do + from(q in query, where: q.app_id == ^app_id) + end + + @spec get_by_id(query, String.t()) :: query + def get_by_id(query \\ Token, id) do + from(q in query, where: q.id == ^id) + end + + @spec get_by_user(query, String.t()) :: query + def get_by_user(query \\ Token, user_id) do + from(q in query, where: q.user_id == ^user_id) + end + + @spec preload(query, any) :: query + def preload(query \\ Token, assoc_preload \\ []) + + def preload(query, assoc_preload) when is_list(assoc_preload) do + from(q in query, preload: ^assoc_preload) + end + + def preload(query, _assoc_preload), do: query +end diff --git a/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..625b0fde2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do + @moduledoc """ + Functions for dealing with refresh token strategy. + """ + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.Revoke + + @doc """ + Will grant access token by refresh token. + """ + @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} + def grant(token) do + access_token = Repo.preload(token, [:user, :app]) + + result = + Repo.transaction(fn -> + token_params = %{ + app: access_token.app, + user: access_token.user, + scopes: access_token.scopes + } + + access_token + |> revoke_access_token() + |> create_access_token(token_params) + end) + + case result do + {:ok, {:error, reason}} -> {:error, reason} + {:ok, {:ok, token}} -> {:ok, token} + {:error, reason} -> {:error, reason} + end + end + + defp revoke_access_token(token) do + Revoke.revoke(token) + end + + defp create_access_token({:error, error}, _), do: {:error, error} + + defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do + Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) + end + + defp add_refresh_token(params, token) do + case Config.get([:oauth2, :issue_new_refresh_token], false) do + true -> Map.put(params, :refresh_token, token) + false -> params + end + end +end diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex new file mode 100644 index 000000000..069c1ee21 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do + @moduledoc """ + Functions for dealing with revocation. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @doc "Finds and revokes access token for app and by token" + @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} + def revoke(%App{} = app, %{"token" => token} = _attrs) do + with {:ok, token} <- Token.get_by_token(app, token), + do: revoke(token) + end + + @doc "Revokes access token" + @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + def revoke(%Token{} = token) do + Repo.delete(token) + end +end diff --git a/lib/pleroma/web/o_auth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex new file mode 100644 index 000000000..43aeab6b0 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/utils.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Utils do + @moduledoc """ + Auxiliary functions for dealing with tokens. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + + @doc "Fetch app by client credentials from request" + @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} + def fetch_app(conn) do + res = + conn + |> fetch_client_credentials() + |> fetch_client + + case res do + %App{} = app -> {:ok, app} + _ -> {:error, :not_found} + end + end + + defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do + Repo.get_by(App, client_id: id, client_secret: secret) + end + + defp fetch_client({_id, _secret}), do: nil + + defp fetch_client_credentials(conn) do + # Per RFC 6749, HTTP Basic is preferred to body params + with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), + {:ok, decoded} <- Base.decode64(encoded), + [id, secret] <- + Enum.map( + String.split(decoded, ":"), + fn s -> URI.decode_www_form(s) end + ) do + {id, secret} + else + _ -> {conn.params["client_id"], conn.params["client_secret"]} + end + end + + @doc "convert token inserted_at to unix timestamp" + def format_created_at(%{inserted_at: inserted_at} = _token) do + inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + end + + @doc false + @spec generate_token(keyword()) :: binary() + def generate_token(opts \\ []) do + opts + |> Keyword.get(:size, 32) + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. + def fix_padding(token) do + token + |> URI.decode() + |> Base.url_decode64!(padding: false) + |> Base.url_encode64(padding: false) + end +end diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex deleted file mode 100644 index 2f1b8708d..000000000 --- a/lib/pleroma/web/oauth.ex +++ /dev/null @@ -1,6 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth do -end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex deleted file mode 100644 index df99472e1..000000000 --- a/lib/pleroma/web/oauth/app.ex +++ /dev/null @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.App do - use Ecto.Schema - import Ecto.Changeset - import Ecto.Query - alias Pleroma.Repo - - @type t :: %__MODULE__{} - - schema "apps" do - field(:client_name, :string) - field(:redirect_uris, :string) - field(:scopes, {:array, :string}, default: []) - field(:website, :string) - field(:client_id, :string) - field(:client_secret, :string) - field(:trusted, :boolean, default: false) - - has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) - has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) - - timestamps() - end - - @spec changeset(t(), map()) :: Ecto.Changeset.t() - def changeset(struct, params) do - cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) - end - - @spec register_changeset(t(), map()) :: Ecto.Changeset.t() - def register_changeset(struct, params \\ %{}) do - changeset = - struct - |> changeset(params) - |> validate_required([:client_name, :redirect_uris, :scopes]) - - if changeset.valid? do - changeset - |> put_change( - :client_id, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - |> put_change( - :client_secret, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - else - changeset - end - end - - @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def create(params) do - %__MODULE__{} - |> register_changeset(params) - |> Repo.insert() - end - - @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def update(id, params) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - app - |> changeset(params) - |> Repo.update() - end - end - - @doc """ - Gets app by attrs or create new with attrs. - And updates the scopes if need. - """ - @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def get_or_make(attrs, scopes) do - with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do - update_scopes(app, scopes) - else - _e -> - %__MODULE__{} - |> register_changeset(Map.put(attrs, :scopes, scopes)) - |> Repo.insert() - end - end - - defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} - defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} - - defp update_scopes(%__MODULE__{} = app, scopes) do - app - |> change(%{scopes: scopes}) - |> Repo.update() - end - - @spec search(map()) :: {:ok, [t()], non_neg_integer()} - def search(params) do - query = from(a in __MODULE__) - - query = - if params[:client_name] do - from(a in query, where: a.client_name == ^params[:client_name]) - else - query - end - - query = - if params[:client_id] do - from(a in query, where: a.client_id == ^params[:client_id]) - else - query - end - - query = - if Map.has_key?(params, :trusted) do - from(a in query, where: a.trusted == ^params[:trusted]) - else - query - end - - query = - from(u in query, - limit: ^params[:page_size], - offset: ^((params[:page] - 1) * params[:page_size]) - ) - - count = Repo.aggregate(__MODULE__, :count, :id) - - {:ok, Repo.all(query), count} - end - - @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def destroy(id) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - Repo.delete(app) - end - end - - @spec errors(Ecto.Changeset.t()) :: map() - def errors(changeset) do - Enum.reduce(changeset.errors, %{}, fn - {:client_name, {error, _}}, acc -> - Map.put(acc, :name, error) - - {key, {error, _}}, acc -> - Map.put(acc, key, error) - end) - end -end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex deleted file mode 100644 index 268ee5b63..000000000 --- a/lib/pleroma/web/oauth/authorization.ex +++ /dev/null @@ -1,95 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Authorization do - use Ecto.Schema - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - - import Ecto.Changeset - import Ecto.Query - - @type t :: %__MODULE__{} - - schema "oauth_authorizations" do - field(:token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - field(:used, :boolean, default: false) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} - def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do - %{ - scopes: scopes || app.scopes, - user_id: user.id, - app_id: app.id - } - |> create_changeset() - |> Repo.insert() - end - - @spec create_changeset(map()) :: Changeset.t() - def create_changeset(attrs \\ %{}) do - %Authorization{} - |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) - |> validate_required([:app_id, :scopes]) - |> add_token() - |> add_lifetime() - end - - defp add_token(changeset) do - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - put_change(changeset, :token, token) - end - - defp add_lifetime(changeset) do - put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) - end - - @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() - def use_changeset(%Authorization{} = auth, params) do - auth - |> cast(params, [:used]) - |> validate_required([:used]) - end - - @spec use_token(Authorization.t()) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} - def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do - if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do - Repo.update(use_changeset(auth, %{used: true})) - else - {:error, "token expired"} - end - end - - def use_token(%Authorization{used: true}), do: {:error, "already used"} - - @spec delete_user_authorizations(User.t()) :: {integer(), any()} - def delete_user_authorizations(%User{} = user) do - user - |> delete_by_user_query - |> Repo.delete_all() - end - - def delete_by_user_query(%User{id: user_id}) do - from(a in __MODULE__, where: a.user_id == ^user_id) - end - - @doc "gets auth for app by token" - @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_token(%App{id: app_id} = _app, token) do - from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) - |> Repo.find_resource() - end -end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex deleted file mode 100644 index a89ced886..000000000 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.FallbackController do - use Pleroma.Web, :controller - alias Pleroma.Web.OAuth.OAuthController - - def call(conn, {:register, :generic_error}) do - conn - |> put_status(:internal_server_error) - |> put_flash( - :error, - dgettext("errors", "Unknown error, please check the details and try again.") - ) - |> OAuthController.registration_details(conn.params) - end - - def call(conn, {:register, _error}) do - conn - |> put_status(:unauthorized) - |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.registration_details(conn.params) - end - - def call(conn, _error) do - conn - |> put_status(:unauthorized) - |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.authorize(conn.params) - end -end diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex deleted file mode 100644 index f102c93e7..000000000 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ /dev/null @@ -1,98 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAController do - @moduledoc """ - The model represents api to use Multi Factor authentications. - """ - - use Pleroma.Web, :controller - - alias Pleroma.MFA - alias Pleroma.Web.Auth.TOTPAuthenticator - alias Pleroma.Web.OAuth.MFAView, as: View - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Token - - plug(:fetch_session when action in [:show, :verify]) - plug(:fetch_flash when action in [:show, :verify]) - - @doc """ - Display form to input mfa code or recovery code. - """ - def show(conn, %{"mfa_token" => mfa_token} = params) do - template = Map.get(params, "challenge_type", "totp") - - conn - |> put_view(View) - |> render("#{template}.html", %{ - mfa_token: mfa_token, - redirect_uri: params["redirect_uri"], - state: params["state"] - }) - end - - @doc """ - Verification code and continue authorization. - """ - def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do - with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, mfa_params) do - conn - |> OAuthController.after_create_authorization(auth, %{ - "authorization" => %{ - "redirect_uri" => mfa_params["redirect_uri"], - "state" => mfa_params["state"] - } - }) - else - _ -> - conn - |> put_flash(:error, "Two-factor authentication failed.") - |> put_status(:unauthorized) - |> show(mfa_params) - end - end - - @doc """ - Verification second step of MFA (or recovery) and returns access token. - - ## Endpoint - POST /oauth/mfa/challenge - - params: - `client_id` - `client_secret` - `mfa_token` - access token to check second step of mfa - `challenge_type` - 'totp' or 'recovery' - `code` - - """ - def challenge(conn, %{"mfa_token" => mfa_token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, params), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> - conn - |> put_status(400) - |> json(%{error: "Invalid code"}) - end - end - - # Verify TOTP Code - defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do - TOTPAuthenticator.verify(code, user) - end - - # Verify Recovery Code - defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do - TOTPAuthenticator.verify_recovery_code(user, code) - end - - defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} -end diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex deleted file mode 100644 index 5d87db268..000000000 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - alias Pleroma.MFA - - def render("mfa_response.json", %{token: token, user: user}) do - %{ - error: "mfa_required", - mfa_token: token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end -end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex deleted file mode 100644 index a4152e840..000000000 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ /dev/null @@ -1,610 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthController do - use Pleroma.Web, :controller - - alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps - alias Pleroma.MFA - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Registration - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Auth.Authenticator - alias Pleroma.Web.ControllerHelper - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.MFAController - alias Pleroma.Web.OAuth.MFAView - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Scopes - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken - alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - - require Logger - - if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - - plug(:fetch_session) - plug(:fetch_flash) - - plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) - - plug(RateLimiter, [name: :authentication] when action == :create_authorization) - - action_fallback(Pleroma.Web.OAuth.FallbackController) - - @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" - - # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do - {auth_attrs, params} = Map.pop(params, "authorization") - authorize(conn, Map.merge(params, auth_attrs)) - end - - def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do - if ControllerHelper.truthy_param?(params["force_login"]) do - do_authorize(conn, params) - else - handle_existing_authorization(conn, params) - end - end - - # Note: the token is set in oauth_plug, but the token and client do not always go together. - # For example, MastodonFE's token is set if user requests with another client, - # after user already authorized to MastodonFE. - # So we have to check client and token. - def authorize( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"client_id" => client_id} = params - ) do - with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), - ^client_id <- t.app.client_id do - handle_existing_authorization(conn, params) - else - _ -> do_authorize(conn, params) - end - end - - def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) - - defp do_authorize(%Plug.Conn{} = conn, params) do - app = Repo.get_by(App, client_id: params["client_id"]) - available_scopes = (app && app.scopes) || [] - scopes = Scopes.fetch_scopes(params, available_scopes) - - scopes = - if scopes == [] do - available_scopes - else - scopes - end - - # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template - render(conn, Authenticator.auth_template(), %{ - response_type: params["response_type"], - client_id: params["client_id"], - available_scopes: available_scopes, - scopes: scopes, - redirect_uri: params["redirect_uri"], - state: params["state"], - params: params - }) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"redirect_uri" => @oob_token_redirect_uri} - ) do - render(conn, "oob_token_exists.html", %{token: token}) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{} = params - ) do - app = Repo.preload(token, :app).app - - redirect_uri = - if is_binary(params["redirect_uri"]) do - params["redirect_uri"] - else - default_redirect_uri(app) - end - - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{access_token: token.token} - url_params = Maps.put_if_present(url_params, :state, params["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - def create_authorization( - %Plug.Conn{} = conn, - %{"authorization" => _} = params, - opts \\ [] - ) do - with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do - after_create_authorization(conn, auth, params) - else - error -> - handle_create_authorization_error(conn, error, params) - end - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} - }) do - # Enforcing the view to reuse the template when calling from other controllers - conn - |> put_view(OAuthView) - |> render("oob_authorization_created.html", %{auth: auth}) - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs - }) do - app = Repo.preload(auth, :app).app - - # An extra safety measure before we redirect (also done in `do_create_authorization/2`) - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{code: auth.token} - url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:error, scopes_issue}, - %{"authorization" => _} = params - ) - when scopes_issue in [:unsupported_scopes, :missing_scopes] do - # Per https://github.com/tootsuite/mastodon/blob/ - # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 - conn - |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) - |> put_status(:unauthorized) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :confirmation_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:mfa_required, user, auth, _}, - params - ) do - {:ok, token} = MFA.Token.create(user, auth) - - data = %{ - "mfa_token" => token.token, - "redirect_uri" => params["authorization"]["redirect_uri"], - "state" => params["authorization"]["state"] - } - - MFAController.show(conn, data) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Password reset is required")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :deactivated}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do - Authenticator.handle_error(conn, error) - end - - @doc "Renew access_token with refresh_token" - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = _params - ) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), - {:ok, token} <- RefreshToken.grant(token) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> render_invalid_credentials_error(conn) - end - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - fixed_token = Token.Utils.fix_padding(params["code"]), - {:ok, auth} <- Authorization.get_by_token(app, fixed_token), - %User{} = user <- User.get_cached_by_id(auth.user_id), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password"} = params - ) do - with {:ok, %User{} = user} <- Authenticator.get_user(conn), - {:ok, app} <- Token.Utils.fetch_app(conn), - requested_scopes <- Scopes.fetch_scopes(params, app.scopes), - {:ok, token} <- login(user, app, requested_scopes) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password", "name" => name, "password" => _password} = params - ) do - params = - params - |> Map.delete("name") - |> Map.put("username", name) - - token_exchange(conn, params) - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, auth} <- Authorization.create_authorization(app, %User{}), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{token: token})) - else - _error -> - handle_token_exchange_error(conn, :invalid_credentails) - end - end - - # Bad request - def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do - conn - |> put_status(:forbidden) - |> json(build_and_response_mfa_token(user, auth)) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do - render_error( - conn, - :forbidden, - "Your account is currently disabled", - %{}, - "account_is_disabled" - ) - end - - defp handle_token_exchange_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending} - ) do - render_error( - conn, - :forbidden, - "Password reset is required", - %{}, - "password_reset_required" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do - render_error( - conn, - :forbidden, - "Your login is missing a confirmed e-mail address", - %{}, - "missing_confirmed_email" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do - render_error( - conn, - :forbidden, - "Your account is awaiting approval.", - %{}, - "awaiting_approval" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do - render_invalid_credentials_error(conn) - end - - def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, _token} <- RevokeToken.revoke(app, params) do - json(conn, %{}) - else - _error -> - # RFC 7009: invalid tokens [in the request] do not cause an error response - json(conn, %{}) - end - end - - def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - # Response for bad request - defp bad_request(%Plug.Conn{} = conn, _) do - render_error(conn, :internal_server_error, "Bad request") - end - - @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(%Plug.Conn{} = conn, %{ - "provider" => provider, - "authorization" => auth_attrs - }) do - scope = - auth_attrs - |> Scopes.fetch_scopes([]) - |> Scopes.to_string() - - state = - auth_attrs - |> Map.delete("scopes") - |> Map.put("scope", scope) - |> Jason.encode!() - - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - - # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) - end - - def request(%Plug.Conn{} = conn, params) do - message = - if params["provider"] do - dgettext("errors", "Unsupported OAuth provider: %{provider}.", - provider: params["provider"] - ) - else - dgettext("errors", "Bad OAuth request.") - end - - conn - |> put_flash(:error, message) - |> redirect(to: "/") - end - - def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do - params = callback_params(params) - messages = for e <- Map.get(failure, :errors, []), do: e.message - message = Enum.join(messages, "; ") - - conn - |> put_flash( - :error, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - - def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) - - with {:ok, registration} <- Authenticator.get_registration(conn) do - auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - - case Repo.get_assoc(registration, :user) do - {:ok, user} -> - create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - - _ -> - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) - - conn - |> put_session_registration_id(registration.id) - |> registration_details(%{"authorization" => registration_params}) - end - else - error -> - Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) - - conn - |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - end - - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) - end - - def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do - render(conn, "register.html", %{ - client_id: auth_attrs["client_id"], - redirect_uri: auth_attrs["redirect_uri"], - state: auth_attrs["state"], - scopes: Scopes.fetch_scopes(auth_attrs, []), - nickname: auth_attrs["nickname"], - email: auth_attrs["email"] - }) - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {_, {:ok, auth, _user}} <- - {:create_authorization, do_create_authorization(conn, params)}, - %User{} = user <- Repo.preload(auth, :user).user, - {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do - conn - |> put_session_registration_id(nil) - |> after_create_authorization(auth, params) - else - {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, params)} - - _ -> - {:register, :generic_error} - end - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {:ok, user} <- Authenticator.create_from_registration(conn, registration) do - conn - |> put_session_registration_id(nil) - |> create_authorization( - params, - user: user - ) - else - {:error, changeset} -> - message = - Enum.map(changeset.errors, fn {field, {error, _}} -> - "#{field} #{error}" - end) - |> Enum.join("; ") - - message = - String.replace( - message, - "ap_id has already been taken", - "nickname has already been taken" - ) - - conn - |> put_status(:forbidden) - |> put_flash(:error, "Error: #{message}.") - |> registration_details(params) - - _ -> - {:register, :generic_error} - end - end - - defp do_create_authorization(conn, auth_attrs, user \\ nil) - - defp do_create_authorization( - %Plug.Conn{} = conn, - %{ - "authorization" => - %{ - "client_id" => client_id, - "redirect_uri" => redirect_uri - } = auth_attrs - }, - user - ) do - with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, - %App{} = app <- Repo.get_by(App, client_id: client_id), - true <- redirect_uri in String.split(app.redirect_uris), - requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), - {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do - {:ok, auth, user} - end - end - - defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) - when is_list(requested_scopes) do - with {:account_status, :active} <- {:account_status, User.account_status(user)}, - {:ok, scopes} <- validate_scopes(app, requested_scopes), - {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do - {:ok, auth} - end - end - - # Note: intended to be a private function but opened for AccountController that logs in on signup - @doc "If checks pass, creates authorization and token for given user, app and requested scopes." - def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do - with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, - {:ok, token} <- Token.exchange_token(app, auth) do - {:ok, token} - end - end - - # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - - defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - - defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), - do: put_session(conn, :registration_id, registration_id) - - defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create(user, auth) do - MFAView.render("mfa_response.json", %{token: token, user: user}) - end - end - - @spec validate_scopes(App.t(), map() | list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(%App{} = app, params) when is_map(params) do - requested_scopes = Scopes.fetch_scopes(params, app.scopes) - validate_scopes(app, requested_scopes) - end - - defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do - Scopes.validate(requested_scopes, app.scopes) - end - - def default_redirect_uri(%App{} = app) do - app.redirect_uris - |> String.split() - |> Enum.at(0) - end - - defp render_invalid_credentials_error(conn) do - render_error(conn, :bad_request, "Invalid credentials") - end -end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex deleted file mode 100644 index f55247ebd..000000000 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - - alias Pleroma.Web.OAuth.Token.Utils - - def render("token.json", %{token: token} = opts) do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: expires_in(), - scope: Enum.join(token.scopes, " "), - created_at: Utils.format_created_at(token) - } - - if user = opts[:user] do - response - |> Map.put(:me, user.ap_id) - else - response - end - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex deleted file mode 100644 index 6f06f1431..000000000 --- a/lib/pleroma/web/oauth/scopes.ex +++ /dev/null @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Scopes do - @moduledoc """ - Functions for dealing with scopes. - """ - - alias Pleroma.Plugs.OAuthScopesPlug - - @doc """ - Fetch scopes from request params. - - Note: `scopes` is used by Mastodon — supporting it but sticking to - OAuth's standard `scope` wherever we control it - """ - @spec fetch_scopes(map() | struct(), list()) :: list() - - def fetch_scopes(params, default) do - parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) - end - - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> to_list - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end - - @doc """ - Convert scopes string to list - """ - @spec to_list(binary()) :: [binary()] - def to_list(nil), do: [] - - def to_list(str) do - str - |> String.trim() - |> String.split(~r/[\s,]+/) - end - - @doc """ - Convert scopes list to string - """ - @spec to_string(list()) :: binary() - def to_string(scopes), do: Enum.join(scopes, " ") - - @doc """ - Validates scopes. - """ - @spec validate(list() | nil, list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], - do: {:error, :missing_scopes} - - def validate(scopes, app_scopes) do - case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do - ^scopes -> {:ok, scopes} - _ -> {:error, :unsupported_scopes} - end - end - - def contains_admin_scopes?(scopes) do - scopes - |> OAuthScopesPlug.filter_descendants(["admin"]) - |> Enum.any?() - end -end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex deleted file mode 100644 index de37998f2..000000000 --- a/lib/pleroma/web/oauth/token.ex +++ /dev/null @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token do - use Ecto.Schema - - import Ecto.Changeset - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Query - - @type t :: %__MODULE__{} - - schema "oauth_tokens" do - field(:token, :string) - field(:refresh_token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @doc "Gets token for app by access token" - @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_token(%App{id: app_id} = _app, token) do - Query.get_by_app(app_id) - |> Query.get_by_token(token) - |> Repo.find_resource() - end - - @doc "Gets token for app by refresh token" - @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_refresh_token(%App{id: app_id} = _app, token) do - Query.get_by_app(app_id) - |> Query.get_by_refresh_token(token) - |> Query.preload([:user]) - |> Repo.find_resource() - end - - @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} - def exchange_token(app, auth) do - with {:ok, auth} <- Authorization.use_token(auth), - true <- auth.app_id == app.id do - user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} - - create( - app, - user, - %{scopes: auth.scopes} - ) - end - end - - defp put_token(changeset) do - changeset - |> change(%{token: Token.Utils.generate_token()}) - |> validate_required([:token]) - |> unique_constraint(:token) - end - - defp put_refresh_token(changeset, attrs) do - refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) - - changeset - |> change(%{refresh_token: refresh_token}) - |> validate_required([:refresh_token]) - |> unique_constraint(:refresh_token) - end - - defp put_valid_until(changeset, attrs) do - expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) - - changeset - |> change(%{valid_until: expires_in}) - |> validate_required([:valid_until]) - end - - @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} - def create(%App{} = app, %User{} = user, attrs \\ %{}) do - with {:ok, token} <- do_create(app, user, attrs) do - if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ - token_id: token.id, - valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), - mod: __MODULE__ - }) - end - - {:ok, token} - end - end - - defp do_create(app, user, attrs) do - %__MODULE__{user_id: user.id, app_id: app.id} - |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) - |> validate_required([:scopes, :app_id]) - |> put_valid_until(attrs) - |> put_token() - |> put_refresh_token(attrs) - |> Repo.insert() - end - - def delete_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Repo.delete_all() - end - - def delete_user_token(%User{id: user_id}, token_id) do - Query.get_by_user(user_id) - |> Query.get_by_id(token_id) - |> Repo.delete_all() - end - - def get_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Query.preload([:app]) - |> Repo.all() - end - - def is_expired?(%__MODULE__{valid_until: valid_until}) do - NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 - end - - def is_expired?(_), do: false - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex deleted file mode 100644 index fd6d9b112..000000000 --- a/lib/pleroma/web/oauth/token/query.ex +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Query do - @moduledoc """ - Contains queries for OAuth Token. - """ - - import Ecto.Query, only: [from: 2] - - @type query :: Ecto.Queryable.t() | Token.t() - - alias Pleroma.Web.OAuth.Token - - @spec get_by_refresh_token(query, String.t()) :: query - def get_by_refresh_token(query \\ Token, refresh_token) do - from(q in query, where: q.refresh_token == ^refresh_token) - end - - @spec get_by_token(query, String.t()) :: query - def get_by_token(query \\ Token, token) do - from(q in query, where: q.token == ^token) - end - - @spec get_by_app(query, String.t()) :: query - def get_by_app(query \\ Token, app_id) do - from(q in query, where: q.app_id == ^app_id) - end - - @spec get_by_id(query, String.t()) :: query - def get_by_id(query \\ Token, id) do - from(q in query, where: q.id == ^id) - end - - @spec get_by_user(query, String.t()) :: query - def get_by_user(query \\ Token, user_id) do - from(q in query, where: q.user_id == ^user_id) - end - - @spec preload(query, any) :: query - def preload(query \\ Token, assoc_preload \\ []) - - def preload(query, assoc_preload) when is_list(assoc_preload) do - from(q in query, preload: ^assoc_preload) - end - - def preload(query, _assoc_preload), do: query -end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex deleted file mode 100644 index 625b0fde2..000000000 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ /dev/null @@ -1,58 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do - @moduledoc """ - Functions for dealing with refresh token strategy. - """ - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.Revoke - - @doc """ - Will grant access token by refresh token. - """ - @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} - def grant(token) do - access_token = Repo.preload(token, [:user, :app]) - - result = - Repo.transaction(fn -> - token_params = %{ - app: access_token.app, - user: access_token.user, - scopes: access_token.scopes - } - - access_token - |> revoke_access_token() - |> create_access_token(token_params) - end) - - case result do - {:ok, {:error, reason}} -> {:error, reason} - {:ok, {:ok, token}} -> {:ok, token} - {:error, reason} -> {:error, reason} - end - end - - defp revoke_access_token(token) do - Revoke.revoke(token) - end - - defp create_access_token({:error, error}, _), do: {:error, error} - - defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do - Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) - end - - defp add_refresh_token(params, token) do - case Config.get([:oauth2, :issue_new_refresh_token], false) do - true -> Map.put(params, :refresh_token, token) - false -> params - end - end -end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex deleted file mode 100644 index 069c1ee21..000000000 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do - @moduledoc """ - Functions for dealing with revocation. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @doc "Finds and revokes access token for app and by token" - @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} - def revoke(%App{} = app, %{"token" => token} = _attrs) do - with {:ok, token} <- Token.get_by_token(app, token), - do: revoke(token) - end - - @doc "Revokes access token" - @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} - def revoke(%Token{} = token) do - Repo.delete(token) - end -end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex deleted file mode 100644 index 43aeab6b0..000000000 --- a/lib/pleroma/web/oauth/token/utils.ex +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Utils do - @moduledoc """ - Auxiliary functions for dealing with tokens. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - - @doc "Fetch app by client credentials from request" - @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} - def fetch_app(conn) do - res = - conn - |> fetch_client_credentials() - |> fetch_client - - case res do - %App{} = app -> {:ok, app} - _ -> {:error, :not_found} - end - end - - defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do - Repo.get_by(App, client_id: id, client_secret: secret) - end - - defp fetch_client({_id, _secret}), do: nil - - defp fetch_client_credentials(conn) do - # Per RFC 6749, HTTP Basic is preferred to body params - with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - Enum.map( - String.split(decoded, ":"), - fn s -> URI.decode_www_form(s) end - ) do - {id, secret} - else - _ -> {conn.params["client_id"], conn.params["client_secret"]} - end - end - - @doc "convert token inserted_at to unix timestamp" - def format_created_at(%{inserted_at: inserted_at} = _token) do - inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - end - - @doc false - @spec generate_token(keyword()) :: binary() - def generate_token(opts \\ []) do - opts - |> Keyword.get(:size, 32) - |> :crypto.strong_rand_bytes() - |> Base.url_encode64(padding: false) - end - - # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be - # decoding it. Investigate sometime. - def fix_padding(token) do - token - |> URI.decode() - |> Base.url_decode64!(padding: false) - |> Base.url_encode64(padding: false) - end -end -- cgit v1.2.3 From e8e4034c4879ebf0bb7fcc7606c97a3957a0ba06 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 19:55:55 +0300 Subject: metadata providers consistency --- lib/pleroma/web/metadata/feed.ex | 23 ---- lib/pleroma/web/metadata/opengraph.ex | 119 --------------------- lib/pleroma/web/metadata/provider.ex | 7 -- lib/pleroma/web/metadata/providers/feed.ex | 23 ++++ lib/pleroma/web/metadata/providers/opengraph.ex | 119 +++++++++++++++++++++ lib/pleroma/web/metadata/providers/provider.ex | 7 ++ lib/pleroma/web/metadata/providers/rel_me.ex | 19 ++++ .../web/metadata/providers/restrict_indexing.ex | 24 +++++ lib/pleroma/web/metadata/providers/twitter_card.ex | 112 +++++++++++++++++++ lib/pleroma/web/metadata/rel_me.ex | 19 ---- lib/pleroma/web/metadata/restrict_indexing.ex | 24 ----- lib/pleroma/web/metadata/twitter_card.ex | 112 ------------------- 12 files changed, 304 insertions(+), 304 deletions(-) delete mode 100644 lib/pleroma/web/metadata/feed.ex delete mode 100644 lib/pleroma/web/metadata/opengraph.ex delete mode 100644 lib/pleroma/web/metadata/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/feed.ex create mode 100644 lib/pleroma/web/metadata/providers/opengraph.ex create mode 100644 lib/pleroma/web/metadata/providers/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/rel_me.ex create mode 100644 lib/pleroma/web/metadata/providers/restrict_indexing.ex create mode 100644 lib/pleroma/web/metadata/providers/twitter_card.ex delete mode 100644 lib/pleroma/web/metadata/rel_me.ex delete mode 100644 lib/pleroma/web/metadata/restrict_indexing.ex delete mode 100644 lib/pleroma/web/metadata/twitter_card.ex (limited to 'lib') diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex deleted file mode 100644 index bd1459a17..000000000 --- a/lib/pleroma/web/metadata/feed.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Feed do - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Router.Helpers - - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - [ - {:link, - [ - rel: "alternate", - type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex deleted file mode 100644 index bb1b23208..000000000 --- a/lib/pleroma/web/metadata/opengraph.ex +++ /dev/null @@ -1,119 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraph do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{ - object: object, - url: url, - user: user - }) do - attachments = build_attachments(object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - ": “" <> scrubbed_content <> "”" - else - "" - end - - # Most previews only show og:title which is inconvenient. Instagram - # hacks this by putting the description in the title and making the - # description longer prefixed by how many likes and shares the post - # has. Here we use the descriptive nickname in the title, and expand - # the full account & nickname in the description. We also use the cute^Wevil - # smart quotes around the status text like Instagram, too. - [ - {:meta, - [ - property: "og:title", - content: "#{user.name}" <> content - ], []}, - {:meta, [property: "og:url", content: url], []}, - {:meta, - [ - property: "og:description", - content: "#{Utils.user_name_string(user)}" <> content - ], []}, - {:meta, [property: "og:type", content: "website"], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - {:meta, - [ - property: "og:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, - {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - end - end - - defp build_attachments(%{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image - # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - | acc - ] - - "video" -> - [ - {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_), do: [] -end diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex deleted file mode 100644 index 767288f9c..000000000 --- a/lib/pleroma/web/metadata/provider.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Provider do - @callback build_tags(map()) :: list() -end diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex new file mode 100644 index 000000000..bd1459a17 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Feed do + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Router.Helpers + + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + [ + {:link, + [ + rel: "alternate", + type: "application/atom+xml", + href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/opengraph.ex b/lib/pleroma/web/metadata/providers/opengraph.ex new file mode 100644 index 000000000..bb1b23208 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/opengraph.ex @@ -0,0 +1,119 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{ + object: object, + url: url, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + ": “" <> scrubbed_content <> "”" + else + "" + end + + # Most previews only show og:title which is inconvenient. Instagram + # hacks this by putting the description in the title and making the + # description longer prefixed by how many likes and shares the post + # has. Here we use the descriptive nickname in the title, and expand + # the full account & nickname in the description. We also use the cute^Wevil + # smart quotes around the status text like Instagram, too. + [ + {:meta, + [ + property: "og:title", + content: "#{user.name}" <> content + ], []}, + {:meta, [property: "og:url", content: url], []}, + {:meta, + [ + property: "og:description", + content: "#{Utils.user_name_string(user)}" <> content + ], []}, + {:meta, [property: "og:type", content: "website"], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + {:meta, + [ + property: "og:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, + {:meta, [property: "og:description", content: truncated_bio], []}, + {:meta, [property: "og:type", content: "website"], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + end + end + + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + | acc + ] + + "video" -> + [ + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex new file mode 100644 index 000000000..767288f9c --- /dev/null +++ b/lib/pleroma/web/metadata/providers/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Provider do + @callback build_tags(map()) :: list() +end diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex new file mode 100644 index 000000000..8905c9c72 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RelMe do + alias Pleroma.Web.Metadata.Providers.Provider + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + bio_tree = Floki.parse_fragment!(user.bio) + + (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ + Floki.attribute(bio_tree, "a[rel~=me]", "href")) + |> Enum.map(fn link -> + {:link, [rel: "me", href: link], []} + end) + end +end diff --git a/lib/pleroma/web/metadata/providers/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex new file mode 100644 index 000000000..a1dcb6e15 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/restrict_indexing.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do + @behaviour Pleroma.Web.Metadata.Providers.Provider + + @moduledoc """ + Restricts indexing of remote users. + """ + + @impl true + def build_tags(%{user: %{local: true, discoverable: true}}), do: [] + + def build_tags(_) do + [ + {:meta, + [ + name: "robots", + content: "noindex, noarchive" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex new file mode 100644 index 000000000..df34b033f --- /dev/null +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server + +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCard do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{activity_id: id, object: object, user: user}) do + attachments = build_attachments(id, object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + "“" <> scrubbed_content <> "”" + else + "" + end + + [ + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + title_tag(user), + {:meta, [property: "twitter:description", content: truncated_bio], []}, + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end + end + + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "80"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, + [ + property: "twitter:player", + content: Utils.attachment_url(url["href"]) + ], []} + | acc + ] + + # TODO: Need the true width and height values here or Twitter renders an iFrame with + # a bad aspect ratio + "video" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_id, _object), do: [] + + defp player_url(id) do + Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) + end +end diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex deleted file mode 100644 index 8905c9c72..000000000 --- a/lib/pleroma/web/metadata/rel_me.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RelMe do - alias Pleroma.Web.Metadata.Providers.Provider - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - bio_tree = Floki.parse_fragment!(user.bio) - - (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ - Floki.attribute(bio_tree, "a[rel~=me]", "href")) - |> Enum.map(fn link -> - {:link, [rel: "me", href: link], []} - end) - end -end diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex deleted file mode 100644 index a1dcb6e15..000000000 --- a/lib/pleroma/web/metadata/restrict_indexing.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do - @behaviour Pleroma.Web.Metadata.Providers.Provider - - @moduledoc """ - Restricts indexing of remote users. - """ - - @impl true - def build_tags(%{user: %{local: true, discoverable: true}}), do: [] - - def build_tags(_) do - [ - {:meta, - [ - name: "robots", - content: "noindex, noarchive" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex deleted file mode 100644 index df34b033f..000000000 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ /dev/null @@ -1,112 +0,0 @@ -# Pleroma: A lightweight social networking server - -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.TwitterCard do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{activity_id: id, object: object, user: user}) do - attachments = build_attachments(id, object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - "“" <> scrubbed_content <> "”" - else - "" - end - - [ - title_tag(user), - {:meta, [property: "twitter:description", content: content], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - title_tag(user), - {:meta, [property: "twitter:description", content: truncated_bio], []}, - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - end - end - - defp title_tag(user) do - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} - end - - def image_tag(user) do - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} - end - - defp build_attachments(id, %{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "80"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, - [ - property: "twitter:player", - content: Utils.attachment_url(url["href"]) - ], []} - | acc - ] - - # TODO: Need the true width and height values here or Twitter renders an iFrame with - # a bad aspect ratio - "video" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_id, _object), do: [] - - defp player_url(id) do - Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) - end -end -- cgit v1.2.3 From fc7151a9c4cbd2fb122d717f54de4b30acffea36 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 20:02:53 +0300 Subject: more files renamings --- lib/phoenix/transports/web_socket/raw.ex | 89 ++++++ lib/pleroma/web.ex | 234 +++++++++++++++ .../web/mongoose_im/mongoose_im_controller.ex | 46 +++ .../web/mongooseim/mongoose_im_controller.ex | 46 --- lib/pleroma/web/o_status/o_status_controller.ex | 151 ++++++++++ lib/pleroma/web/ostatus/ostatus_controller.ex | 151 ---------- lib/pleroma/web/plug.ex | 8 + lib/pleroma/web/push.ex | 37 +++ lib/pleroma/web/push/push.ex | 37 --- lib/pleroma/web/rich_media/parsers/o_embed.ex | 29 ++ .../web/rich_media/parsers/oembed_parser.ex | 29 -- lib/pleroma/web/streamer.ex | 331 +++++++++++++++++++++ lib/pleroma/web/streamer/streamer.ex | 331 --------------------- lib/pleroma/web/twitter_api/controller.ex | 100 +++++++ .../web/twitter_api/twitter_api_controller.ex | 100 ------- lib/pleroma/web/web.ex | 239 --------------- lib/pleroma/web/web_finger.ex | 201 +++++++++++++ lib/pleroma/web/web_finger/web_finger.ex | 201 ------------- lib/pleroma/web/xml.ex | 45 +++ lib/pleroma/web/xml/xml.ex | 45 --- lib/pleroma/xml_builder.ex | 49 +++ lib/transports.ex | 89 ------ lib/xml_builder.ex | 49 --- 23 files changed, 1320 insertions(+), 1317 deletions(-) create mode 100644 lib/phoenix/transports/web_socket/raw.ex create mode 100644 lib/pleroma/web.ex create mode 100644 lib/pleroma/web/mongoose_im/mongoose_im_controller.ex delete mode 100644 lib/pleroma/web/mongooseim/mongoose_im_controller.ex create mode 100644 lib/pleroma/web/o_status/o_status_controller.ex delete mode 100644 lib/pleroma/web/ostatus/ostatus_controller.ex create mode 100644 lib/pleroma/web/plug.ex create mode 100644 lib/pleroma/web/push.ex delete mode 100644 lib/pleroma/web/push/push.ex create mode 100644 lib/pleroma/web/rich_media/parsers/o_embed.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/oembed_parser.ex create mode 100644 lib/pleroma/web/streamer.ex delete mode 100644 lib/pleroma/web/streamer/streamer.ex create mode 100644 lib/pleroma/web/twitter_api/controller.ex delete mode 100644 lib/pleroma/web/twitter_api/twitter_api_controller.ex delete mode 100644 lib/pleroma/web/web.ex create mode 100644 lib/pleroma/web/web_finger.ex delete mode 100644 lib/pleroma/web/web_finger/web_finger.ex create mode 100644 lib/pleroma/web/xml.ex delete mode 100644 lib/pleroma/web/xml/xml.ex create mode 100644 lib/pleroma/xml_builder.ex delete mode 100644 lib/transports.ex delete mode 100644 lib/xml_builder.ex (limited to 'lib') diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex new file mode 100644 index 000000000..aab7fad99 --- /dev/null +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Phoenix.Transports.WebSocket.Raw do + import Plug.Conn, + only: [ + fetch_query_params: 1, + send_resp: 3 + ] + + alias Phoenix.Socket.Transport + + def default_config do + [ + timeout: 60_000, + transport_log: false, + cowboy: Phoenix.Endpoint.CowboyWebSocket + ] + end + + def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do + {_, opts} = handler.__transport__(transport) + + conn = + conn + |> fetch_query_params + |> Transport.transport_log(opts[:transport_log]) + |> Transport.force_ssl(handler, endpoint, opts) + |> Transport.check_origin(handler, endpoint, opts) + + case conn do + %{halted: false} = conn -> + case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + {:ok, socket} -> + {:ok, conn, {__MODULE__, {socket, opts}}} + + :error -> + send_resp(conn, :forbidden, "") + {:error, conn} + end + + _ -> + {:error, conn} + end + end + + def init(conn, _) do + send_resp(conn, :bad_request, "") + {:error, conn} + end + + def ws_init({socket, config}) do + Process.flag(:trap_exit, true) + {:ok, %{socket: socket}, config[:timeout]} + end + + def ws_handle(op, data, state) do + state.socket.handler + |> apply(:handle, [op, data, state]) + |> case do + {op, data} -> + {:reply, {op, data}, state} + + {op, data, state} -> + {:reply, {op, data}, state} + + %{} = state -> + {:ok, state} + + _ -> + {:ok, state} + end + end + + def ws_info({_, _} = tuple, state) do + {:reply, tuple, state} + end + + def ws_info(_tuple, state), do: {:ok, state} + + def ws_close(state) do + ws_handle(:closed, :normal, state) + end + + def ws_terminate(reason, state) do + ws_handle(:closed, reason, state) + end +end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex new file mode 100644 index 000000000..9ca52733d --- /dev/null +++ b/lib/pleroma/web.ex @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web do + @moduledoc """ + A module that keeps using definitions for controllers, + views and so on. + + This can be used in your application as: + + use Pleroma.Web, :controller + use Pleroma.Web, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. + """ + + alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + + def controller do + quote do + use Phoenix.Controller, namespace: Pleroma.Web + + import Plug.Conn + + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + import Pleroma.Web.TranslationHelpers + + plug(:set_put_layout) + + defp set_put_layout(conn, _) do + put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) + end + + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain + defp skip_plug(conn, plug_modules) do + plug_modules + |> List.wrap() + |> Enum.reduce( + conn, + fn plug_module, conn -> + try do + plug_module.skip_plug(conn) + rescue + UndefinedFunctionError -> + raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + end + ) + end + + # Executed just before actual controller action, invokes before-action hooks (callbacks) + defp action(conn, params) do + with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), + %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), + %{halted: false} = conn <- maybe_perform_authenticated_check(conn), + %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do + super(conn, params) + end + end + + # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored + # (neither performed nor explicitly skipped) + defp maybe_drop_authentication_if_oauth_check_ignored(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + OAuthScopesPlug.drop_auth_info(conn) + else + conn + end + end + + # Ensures instance is public -or- user is authenticated if such check was scheduled + defp maybe_perform_public_or_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do + EnsurePublicOrAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Ensures user is authenticated if such check was scheduled + # Note: runs prior to action even if it was already executed earlier in plug chain + # (since OAuthScopesPlug has option of proceeding unauthenticated) + defp maybe_perform_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do + EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check + defp maybe_halt_on_missing_oauth_scopes_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + conn + |> render_error( + :forbidden, + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + |> halt() + else + conn + end + end + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/pleroma/web/templates", + namespace: Pleroma.Web + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + + import Pleroma.Web.ErrorHelpers + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + + require Logger + + @doc "Same as `render/3` but wrapped in a rescue block" + def safe_render(view, template, assigns \\ %{}) do + Phoenix.View.render(view, template, assigns) + rescue + error -> + Logger.error( + "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> + Exception.format(:error, error, __STACKTRACE__) + ) + + nil + end + + @doc """ + Same as `render_many/4` but wrapped in rescue block. + """ + def safe_render_many(collection, view, template, assigns \\ %{}) do + Enum.map(collection, fn resource -> + as = Map.get(assigns, :as) || view.__resource__ + assigns = Map.put(assigns, as, resource) + safe_render(view, template, assigns) + end) + |> Enum.filter(& &1) + end + end + end + + def router do + quote do + use Phoenix.Router + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Phoenix.Channel + import Pleroma.Web.Gettext + end + end + + def plug do + quote do + @behaviour Pleroma.Web.Plug + @behaviour Plug + + @doc """ + Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. + """ + def skip_plug(conn) do + PlugHelper.append_to_private_list( + conn, + PlugHelper.skipped_plugs_list_id(), + __MODULE__ + ) + end + + @impl Plug + @doc """ + Before-plug hook that + * ensures the plug is not skipped + * processes `:if_func` / `:unless_func` functional pre-run conditions + * adds plug to the list of called plugs and calls `perform/2` if checks are passed + + Note: multiple invocations of the same plug (with different or same options) are allowed. + """ + def call(%Plug.Conn{} = conn, options) do + if PlugHelper.plug_skipped?(conn, __MODULE__) || + (options[:if_func] && !options[:if_func].(conn)) || + (options[:unless_func] && options[:unless_func].(conn)) do + conn + else + conn = + PlugHelper.append_to_private_list( + conn, + PlugHelper.called_plugs_list_id(), + __MODULE__ + ) + + apply(__MODULE__, :perform, [conn, options]) + end + end + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end + + def base_url do + Pleroma.Web.Endpoint.url() + end +end diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex new file mode 100644 index 000000000..6cbbe8fd8 --- /dev/null +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIM.MongooseIMController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo + alias Pleroma.User + + plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) + plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) + + def user_exists(conn, %{"user" => username}) do + with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do + conn + |> json(true) + else + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end + + def check_password(conn, %{"user" => username, "pass" => password}) do + with %User{password_hash: password_hash, deactivated: false} <- + Repo.get_by(User, nickname: username, local: true), + true <- AuthenticationPlug.checkpw(password, password_hash) do + conn + |> json(true) + else + false -> + conn + |> put_status(:forbidden) + |> json(false) + + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex deleted file mode 100644 index 6cbbe8fd8..000000000 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIM.MongooseIMController do - use Pleroma.Web, :controller - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Repo - alias Pleroma.User - - plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) - plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) - - def user_exists(conn, %{"user" => username}) do - with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do - conn - |> json(true) - else - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end - - def check_password(conn, %{"user" => username, "pass" => password}) do - with %User{password_hash: password_hash, deactivated: false} <- - Repo.get_by(User, nickname: username, local: true), - true <- AuthenticationPlug.checkpw(password, password_hash) do - conn - |> json(true) - else - false -> - conn - |> put_status(:forbidden) - |> json(false) - - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end -end diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex new file mode 100644 index 000000000..de1b0b3f0 --- /dev/null +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusController do + use Pleroma.Web, :controller + + alias Fallback.RedirectController + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Plugs.RateLimiter + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPubController + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.PlayerView + alias Pleroma.Web.Router + + plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + ) + + plug( + RateLimiter, + [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] + ) + + plug( + Pleroma.Plugs.SetFormatPlug + when action in [:object, :activity, :notice] + ) + + action_fallback(:errors) + + def object(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :object) + end + + def object(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- + {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def activity(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :activity) + end + + def activity(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + cond do + format in ["json", "activity+json"] -> + if activity.local do + %{data: %{"id" => redirect_url}} = Object.normalize(activity) + redirect(conn, external: redirect_url) + else + {:error, :not_found} + end + + activity.data["type"] == "Create" -> + %Object{} = object = Object.normalize(activity) + + RedirectController.redirector_with_meta( + conn, + %{ + activity_id: activity.id, + object: object, + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + user: user + } + ) + + true -> + RedirectController.redirector(conn, nil) + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + + e -> + e + end + end + + # Returns an HTML embedded