服务器配置
This commit is contained in:
1 parent
a1b4a63552
commit
fa7c8244b0
33 files changed
+1882
-363
No files matched your search
@@ -1,5 +1,8 @@
|
||||
import {
|
||||
ALLOWED_MIME_TYPES,
|
||||
FORBIDDEN_FILE_EXTENSIONS,
|
||||
VIDEO_EXTENSIONS,
|
||||
type VideoExtension,
|
||||
type AllowedMimeType
|
||||
} from './constants'
|
||||
import { getSetting, setSetting } from './db'
|
||||
@@ -8,6 +11,8 @@ import { TELEGRAM_MAX_FILE_BYTES } from './storage/telegram'
|
||||
|
||||
export const SETTINGS_UPLOAD_MAX_FILE_SIZE_MB = 'upload_max_file_size_mb'
|
||||
export const SETTINGS_ALLOWED_MIME_TYPES = 'allowed_mime_types'
|
||||
export const SETTINGS_ALLOWED_VIDEO_EXTENSIONS = 'allowed_video_extensions'
|
||||
export const SETTINGS_FORBIDDEN_FILE_EXTENSIONS = 'forbidden_file_extensions'
|
||||
export const SETTINGS_UPLOAD_RATE_IP_MAX = 'upload_rate_ip_max'
|
||||
export const SETTINGS_UPLOAD_RATE_TOKEN_MAX = 'upload_rate_token_max'
|
||||
export const SETTINGS_UPLOAD_RATE_WINDOW_MINUTES = 'upload_rate_window_minutes'
|
||||
@@ -41,6 +46,37 @@ export const ALL_MIME_TYPE_OPTIONS = [
|
||||
{ mime: 'image/x-icon', label: 'ICO' }
|
||||
] as const satisfies ReadonlyArray<{ mime: AllowedMimeType, label: string }>
|
||||
|
||||
export const ALL_VIDEO_EXTENSION_OPTIONS = VIDEO_EXTENSIONS.map(extension => ({
|
||||
extension,
|
||||
label: extension.toUpperCase()
|
||||
}))
|
||||
|
||||
export const FORBIDDEN_FILE_EXTENSION_OPTIONS = [
|
||||
'html',
|
||||
'htm',
|
||||
'xhtml',
|
||||
'svg',
|
||||
'xml',
|
||||
'js',
|
||||
'mjs',
|
||||
'cjs',
|
||||
'php',
|
||||
'phtml',
|
||||
'sh',
|
||||
'bash',
|
||||
'zsh',
|
||||
'bat',
|
||||
'cmd',
|
||||
'ps1',
|
||||
'exe',
|
||||
'dll',
|
||||
'msi',
|
||||
'scr'
|
||||
].map(extension => ({
|
||||
extension,
|
||||
label: extension.toUpperCase()
|
||||
}))
|
||||
|
||||
export type PolicySource = 'env' | 'db' | 'default'
|
||||
|
||||
function readEnv(name: string): string {
|
||||
@@ -119,12 +155,79 @@ export function serializeAllowedMimeTypes(mimes: readonly string[]): string {
|
||||
.join(',')
|
||||
}
|
||||
|
||||
export function normalizeAllowedVideoExtensions(raw: string): VideoExtension[] {
|
||||
const allowedSet = new Set<string>(VIDEO_EXTENSIONS)
|
||||
const seen = new Set<string>()
|
||||
const result: VideoExtension[] = []
|
||||
for (const part of raw.split(',')) {
|
||||
const extension = part.trim().toLowerCase().replace(/^\.+/, '')
|
||||
if (!extension || seen.has(extension) || !allowedSet.has(extension)) continue
|
||||
seen.add(extension)
|
||||
result.push(extension as VideoExtension)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function serializeAllowedVideoExtensions(extensions: readonly string[]): string {
|
||||
const normalized = new Set(normalizeAllowedVideoExtensions(extensions.join(',')))
|
||||
return VIDEO_EXTENSIONS
|
||||
.filter(extension => normalized.has(extension))
|
||||
.join(',')
|
||||
}
|
||||
|
||||
export function normalizeForbiddenFileExtensions(raw: string): string[] {
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const part of raw.split(',')) {
|
||||
const extension = part
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\.+/, '')
|
||||
if (!extension || seen.has(extension)) continue
|
||||
if (!/^[a-z0-9][a-z0-9_-]{0,15}$/.test(extension)) continue
|
||||
seen.add(extension)
|
||||
result.push(extension)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function serializeForbiddenFileExtensions(extensions: readonly string[]): string {
|
||||
const configuredOrder = new Map(
|
||||
FORBIDDEN_FILE_EXTENSION_OPTIONS.map((option, index) => [option.extension, index])
|
||||
)
|
||||
return normalizeForbiddenFileExtensions(extensions.join(','))
|
||||
.sort((left, right) => {
|
||||
const leftOrder = configuredOrder.get(left)
|
||||
const rightOrder = configuredOrder.get(right)
|
||||
if (leftOrder !== undefined || rightOrder !== undefined) {
|
||||
return (leftOrder ?? Number.MAX_SAFE_INTEGER)
|
||||
- (rightOrder ?? Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
return left.localeCompare(right)
|
||||
})
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function getUploadMaxFileSizeMbSource(): PolicySource {
|
||||
if (getSetting(SETTINGS_UPLOAD_MAX_FILE_SIZE_MB) !== null) return 'db'
|
||||
if (parseUploadMaxFileSizeMb(readEnv('UPLOAD_MAX_FILE_SIZE_MB')) !== null) return 'env'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function getAllowedVideoExtensionsSource(): PolicySource {
|
||||
if (getSetting(SETTINGS_ALLOWED_VIDEO_EXTENSIONS) !== null) return 'db'
|
||||
if (normalizeAllowedVideoExtensions(readEnv('ALLOWED_VIDEO_EXTENSIONS')).length > 0) {
|
||||
return 'env'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function getForbiddenFileExtensionsSource(): PolicySource {
|
||||
if (getSetting(SETTINGS_FORBIDDEN_FILE_EXTENSIONS) !== null) return 'db'
|
||||
if (readEnv('FORBIDDEN_FILE_EXTENSIONS')) return 'env'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function getAllowedMimeTypesSource(): PolicySource {
|
||||
if (getSetting(SETTINGS_ALLOWED_MIME_TYPES) !== null) return 'db'
|
||||
if (normalizeAllowedMimeTypes(readEnv('ALLOWED_MIME_TYPES')).length > 0) return 'env'
|
||||
@@ -202,6 +305,35 @@ export function getAllowedMimeTypesList(): AllowedMimeType[] {
|
||||
return [...ALLOWED_MIME_TYPES]
|
||||
}
|
||||
|
||||
export function getAllowedVideoExtensions(): VideoExtension[] {
|
||||
const dbRaw = getSetting(SETTINGS_ALLOWED_VIDEO_EXTENSIONS)
|
||||
if (dbRaw !== null) {
|
||||
const parsed = normalizeAllowedVideoExtensions(dbRaw)
|
||||
if (parsed.length > 0) return parsed
|
||||
}
|
||||
const fromEnv = normalizeAllowedVideoExtensions(readEnv('ALLOWED_VIDEO_EXTENSIONS'))
|
||||
if (fromEnv.length > 0) return fromEnv
|
||||
return [...VIDEO_EXTENSIONS]
|
||||
}
|
||||
|
||||
export function getForbiddenFileExtensions(): string[] {
|
||||
const dbRaw = getSetting(SETTINGS_FORBIDDEN_FILE_EXTENSIONS)
|
||||
if (dbRaw !== null) return normalizeForbiddenFileExtensions(dbRaw)
|
||||
const envRaw = readEnv('FORBIDDEN_FILE_EXTENSIONS')
|
||||
if (envRaw) return normalizeForbiddenFileExtensions(envRaw)
|
||||
return [...FORBIDDEN_FILE_EXTENSIONS]
|
||||
}
|
||||
|
||||
export function isVideoExtensionAllowed(extension: string): boolean {
|
||||
const normalized = extension.trim().toLowerCase().replace(/^\.+/, '')
|
||||
return getAllowedVideoExtensions().includes(normalized as VideoExtension)
|
||||
}
|
||||
|
||||
export function isForbiddenFileExtension(extension: string): boolean {
|
||||
const normalized = extension.trim().toLowerCase().replace(/^\.+/, '')
|
||||
return Boolean(normalized) && getForbiddenFileExtensions().includes(normalized)
|
||||
}
|
||||
|
||||
export function isMimeTypeAllowed(mime: string): mime is AllowedMimeType {
|
||||
return getAllowedMimeTypesList().includes(mime as AllowedMimeType)
|
||||
}
|
||||
@@ -312,6 +444,16 @@ export function getUploadPolicySettingsPayload() {
|
||||
allowedMimeTypes: getAllowedMimeTypesList(),
|
||||
allowedMimeTypesSource: getAllowedMimeTypesSource(),
|
||||
allowedMimeTypesEnvFallback: normalizeAllowedMimeTypes(readEnv('ALLOWED_MIME_TYPES')),
|
||||
allowedVideoExtensions: getAllowedVideoExtensions(),
|
||||
allowedVideoExtensionsSource: getAllowedVideoExtensionsSource(),
|
||||
allowedVideoExtensionsEnvFallback: normalizeAllowedVideoExtensions(
|
||||
readEnv('ALLOWED_VIDEO_EXTENSIONS')
|
||||
),
|
||||
forbiddenFileExtensions: getForbiddenFileExtensions(),
|
||||
forbiddenFileExtensionsSource: getForbiddenFileExtensionsSource(),
|
||||
forbiddenFileExtensionsEnvFallback: normalizeForbiddenFileExtensions(
|
||||
readEnv('FORBIDDEN_FILE_EXTENSIONS')
|
||||
),
|
||||
uploadRateIpMax: getUploadRateLimitSettings().ipMax,
|
||||
uploadRateIpMaxSource: getUploadRateIpMaxSource(),
|
||||
uploadRateTokenMax: getUploadRateLimitSettings().tokenMax,
|
||||
@@ -327,13 +469,17 @@ export function getUploadPolicySettingsPayload() {
|
||||
mimeTypeOptions: ALL_MIME_TYPE_OPTIONS.map(option => ({
|
||||
mime: option.mime,
|
||||
label: option.label
|
||||
}))
|
||||
})),
|
||||
videoExtensionOptions: ALL_VIDEO_EXTENSION_OPTIONS,
|
||||
forbiddenFileExtensionOptions: FORBIDDEN_FILE_EXTENSION_OPTIONS
|
||||
}
|
||||
}
|
||||
|
||||
export function setUploadPolicySettings(input: {
|
||||
uploadMaxFileSizeMb?: number
|
||||
allowedMimeTypes?: readonly string[]
|
||||
allowedVideoExtensions?: readonly string[]
|
||||
forbiddenFileExtensions?: readonly string[]
|
||||
uploadRateIpMax?: number
|
||||
uploadRateTokenMax?: number
|
||||
uploadRateWindowMinutes?: number
|
||||
@@ -347,6 +493,18 @@ export function setUploadPolicySettings(input: {
|
||||
if (input.allowedMimeTypes !== undefined) {
|
||||
setSetting(SETTINGS_ALLOWED_MIME_TYPES, serializeAllowedMimeTypes(input.allowedMimeTypes))
|
||||
}
|
||||
if (input.allowedVideoExtensions !== undefined) {
|
||||
setSetting(
|
||||
SETTINGS_ALLOWED_VIDEO_EXTENSIONS,
|
||||
serializeAllowedVideoExtensions(input.allowedVideoExtensions)
|
||||
)
|
||||
}
|
||||
if (input.forbiddenFileExtensions !== undefined) {
|
||||
setSetting(
|
||||
SETTINGS_FORBIDDEN_FILE_EXTENSIONS,
|
||||
serializeForbiddenFileExtensions(input.forbiddenFileExtensions)
|
||||
)
|
||||
}
|
||||
if (input.uploadRateIpMax !== undefined) {
|
||||
setSetting(SETTINGS_UPLOAD_RATE_IP_MAX, String(input.uploadRateIpMax))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user