@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.
@@ -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}/firewall/rules`
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
- return response
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}/firewall/rules`
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([firewallRule])
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 (id, firewallRule) {
235
- const url = CLOUDFLARE_API_URL + `zones/${this.zoneId}/firewall/rules/${id}`
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(firewallRule)
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.result.find(
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.4",
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": "^5.10.0"
18
+ "undici": "^7.3.0"
19
19
  }
20
20
  }