diff --git a/apps/api/prisma/migrations/20251104025114_add_client_max_body_size/migration.sql b/apps/api/prisma/migrations/20251104025114_add_client_max_body_size/migration.sql new file mode 100644 index 0000000..778901e --- /dev/null +++ b/apps/api/prisma/migrations/20251104025114_add_client_max_body_size/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "domains" ADD COLUMN "clientMaxBodySize" INTEGER DEFAULT 100; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 3ad997c..5574ac5 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -184,10 +184,11 @@ model Domain { realIpCustomCidrs String[] @default([]) // Custom CIDR ranges for set_real_ip_from // Advanced Configuration - hstsEnabled Boolean @default(false) // HTTP Strict Transport Security - http2Enabled Boolean @default(true) // Enable HTTP/2 - grpcEnabled Boolean @default(false) // Enable gRPC/gRPCs support - customLocations Json? // Custom location blocks configuration + hstsEnabled Boolean @default(false) // HTTP Strict Transport Security + http2Enabled Boolean @default(true) // Enable HTTP/2 + grpcEnabled Boolean @default(false) // Enable gRPC/gRPCs support + clientMaxBodySize Int? @default(100) // Maximum request body size in MB (client_max_body_size) + customLocations Json? // Custom location blocks configuration // Relations upstreams Upstream[] diff --git a/apps/api/src/domains/domains/domains.repository.ts b/apps/api/src/domains/domains/domains.repository.ts index 4fd02c1..589bff8 100644 --- a/apps/api/src/domains/domains/domains.repository.ts +++ b/apps/api/src/domains/domains/domains.repository.ts @@ -7,6 +7,7 @@ import { CreateUpstreamData, } from './domains.types'; import { PaginationMeta } from '../../shared/types/common.types'; +import { DEFAULT_CLIENT_MAX_BODY_SIZE } from '../../shared/constants/domain.constants'; /** * Repository for domain database operations @@ -153,6 +154,7 @@ export class DomainsRepository { hstsEnabled: input.advancedConfig?.hstsEnabled || false, http2Enabled: input.advancedConfig?.http2Enabled !== undefined ? input.advancedConfig.http2Enabled : true, grpcEnabled: input.advancedConfig?.grpcEnabled || false, + clientMaxBodySize: input.advancedConfig?.clientMaxBodySize !== undefined ? input.advancedConfig.clientMaxBodySize : DEFAULT_CLIENT_MAX_BODY_SIZE, customLocations: input.advancedConfig?.customLocations ? JSON.parse(JSON.stringify(input.advancedConfig.customLocations)) : null, upstreams: { create: input.upstreams.map((u: CreateUpstreamData) => ({ @@ -262,6 +264,10 @@ export class DomainsRepository { input.advancedConfig?.grpcEnabled !== undefined ? input.advancedConfig.grpcEnabled : currentDomain.grpcEnabled, + clientMaxBodySize: + input.advancedConfig?.clientMaxBodySize !== undefined + ? input.advancedConfig.clientMaxBodySize + : currentDomain.clientMaxBodySize, customLocations: input.advancedConfig?.customLocations !== undefined ? JSON.parse(JSON.stringify(input.advancedConfig.customLocations)) diff --git a/apps/api/src/domains/domains/domains.types.ts b/apps/api/src/domains/domains/domains.types.ts index 22d8910..16af891 100644 --- a/apps/api/src/domains/domains/domains.types.ts +++ b/apps/api/src/domains/domains/domains.types.ts @@ -53,6 +53,7 @@ export interface AdvancedConfigData { hstsEnabled?: boolean; // Enable HSTS header http2Enabled?: boolean; // Enable HTTP/2 grpcEnabled?: boolean; // Enable gRPC support (default proxy_pass replacement) + clientMaxBodySize?: number; // Maximum request body size in MB (default: 100) customLocations?: CustomLocationData[]; // Custom location blocks } diff --git a/apps/api/src/domains/domains/services/nginx-config.service.ts b/apps/api/src/domains/domains/services/nginx-config.service.ts index 741431b..1628341 100644 --- a/apps/api/src/domains/domains/services/nginx-config.service.ts +++ b/apps/api/src/domains/domains/services/nginx-config.service.ts @@ -6,6 +6,7 @@ import logger from '../../../utils/logger'; import { PATHS } from '../../../shared/constants/paths.constants'; import { DomainWithRelations } from '../domains.types'; import { cloudflareIpsService } from './cloudflare-ips.service'; +import { DEFAULT_CLIENT_MAX_BODY_SIZE } from '../../../shared/constants/domain.constants'; const execAsync = promisify(exec); @@ -117,6 +118,14 @@ export class NginxConfigService { } } + /** + * Get client max body size for a domain + * Returns the configured value or default if not set + */ + private getClientMaxBodySize(domain: DomainWithRelations): number { + return domain.clientMaxBodySize || DEFAULT_CLIENT_MAX_BODY_SIZE; + } + /** * Generate WebSocket map block for connection upgrade * This enables WebSocket support for all domains by default @@ -246,6 +255,9 @@ ${realIpBlock} // Generate Access Lists block const accessListsBlock = this.generateAccessListsBlock(domain); + // Client max body size + const clientMaxBodySize = this.getClientMaxBodySize(domain); + // HTTP server with full proxy configuration return ` server { @@ -260,6 +272,9 @@ ${accessListsBlock} # Include ACME challenge location for ZeroSSL/Let's Encrypt include /etc/nginx/snippets/acme-challenge.conf; + # Maximum request body size + client_max_body_size ${clientMaxBodySize}M; + ${domain.modsecEnabled ? 'modsecurity on;' : 'modsecurity off;'} access_log /var/log/nginx/${domain.name}_access.log main; @@ -312,6 +327,9 @@ ${accessListsBlock} // Generate Access Lists block const accessListsBlock = this.generateAccessListsBlock(domain); + // Client max body size + const clientMaxBodySize = this.getClientMaxBodySize(domain); + return ` server { listen 443 ssl${http2Support}; @@ -342,6 +360,9 @@ ${accessListsBlock} add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; + # Maximum request body size + client_max_body_size ${clientMaxBodySize}M; + ${domain.modsecEnabled ? 'modsecurity on;' : 'modsecurity off;'} access_log /var/log/nginx/${domain.name}_ssl_access.log main; diff --git a/apps/api/src/shared/constants/domain.constants.ts b/apps/api/src/shared/constants/domain.constants.ts new file mode 100644 index 0000000..ac7d20f --- /dev/null +++ b/apps/api/src/shared/constants/domain.constants.ts @@ -0,0 +1,9 @@ +/** + * Domain-related constants + */ + +/** + * Default maximum request body size in MB + * Used for nginx client_max_body_size directive + */ +export const DEFAULT_CLIENT_MAX_BODY_SIZE = 100; diff --git a/apps/web/src/components/domains/DomainDialogV2.tsx b/apps/web/src/components/domains/DomainDialogV2.tsx index db60638..14cede2 100644 --- a/apps/web/src/components/domains/DomainDialogV2.tsx +++ b/apps/web/src/components/domains/DomainDialogV2.tsx @@ -71,6 +71,7 @@ interface FormData { hstsEnabled: boolean; http2Enabled: boolean; grpcEnabled: boolean; + clientMaxBodySize: number; customLocations: CustomLocationFormData[]; } @@ -109,6 +110,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = hstsEnabled: false, http2Enabled: true, grpcEnabled: false, + clientMaxBodySize: 100, customLocations: [], }, }); @@ -157,6 +159,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = hstsEnabled: (domain as any).hstsEnabled || false, http2Enabled: (domain as any).http2Enabled !== undefined ? (domain as any).http2Enabled : true, grpcEnabled: (domain as any).grpcEnabled || false, + clientMaxBodySize: (domain as any).clientMaxBodySize || 100, customLocations: (domain as any).customLocations || [], }); } else { @@ -178,6 +181,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = hstsEnabled: false, http2Enabled: true, grpcEnabled: false, + clientMaxBodySize: 100, customLocations: [], }); } @@ -231,6 +235,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = hstsEnabled: data.hstsEnabled, http2Enabled: data.http2Enabled, grpcEnabled: data.grpcEnabled, + clientMaxBodySize: Number(data.clientMaxBodySize), customLocations: data.customLocations.filter(loc => loc.path && loc.upstreams.length > 0), }, }; @@ -663,6 +668,31 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = )} /> + +
+ Maximum size of the request body (client_max_body_size). Default is 100MB. +
+ + {errors.clientMaxBodySize && ( +{errors.clientMaxBodySize.message}
+ )} +