Skip to content

Commit

Permalink
scratch - cors
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielSchaffer committed Feb 4, 2020
1 parent ff1e059 commit c3dd2f0
Show file tree
Hide file tree
Showing 17 changed files with 528 additions and 239 deletions.
7 changes: 4 additions & 3 deletions packages/dandi/http-pipeline/src/cors/cors-config.ts
@@ -1,11 +1,12 @@
import { HttpResponseHeader } from '@dandi/http'

import { CorsAllowOriginFn } from './cors'
import { CorsHeaderValues } from './cors-headers'
import { CorsOriginWhitelist } from './cors-origin-whitelist'

export interface CorsConfig {
allowCredentials?: true
allowHeaders?: CorsHeaderValues
allowHeaders?: HttpResponseHeader[]
allowOrigin?: CorsOriginWhitelist | CorsAllowOriginFn
exposeHeaders?: CorsHeaderValues
exposeHeaders?: HttpResponseHeader[]
maxAge?: number
}
2 changes: 2 additions & 0 deletions packages/dandi/http-pipeline/src/cors/cors-headers.ts
Expand Up @@ -33,6 +33,8 @@ export type CorsResponseHeaders = Pick<HttpResponseHeaders,
HttpHeader.accessControlMaxAge
>

export type CorsResponseHeader = keyof HttpResponseHeaders

@Injectable(RestrictScope(HttpRequestScope))
export class CorsHeaderValues implements Partial<CorsResponseHeaders> {

Expand Down
4 changes: 2 additions & 2 deletions packages/dandi/http-pipeline/src/cors/cors-preparer.ts
@@ -1,5 +1,5 @@
import { Inject } from '@dandi/core'
import { HttpRequest } from '@dandi/http'
import { HttpRequest, HttpRequestHeadersAccessor } from '@dandi/http'

import { HttpPipelinePreparer, HttpPipelinePreparerResult } from '../http-pipeline-preparer'

Expand All @@ -19,7 +19,7 @@ export class CorsPreparer implements HttpPipelinePreparer {
return [{
provide: CorsAllowRequest,
useFactory: corsRequestAllowed,
deps: [CorsHeaderValues],
deps: [CorsHeaderValues, HttpRequestHeadersAccessor],
}]
}

Expand Down
126 changes: 126 additions & 0 deletions packages/dandi/http-pipeline/src/cors/cors-util.spec.ts
@@ -0,0 +1,126 @@
import { corsRequestAllowed, CorsResponseHeaders, isCorsRequest } from '@dandi/http-pipeline'
import {
HttpHeader,
HttpMethod,
HttpRequest,
HttpRequestHeadersAccessor,
HttpRequestHeadersHashAccessor,
} from '@dandi/http'
import { createStubInstance, stub } from '@dandi/core/testing'

import { expect } from 'chai'
import { SinonStubbedInstance } from 'sinon'

describe('corsRequestAllowed', () => {

let headersStub: SinonStubbedInstance<HttpRequestHeadersAccessor>
let headers: HttpRequestHeadersAccessor

beforeEach(() => {
headersStub = createStubInstance(HttpRequestHeadersHashAccessor)
headers = headersStub as HttpRequestHeadersAccessor
})
afterEach(() => {
headersStub = undefined
headers = undefined
})

it('returns false if the Access-Control-Allow-Origin response header is empty', () => {
expect(corsRequestAllowed({ [HttpHeader.accessControlAllowMethods]: 'GET' }, headers)).to.be.false
})

it('returns false if the Access-Control-Allow-Methods response header is empty', () => {
expect(corsRequestAllowed({ [HttpHeader.accessControlAllowOrigin]: 'foo.com' }, headers)).to.be.false
})

it('returns false if not all of the headers in the Access-Control-Request-Headers request header are included in the' +
'Access-Control-Allow-Headers response header', () => {

headersStub.get.withArgs(HttpHeader.accessControlRequestHeaders).returns([HttpHeader.contentType])
const corsHeaders: Partial<CorsResponseHeaders> = {
[HttpHeader.accessControlAllowHeaders]: `${HttpHeader.contentLanguage} ${HttpHeader.cacheControl}`,
}
headersStub.get.withArgs(HttpHeader.accessControlRequestHeaders).returns([HttpHeader.contentType])

expect(corsRequestAllowed(corsHeaders, headers)).to.be.false
})

it('returns true if the Access-Control-Allow-Origin and Access-Control-Allow-Methods response headers both have' +
'values, and the Access-Control-Request-Headers request header is not specified', () => {

const corsHeaders: Partial<CorsResponseHeaders> = {
[HttpHeader.accessControlAllowOrigin]: 'foo.com',
[HttpHeader.accessControlAllowMethods]: HttpMethod.get,
}

expect(corsRequestAllowed(corsHeaders, headers)).to.be.true
})

it('returns true if the Access-Control-Allow-Origin and Access-Control-Allow-Methods response headers both have' +
'values, and the Access-Control-Allow-Headers response header includes all requested headers from the ' +
'Access-Control-Request-Headers request header', () => {

const corsHeaders: Partial<CorsResponseHeaders> = {
[HttpHeader.accessControlAllowOrigin]: 'foo.com',
[HttpHeader.accessControlAllowMethods]: HttpMethod.get,
[HttpHeader.accessControlAllowHeaders]: `${HttpHeader.contentType} ${HttpHeader.cacheControl}`,
}
headersStub.get.withArgs(HttpHeader.accessControlRequestHeaders).returns([HttpHeader.contentType, HttpHeader.cacheControl])

expect(corsRequestAllowed(corsHeaders, headers)).to.be.true
})

it('returns false if the Access-Control-Allow-Origin and Access-Control-Allow-Methods response headers both have' +
'values, but the Access-Control-Allow-Headers response header does not include all requested headers from the ' +
'Access-Control-Request-Headers request header', () => {

const corsHeaders: Partial<CorsResponseHeaders> = {
[HttpHeader.accessControlAllowOrigin]: 'foo.com',
[HttpHeader.accessControlAllowMethods]: HttpMethod.get,
[HttpHeader.accessControlAllowHeaders]: `${HttpHeader.contentType}`,
}
headersStub.get.withArgs(HttpHeader.accessControlRequestHeaders).returns([HttpHeader.contentType, HttpHeader.cacheControl])

expect(corsRequestAllowed(corsHeaders, headers)).to.be.false
})

})

describe('isCorsRequest', () => {

let req: SinonStubbedInstance<HttpRequest>

beforeEach(() => {
req = {
get: stub(),
} as SinonStubbedInstance<HttpRequest>
})
afterEach(() => {
req = undefined
})

it('returns false if there is no origin request header', () => {
expect(isCorsRequest(req)).to.be.false
})

it('returns false if the origin matches the host', () => {
req.get
.withArgs(HttpHeader.origin)
.returns('foo.com')
.withArgs(HttpHeader.host)
.returns('foo.com')

expect(isCorsRequest(req)).to.be.false
})

it('returns false if the origin does not match the host', () => {
req.get
.withArgs(HttpHeader.origin)
.returns('foo.com')
.withArgs(HttpHeader.host)
.returns('bar.com')

expect(isCorsRequest(req)).to.be.true
})

})
11 changes: 1 addition & 10 deletions packages/dandi/http-pipeline/src/cors/cors.ts
@@ -1,11 +1,9 @@
import { InjectionToken, Provider } from '@dandi/core'
import { InjectionToken } from '@dandi/core'
import {
HttpHeader,
HttpMethod,
HttpRequestScope,
HttpRequestHeader,
HttpHeaderWildcard,
HttpRequestHeadersAccessor,
} from '@dandi/http'

import { localOpinionatedToken } from '../local-token'
Expand All @@ -15,13 +13,6 @@ export type CorsHeaders = (HttpRequestHeader | string | HttpHeaderWildcard)[]
export const CorsOrigin: InjectionToken<string> = localOpinionatedToken<string>('CorsOrigin', {
restrictScope: HttpRequestScope,
})
export const CorsOriginProvider: Provider<string> = {
provide: CorsOrigin,
useFactory: function corsOriginFactory(headers: HttpRequestHeadersAccessor): string {
return headers.get(HttpHeader.origin)
},
}

export type CorsAllowOrigin = string
export type CorsAllowOriginFn = (origin: string) => CorsAllowOrigin | Promise<CorsAllowOrigin>
export const CorsAllowOrigin: InjectionToken<string> = localOpinionatedToken<string>('CorsAllowOrigin', {
Expand Down
4 changes: 2 additions & 2 deletions packages/dandi/mvc-hal/src/default-resource-composer.spec.ts
Expand Up @@ -17,7 +17,7 @@ import { Property } from '@dandi/model'
import { ModelBuilder } from '@dandi/model-builder'
import {
Controller,
DefaultRouteInitializer,
DandiRouteInitializer,
HttpGet,
Route,
Routes,
Expand All @@ -38,7 +38,7 @@ import { createStubInstance, stub } from 'sinon'
/* eslint-disable @typescript-eslint/no-unused-vars */
describe('DefaultResourceComposer', function() {

const harness = testHarness(DefaultResourceComposer, DefaultRouteInitializer)
const harness = testHarness(DefaultResourceComposer, DandiRouteInitializer)

describe('compose', function() {

Expand Down
6 changes: 3 additions & 3 deletions packages/dandi/mvc/index.ts
Expand Up @@ -9,9 +9,9 @@ export * from './src/condition.decorator'
export { Controller, MissingControllerPathError } from './src/controller-decorator'
export * from './src/controller-metadata'
export * from './src/cors.decorator'
export * from './src/decorator-route-generator'
export * from './src/default-route-executor'
export * from './src/default-route-initializer'
export * from './src/dandi-route-executor'
export * from './src/dandi-route-generator'
export * from './src/dandi-route-initializer'
export { HttpGet, HttpDelete, HttpOptions, HttpPatch, HttpPost, HttpPut } from './src/http-method-decorator'
export * from './src/mvc.module'
export * from './src/request-authorization.service'
Expand Down
8 changes: 4 additions & 4 deletions packages/dandi/mvc/src/condition.decorator.spec.ts
Expand Up @@ -11,8 +11,8 @@ import {
Authorized,
CollectionResource,
Controller,
DecoratorRouteGenerator,
DefaultRouteInitializer,
DandiRouteGenerator,
DandiRouteInitializer,
getControllerMetadata,
HttpGet,
RouteGenerator,
Expand Down Expand Up @@ -75,8 +75,8 @@ xdescribe('ConditionDecorator', function() {
authService,
TestController,
AuthorizationAuthProviderFactory,
DecoratorRouteGenerator,
DefaultRouteInitializer,
DandiRouteGenerator,
DandiRouteInitializer,
ModelBuilderModule,
)

Expand Down
17 changes: 10 additions & 7 deletions packages/dandi/mvc/src/cors.decorator.spec.ts
@@ -1,8 +1,7 @@
import { Controller, HttpGet, getCorsConfig } from '@dandi/mvc'
import { expect } from 'chai'
import { HttpHeader } from '@dandi/http'
import { Controller, HttpGet, getCorsConfig, Cors, getControllerMetadata } from '@dandi/mvc'

import { getControllerMetadata } from './controller-metadata'
import { Cors } from './cors.decorator'
import { expect } from 'chai'

describe('@Cors', () => {
describe('as a class decorator', () => {
Expand Down Expand Up @@ -57,9 +56,13 @@ describe('getCorsConfig', () => {
})

it('merges the configs if both controller and method cors are truthy', () => {
expect(getCorsConfig({ allowHeaders: ['bar'] }, { exposeHeaders: ['foo'] })).to.deep.equal({
allowHeaders: ['bar'],
exposeHeaders: ['foo'],
const config = getCorsConfig(
{ allowHeaders: [HttpHeader.contentType] },
{ exposeHeaders: [HttpHeader.cacheControl] },
)
expect(config).to.deep.equal({
allowHeaders: [HttpHeader.contentType],
exposeHeaders: [HttpHeader.cacheControl],
})
})
})
Expand Up @@ -4,7 +4,7 @@ import { Inject, Provider, SymbolToken } from '@dandi/core'
import { stubHarness, stub } from '@dandi/core/testing'
import { HttpMethod, HttpRequest, HttpResponse, HttpStatusCode } from '@dandi/http'
import { HttpPipeline, HttpRequestInfo } from '@dandi/http-pipeline'
import { AuthorizationCondition, DefaultRouteExecutor, Route, RouteInitializer } from '@dandi/mvc'
import { AuthorizationCondition, DandiRouteExecutor, Route, RouteInitializer } from '@dandi/mvc'

import { expect } from 'chai'
import { SinonStubbedInstance } from 'sinon'
Expand All @@ -23,7 +23,7 @@ describe('DefaultRouteExecutor', () => {
}
}

const harness = stubHarness(DefaultRouteExecutor,
const harness = stubHarness(DandiRouteExecutor,
{
provide: Route,
useFactory: () => route,
Expand Down Expand Up @@ -56,7 +56,7 @@ describe('DefaultRouteExecutor', () => {
)

let providers: Provider<any>[]
let routeExec: DefaultRouteExecutor
let routeExec: DandiRouteExecutor
let routeInit: SinonStubbedInstance<RouteInitializer>
let route: Route
let req: HttpRequest
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('DefaultRouteExecutor', () => {
status: stub().returnsThis(),
end: stub().returnsThis(),
} as any
routeExec = await harness.inject(DefaultRouteExecutor)
routeExec = await harness.inject(DandiRouteExecutor)
})
afterEach(() => {
providers = undefined
Expand Down
Expand Up @@ -10,7 +10,7 @@ import { RouteExecutor } from './route-executor'
import { RouteInitializer } from './route-initializer'

@Injectable(RouteExecutor)
export class DefaultRouteExecutor implements RouteExecutor {
export class DandiRouteExecutor implements RouteExecutor {
constructor(
@Inject(Injector) private injector: Injector,
@Inject(RouteInitializer) private routeInitializer: RouteInitializer,
Expand All @@ -19,7 +19,7 @@ export class DefaultRouteExecutor implements RouteExecutor {
) {}

public async execRoute(route: Route, req: HttpRequest, res: HttpResponse): Promise<void> {
const performance = new PerfRecord('DefaultRouteExecutor.execRoute', 'begin')
const performance = new PerfRecord('DandiRouteExecutor.execRoute', 'begin')

this.logger.debug(
`begin execRoute ${route.controllerCtr.name}.${route.controllerMethod.toString()}:`,
Expand All @@ -35,9 +35,9 @@ export class DefaultRouteExecutor implements RouteExecutor {
route.path,
)

performance.mark('DefaultRouteExecutor.execRoute', 'beforeInitRouteRequest')
performance.mark('DandiRouteExecutor.execRoute', 'beforeInitRouteRequest')
const requestProviders = this.routeInitializer.initRouteRequest(route, req, { requestId, performance }, res)
performance.mark('DefaultRouteExecutor.execRoute', 'afterInitRouteRequest')
performance.mark('DandiRouteExecutor.execRoute', 'afterInitRouteRequest')

this.logger.debug(
`after initRouteRequest ${route.controllerCtr.name}.${route.controllerMethod.toString()}:`,
Expand All @@ -51,12 +51,12 @@ export class DefaultRouteExecutor implements RouteExecutor {
route.path,
)

performance.mark('DefaultRouteExecutor.execRoute', 'beforeHandleRequest')
performance.mark('DandiRouteExecutor.execRoute', 'beforeHandleRequest')
await Disposable.useAsync(this.injector.createChild(createHttpRequestScope(req), requestProviders), async requestInjector => {
await requestInjector.invoke(this as DefaultRouteExecutor, 'checkAuthorizationConditions')
await requestInjector.invoke(this as DandiRouteExecutor, 'checkAuthorizationConditions')
await requestInjector.invoke(this.pipeline, 'handleRequest')
})
performance.mark('DefaultRouteExecutor.execRoute', 'afterHandleRequest')
performance.mark('DandiRouteExecutor.execRoute', 'afterHandleRequest')

this.logger.debug(
`after handleRouteRequest ${route.controllerCtr.name}.${route.controllerMethod.toString()}:`,
Expand Down Expand Up @@ -87,7 +87,7 @@ export class DefaultRouteExecutor implements RouteExecutor {
route.path,
)

performance.mark('DefaultRouteExecutor.execRoute', 'end')
performance.mark('DandiRouteExecutor.execRoute', 'end')
this.logger.debug(performance.toString())
}
}
Expand Down
Expand Up @@ -5,7 +5,7 @@ import {
Authorized,
Controller,
Cors,
DecoratorRouteGenerator,
DandiRouteGenerator,
HttpDelete,
HttpGet,
HttpPost,
Expand Down Expand Up @@ -47,10 +47,10 @@ describe('DecoratorRouteGenerator', function() {
public testMethod(): void {}
}

const harness = stubHarness(DecoratorRouteGenerator)
const harness = stubHarness(DandiRouteGenerator)

beforeEach(async function() {
this.generator = await harness.inject(DecoratorRouteGenerator)
this.generator = await harness.inject(DandiRouteGenerator)
this.repository = Repository.for(Controller)

this.findRoute = (path, httpMethod): Route => {
Expand Down
Expand Up @@ -12,7 +12,7 @@ import { RouteTransformer } from './route-transformer'
import { RouteGeneratorError } from './route-generator.error'

@Injectable(RouteGenerator)
export class DecoratorRouteGenerator implements RouteGenerator {
export class DandiRouteGenerator implements RouteGenerator {
constructor(
@Inject(Logger) private logger: Logger,
@Inject(RouteTransformer) @Optional() private readonly routeTransformers?: RouteTransformer[],
Expand Down

0 comments on commit c3dd2f0

Please sign in to comment.