0ec62acb9d
This actually was already intended before to eradict all future path-traversal-style exploits and to fix issues with some characters like akkoma#610 in0b2ec0ccee
. However, Dedupe and AnonymizeFilename got mixed up. The latter only anonymises the name in Content-Disposition headers GET parameters (with link_name), _not_ the upload path. Even without Dedupe, the upload path is prefixed by an UUID, so it _should_ already be hard to guess for attackers. But now we actually can be sure no path shenanigangs occur, uploads reliably work and save some disk space. While this makes the final path predictable, this prediction is not exploitable. Insertion of a back-reference to the upload itself requires pulling off a successfull preimage attack against SHA-256, which is deemed infeasible for the foreseeable futures. Dedupe was already included in the default list in config.exs since28cfb2c37a
, but this will get overridde by whatever the config generated by the "pleroma.instance gen" task chose. Upload+delete tests running in parallel using Dedupe might be flaky, but this was already true before and needs its own commit to fix eventually.
324 lines
9.5 KiB
Elixir
324 lines
9.5 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Mix.Tasks.Pleroma.Instance do
|
|
use Mix.Task
|
|
import Mix.Pleroma
|
|
|
|
alias Pleroma.Config
|
|
|
|
@shortdoc "Manages Pleroma instance"
|
|
@moduledoc File.read!("docs/docs/administration/CLI_tasks/instance.md")
|
|
|
|
def run(["gen" | rest]) do
|
|
{options, [], []} =
|
|
OptionParser.parse(
|
|
rest,
|
|
strict: [
|
|
force: :boolean,
|
|
output: :string,
|
|
output_psql: :string,
|
|
domain: :string,
|
|
media_url: :string,
|
|
instance_name: :string,
|
|
admin_email: :string,
|
|
notify_email: :string,
|
|
dbhost: :string,
|
|
dbname: :string,
|
|
dbuser: :string,
|
|
dbpass: :string,
|
|
rum: :string,
|
|
indexable: :string,
|
|
db_configurable: :string,
|
|
uploads_dir: :string,
|
|
static_dir: :string,
|
|
listen_ip: :string,
|
|
listen_port: :string,
|
|
strip_uploads: :string,
|
|
anonymize_uploads: :string
|
|
],
|
|
aliases: [
|
|
o: :output,
|
|
f: :force
|
|
]
|
|
)
|
|
|
|
paths =
|
|
[config_path, psql_path] = [
|
|
Keyword.get(options, :output, "config/generated_config.exs"),
|
|
Keyword.get(options, :output_psql, "config/setup_db.psql")
|
|
]
|
|
|
|
will_overwrite = Enum.filter(paths, &File.exists?/1)
|
|
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
|
|
|
|
if proceed? do
|
|
[domain, port | _] =
|
|
String.split(
|
|
get_option(
|
|
options,
|
|
:domain,
|
|
"What domain will your instance use? (e.g akkoma.example.com)"
|
|
),
|
|
":"
|
|
) ++ [443]
|
|
|
|
media_url =
|
|
get_option(
|
|
options,
|
|
:media_url,
|
|
"What base url will uploads use? (e.g https://media.example.com/media)\n" <>
|
|
" Generally this should NOT use the same domain as the instance "
|
|
)
|
|
|
|
name =
|
|
get_option(
|
|
options,
|
|
:instance_name,
|
|
"What is the name of your instance? (e.g. The Corndog Emporium)",
|
|
domain
|
|
)
|
|
|
|
email = get_option(options, :admin_email, "What is your admin email address?")
|
|
|
|
notify_email =
|
|
get_option(
|
|
options,
|
|
:notify_email,
|
|
"What email address do you want to use for sending email notifications?",
|
|
email
|
|
)
|
|
|
|
indexable =
|
|
get_option(
|
|
options,
|
|
:indexable,
|
|
"Do you want search engines to index your site? (y/n)",
|
|
"y"
|
|
) === "y"
|
|
|
|
db_configurable? =
|
|
get_option(
|
|
options,
|
|
:db_configurable,
|
|
"Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)",
|
|
"n"
|
|
) === "y"
|
|
|
|
dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
|
|
|
|
dbname = get_option(options, :dbname, "What is the name of your database?", "akkoma")
|
|
|
|
dbuser =
|
|
get_option(
|
|
options,
|
|
:dbuser,
|
|
"What is the user used to connect to your database?",
|
|
"akkoma"
|
|
)
|
|
|
|
dbpass =
|
|
get_option(
|
|
options,
|
|
:dbpass,
|
|
"What is the password used to connect to your database?",
|
|
:crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64),
|
|
"autogenerated"
|
|
)
|
|
|
|
rum_enabled =
|
|
get_option(
|
|
options,
|
|
:rum,
|
|
"Would you like to use RUM indices?",
|
|
"n"
|
|
) === "y"
|
|
|
|
listen_port =
|
|
get_option(
|
|
options,
|
|
:listen_port,
|
|
"What port will the app listen to (leave it if you are using the default setup with nginx)?",
|
|
4000
|
|
)
|
|
|
|
listen_ip =
|
|
get_option(
|
|
options,
|
|
:listen_ip,
|
|
"What ip will the app listen to (leave it if you are using the default setup with nginx)?",
|
|
"127.0.0.1"
|
|
)
|
|
|
|
uploads_dir =
|
|
get_option(
|
|
options,
|
|
:uploads_dir,
|
|
"What directory should media uploads go in (when using the local uploader)?",
|
|
Config.get([Pleroma.Uploaders.Local, :uploads])
|
|
)
|
|
|> Path.expand()
|
|
|
|
static_dir =
|
|
get_option(
|
|
options,
|
|
:static_dir,
|
|
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
|
|
Config.get([:instance, :static_dir])
|
|
)
|
|
|> Path.expand()
|
|
|
|
{strip_uploads_message, strip_uploads_default} =
|
|
if Pleroma.Utils.command_available?("exiftool") do
|
|
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
|
|
"y"}
|
|
else
|
|
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
|
|
"n"}
|
|
end
|
|
|
|
strip_uploads =
|
|
get_option(
|
|
options,
|
|
:strip_uploads,
|
|
strip_uploads_message,
|
|
strip_uploads_default
|
|
) === "y"
|
|
|
|
anonymize_uploads =
|
|
get_option(
|
|
options,
|
|
:anonymize_uploads,
|
|
"Do you want to anonymize the filenames of uploads? (y/n)",
|
|
"n"
|
|
) === "y"
|
|
|
|
Config.put([:instance, :static_dir], static_dir)
|
|
|
|
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
|
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
|
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
|
|
lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
|
|
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
|
|
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
|
|
|
|
result_config =
|
|
EEx.eval_file(
|
|
template_dir <> "/sample_config.eex",
|
|
domain: domain,
|
|
media_url: media_url,
|
|
port: port,
|
|
email: email,
|
|
notify_email: notify_email,
|
|
name: name,
|
|
dbhost: dbhost,
|
|
dbname: dbname,
|
|
dbuser: dbuser,
|
|
dbpass: dbpass,
|
|
secret: secret,
|
|
jwt_secret: jwt_secret,
|
|
signing_salt: signing_salt,
|
|
lv_signing_salt: lv_signing_salt,
|
|
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
|
|
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
|
|
db_configurable?: db_configurable?,
|
|
static_dir: static_dir,
|
|
uploads_dir: uploads_dir,
|
|
rum_enabled: rum_enabled,
|
|
listen_ip: listen_ip,
|
|
listen_port: listen_port,
|
|
upload_filters:
|
|
upload_filters(%{
|
|
strip: strip_uploads,
|
|
anonymize: anonymize_uploads
|
|
})
|
|
)
|
|
|
|
result_psql =
|
|
EEx.eval_file(
|
|
template_dir <> "/sample_psql.eex",
|
|
dbname: dbname,
|
|
dbuser: dbuser,
|
|
dbpass: dbpass,
|
|
rum_enabled: rum_enabled
|
|
)
|
|
|
|
config_dir = Path.dirname(config_path)
|
|
psql_dir = Path.dirname(psql_path)
|
|
|
|
# Note: Distros requiring group read (0o750) on those directories should
|
|
# pre-create the directories.
|
|
to_create =
|
|
[config_dir, psql_dir, static_dir, uploads_dir]
|
|
|> Enum.reject(&File.exists?/1)
|
|
|
|
for dir <- to_create do
|
|
File.mkdir_p!(dir)
|
|
File.chmod!(dir, 0o700)
|
|
end
|
|
|
|
shell_info("Writing config to #{config_path}.")
|
|
|
|
# Sadly no fchmod(2) equivalent in Elixir…
|
|
File.touch!(config_path)
|
|
File.chmod!(config_path, 0o640)
|
|
File.write(config_path, result_config)
|
|
shell_info("Writing the postgres script to #{psql_path}.")
|
|
File.write(psql_path, result_psql)
|
|
|
|
write_robots_txt(static_dir, indexable, template_dir)
|
|
|
|
shell_info(
|
|
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
|
|
)
|
|
|
|
if db_configurable? do
|
|
shell_info(
|
|
" Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
|
|
)
|
|
end
|
|
else
|
|
shell_error(
|
|
"The task would have overwritten the following files:\n" <>
|
|
Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them."
|
|
)
|
|
end
|
|
end
|
|
|
|
defp write_robots_txt(static_dir, indexable, template_dir) do
|
|
robots_txt =
|
|
EEx.eval_file(
|
|
template_dir <> "/robots_txt.eex",
|
|
indexable: indexable
|
|
)
|
|
|
|
robots_txt_path = Path.join(static_dir, "robots.txt")
|
|
|
|
if File.exists?(robots_txt_path) do
|
|
File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
|
|
shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|