mirror of
https://iceshrimp.dev/crimekillz/trashposs
synced 2024-11-23 17:29:05 +01:00
Critical - Upstream: zotan - Don't treat HTTP 429 errors as non-retryable, Apply rate limits to proxyServer and fileServer
This commit is contained in:
parent
24d380d37e
commit
b09148840f
@ -195,6 +195,7 @@ export class StatusError extends Error {
|
|||||||
public statusCode: number;
|
public statusCode: number;
|
||||||
public statusMessage?: string;
|
public statusMessage?: string;
|
||||||
public isClientError: boolean;
|
public isClientError: boolean;
|
||||||
|
public isRetryable: boolean;
|
||||||
|
|
||||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
@ -205,5 +206,6 @@ export class StatusError extends Error {
|
|||||||
typeof this.statusCode === "number" &&
|
typeof this.statusCode === "number" &&
|
||||||
this.statusCode >= 400 &&
|
this.statusCode >= 400 &&
|
||||||
this.statusCode < 500;
|
this.statusCode < 500;
|
||||||
|
this.isRetryable = this.isClientError && this.statusCode != 429;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
|||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
return `${res.statusCode} ${res.statusMessage}`;
|
||||||
|
@ -75,7 +75,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Skip if target is 4xx
|
// Skip if target is 4xx
|
||||||
if (e instanceof StatusError) {
|
if (e instanceof StatusError) {
|
||||||
if (e.isClientError) {
|
if (!e.isRetryable) {
|
||||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -52,7 +52,7 @@ export default async (job: Bull.Job<WebhookDeliverJobData>) => {
|
|||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
return `${res.statusCode} ${res.statusMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export default async function (
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Skip if target is 4xx
|
// Skip if target is 4xx
|
||||||
if (e instanceof StatusError) {
|
if (e instanceof StatusError) {
|
||||||
if (e.isClientError) {
|
if (!e.isRetryable) {
|
||||||
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
|
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export default async function (
|
|||||||
await createNote(note, resolver, silent);
|
await createNote(note, resolver, silent);
|
||||||
return "ok";
|
return "ok";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof StatusError && e.isClientError) {
|
if (e instanceof StatusError && !e.isRetryable) {
|
||||||
return `skip ${e.statusCode}`;
|
return `skip ${e.statusCode}`;
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -290,7 +290,7 @@ export async function createNote(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status:
|
status:
|
||||||
e instanceof StatusError && e.isClientError
|
e instanceof StatusError && !e.isRetryable
|
||||||
? "permerror"
|
? "permerror"
|
||||||
: "temperror",
|
: "temperror",
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,11 @@ import { detectType } from "@/misc/get-file-info.js";
|
|||||||
import { convertToWebp } from "@/services/drive/image-processor.js";
|
import { convertToWebp } from "@/services/drive/image-processor.js";
|
||||||
import { GenerateVideoThumbnail } from "@/services/drive/generate-video-thumbnail.js";
|
import { GenerateVideoThumbnail } from "@/services/drive/generate-video-thumbnail.js";
|
||||||
import { StatusError } from "@/misc/fetch.js";
|
import { StatusError } from "@/misc/fetch.js";
|
||||||
import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
import { FILE_TYPE_BROWSERSAFE, MINUTE } from "@/const.js";
|
||||||
|
|
||||||
|
import { IEndpointMeta } from "@/server/api/endpoints.js";
|
||||||
|
import { getIpHash } from "@/misc/get-ip-hash.js";
|
||||||
|
import { limiter } from "@/server/api/limiter.js";
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
@ -31,6 +35,30 @@ const commonReadableHandlerGenerator =
|
|||||||
export default async function (ctx: Koa.Context) {
|
export default async function (ctx: Koa.Context) {
|
||||||
const key = ctx.params.key;
|
const key = ctx.params.key;
|
||||||
|
|
||||||
|
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||||
|
const limitActor = getIpHash(ctx.ip);
|
||||||
|
|
||||||
|
const limit: IEndpointMeta["limit"] = {
|
||||||
|
key: `drive-file:${key}`,
|
||||||
|
duration: MINUTE * 10,
|
||||||
|
max: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
await limiter(
|
||||||
|
limit as IEndpointMeta["limit"] & { key: NonNullable<string> },
|
||||||
|
limitActor,
|
||||||
|
).catch((e) => {
|
||||||
|
const remainingTime = e.remainingTime
|
||||||
|
? `Please try again in ${e.remainingTime}.`
|
||||||
|
: "Please try again later.";
|
||||||
|
|
||||||
|
ctx.status = 429;
|
||||||
|
ctx.body = `Rate limit exceeded. ${remainingTime}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ctx.status === 429) return;
|
||||||
|
|
||||||
// Fetch drive file
|
// Fetch drive file
|
||||||
const file = await DriveFiles.createQueryBuilder("file")
|
const file = await DriveFiles.createQueryBuilder("file")
|
||||||
.where("file.accessKey = :accessKey", { accessKey: key })
|
.where("file.accessKey = :accessKey", { accessKey: key })
|
||||||
@ -106,7 +134,7 @@ export default async function (ctx: Koa.Context) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
serverLogger.error(`${e}`);
|
serverLogger.error(`${e}`);
|
||||||
|
|
||||||
if (e instanceof StatusError && e.isClientError) {
|
if (e instanceof StatusError && !e.isRetryable) {
|
||||||
ctx.status = e.statusCode;
|
ctx.status = e.statusCode;
|
||||||
ctx.set("Cache-Control", "max-age=86400");
|
ctx.set("Cache-Control", "max-age=86400");
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,9 +9,12 @@ import { createTemp } from "@/misc/create-temp.js";
|
|||||||
import { downloadUrl } from "@/misc/download-url.js";
|
import { downloadUrl } from "@/misc/download-url.js";
|
||||||
import { detectType } from "@/misc/get-file-info.js";
|
import { detectType } from "@/misc/get-file-info.js";
|
||||||
import { StatusError } from "@/misc/fetch.js";
|
import { StatusError } from "@/misc/fetch.js";
|
||||||
import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
import { FILE_TYPE_BROWSERSAFE, MINUTE } from "@/const.js";
|
||||||
import { serverLogger } from "../index.js";
|
import { serverLogger } from "../index.js";
|
||||||
import { isMimeImage } from "@/misc/is-mime-image.js";
|
import { isMimeImage } from "@/misc/is-mime-image.js";
|
||||||
|
import { getIpHash } from "@/misc/get-ip-hash.js";
|
||||||
|
import { limiter } from "@/server/api/limiter.js";
|
||||||
|
import { IEndpointMeta } from "@/server/api/endpoints.js";
|
||||||
|
|
||||||
export async function proxyMedia(ctx: Koa.Context) {
|
export async function proxyMedia(ctx: Koa.Context) {
|
||||||
const url = "url" in ctx.query ? ctx.query.url : `https://${ctx.params.url}`;
|
const url = "url" in ctx.query ? ctx.query.url : `https://${ctx.params.url}`;
|
||||||
@ -21,6 +24,32 @@ export async function proxyMedia(ctx: Koa.Context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||||
|
const limitActor = getIpHash(ctx.ip);
|
||||||
|
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
|
const limit: IEndpointMeta["limit"] = {
|
||||||
|
key: `media-proxy:${parsedUrl.host}:${parsedUrl.pathname}`,
|
||||||
|
duration: MINUTE * 10,
|
||||||
|
max: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
await limiter(
|
||||||
|
limit as IEndpointMeta["limit"] & { key: NonNullable<string> },
|
||||||
|
limitActor,
|
||||||
|
).catch((e) => {
|
||||||
|
const remainingTime = e.remainingTime
|
||||||
|
? `Please try again in ${e.remainingTime}.`
|
||||||
|
: "Please try again later.";
|
||||||
|
|
||||||
|
ctx.status = 429;
|
||||||
|
ctx.body = `Rate limit exceeded. ${remainingTime}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ctx.status === 429) return;
|
||||||
|
|
||||||
const { hostname } = new URL(url);
|
const { hostname } = new URL(url);
|
||||||
let resolvedIps;
|
let resolvedIps;
|
||||||
try {
|
try {
|
||||||
|
Loading…
Reference in New Issue
Block a user