@nodeart/cloudflare-provisioning 1.0.4 → 1.0.6
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/.github/workflows/test-on-pr.yaml +18 -0
- package/cloudflare.js +238 -12
- package/index.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: run test on PR
|
|
2
|
+
'on': pull_request
|
|
3
|
+
|
|
4
|
+
# cancel in-progress runs on new commits to same PR (gitub.event.number)
|
|
5
|
+
concurrency:
|
|
6
|
+
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
|
|
7
|
+
cancel-in-progress: true
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v3
|
|
14
|
+
- uses: actions/setup-node@v3
|
|
15
|
+
with:
|
|
16
|
+
node-version: 16
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: npm test
|
package/cloudflare.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs/promises')
|
|
3
4
|
const { request } = require('undici')
|
|
4
5
|
|
|
5
6
|
const CLOUDFLARE_API_URL = 'https://api.cloudflare.com/client/v4/'
|
|
@@ -191,7 +192,7 @@ class CloudFlare {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
async getFirewallRules () {
|
|
194
|
-
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/
|
|
195
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/phases/http_request_firewall_custom/entrypoint`
|
|
195
196
|
|
|
196
197
|
const { statusCode, body } = await request(url, {
|
|
197
198
|
method: 'GET',
|
|
@@ -207,11 +208,20 @@ class CloudFlare {
|
|
|
207
208
|
throw new Error(`Could not get firewall rules: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
const { id, rules } = response?.result ?? {}
|
|
212
|
+
if (!id) {
|
|
213
|
+
throw new Error(`Could not get firewall rules ruleset ID: got ${id}, received value: ${JSON.stringify(response)}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { id, rules }
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
async createFirewallRule (firewallRule) {
|
|
214
|
-
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/
|
|
219
|
+
async createFirewallRule (rulesetId, firewallRule) {
|
|
220
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/${rulesetId}/rules`
|
|
221
|
+
// Spread "filter" property from deprecated rule API
|
|
222
|
+
const filter = firewallRule?.filter ?? {}
|
|
223
|
+
const rule = { ...firewallRule, ...filter }
|
|
224
|
+
delete rule.filter
|
|
215
225
|
|
|
216
226
|
const { statusCode, body } = await request(url, {
|
|
217
227
|
method: 'POST',
|
|
@@ -219,7 +229,7 @@ class CloudFlare {
|
|
|
219
229
|
...this.authorizationHeaders,
|
|
220
230
|
'Content-Type': 'application/json'
|
|
221
231
|
},
|
|
222
|
-
body: JSON.stringify(
|
|
232
|
+
body: JSON.stringify(rule)
|
|
223
233
|
})
|
|
224
234
|
|
|
225
235
|
const response = await body.json()
|
|
@@ -231,8 +241,12 @@ class CloudFlare {
|
|
|
231
241
|
return response
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
async updateFirewallRule (
|
|
235
|
-
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/
|
|
244
|
+
async updateFirewallRule (rulesetId, ruleId, firewallRule) {
|
|
245
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/${rulesetId}/rules/${ruleId}`
|
|
246
|
+
// Spread "filter" property from deprecated rule API
|
|
247
|
+
const filter = firewallRule?.filter ?? {}
|
|
248
|
+
const rule = { ...firewallRule, ...filter }
|
|
249
|
+
delete rule.filter
|
|
236
250
|
|
|
237
251
|
const { statusCode, body } = await request(url, {
|
|
238
252
|
method: 'PATCH',
|
|
@@ -240,7 +254,7 @@ class CloudFlare {
|
|
|
240
254
|
...this.authorizationHeaders,
|
|
241
255
|
'Content-Type': 'application/json'
|
|
242
256
|
},
|
|
243
|
-
body: JSON.stringify(
|
|
257
|
+
body: JSON.stringify(rule)
|
|
244
258
|
})
|
|
245
259
|
|
|
246
260
|
const response = await body.json()
|
|
@@ -253,18 +267,18 @@ class CloudFlare {
|
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
async rewriteFirewallRules (firewallRules) {
|
|
256
|
-
const currentFirewallRules = await this.getFirewallRules()
|
|
270
|
+
const { id: rulesetId, rules: currentFirewallRules } = await this.getFirewallRules()
|
|
257
271
|
|
|
258
272
|
for (const firewallRule of firewallRules) {
|
|
259
|
-
const currentFirewallRule = currentFirewallRules.
|
|
273
|
+
const currentFirewallRule = currentFirewallRules.find(
|
|
260
274
|
rule => rule.description === firewallRule.description
|
|
261
275
|
)
|
|
262
276
|
|
|
263
277
|
try {
|
|
264
278
|
if (currentFirewallRule) {
|
|
265
|
-
await this.updateFirewallRule(currentFirewallRule.id, firewallRule)
|
|
279
|
+
await this.updateFirewallRule(rulesetId, currentFirewallRule.id, firewallRule)
|
|
266
280
|
} else {
|
|
267
|
-
await this.createFirewallRule(firewallRule)
|
|
281
|
+
await this.createFirewallRule(rulesetId, firewallRule)
|
|
268
282
|
}
|
|
269
283
|
} catch (error) {
|
|
270
284
|
console.error(`Could not update firewall rule for domain ${this.domain}: ${JSON.stringify(firewallRule)}, error: ${error}`)
|
|
@@ -272,6 +286,127 @@ class CloudFlare {
|
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
async getRedirectRules () {
|
|
290
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/phases/http_request_dynamic_redirect/entrypoint`
|
|
291
|
+
|
|
292
|
+
const { statusCode, body } = await request(url, {
|
|
293
|
+
method: 'GET',
|
|
294
|
+
headers: {
|
|
295
|
+
...this.authorizationHeaders,
|
|
296
|
+
'Content-Type': 'application/json'
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
const response = await body.json()
|
|
301
|
+
|
|
302
|
+
if (statusCode === 404) {
|
|
303
|
+
// Create http_request_dynamic_redirect ruleset if one doesn't exist
|
|
304
|
+
console.log('Ruleset was not found. Initializing redirect ruleset creation...')
|
|
305
|
+
const createRulesetUrl = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets`
|
|
306
|
+
const payload = {
|
|
307
|
+
name: 'Redirect rules ruleset',
|
|
308
|
+
kind: 'zone',
|
|
309
|
+
phase: 'http_request_dynamic_redirect',
|
|
310
|
+
rules: []
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { statusCode: createStatusCode, body: createBody } = await request(createRulesetUrl, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: {
|
|
316
|
+
...this.authorizationHeaders,
|
|
317
|
+
'Content-Type': 'application/json'
|
|
318
|
+
},
|
|
319
|
+
body: JSON.stringify(payload)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const createResponse = await createBody.json()
|
|
323
|
+
|
|
324
|
+
if (createStatusCode !== 200) {
|
|
325
|
+
throw new Error(`Could not create redirect ruleset: ${statusCode}, error: ${JSON.stringify(createResponse)}`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const { id, rules } = createResponse?.result ?? {}
|
|
329
|
+
if (!id) {
|
|
330
|
+
throw new Error(`Could not get redirect rules ruleset ID: got ${id}, received value: ${JSON.stringify(response)}`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { id, rules: rules ?? [] }
|
|
334
|
+
} else {
|
|
335
|
+
if (statusCode !== 200) {
|
|
336
|
+
throw new Error(`Could not get redirect rules: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { id, rules } = response?.result ?? {}
|
|
340
|
+
if (!id) {
|
|
341
|
+
throw new Error(`Could not get redirect rules ruleset ID: got ${id}, received value: ${JSON.stringify(response)}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { id, rules: rules ?? [] }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async createRedirectRule (rulesetId, redirectRule) {
|
|
349
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/${rulesetId}/rules`
|
|
350
|
+
|
|
351
|
+
const { statusCode, body } = await request(url, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: {
|
|
354
|
+
...this.authorizationHeaders,
|
|
355
|
+
'Content-Type': 'application/json'
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify(redirectRule)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const response = await body.json()
|
|
361
|
+
|
|
362
|
+
if (statusCode !== 200) {
|
|
363
|
+
throw new Error(`Could not create a redirect rule: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return response
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async updateRedirectRule (rulesetId, ruleId, redirectRule) {
|
|
370
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/rulesets/${rulesetId}/rules/${ruleId}`
|
|
371
|
+
|
|
372
|
+
const { statusCode, body } = await request(url, {
|
|
373
|
+
method: 'PATCH',
|
|
374
|
+
headers: {
|
|
375
|
+
...this.authorizationHeaders,
|
|
376
|
+
'Content-Type': 'application/json'
|
|
377
|
+
},
|
|
378
|
+
body: JSON.stringify(redirectRule)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const response = await body.json()
|
|
382
|
+
|
|
383
|
+
if (statusCode !== 200) {
|
|
384
|
+
throw new Error(`Could not update a redirect rule: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return response
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async rewriteRedirectRules (redirectRules) {
|
|
391
|
+
const { id: rulesetId, rules: currentRedirectRules } = await this.getRedirectRules()
|
|
392
|
+
|
|
393
|
+
for (const redirectRule of redirectRules) {
|
|
394
|
+
const currentRedirectRule = currentRedirectRules.find(
|
|
395
|
+
rule => rule.description === redirectRule.description
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
if (currentRedirectRule) {
|
|
400
|
+
await this.updateRedirectRule(rulesetId, currentRedirectRule.id, redirectRule)
|
|
401
|
+
} else {
|
|
402
|
+
await this.createRedirectRule(rulesetId, redirectRule)
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`Could not update redirect rule for domain ${this.domain}: ${JSON.stringify(redirectRule)}, error: ${error}`)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
275
410
|
async setPolish (value) {
|
|
276
411
|
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/settings/polish`
|
|
277
412
|
|
|
@@ -668,6 +803,97 @@ class CloudFlare {
|
|
|
668
803
|
|
|
669
804
|
return response
|
|
670
805
|
}
|
|
806
|
+
|
|
807
|
+
async uploadTlsClientAuth ({ clientKey, clientCert, caCert }) {
|
|
808
|
+
try {
|
|
809
|
+
await fs.access(clientKey, fs.constants.R_OK)
|
|
810
|
+
await fs.access(clientCert, fs.constants.R_OK)
|
|
811
|
+
await fs.access(caCert, fs.constants.R_OK)
|
|
812
|
+
} catch (e) {
|
|
813
|
+
throw new Error(`Cancelling cert upload for domain ${this.domain}. Cannot access file: ${e?.message}`)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const clientKeyContents = await fs.readFile(clientKey, 'utf8')
|
|
817
|
+
const clientCertContents = await fs.readFile(clientCert, 'utf8')
|
|
818
|
+
const caCertContents = await fs.readFile(caCert, 'utf8')
|
|
819
|
+
|
|
820
|
+
await this.uploadCertAndKey(clientCertContents, clientKeyContents)
|
|
821
|
+
await this.uploadCaCert(caCertContents)
|
|
822
|
+
await this.enableTLSClientAuth()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async uploadCertAndKey (clientCert, clientKey) {
|
|
826
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/origin_tls_client_auth`
|
|
827
|
+
const payload = {
|
|
828
|
+
certificate: clientCert.replace(/\r?\n/g, '\n'),
|
|
829
|
+
private_key: clientKey.replace(/\r?\n/g, '\n')
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const { statusCode, body } = await request(url, {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
...this.authorizationHeaders,
|
|
836
|
+
'Content-Type': 'application/json'
|
|
837
|
+
},
|
|
838
|
+
body: JSON.stringify(payload)
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
const response = await body.json()
|
|
842
|
+
|
|
843
|
+
if (statusCode !== 200 && statusCode !== 201) {
|
|
844
|
+
const errors = response?.errors ?? []
|
|
845
|
+
if (errors.find((error) => error.code === 1406 && error.message === 'This certificate already exists for this zone.')) {
|
|
846
|
+
console.log(`This certificate already exists for domain ${this.domain}. Continuing...`)
|
|
847
|
+
} else {
|
|
848
|
+
throw new Error(`Could not upload certificate and private key: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async uploadCaCert (caCert) {
|
|
854
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/acm/custom_trust_store`
|
|
855
|
+
const payload = {
|
|
856
|
+
certificate: caCert.replace(/\r?\n/g, '\n')
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const { statusCode, body } = await request(url, {
|
|
860
|
+
method: 'POST',
|
|
861
|
+
headers: {
|
|
862
|
+
...this.authorizationHeaders,
|
|
863
|
+
'Content-Type': 'application/json'
|
|
864
|
+
},
|
|
865
|
+
body: JSON.stringify(payload)
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
const response = await body.json()
|
|
869
|
+
|
|
870
|
+
if (statusCode !== 200 && statusCode !== 201) {
|
|
871
|
+
const errors = response?.errors ?? []
|
|
872
|
+
if (errors.find((error) => error.code === 1406 && error.message === 'This certificate already exists for this zone.')) {
|
|
873
|
+
console.log(`This CA certificate already exists for domain ${this.domain}. Continuing...`)
|
|
874
|
+
} else {
|
|
875
|
+
throw new Error(`Could not upload CA certificate: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async enableTLSClientAuth () {
|
|
881
|
+
const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/settings/tls_client_auth`
|
|
882
|
+
const { statusCode, body } = await request(url, {
|
|
883
|
+
method: 'PATCH',
|
|
884
|
+
headers: {
|
|
885
|
+
...this.authorizationHeaders,
|
|
886
|
+
'Content-Type': 'application/json'
|
|
887
|
+
},
|
|
888
|
+
body: JSON.stringify({ value: 'on' })
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
const response = await body.json()
|
|
892
|
+
|
|
893
|
+
if (statusCode !== 200) {
|
|
894
|
+
throw new Error(`Could not enable TSL Client Auth setting: ${statusCode}, error: ${JSON.stringify(response)}`)
|
|
895
|
+
}
|
|
896
|
+
}
|
|
671
897
|
}
|
|
672
898
|
|
|
673
899
|
module.exports = CloudFlare
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const cloudflareSettingsHandlers = {
|
|
|
10
10
|
brotli: CloudFlare.prototype.setBrotli,
|
|
11
11
|
dnsRecords: CloudFlare.prototype.rewriteDNSRecords,
|
|
12
12
|
firewallRules: CloudFlare.prototype.rewriteFirewallRules,
|
|
13
|
+
redirectRules: CloudFlare.prototype.rewriteRedirectRules,
|
|
13
14
|
polish: CloudFlare.prototype.setPolish,
|
|
14
15
|
minify: CloudFlare.prototype.setMinify,
|
|
15
16
|
http2Prioritization: CloudFlare.prototype.setHTTP2Prioritization,
|
|
@@ -20,7 +21,8 @@ const cloudflareSettingsHandlers = {
|
|
|
20
21
|
argoSmartRouting: CloudFlare.prototype.setArgoSmartRouting,
|
|
21
22
|
workers: CloudFlare.prototype.rewriteWorkerRoutes,
|
|
22
23
|
pageRules: CloudFlare.prototype.rewritePageRules,
|
|
23
|
-
hotlinkProtection: CloudFlare.prototype.setHotlinkProtection
|
|
24
|
+
hotlinkProtection: CloudFlare.prototype.setHotlinkProtection,
|
|
25
|
+
tlsClientAuth: CloudFlare.prototype.uploadTlsClientAuth
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function substituteDomainName (settings, domainName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodeart/cloudflare-provisioning",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
"standard": "^17.0.0"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"undici": "^
|
|
18
|
+
"undici": "^7.3.0"
|
|
19
19
|
}
|
|
20
20
|
}
|