Skip to content

Commit

Permalink
Rewrite images to use local proxy (#4035)
Browse files Browse the repository at this point in the history
* Add markdown rule to add rel=nofollow for all links

* Add markdown image rule to add local image proxy (fixes #1036)

* comments

* rewrite markdown image links working

* add comment

* perform markdown image processing in api/apub receivers

* clippy

* add db table to validate proxied links

* rewrite link fields for avatar, banner etc

* sql fmt

* proxy links received over federation

* add config option

* undo post.url rewriting, move http route definition

* add tests

* proxy images through pictrs

* testing

* cleanup request.rs file

* more cleanup (fixes #2611)

* include url content type when sending post over apub (fixes #2611)

* store post url content type in db

* should be media_type

* get rid of cache_remote_thumbnails setting, instead automatically
take thumbnail from federation data if available.

* fix tests

* add setting disable_external_link_previews

* federate post url as image depending on mime type

* change setting again

* machete

* invert

* support custom emoji

* clippy

* update defaults

* add image proxy test, fix test

* fix test

* clippy

* revert accidental changes

* address review

* clippy

* Markdown link rule-dess (#4356)

* Extracting opengraph_data to its own type.

* A few additions for markdown-link-rule.

---------

Co-authored-by: Nutomic <[email protected]>

* fix setting

* use enum for image proxy setting

* fix test configs

* add config backwards compat

* clippy

* machete

---------

Co-authored-by: Dessalines <[email protected]>
  • Loading branch information
Nutomic and dessalines authored Jan 25, 2024
1 parent 1782aaf commit e8a52d3
Show file tree
Hide file tree
Showing 64 changed files with 1,455 additions and 695 deletions.
11 changes: 7 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ strum_macros = "0.25.3"
itertools = "0.12.0"
futures = "0.3.30"
http = "0.2.11"
percent-encoding = "2.3.1"
rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
Expand All @@ -155,6 +154,7 @@ rustls = { version = "0.21.10", features = ["dangerous_configuration"] }
futures-util = "0.3.30"
tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.7"
moka = { version = "0.12.4", features = ["future"] }
i-love-jesus = { version = "0.1.0" }
Expand Down
92 changes: 86 additions & 6 deletions api_tests/src/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import {
PurgePost,
} from "lemmy-js-client";
import {
alpha,
alphaImage,
alphaUrl,
beta,
betaUrl,
createCommunity,
createPost,
delta,
epsilon,
gamma,
getSite,
registerUser,
resolveBetaCommunity,
resolvePost,
setupLogins,
unfollowRemotes,
waitForPost,
} from "./shared";
const downloadFileSync = require("download-file-sync");

Expand All @@ -29,9 +36,8 @@ afterAll(() => {
test("Upload image and delete it", async () => {
// Upload test image. We use a simple string buffer as pictrs doesnt require an actual image
// in testing mode.
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
image: Buffer.from("test"),
};
const upload = await alphaImage.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined();
Expand Down Expand Up @@ -60,9 +66,8 @@ test("Purge user, uploaded image removed", async () => {
let user = await registerUser(alphaImage, alphaUrl);

// upload test image
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined();
Expand Down Expand Up @@ -91,9 +96,8 @@ test("Purge post, linked image removed", async () => {
let user = await registerUser(beta, betaUrl);

// upload test image
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined();
Expand Down Expand Up @@ -124,3 +128,79 @@ test("Purge post, linked image removed", async () => {
const content2 = downloadFileSync(upload.url);
expect(content2).toBe("");
});

test("Images in remote post are proxied if setting enabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(gamma);

const upload_form: UploadImage = {
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
let post = await createPost(
gamma,
community.community_view.community.id,
upload.url,
"![](http://example.com/image2.png)",
);
expect(post.post_view.post).toBeDefined();

// remote image gets proxied after upload
expect(
post.post_view.post.url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy();
expect(
post.post_view.post.body?.startsWith(
"![](http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy();

let epsilonPost = await resolvePost(epsilon, post.post_view.post);
expect(epsilonPost.post).toBeDefined();

// remote image gets proxied after federation
expect(
epsilonPost.post!.post.url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
expect(
epsilonPost.post!.post.body?.startsWith(
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
});

test("No image proxying if setting is disabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(alpha);

const upload_form: UploadImage = {
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
let post = await createPost(
alpha,
community.community_view.community.id,
upload.url,
"![](http://example.com/image2.png)",
);
expect(post.post_view.post).toBeDefined();

// remote image doesnt get proxied after upload
expect(
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy();
expect(post.post_view.post.body).toBe("![](http://example.com/image2.png)");

let gammaPost = await resolvePost(delta, post.post_view.post);
expect(gammaPost.post).toBeDefined();

// remote image doesnt get proxied after federation
expect(
gammaPost.post!.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy();
expect(gammaPost.post!.post.body).toBe("![](http://example.com/image2.png)");
});
18 changes: 17 additions & 1 deletion api_tests/src/post.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
loginUser,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { ResolveObject } from "lemmy-js-client";
import { EditSite, ResolveObject } from "lemmy-js-client";

let betaCommunity: CommunityView | undefined;

Expand Down Expand Up @@ -72,6 +72,16 @@ function assertPostFederation(postOne?: PostView, postTwo?: PostView) {
}

test("Create a post", async () => {
// Setup some allowlists and blocklists
let editSiteForm: EditSite = {
allowed_instances: ["lemmy-beta"],
};
await delta.editSite(editSiteForm);

editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"];
await epsilon.editSite(editSiteForm);

if (!betaCommunity) {
throw "Missing beta community";
}
Expand Down Expand Up @@ -109,6 +119,12 @@ test("Create a post", async () => {
await expect(
resolvePost(epsilon, postRes.post_view.post),
).rejects.toStrictEqual(Error("couldnt_find_object"));

// remove added allow/blocklists
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = [];
await delta.editSite(editSiteForm);
await epsilon.editSite(editSiteForm);
});

test("Create a post in a non-existent community", async () => {
Expand Down
11 changes: 2 additions & 9 deletions api_tests/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,6 @@ export async function setupLogins() {
];
await gamma.editSite(editSiteForm);

editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm);

editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"];
await epsilon.editSite(editSiteForm);

// Create the main alpha/beta communities
// Ignore thrown errors of duplicates
try {
Expand All @@ -203,10 +196,10 @@ export async function createPost(
api: LemmyHttp,
community_id: number,
url: string = "https://example.com/",
body = randomString(10),
// use example.com for consistent title and embed description
name: string = randomString(5),
): Promise<PostResponse> {
let body = randomString(10);
let form: CreatePost = {
name,
url,
Expand Down Expand Up @@ -528,7 +521,7 @@ export async function likeComment(

export async function createCommunity(
api: LemmyHttp,
name_: string = randomString(5),
name_: string = randomString(10),
): Promise<CommunityResponse> {
let description = "a sample description";
let form: CreateCommunity = {
Expand Down
35 changes: 27 additions & 8 deletions config/defaults.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,41 @@
# Maximum number of active sql connections
pool_size: 30
}
# Settings related to activitypub federation
# Pictrs image server configuration.
pictrs: {
# Address where pictrs is available (for image hosting)
url: "http://localhost:8080/"
# Set a custom pictrs API key. ( Required for deleting images )
api_key: "string"
# By default the thumbnails for external links are stored in pict-rs. This ensures that they
# can be reliably retrieved and can be resized using pict-rs APIs. However it also increases
# storage usage. In case this is disabled, the Opengraph image is directly returned as
# thumbnail.
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
# equivalent to `image_mode: StoreLinkPreviews`.
#
# In some countries it is forbidden to copy preview images from newspaper articles and only
# hotlinking is allowed. If that is the case for your instance, make sure that this setting is
# disabled.
# To be removed in 0.20
cache_external_link_previews: true
# Specifies how to handle remote images, so that users don't have to connect directly to remote servers.
image_mode:
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the the
# Opengraph image is directly returned as thumbnail
"None"

# or

# Generate thumbnails for external post urls and store them persistently in pict-rs. This
# ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
# it also increases storage usage.
#
# This is the default behaviour, and also matches Lemmy 0.18.
"StoreLinkPreviews"

# or

# If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`,
# including embedded images in markdown. Images are stored temporarily in pict-rs for caching.
# This improves privacy as users don't expose their IP to untrusted servers, and decreases load
# on other servers. However it increases bandwidth use for the local server.
#
# Requires pict-rs 0.5
"ProxyAllImages"
# Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30
}
Expand Down
17 changes: 12 additions & 5 deletions crates/api/src/local_user/save_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::SaveUserSettings,
utils::send_verification_email,
utils::{
local_site_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_api,
send_verification_email,
},
SuccessResponse,
};
use lemmy_db_schema::{
Expand All @@ -12,7 +17,7 @@ use lemmy_db_schema::{
person::{Person, PersonUpdateForm},
},
traits::Crud,
utils::{diesel_option_overwrite, diesel_option_overwrite_to_url},
utils::diesel_option_overwrite,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
Expand All @@ -28,9 +33,11 @@ pub async fn save_user_settings(
) -> Result<Json<SuccessResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;

let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let bio = diesel_option_overwrite(data.bio.clone());
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let bio = diesel_option_overwrite(process_markdown_opt(&data.bio, &slur_regex, &context).await?);

let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
let display_name = diesel_option_overwrite(data.display_name.clone());
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
let email_deref = data.email.as_deref().map(str::to_lowercase);
Expand Down
Loading

0 comments on commit e8a52d3

Please sign in to comment.