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
6 changes: 5 additions & 1 deletion .changeset/cold-needles-warn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
'@fingerprint/aws-cloudfront-proxy': minor
---

Add support for API V4
Add support for API V4.

Docs:
- [CloudFront JavaScript Agent V4 Migration Guide](https://docs.fingerprint.com/docs/cloudfront-integration-migration-to-js-agent-v4)
- [CloudFront Terraform guide](https://docs.fingerprint.com/docs/aws-cloudfront-integration-via-terraform)
5 changes: 5 additions & 0 deletions .changeset/fifty-bars-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fingerprint/aws-cloudfront-proxy': minor
---

Add FPJS_INTEGRATION_PATH_DEPTH header to CloudFormation template
9 changes: 2 additions & 7 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
{
"mode": "pre",
"mode": "exit",
"tag": "rc",
"initialVersions": {
"@fingerprint/aws-cloudfront-proxy": "2.1.1",
"e2e-tests": "0.0.0",
"website": "0.0.0"
},
"changesets": [
"cold-needles-warn",
"cuddly-tires-tan",
"slimy-tables-shave",
"yummy-bananas-worry"
]
"changesets": ["cold-needles-warn", "cuddly-tires-tan", "yummy-bananas-worry"]
}
5 changes: 0 additions & 5 deletions .changeset/slimy-tables-shave.md

This file was deleted.

3 changes: 3 additions & 0 deletions cloudformation/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ Resources:
OriginCustomHeaders:
- HeaderName: FPJS_SECRET_NAME
HeaderValue: !Ref FingerprintIntegrationSettingsSecret

- HeaderName: FPJS_INTEGRATION_PATH_DEPTH
HeaderValue: '0'
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
Expand Down
20 changes: 20 additions & 0 deletions proxy/test/handlers/v4/handleAgentDownloading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'
import { mockEvent, mockRequest } from '../../aws'
import { handler } from '../../../app'
import { generateErrorResponse } from '../../../utils/generateErrorResponse'
import { CustomerVariableName } from '../../../utils/customer-variables/types'

const requestUri = '/behavior/web/v4/ujKG34hUYKLJKJ1F'
describe('Download agent endpoint V4', () => {
Expand Down Expand Up @@ -71,6 +72,25 @@ describe('Download agent endpoint V4', () => {
expect(url.toString()).toEqual(`https://${origin}/web/v4/ujKG34hUYKLJKJ1F`)
})

test('Successful call with nested behavior path', async () => {
const event = mockEvent(mockRequest({ uri: `/nested${requestUri}`, querystring: '', method: 'GET' }))

event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [
{
key: CustomerVariableName.BehaviorPathNestLevel,
value: '2',
},
]

await handler(event)

expect(requestSpy).toHaveBeenCalledTimes(1)

const [url] = requestSpy.mock.calls[0]

expect(url.toString()).toEqual(`https://${origin}/web/v4/ujKG34hUYKLJKJ1F`)
})

test('Call with a custom query', async () => {
const request = mockRequest({
uri: requestUri,
Expand Down
88 changes: 88 additions & 0 deletions proxy/test/handlers/v4/handleIngress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import https, { Agent } from 'https'
import { EventEmitter } from 'events'
import { ClientRequest, IncomingMessage } from 'http'
import { Socket } from 'net'
import { CustomerVariableName } from '../../../utils/customer-variables/types'

describe('Result Endpoint V4', () => {
const requestUri = '/behavior'
Expand Down Expand Up @@ -148,6 +149,93 @@ describe('Result Endpoint V4', () => {
)
})

test('Call with suffix and nested behavior path', async () => {
const event = mockEvent(
mockRequest({
uri: '/behavior/nested/with/suffix',
querystring: '',
})
)
event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [
{
key: CustomerVariableName.BehaviorPathNestLevel,
value: '2',
},
]
await handler(event)

expect(https.request).toHaveBeenCalledWith(
`https://${origin}/with/suffix${queryString}`,
expect.anything(),
expect.anything()
)
})

test('Call with integration served under root path', async () => {
const event = mockEvent(
mockRequest({
uri: '/',
querystring: '',
})
)
event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [
{
key: CustomerVariableName.BehaviorPathNestLevel,
value: '0',
},
]
await handler(event)

expect(https.request).toHaveBeenCalledWith(`https://${origin}/${queryString}`, expect.anything(), expect.anything())
})

test('Call suffix and with integration served under root path', async () => {
const event = mockEvent(
mockRequest({
uri: '/with/suffix',
querystring: '',
})
)
event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [
{
key: CustomerVariableName.BehaviorPathNestLevel,
value: '0',
},
]
await handler(event)

expect(https.request).toHaveBeenCalledWith(
`https://${origin}/with/suffix${queryString}`,
expect.anything(),
expect.anything()
)
})

test.each([NaN, -1, 'test', ' 1', Infinity, -Infinity])(
'Call with suffix and invalid nested path level variable: %s',
async (value) => {
const event = mockEvent(
mockRequest({
uri: '/behavior/with/suffix',
querystring: '',
})
)
event.Records[0].cf.request.origin!.s3!.customHeaders[CustomerVariableName.BehaviorPathNestLevel] = [
{
key: CustomerVariableName.BehaviorPathNestLevel,
value: value as any,
},
]
await handler(event)

expect(https.request).toHaveBeenCalledWith(
`https://${origin}/with/suffix${queryString}`,
expect.anything(),
expect.anything()
)
}
)

test('Traffic monitoring', async () => {
const event = mockEvent(mockRequest({ uri: requestUri, querystring: '' }))
await handler(event)
Expand Down
4 changes: 3 additions & 1 deletion proxy/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNonNegativeInteger } from './validation'

interface CacheItem<T> {
value: T
expiresAt: number
Expand Down Expand Up @@ -51,6 +53,6 @@ export class TTLCache<K, V> {
}

static isValidTTL(value?: number): value is number {
return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value) && value >= 0
return isNonNegativeInteger(value)
}
}
15 changes: 12 additions & 3 deletions proxy/utils/customer-variables/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNonNegativeInteger } from '../validation'

export enum CustomerVariableName {
GetResultPath = 'fpjs_get_result_path',
BehaviorPathNestLevel = 'fpjs_integration_path_depth',
Expand All @@ -12,16 +14,23 @@ export const internalVariables: Set<CustomerVariableName> = new Set<CustomerVari
CustomerVariableName.FpIngressBaseHost,
])

type ParserValidator<T> = (value: T) => boolean

const stringParser = (value: string) => value

const intParser = (fallbackValue: number) => (value: string) => {
const intParser = (fallbackValue: number, validation: ParserValidator<number>) => (value: string) => {
const parsed = parseInt(value)
return isNaN(parsed) ? fallbackValue : parsed

if (validation(parsed)) {
return parsed
}

return fallbackValue
}

export const customerVariableParsers = {
[CustomerVariableName.GetResultPath]: stringParser,
[CustomerVariableName.BehaviorPathNestLevel]: intParser(1),
[CustomerVariableName.BehaviorPathNestLevel]: intParser(1, isNonNegativeInteger),
[CustomerVariableName.PreSharedSecret]: stringParser,
[CustomerVariableName.AgentDownloadPath]: stringParser,
[CustomerVariableName.FpCdnUrl]: stringParser,
Expand Down
3 changes: 3 additions & 0 deletions proxy/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isNonNegativeInteger(value?: unknown): value is number {
return Boolean(typeof value === 'number' && Number.isInteger(value) && value >= 0)
}
Loading