@serve.zone/dcrouter 13.41.2 → 13.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1025 -983
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.db-seeder.js +29 -2
- package/dist_ts/config/classes.reference-resolver.d.ts +5 -0
- package/dist_ts/config/classes.reference-resolver.js +79 -15
- package/dist_ts/config/classes.route-config-manager.d.ts +5 -1
- package/dist_ts/config/classes.route-config-manager.js +136 -6
- package/dist_ts/config/classes.source-policy-compiler.d.ts +35 -0
- package/dist_ts/config/classes.source-policy-compiler.js +497 -0
- package/dist_ts/config/index.d.ts +1 -0
- package/dist_ts/config/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +39 -0
- package/dist_ts_interfaces/data/route-management.js +65 -1
- package/dist_ts_migrations/index.js +67 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-routes.js +237 -11
- package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +42 -5
- package/package.json +3 -3
- package/readme.md +74 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.db-seeder.ts +28 -1
- package/ts/config/classes.reference-resolver.ts +94 -14
- package/ts/config/classes.route-config-manager.ts +162 -5
- package/ts/config/classes.source-policy-compiler.ts +614 -0
- package/ts/config/index.ts +1 -0
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +257 -10
- package/ts_web/elements/network/ops-view-sourceprofiles.ts +41 -4
|
@@ -24,11 +24,176 @@ const tlsCertOptions = [
|
|
|
24
24
|
{ key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
|
|
25
25
|
{ key: 'custom', option: 'Custom certificate' },
|
|
26
26
|
];
|
|
27
|
+
const sourcePolicyPresetOptions = [
|
|
28
|
+
{ key: 'manual', option: 'Manual source policy' },
|
|
29
|
+
{ key: 'gitea', option: 'Gitea bot protection' },
|
|
30
|
+
];
|
|
31
|
+
const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
|
|
32
|
+
|
|
33
|
+
function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
|
|
34
|
+
return { enabled: true, maxRequests, window: 60, keyBy: 'ip' };
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
function getDropdownKey(value: any): string {
|
|
29
38
|
return typeof value === 'string' ? value : value?.key || '';
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
function getSourcePolicyRefsFromFormData(formData: Record<string, any>): string[] {
|
|
42
|
+
const refs: string[] = [];
|
|
43
|
+
for (let index = 0; index < 4; index++) {
|
|
44
|
+
const ref = getDropdownKey(formData[`sourcePolicyProfileRef${index}`]);
|
|
45
|
+
if (ref && !refs.includes(ref)) {
|
|
46
|
+
refs.push(ref);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildSourcePolicyMetadata(
|
|
53
|
+
profileRefs: string[],
|
|
54
|
+
existingSourcePolicy?: interfaces.data.IRouteSourcePolicy,
|
|
55
|
+
): interfaces.data.IRouteSourcePolicy {
|
|
56
|
+
return {
|
|
57
|
+
bindings: profileRefs.map((sourceProfileRef) => {
|
|
58
|
+
const existingBinding = existingSourcePolicy?.bindings.find((binding) => binding.sourceProfileRef === sourceProfileRef);
|
|
59
|
+
return existingBinding
|
|
60
|
+
? {
|
|
61
|
+
...existingBinding,
|
|
62
|
+
sourceProfileRef,
|
|
63
|
+
onExceeded: existingBinding.onExceeded || { type: '429' as const },
|
|
64
|
+
}
|
|
65
|
+
: {
|
|
66
|
+
sourceProfileRef,
|
|
67
|
+
onExceeded: { type: '429' as const },
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]): {
|
|
74
|
+
refs: string[];
|
|
75
|
+
missingNames: string[];
|
|
76
|
+
} {
|
|
77
|
+
const refs: string[] = [];
|
|
78
|
+
const missingNames: string[] = [];
|
|
79
|
+
for (const profileName of giteaSourcePolicyProfileNames) {
|
|
80
|
+
const profile = profiles.find((item) => item.name.trim().toUpperCase() === profileName);
|
|
81
|
+
if (profile) {
|
|
82
|
+
refs.push(profile.id);
|
|
83
|
+
} else {
|
|
84
|
+
missingNames.push(profileName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { refs, missingNames };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildGiteaSourcePolicyMetadata(profileRefs: string[]): interfaces.data.IRouteSourcePolicy {
|
|
91
|
+
const [trustedRef, aiRef, publicRef] = profileRefs;
|
|
92
|
+
return {
|
|
93
|
+
bindings: [
|
|
94
|
+
{
|
|
95
|
+
sourceProfileRef: trustedRef,
|
|
96
|
+
onExceeded: { type: '429' as const },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
sourceProfileRef: aiRef,
|
|
100
|
+
onExceeded: { type: '429' as const },
|
|
101
|
+
pathPolicies: [
|
|
102
|
+
{ pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
|
|
103
|
+
{ pathClass: 'static', rateLimit: rateLimit(240) },
|
|
104
|
+
{ pathClass: 'raw', rateLimit: rateLimit(20) },
|
|
105
|
+
{ pathClass: 'archive', rateLimit: rateLimit(6) },
|
|
106
|
+
{ pathClass: 'expensive-html', rateLimit: rateLimit(6) },
|
|
107
|
+
{ pathClass: 'normal-html', rateLimit: rateLimit(20) },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
sourceProfileRef: publicRef,
|
|
112
|
+
onExceeded: { type: '429' as const },
|
|
113
|
+
pathPolicies: [
|
|
114
|
+
{ pathClass: 'git-smart-http', rateLimit: rateLimit(1200) },
|
|
115
|
+
{ pathClass: 'static', rateLimit: rateLimit(600) },
|
|
116
|
+
{ pathClass: 'raw', rateLimit: rateLimit(120) },
|
|
117
|
+
{ pathClass: 'archive', rateLimit: rateLimit(30) },
|
|
118
|
+
{ pathClass: 'expensive-html', rateLimit: rateLimit(30) },
|
|
119
|
+
{ pathClass: 'normal-html', rateLimit: rateLimit(120) },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getGiteaPresetSourcePolicy(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourcePolicy | null {
|
|
127
|
+
const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
|
|
128
|
+
if (missingNames.length > 0) {
|
|
129
|
+
alert(`Gitea source-policy preset needs these seeded profiles: ${missingNames.join(', ')}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (!validateSourcePolicySelection(refs, profiles)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return buildGiteaSourcePolicyMetadata(refs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function metadataUsesPathPolicies(metadata?: interfaces.data.IRouteMetadata): boolean {
|
|
139
|
+
return Boolean(metadata?.sourcePolicy?.bindings.some((binding) => binding.pathPolicies?.length));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sourceProfileMatchesAll(profile: interfaces.data.ISourceProfile): boolean {
|
|
143
|
+
return (profile.security?.ipAllowList || []).some((entry) => {
|
|
144
|
+
const source = typeof entry === 'string' ? entry : entry.ip;
|
|
145
|
+
return ['*', '0.0.0.0/0', '::/0'].includes(source.trim());
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile): boolean {
|
|
150
|
+
return (profile.security?.ipAllowList || []).some((entry) => {
|
|
151
|
+
const source = typeof entry === 'string' ? entry : entry.ip;
|
|
152
|
+
return source.trim().length > 0;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateSourcePolicySelection(
|
|
157
|
+
profileRefs: string[],
|
|
158
|
+
profiles: interfaces.data.ISourceProfile[],
|
|
159
|
+
): boolean {
|
|
160
|
+
if (profileRefs.length === 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const selectedProfiles = profileRefs
|
|
165
|
+
.map((profileRef) => profiles.find((profile) => profile.id === profileRef))
|
|
166
|
+
.filter(Boolean) as interfaces.data.ISourceProfile[];
|
|
167
|
+
|
|
168
|
+
if (selectedProfiles.length !== profileRefs.length) {
|
|
169
|
+
alert('One or more selected source profiles could not be found. Refresh profiles and try again.');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const profilesWithoutMatches = selectedProfiles.filter((profile) => !sourceProfileHasSourceMatches(profile));
|
|
174
|
+
if (profilesWithoutMatches.length > 0) {
|
|
175
|
+
alert(`Source profiles need IP/CIDR match entries before use: ${profilesWithoutMatches.map((profile) => profile.name).join(', ')}`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const fallbackProfile = selectedProfiles[selectedProfiles.length - 1];
|
|
180
|
+
if (!sourceProfileMatchesAll(fallbackProfile)) {
|
|
181
|
+
alert('Source policy needs an explicit public/wildcard fallback profile as the last binding. Add a profile with IP Allow List "*".');
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (selectedProfiles.slice(0, -1).some((profile) => sourceProfileMatchesAll(profile))) {
|
|
186
|
+
alert('Wildcard source profiles must be last. Earlier wildcard profiles would shadow all following profiles.');
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (fallbackProfile.security?.rateLimit?.enabled !== true) {
|
|
191
|
+
return confirm(`The fallback profile "${fallbackProfile.name}" has no enabled rate limit. Save anyway?`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
32
197
|
function parseTargetPort(value: any): number | undefined {
|
|
33
198
|
const parsed = typeof value === 'number'
|
|
34
199
|
? value
|
|
@@ -355,6 +520,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
355
520
|
|
|
356
521
|
const meta = merged.metadata;
|
|
357
522
|
const isSystemManaged = this.isSystemManagedRoute(merged);
|
|
523
|
+
const sourcePolicySummary = this.describeSourcePolicy(meta);
|
|
358
524
|
await DeesModal.createAndShow({
|
|
359
525
|
heading: `Route: ${merged.route.name}`,
|
|
360
526
|
content: html`
|
|
@@ -364,7 +530,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
364
530
|
${merged.route.vpnOnly ? html`<p>Access: <strong style="color: #22c55e;">VPN only</strong></p>` : ''}
|
|
365
531
|
<p>ID: <code style="color: #888;">${merged.id}</code></p>
|
|
366
532
|
${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
|
|
367
|
-
${
|
|
533
|
+
${sourcePolicySummary ? html`<p>Source Policy: <strong style="color: #a78bfa;">${sourcePolicySummary}</strong></p>` : ''}
|
|
368
534
|
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
|
|
369
535
|
</div>
|
|
370
536
|
`,
|
|
@@ -496,6 +662,8 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
496
662
|
const currentVpnOnly = route.vpnOnly === true;
|
|
497
663
|
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
|
|
498
664
|
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
|
|
665
|
+
const currentSourcePolicyRefs = this.getSourcePolicyRefs(merged.metadata);
|
|
666
|
+
const currentSourcePolicyPreset = metadataUsesPathPolicies(merged.metadata) ? 'gitea' : 'manual';
|
|
499
667
|
|
|
500
668
|
// Compute current TLS state for pre-population
|
|
501
669
|
const currentTls = (route.action as any).tls;
|
|
@@ -516,7 +684,25 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
516
684
|
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .value=${currentPorts} .required=${true}></dees-input-text>
|
|
517
685
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
|
|
518
686
|
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
|
|
519
|
-
<
|
|
687
|
+
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
|
|
688
|
+
<strong>Source Policy</strong>
|
|
689
|
+
<small>First matching profile wins. Exceeded limits return 429 and do not fall through.</small>
|
|
690
|
+
<dees-input-dropdown
|
|
691
|
+
.key=${'sourcePolicyPreset'}
|
|
692
|
+
.label=${'Source Policy Preset'}
|
|
693
|
+
.options=${sourcePolicyPresetOptions}
|
|
694
|
+
.selectedOption=${sourcePolicyPresetOptions.find((o) => o.key === currentSourcePolicyPreset) || sourcePolicyPresetOptions[0]}
|
|
695
|
+
></dees-input-dropdown>
|
|
696
|
+
<small>Gitea preset uses TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and applies path-class limits.</small>
|
|
697
|
+
${[0, 1, 2, 3].map((index) => html`
|
|
698
|
+
<dees-input-dropdown
|
|
699
|
+
.key=${`sourcePolicyProfileRef${index}`}
|
|
700
|
+
.label=${`Source Profile ${index + 1}`}
|
|
701
|
+
.options=${profileOptions}
|
|
702
|
+
.selectedOption=${profileOptions.find((o) => o.key === (currentSourcePolicyRefs[index] || '')) || profileOptions[0]}
|
|
703
|
+
></dees-input-dropdown>
|
|
704
|
+
`)}
|
|
705
|
+
</div>
|
|
520
706
|
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
|
|
521
707
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
|
|
522
708
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
|
|
@@ -557,7 +743,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
557
743
|
: [];
|
|
558
744
|
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
|
559
745
|
|
|
560
|
-
const
|
|
746
|
+
const sourcePolicyPreset = getDropdownKey(formData.sourcePolicyPreset) || 'manual';
|
|
747
|
+
const sourcePolicyRefs = sourcePolicyPreset === 'gitea'
|
|
748
|
+
? []
|
|
749
|
+
: getSourcePolicyRefsFromFormData(formData);
|
|
750
|
+
if (sourcePolicyPreset !== 'gitea' && !validateSourcePolicySelection(sourcePolicyRefs, profiles)) return;
|
|
561
751
|
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
562
752
|
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
|
563
753
|
const targetPort = preserveMatchPort
|
|
@@ -621,9 +811,18 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
621
811
|
}
|
|
622
812
|
|
|
623
813
|
const metadata: any = {};
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
814
|
+
if (sourcePolicyPreset === 'gitea') {
|
|
815
|
+
const sourcePolicy = getGiteaPresetSourcePolicy(profiles);
|
|
816
|
+
if (!sourcePolicy) return;
|
|
817
|
+
metadata.sourcePolicy = sourcePolicy;
|
|
818
|
+
metadata.sourceProfileRef = '';
|
|
819
|
+
metadata.sourceProfileName = '';
|
|
820
|
+
} else if (sourcePolicyRefs.length > 0) {
|
|
821
|
+
metadata.sourcePolicy = buildSourcePolicyMetadata(sourcePolicyRefs, merged.metadata?.sourcePolicy);
|
|
822
|
+
metadata.sourceProfileRef = '';
|
|
823
|
+
metadata.sourceProfileName = '';
|
|
824
|
+
} else if (merged.metadata?.sourcePolicy || merged.metadata?.sourceProfileRef) {
|
|
825
|
+
metadata.sourcePolicy = { bindings: [] };
|
|
627
826
|
metadata.sourceProfileRef = '';
|
|
628
827
|
metadata.sourceProfileName = '';
|
|
629
828
|
}
|
|
@@ -685,7 +884,25 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
685
884
|
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .required=${true}></dees-input-text>
|
|
686
885
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
|
|
687
886
|
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
|
|
688
|
-
<
|
|
887
|
+
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
|
|
888
|
+
<strong>Source Policy</strong>
|
|
889
|
+
<small>First matching profile wins. Exceeded limits return 429 and do not fall through.</small>
|
|
890
|
+
<dees-input-dropdown
|
|
891
|
+
.key=${'sourcePolicyPreset'}
|
|
892
|
+
.label=${'Source Policy Preset'}
|
|
893
|
+
.options=${sourcePolicyPresetOptions}
|
|
894
|
+
.selectedOption=${sourcePolicyPresetOptions[0]}
|
|
895
|
+
></dees-input-dropdown>
|
|
896
|
+
<small>Gitea preset uses TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and applies path-class limits.</small>
|
|
897
|
+
${[0, 1, 2, 3].map((index) => html`
|
|
898
|
+
<dees-input-dropdown
|
|
899
|
+
.key=${`sourcePolicyProfileRef${index}`}
|
|
900
|
+
.label=${`Source Profile ${index + 1}`}
|
|
901
|
+
.options=${profileOptions}
|
|
902
|
+
.selectedOption=${profileOptions[0]}
|
|
903
|
+
></dees-input-dropdown>
|
|
904
|
+
`)}
|
|
905
|
+
</div>
|
|
689
906
|
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
|
|
690
907
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
|
|
691
908
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
|
|
@@ -726,7 +943,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
726
943
|
: [];
|
|
727
944
|
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
|
728
945
|
|
|
729
|
-
const
|
|
946
|
+
const sourcePolicyPreset = getDropdownKey(formData.sourcePolicyPreset) || 'manual';
|
|
947
|
+
const sourcePolicyRefs = sourcePolicyPreset === 'gitea'
|
|
948
|
+
? []
|
|
949
|
+
: getSourcePolicyRefsFromFormData(formData);
|
|
950
|
+
if (sourcePolicyPreset !== 'gitea' && !validateSourcePolicySelection(sourcePolicyRefs, profiles)) return;
|
|
730
951
|
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
731
952
|
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
|
732
953
|
const targetPort = preserveMatchPort
|
|
@@ -791,8 +1012,12 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
791
1012
|
|
|
792
1013
|
// Build metadata if profile/target selected
|
|
793
1014
|
const metadata: any = {};
|
|
794
|
-
if (
|
|
795
|
-
|
|
1015
|
+
if (sourcePolicyPreset === 'gitea') {
|
|
1016
|
+
const sourcePolicy = getGiteaPresetSourcePolicy(profiles);
|
|
1017
|
+
if (!sourcePolicy) return;
|
|
1018
|
+
metadata.sourcePolicy = sourcePolicy;
|
|
1019
|
+
} else if (sourcePolicyRefs.length > 0) {
|
|
1020
|
+
metadata.sourcePolicy = buildSourcePolicyMetadata(sourcePolicyRefs);
|
|
796
1021
|
}
|
|
797
1022
|
if (targetKey) {
|
|
798
1023
|
metadata.networkTargetRef = targetKey;
|
|
@@ -823,6 +1048,28 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
823
1048
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
824
1049
|
}
|
|
825
1050
|
|
|
1051
|
+
private getSourcePolicyRefs(metadata?: interfaces.data.IRouteMetadata): string[] {
|
|
1052
|
+
const policyRefs = metadata?.sourcePolicy?.bindings
|
|
1053
|
+
?.map((binding) => binding.sourceProfileRef)
|
|
1054
|
+
.filter(Boolean) || [];
|
|
1055
|
+
if (policyRefs.length > 0) {
|
|
1056
|
+
return policyRefs;
|
|
1057
|
+
}
|
|
1058
|
+
return metadata?.sourceProfileRef ? [metadata.sourceProfileRef] : [];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private describeSourcePolicy(metadata?: interfaces.data.IRouteMetadata): string {
|
|
1062
|
+
const refs = this.getSourcePolicyRefs(metadata);
|
|
1063
|
+
if (refs.length === 0) {
|
|
1064
|
+
return '';
|
|
1065
|
+
}
|
|
1066
|
+
return refs.map((ref) => {
|
|
1067
|
+
const binding = metadata?.sourcePolicy?.bindings?.find((item) => item.sourceProfileRef === ref);
|
|
1068
|
+
const profile = this.profilesTargetsState.profiles.find((item) => item.id === ref);
|
|
1069
|
+
return binding?.sourceProfileName || profile?.name || ref.slice(0, 8);
|
|
1070
|
+
}).join(' → ');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
826
1073
|
private findMergedRoute(clickedRoute: { id?: string; name?: string }): interfaces.data.IMergedRoute | undefined {
|
|
827
1074
|
if (clickedRoute.id) {
|
|
828
1075
|
const routeById = this.routeState.mergedRoutes.find((mr) => mr.id === clickedRoute.id);
|
|
@@ -12,6 +12,33 @@ import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
|
12
12
|
import { viewHostCss } from '../shared/css.js';
|
|
13
13
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
14
14
|
|
|
15
|
+
function parseOptionalPositiveInteger(value: unknown): number | undefined {
|
|
16
|
+
const parsed = typeof value === 'number'
|
|
17
|
+
? value
|
|
18
|
+
: typeof value === 'string'
|
|
19
|
+
? parseInt(value.trim(), 10)
|
|
20
|
+
: Number.NaN;
|
|
21
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildRateLimitFromFormData(data: Record<string, any>) {
|
|
25
|
+
if (!Boolean(data.rateLimitEnabled)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const maxRequests = parseOptionalPositiveInteger(data.rateLimitMaxRequests);
|
|
29
|
+
const window = parseOptionalPositiveInteger(data.rateLimitWindow);
|
|
30
|
+
if (!maxRequests || !window) {
|
|
31
|
+
alert('Rate limit requires positive Max Requests and Window values.');
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
enabled: true,
|
|
36
|
+
maxRequests,
|
|
37
|
+
window,
|
|
38
|
+
keyBy: 'ip' as const,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
declare global {
|
|
16
43
|
interface HTMLElementTagNameMap {
|
|
17
44
|
'ops-view-sourceprofiles': OpsViewSourceProfiles;
|
|
@@ -138,6 +165,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
138
165
|
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
|
|
139
166
|
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
|
|
140
167
|
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
|
|
168
|
+
<dees-input-checkbox .key=${'rateLimitEnabled'} .label=${'Enable Rate Limit'} .description=${'Per source IP. Exceeded requests receive 429.'} .value=${false}></dees-input-checkbox>
|
|
169
|
+
<dees-input-text .key=${'rateLimitMaxRequests'} .label=${'Max Requests'} .description=${'Requests per source IP'}></dees-input-text>
|
|
170
|
+
<dees-input-text .key=${'rateLimitWindow'} .label=${'Window Seconds'}></dees-input-text>
|
|
141
171
|
</dees-form>
|
|
142
172
|
`,
|
|
143
173
|
menuOptions: [
|
|
@@ -150,8 +180,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
150
180
|
const data = await form.collectFormData();
|
|
151
181
|
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
|
152
182
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
|
153
|
-
const
|
|
154
|
-
const
|
|
183
|
+
const maxConnections = parseOptionalPositiveInteger(data.maxConnections);
|
|
184
|
+
const rateLimit = buildRateLimitFromFormData(data);
|
|
185
|
+
if (rateLimit === null) return;
|
|
155
186
|
|
|
156
187
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
|
|
157
188
|
name: String(data.name),
|
|
@@ -160,6 +191,7 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
160
191
|
...(ipAllowList.length > 0 ? { ipAllowList } : {}),
|
|
161
192
|
...(ipBlockList.length > 0 ? { ipBlockList } : {}),
|
|
162
193
|
...(maxConnections ? { maxConnections } : {}),
|
|
194
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
163
195
|
},
|
|
164
196
|
});
|
|
165
197
|
modalArg.destroy();
|
|
@@ -180,6 +212,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
180
212
|
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipAllowList || []}></dees-input-list>
|
|
181
213
|
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipBlockList || []}></dees-input-list>
|
|
182
214
|
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
|
|
215
|
+
<dees-input-checkbox .key=${'rateLimitEnabled'} .label=${'Enable Rate Limit'} .description=${'Per source IP. Exceeded requests receive 429.'} .value=${profile.security?.rateLimit?.enabled === true}></dees-input-checkbox>
|
|
216
|
+
<dees-input-text .key=${'rateLimitMaxRequests'} .label=${'Max Requests'} .description=${'Requests per source IP'} .value=${String(profile.security?.rateLimit?.maxRequests || '')}></dees-input-text>
|
|
217
|
+
<dees-input-text .key=${'rateLimitWindow'} .label=${'Window Seconds'} .value=${String(profile.security?.rateLimit?.window || '')}></dees-input-text>
|
|
183
218
|
</dees-form>
|
|
184
219
|
`,
|
|
185
220
|
menuOptions: [
|
|
@@ -192,8 +227,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
192
227
|
const data = await form.collectFormData();
|
|
193
228
|
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
|
194
229
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
|
195
|
-
const
|
|
196
|
-
const
|
|
230
|
+
const maxConnections = parseOptionalPositiveInteger(data.maxConnections);
|
|
231
|
+
const rateLimit = buildRateLimitFromFormData(data);
|
|
232
|
+
if (rateLimit === null) return;
|
|
197
233
|
|
|
198
234
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
|
|
199
235
|
id: profile.id,
|
|
@@ -203,6 +239,7 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
203
239
|
ipAllowList,
|
|
204
240
|
ipBlockList,
|
|
205
241
|
...(maxConnections ? { maxConnections } : {}),
|
|
242
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
206
243
|
},
|
|
207
244
|
});
|
|
208
245
|
modalArg.destroy();
|