Skip to content

Commit 2d0d4cf

Browse files
committed
upcoming: [DPS-36578] - Custom HTTPS form - additional validation
1 parent 58bee40 commit 2d0d4cf

File tree

2 files changed

+85
-30
lines changed

2 files changed

+85
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/validation": Changed
3+
---
4+
5+
Delivery Logs - additional validation in Endpoint URL and Custom Header Name fields ([#13392](https://github.com/linode/manager/pull/13392))

packages/validation/src/delivery.schema.ts

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,15 @@ const clientCertificateDetailsSchema = object({
5656
}).test(
5757
'all-or-nothing-cert-details',
5858
'If any certificate detail is provided, all are required.',
59-
function (value, context) {
59+
function (value) {
6060
if (!value) {
6161
return true;
6262
}
6363

64-
const {
65-
client_ca_certificate,
66-
client_certificate,
67-
client_private_key,
68-
tls_hostname,
69-
} = value;
64+
const { client_ca_certificate, client_certificate, client_private_key } =
65+
value;
7066

7167
const fields = [
72-
tls_hostname,
7368
client_ca_certificate,
7469
client_certificate,
7570
client_private_key,
@@ -82,39 +77,30 @@ const clientCertificateDetailsSchema = object({
8277
}
8378

8479
const errors: ValidationError[] = [];
85-
if (!hasValue(tls_hostname)) {
86-
errors.push(
87-
context.createError({
88-
path: `${this.path}.tls_hostname`,
89-
message:
90-
'TLS Hostname is required when other Client Certificate details are provided.',
91-
}),
92-
);
93-
}
9480
if (!hasValue(client_ca_certificate)) {
9581
errors.push(
96-
context.createError({
82+
this.createError({
9783
path: `${this.path}.client_ca_certificate`,
9884
message:
99-
'CA Certificate is required when other Client Certificate details are provided.',
85+
'CA Certificate is required when other client certificate details are provided.',
10086
}),
10187
);
10288
}
10389
if (!hasValue(client_certificate)) {
10490
errors.push(
105-
context.createError({
91+
this.createError({
10692
path: `${this.path}.client_certificate`,
10793
message:
108-
'Client Certificate is required when other Client Certificate details are provided.',
94+
'Client Certificate is required when other client certificate details are provided.',
10995
}),
11096
);
11197
}
11298
if (!hasValue(client_private_key)) {
11399
errors.push(
114-
context.createError({
100+
this.createError({
115101
path: `${this.path}.client_private_key`,
116102
message:
117-
'Client Key is required when other Client Certificate details are provided.',
103+
'Client Key is required when other client certificate details are provided.',
118104
}),
119105
);
120106
}
@@ -123,27 +109,91 @@ const clientCertificateDetailsSchema = object({
123109
},
124110
);
125111

112+
const forbiddenCustomHeaderNames = [
113+
'content-type',
114+
'encoding',
115+
'authorization',
116+
'host',
117+
'akamai',
118+
];
119+
126120
const customHeaderSchema = object({
127121
name: string()
128122
.max(maxLength, maxLengthMessage)
129-
.required('Custom Header Name is required.'),
123+
.required('Custom Header Name is required.')
124+
.test(
125+
'non-empty-name',
126+
'Custom Header Name cannot be empty or whitespace only.',
127+
(value) => hasValue(value),
128+
)
129+
.test(
130+
'forbidden-custom-header-name',
131+
'This header name is not allowed.',
132+
(value) =>
133+
!forbiddenCustomHeaderNames.includes(value.trim().toLowerCase()),
134+
),
130135
value: string()
131136
.max(maxLength, maxLengthMessage)
132-
.required('Custom Header Value is required'),
137+
.required('Custom Header Value is required.')
138+
.test(
139+
'non-empty-value',
140+
'Custom Header Value cannot be empty or whitespace only.',
141+
(value) => hasValue(value),
142+
),
133143
});
134144

145+
const urlRgx = /^(https?:\/\/)?(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z]+)+(\/\S*)?$/;
146+
135147
const customHTTPSDetailsSchema = object({
136148
authentication: authenticationSchema.required(),
137149
client_certificate_details: clientCertificateDetailsSchema.optional(),
138150
content_type: string()
139151
.oneOf(['application/json', 'application/json; charset=utf-8'])
140152
.nullable()
141153
.optional(),
142-
custom_headers: array().of(customHeaderSchema).min(1).optional(),
154+
custom_headers: array()
155+
.of(customHeaderSchema)
156+
.min(1)
157+
.optional()
158+
.test(
159+
'unique-header-names',
160+
'Custom Header Names must be unique.',
161+
function (headers) {
162+
if (!headers || headers.length === 0) {
163+
return true;
164+
}
165+
166+
const seenNames = new Set<string>();
167+
const errors: ValidationError[] = [];
168+
169+
headers.forEach((header, index) => {
170+
const trimmedName = header?.name?.trim().toLowerCase();
171+
if (!trimmedName) {
172+
return;
173+
}
174+
175+
if (seenNames.has(trimmedName)) {
176+
errors.push(
177+
this.createError({
178+
path: `${this.path}[${index}].name`,
179+
message: 'Custom Header Name must be unique.',
180+
}),
181+
);
182+
} else {
183+
seenNames.add(trimmedName);
184+
}
185+
});
186+
187+
return errors.length === 0 || new ValidationError(errors);
188+
},
189+
),
143190
data_compression: string().oneOf(['gzip', 'None']).required(),
144191
endpoint_url: string()
145192
.max(maxLength, maxLengthMessage)
146-
.required('Endpoint URL is required.'),
193+
.required('Endpoint URL is required.')
194+
.test('is-valid-url', 'Endpoint URL must be a valid URL.', (value) =>
195+
urlRgx.test(value),
196+
),
147197
});
148198

149199
const hostRgx =
@@ -298,7 +348,7 @@ const detailsShouldNotExistOrBeNull = (schema: MixedSchema) =>
298348

299349
const streamSchemaBase = object({
300350
label: string()
301-
.min(3, 'Stream name must have at least 3 characters')
351+
.min(3, 'Stream name must have at least 3 characters.')
302352
.max(maxLength, maxLengthMessage)
303353
.required('Stream name is required.'),
304354
status: mixed<'active' | 'inactive' | 'provisioning'>().oneOf([
@@ -338,7 +388,7 @@ export const updateStreamSchema = streamSchemaBase
338388
return detailsShouldNotExistOrBeNull(mixed());
339389
}),
340390
})
341-
.noUnknown('Object contains unknown fields');
391+
.noUnknown('Object contains unknown fields.');
342392

343393
export const streamAndDestinationFormSchema = object({
344394
stream: streamSchemaBase.shape({
@@ -349,7 +399,7 @@ export const streamAndDestinationFormSchema = object({
349399
otherwise: (schema) =>
350400
schema
351401
.nullable()
352-
.equals([null], 'Details must be null for audit_logs type'),
402+
.equals([null], 'Details must be null for audit_logs type.'),
353403
}) as Schema<InferType<typeof streamDetailsSchema> | null>,
354404
}),
355405
destination: destinationFormSchema.defined().when('stream.destinations', {

0 commit comments

Comments
 (0)