Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "domains" ADD COLUMN "clientMaxBodySize" INTEGER DEFAULT 100;
9 changes: 5 additions & 4 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/domains/domains/domains.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => ({
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/domains/domains/domains.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
21 changes: 21 additions & 0 deletions apps/api/src/domains/domains/services/nginx-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/shared/constants/domain.constants.ts
Original file line number Diff line number Diff line change
@@ -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;
30 changes: 30 additions & 0 deletions apps/web/src/components/domains/DomainDialogV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ interface FormData {
hstsEnabled: boolean;
http2Enabled: boolean;
grpcEnabled: boolean;
clientMaxBodySize: number;
customLocations: CustomLocationFormData[];
}

Expand Down Expand Up @@ -109,6 +110,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading =
hstsEnabled: false,
http2Enabled: true,
grpcEnabled: false,
clientMaxBodySize: 100,
customLocations: [],
},
});
Expand Down Expand Up @@ -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 {
Expand All @@ -178,6 +181,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading =
hstsEnabled: false,
http2Enabled: true,
grpcEnabled: false,
clientMaxBodySize: 100,
customLocations: [],
});
}
Expand Down Expand Up @@ -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),
},
};
Expand Down Expand Up @@ -663,6 +668,31 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading =
)}
/>
</div>

<div className="p-3 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="clientMaxBodySize">Maximum Request Body Size (MB)</Label>
<p className="text-sm text-muted-foreground mb-2">
Maximum size of the request body (client_max_body_size). Default is 100MB.
</p>
<Input
id="clientMaxBodySize"
type="number"
min="1"
max="10000"
{...register('clientMaxBodySize', {
required: 'Client max body size is required',
min: { value: 1, message: 'Minimum size is 1MB' },
max: { value: 10000, message: 'Maximum size is 10000MB (10GB)' }
})}
placeholder="100"
disabled={isLoading}
/>
{errors.clientMaxBodySize && (
<p className="text-sm text-destructive">{errors.clientMaxBodySize.message}</p>
)}
</div>
</div>
</div>
</div>

Expand Down