diff --git a/examples/hello-world/src/application.ts b/examples/hello-world/src/application.ts index bfa570c88383..fac54babe984 100644 --- a/examples/hello-world/src/application.ts +++ b/examples/hello-world/src/application.ts @@ -13,8 +13,8 @@ export class HelloWorldApplication extends RestApplication { // returns the same HTTP response: Hello World! // Learn more about the concept of Sequence in our docs: // http://loopback.io/doc/en/lb4/Sequence.html - this.handler((sequence, request, response) => { - sequence.send(response, 'Hello World!'); + this.handler((sequence, httpCtx) => { + sequence.send(httpCtx.response, 'Hello World!'); }); } diff --git a/examples/log-extension/src/providers/log-action.provider.ts b/examples/log-extension/src/providers/log-action.provider.ts index 9d477ba219a4..ac9b91a72c4d 100644 --- a/examples/log-extension/src/providers/log-action.provider.ts +++ b/examples/log-extension/src/providers/log-action.provider.ts @@ -5,8 +5,8 @@ import {inject, Provider, Constructor, Getter} from '@loopback/context'; import {CoreBindings} from '@loopback/core'; -import {OperationArgs, ParsedRequest} from '@loopback/rest'; -import {getLogMetadata} from '../decorators'; +import {OperationArgs, Request} from '@loopback/rest'; +import {getLogMetadata} from '../decorators/log.decorator'; import {EXAMPLE_LOG_BINDINGS, LOG_LEVEL} from '../keys'; import { LogFn, @@ -35,7 +35,7 @@ export class LogActionProvider implements Provider { value(): LogFn { const fn = (( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, @@ -52,7 +52,7 @@ export class LogActionProvider implements Provider { } private async action( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, diff --git a/examples/log-extension/src/types.ts b/examples/log-extension/src/types.ts index 8e060d95e391..4db8be3559ee 100644 --- a/examples/log-extension/src/types.ts +++ b/examples/log-extension/src/types.ts @@ -5,14 +5,14 @@ // Types and interfaces exposed by the extension go here -import {ParsedRequest, OperationArgs} from '@loopback/rest'; +import {Request, OperationArgs} from '@loopback/rest'; /** * A function to perform REST req/res logging action */ export interface LogFn { ( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, diff --git a/examples/log-extension/test/acceptance/log.extension.acceptance.ts b/examples/log-extension/test/acceptance/log.extension.acceptance.ts index 5cc752dc4337..4217e0bef371 100644 --- a/examples/log-extension/test/acceptance/log.extension.acceptance.ts +++ b/examples/log-extension/test/acceptance/log.extension.acceptance.ts @@ -12,8 +12,7 @@ import { InvokeMethod, Send, Reject, - ParsedRequest, - ServerResponse, + HttpContext, } from '@loopback/rest'; import {get, param} from '@loopback/openapi-v3'; import { @@ -73,7 +72,7 @@ describe('log extension acceptance test', () => { it('logs information at DEBUG or higher', async () => { setAppLogToDebug(); - const client: Client = createClientForHandler(app.requestHandler); + const client: Client = createClientForHandler(app.requestListener); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -99,7 +98,7 @@ describe('log extension acceptance test', () => { it('logs information at INFO or higher', async () => { setAppLogToInfo(); - const client: Client = createClientForHandler(app.requestHandler); + const client: Client = createClientForHandler(app.requestListener); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -125,7 +124,7 @@ describe('log extension acceptance test', () => { it('logs information at WARN or higher', async () => { setAppLogToWarn(); - const client: Client = createClientForHandler(app.requestHandler); + const client: Client = createClientForHandler(app.requestListener); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -151,7 +150,7 @@ describe('log extension acceptance test', () => { it('logs information at ERROR', async () => { setAppLogToError(); - const client: Client = createClientForHandler(app.requestHandler); + const client: Client = createClientForHandler(app.requestListener); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -177,7 +176,7 @@ describe('log extension acceptance test', () => { it('logs no information when logLevel is set to OFF', async () => { setAppLogToOff(); - const client: Client = createClientForHandler(app.requestHandler); + const client: Client = createClientForHandler(app.requestListener); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -214,7 +213,7 @@ describe('log extension acceptance test', () => { @inject(EXAMPLE_LOG_BINDINGS.LOG_ACTION) protected logger: LogFn, ) {} - async handle(req: ParsedRequest, res: ServerResponse) { + async handle({request: req, response: res}: HttpContext) { // tslint:disable-next-line:no-any let args: any = []; // tslint:disable-next-line:no-any diff --git a/examples/log-extension/test/unit/providers/log-action.provider.unit.ts b/examples/log-extension/test/unit/providers/log-action.provider.unit.ts index eb98ee81858c..8afc25ac617b 100644 --- a/examples/log-extension/test/unit/providers/log-action.provider.unit.ts +++ b/examples/log-extension/test/unit/providers/log-action.provider.unit.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {sinon} from '@loopback/testlab'; -import {ParsedRequest} from '@loopback/rest'; +import {Request} from '@loopback/rest'; import { LogActionProvider, LogFn, @@ -21,7 +21,7 @@ import {logToMemory} from '../../in-memory-logger'; describe('LogActionProvider with in-memory logger', () => { let spy: sinon.SinonSpy; let logger: LogFn; - const req = {url: '/test'}; + const req = {url: '/test'}; beforeEach(() => { spy = createLogSpy(); @@ -60,7 +60,7 @@ describe('LogActionProvider with in-memory logger', () => { describe('LogActionProvider with default logger', () => { let stub: sinon.SinonSpy; let logger: LogFn; - const req = {url: '/test'}; + const req = {url: '/test'}; beforeEach(() => { stub = createConsoleStub(); diff --git a/examples/todo/src/sequence.ts b/examples/todo/src/sequence.ts index 55f2eb029e03..84d9b09d24c1 100644 --- a/examples/todo/src/sequence.ts +++ b/examples/todo/src/sequence.ts @@ -7,14 +7,13 @@ import {Context, inject} from '@loopback/context'; import { FindRoute, InvokeMethod, - ParsedRequest, ParseParams, Reject, RestBindings, Send, SequenceHandler, + HttpContext, } from '@loopback/rest'; -import {ServerResponse} from 'http'; const SequenceActions = RestBindings.SequenceActions; @@ -28,14 +27,14 @@ export class MySequence implements SequenceHandler { @inject(SequenceActions.REJECT) public reject: Reject, ) {} - async handle(req: ParsedRequest, res: ServerResponse) { + async handle({request, response}: HttpContext) { try { - const route = this.findRoute(req); - const args = await this.parseParams(req, route); + const route = this.findRoute(request); + const args = await this.parseParams(request, route); const result = await this.invoke(route, args); - this.send(res, result); + this.send(response, result); } catch (err) { - this.reject(res, req, err); + this.reject(response, request, err); } } } diff --git a/examples/todo/test/acceptance/application.acceptance.ts b/examples/todo/test/acceptance/application.acceptance.ts index d463e313e6ee..85d2f498e11a 100644 --- a/examples/todo/test/acceptance/application.acceptance.ts +++ b/examples/todo/test/acceptance/application.acceptance.ts @@ -24,7 +24,7 @@ describe('Application', () => { before(givenARestServer); before(givenTodoRepository); before(() => { - client = createClientForHandler(server.requestHandler); + client = createClientForHandler(server.requestListener); }); after(async () => { await app.stop(); diff --git a/packages/authentication/src/providers/authentication.provider.ts b/packages/authentication/src/providers/authentication.provider.ts index bc671b5baecd..003b8cd362a6 100644 --- a/packages/authentication/src/providers/authentication.provider.ts +++ b/packages/authentication/src/providers/authentication.provider.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ParsedRequest} from '@loopback/rest'; +import {Request} from '@loopback/rest'; import {inject} from '@loopback/core'; import {Provider, Getter, Setter} from '@loopback/context'; import {Strategy} from 'passport'; @@ -14,14 +14,14 @@ import {AuthenticationBindings} from '../keys'; * Passport monkey-patches Node.js' IncomingMessage prototype * and adds extra methods like "login" and "isAuthenticated" */ -export type PassportRequest = ParsedRequest & Express.Request; +export type PassportRequest = Request & Express.Request; /** * interface definition of a function which accepts a request * and returns an authenticated user */ export interface AuthenticateFn { - (request: ParsedRequest): Promise; + (request: Request): Promise; } /** @@ -65,7 +65,7 @@ export class AuthenticateActionProvider implements Provider { * The implementation of authenticate() sequence action. * @param request Parsed Request */ - async action(request: ParsedRequest): Promise { + async action(request: Request): Promise { const strategy = await this.getStrategy(); if (!strategy) { // The invoked operation does not require authentication. diff --git a/packages/authentication/src/strategy-adapter.ts b/packages/authentication/src/strategy-adapter.ts index f07e4bb79021..cd514168314e 100644 --- a/packages/authentication/src/strategy-adapter.ts +++ b/packages/authentication/src/strategy-adapter.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {HttpErrors, ParsedRequest} from '@loopback/rest'; +import {HttpErrors, Request} from '@loopback/rest'; import {Strategy} from 'passport'; import {UserProfile} from './providers/authentication.provider'; @@ -19,7 +19,7 @@ export class ShimRequest implements Express.Request { path: string; method: string; - constructor(request: ParsedRequest) { + constructor(request: Request) { this.headers = request.headers; this.query = request.query; this.url = request.url; @@ -79,7 +79,7 @@ export class StrategyAdapter { * 3. authenticate using the strategy * @param req {http.ServerRequest} The incoming request. */ - authenticate(req: ParsedRequest) { + authenticate(req: Request) { const shimReq = new ShimRequest(req); return new Promise((resolve, reject) => { // create a prototype chain of an instance of a passport strategy diff --git a/packages/authentication/test/acceptance/basic-auth.acceptance.ts b/packages/authentication/test/acceptance/basic-auth.acceptance.ts index 46c0a5f0fda7..eb2598f8014c 100644 --- a/packages/authentication/test/acceptance/basic-auth.acceptance.ts +++ b/packages/authentication/test/acceptance/basic-auth.acceptance.ts @@ -6,8 +6,6 @@ import {Application} from '@loopback/core'; import { RestBindings, - ServerResponse, - ParsedRequest, ParseParams, FindRoute, InvokeMethod, @@ -16,6 +14,7 @@ import { SequenceHandler, RestServer, RestComponent, + HttpContext, } from '@loopback/rest'; import {api, get} from '@loopback/openapi-v3'; import {Client, createClientForHandler} from '@loopback/testlab'; @@ -137,7 +136,7 @@ describe('Basic Authentication', () => { protected authenticateRequest: AuthenticateFn, ) {} - async handle(req: ParsedRequest, res: ServerResponse) { + async handle({request: req, response: res}: HttpContext) { try { const route = this.findRoute(req); @@ -188,7 +187,7 @@ describe('Basic Authentication', () => { } function whenIMakeRequestTo(restServer: RestServer): Client { - return createClientForHandler(restServer.requestHandler); + return createClientForHandler(restServer.requestListener); } }); diff --git a/packages/authentication/test/unit/providers/authentication.provider.unit.ts b/packages/authentication/test/unit/providers/authentication.provider.unit.ts index 64901c1f9715..b8bf148c5b62 100644 --- a/packages/authentication/test/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/test/unit/providers/authentication.provider.unit.ts @@ -5,7 +5,7 @@ import {expect} from '@loopback/testlab'; import {Context, instantiateClass} from '@loopback/context'; -import {ParsedRequest} from '@loopback/rest'; +import {Request} from '@loopback/rest'; import { AuthenticateActionProvider, AuthenticateFn, @@ -42,14 +42,14 @@ describe('AuthenticateActionProvider', () => { const authenticate: AuthenticateFn = await Promise.resolve( provider.value(), ); - const request = {}; + const request = {}; const user = await authenticate(request); expect(user).to.be.equal(mockUser); }); it('updates current user', async () => { const authenticate = await Promise.resolve(provider.value()); - const request = {}; + const request = {}; await authenticate(request); expect(currentUser).to.equal(mockUser); }); @@ -61,7 +61,7 @@ describe('AuthenticateActionProvider', () => { context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticateActionProvider); - const request = {}; + const request = {}; const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); @@ -78,7 +78,7 @@ describe('AuthenticateActionProvider', () => { const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); - const request = {}; + const request = {}; let error; try { await authenticate(request); @@ -97,7 +97,7 @@ describe('AuthenticateActionProvider', () => { const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); - const request = {}; + const request = {}; request.headers = {testState: 'fail'}; let error; try { diff --git a/packages/authentication/test/unit/strategy-adapter.unit.ts b/packages/authentication/test/unit/strategy-adapter.unit.ts index 6cae799d1ace..796976204c7e 100644 --- a/packages/authentication/test/unit/strategy-adapter.unit.ts +++ b/packages/authentication/test/unit/strategy-adapter.unit.ts @@ -5,7 +5,7 @@ import {expect} from '@loopback/testlab'; import {StrategyAdapter, UserProfile} from '../..'; -import {ParsedRequest, HttpErrors} from '@loopback/rest'; +import {Request, HttpErrors} from '@loopback/rest'; import {MockStrategy} from './fixtures/mock-strategy'; import {AuthenticateOptions} from 'passport'; @@ -28,7 +28,7 @@ describe('Strategy Adapter', () => { } const strategy = new Strategy(); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; await adapter.authenticate(request); expect(calledFlag).to.be.true(); }); @@ -37,7 +37,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; const user: Object = await adapter.authenticate(request); expect(user).to.be.eql(mockUser); }); @@ -46,7 +46,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; request.headers = {testState: 'fail'}; let error; try { @@ -61,7 +61,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; request.headers = {testState: 'error'}; let error; try { diff --git a/packages/boot/test/acceptance/controller.booter.acceptance.ts b/packages/boot/test/acceptance/controller.booter.acceptance.ts index 439f72ed8482..0ece6629d958 100644 --- a/packages/boot/test/acceptance/controller.booter.acceptance.ts +++ b/packages/boot/test/acceptance/controller.booter.acceptance.ts @@ -23,7 +23,7 @@ describe('controller booter acceptance tests', () => { await app.start(); const server: RestServer = await app.getServer(RestServer); - const client: Client = createClientForHandler(server.requestHandler); + const client: Client = createClientForHandler(server.requestListener); // Default Controllers = /controllers with .controller.js ending (nested = true); await client.get('/one').expect(200, 'ControllerOne.one()'); diff --git a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs index 047d2f3af1ae..6336675b3062 100644 --- a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs +++ b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs @@ -1,4 +1,4 @@ -import {ParsedRequest, RestBindings} from '@loopback/rest'; +import {Request, RestBindings} from '@loopback/rest'; import {get} from '@loopback/openapi-v3'; import {inject} from '@loopback/context'; @@ -6,7 +6,7 @@ import {inject} from '@loopback/context'; * A simple controller to bounce back http requests */ export class PingController { - constructor(@inject(RestBindings.Http.REQUEST) private req: ParsedRequest) {} + constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {} // Map to `GET /ping` @get('/ping') diff --git a/packages/cli/generators/app/templates/src/sequence.ts.ejs b/packages/cli/generators/app/templates/src/sequence.ts.ejs index 962946f894ce..bdfa1e882e2e 100644 --- a/packages/cli/generators/app/templates/src/sequence.ts.ejs +++ b/packages/cli/generators/app/templates/src/sequence.ts.ejs @@ -2,14 +2,13 @@ import {Context, inject} from '@loopback/context'; import { FindRoute, InvokeMethod, - ParsedRequest, + HttpContext, ParseParams, Reject, RestBindings, Send, SequenceHandler, } from '@loopback/rest'; -import {ServerResponse} from 'http'; const SequenceActions = RestBindings.SequenceActions; @@ -23,14 +22,14 @@ export class MySequence implements SequenceHandler { @inject(SequenceActions.REJECT) public reject: Reject, ) {} - async handle(req: ParsedRequest, res: ServerResponse) { + async handle({request, response}: HttpContext) { try { - const route = this.findRoute(req); - const args = await this.parseParams(req, route); + const route = this.findRoute(request); + const args = await this.parseParams(request, route); const result = await this.invoke(route, args); - this.send(res, result); + this.send(response, result); } catch (err) { - this.reject(res, req, err); + this.reject(response, request, err); } } } diff --git a/packages/cli/generators/app/templates/test/ping.controller.acceptance.ts.ejs b/packages/cli/generators/app/templates/test/ping.controller.acceptance.ts.ejs index c55abfc63d4a..338f640b030c 100644 --- a/packages/cli/generators/app/templates/test/ping.controller.acceptance.ts.ejs +++ b/packages/cli/generators/app/templates/test/ping.controller.acceptance.ts.ejs @@ -17,7 +17,7 @@ describe('PingController', () => { }); before(() => { - client = createClientForHandler(server.requestHandler); + client = createClientForHandler(server.requestListener); }); after(async () => { diff --git a/packages/http-server-express/.npmrc b/packages/http-server-express/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/http-server-express/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/http-server-express/LICENSE b/packages/http-server-express/LICENSE new file mode 100644 index 000000000000..e40aabc17e84 --- /dev/null +++ b/packages/http-server-express/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. +Node module: @loopback/http-server +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/http-server-express/README.md b/packages/http-server-express/README.md new file mode 100644 index 000000000000..5e72cd4d8595 --- /dev/null +++ b/packages/http-server-express/README.md @@ -0,0 +1,20 @@ +# @loopback/http-server-express + +Express implementation of http server + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See [all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/http-server-express/docs.json b/packages/http-server-express/docs.json new file mode 100644 index 000000000000..10aca2fd3703 --- /dev/null +++ b/packages/http-server-express/docs.json @@ -0,0 +1,8 @@ +{ + "content": [ + "index.ts", + "src/http-server-express.ts", + "src/index.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/http-server-express/index.d.ts b/packages/http-server-express/index.d.ts new file mode 100644 index 000000000000..f9ff33f734e2 --- /dev/null +++ b/packages/http-server-express/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server-express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/http-server-express/index.js b/packages/http-server-express/index.js new file mode 100644 index 000000000000..8e2abfc30655 --- /dev/null +++ b/packages/http-server-express/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server-express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/http-server-express/index.ts b/packages/http-server-express/index.ts new file mode 100644 index 000000000000..cab326117c18 --- /dev/null +++ b/packages/http-server-express/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/http-server-express/package.json b/packages/http-server-express/package.json new file mode 100644 index 000000000000..4e5d51f038e5 --- /dev/null +++ b/packages/http-server-express/package.json @@ -0,0 +1,47 @@ +{ + "name": "@loopback/http-server-express", + "version": "0.2.0", + "description": "", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-http-server-express*.tgz dist package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build", + "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-http-server-express*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^0.2.1", + "@loopback/http-server": "^0.2.0", + "@types/express": "^4.11.1", + "debug": "^3.1.0", + "express": "^4.16.2" + }, + "devDependencies": { + "@loopback/build": "^0.2.0", + "@loopback/testlab": "^0.3.0", + "@types/debug": "^0.0.30" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/http-server-express/src/http-server-express.ts b/packages/http-server-express/src/http-server-express.ts new file mode 100644 index 000000000000..9d967d82b3a4 --- /dev/null +++ b/packages/http-server-express/src/http-server-express.ts @@ -0,0 +1,121 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as http from 'http'; +import * as https from 'https'; +import * as debugModule from 'debug'; +const debug = debugModule('loopback:http:server:express'); + +import { + BaseHttpContext, + BaseHttpHandler, + HttpFactory, + BaseHttpEndpoint, + HttpServerConfig, + DefaultHttpEndpoint, +} from '@loopback/http-server'; + +import * as express from 'express'; +import {Request, Response, Application as HttpApplication} from 'express'; + +export { + Request, + Response, + NextFunction, + Application as HttpApplication, +} from 'express'; + +export { + HttpServerConfig, + HttpRequestListener, + HttpServerLike, +} from '@loopback/http-server'; + +export type HttpContext = BaseHttpContext; +export type HttpHandler = BaseHttpHandler; +export type HttpEndpoint = BaseHttpEndpoint; + +function toMiddleware(handler: HttpHandler): express.RequestHandler { + return (request, response, next) => { + debug('Handling request: %s', request.originalUrl); + const httpCtx: HttpContext = { + req: request, + res: response, + request, + response, + next, + }; + handler(httpCtx) + .then(() => { + debug('Finishing request: %s', request.originalUrl); + next(); + }) + .catch(err => next(err)); + }; +} + +class ExpressHttpFactory + implements HttpFactory { + createEndpoint(config: HttpServerConfig, handler: HttpHandler) { + // Create an express representing the server endpoint + const app = express() as HttpApplication; + app.use(toMiddleware(handler)); + + let server: http.Server | https.Server; + if (config.protocol === 'https') { + server = https.createServer(config.httpsServerOptions || {}, app); + } else { + // default to http + server = http.createServer(app); + } + return new ExpressHttpEndpoint(config, server, app, app); + } + + createHttpContext( + req: http.IncomingMessage, + res: http.ServerResponse, + ): HttpContext { + // Run the express middleware to parse query parameters + // tslint:disable-next-line:no-any + expressQuery()(req, res, (err: any) => { + if (err) throw err; + }); + const request = Object.setPrototypeOf(req, expressRequest); + const response = Object.setPrototypeOf(res, expressResponse); + return { + req, + res, + request, + response, + }; + } + + createApp() { + return express() as HttpApplication; + } +} + +class ExpressHttpEndpoint extends DefaultHttpEndpoint< + Request, + Response, + HttpApplication +> { + use(handler: HttpHandler) { + const app = this.app; + app.use(toMiddleware(handler)); + } +} + +export const HTTP_FACTORY: HttpFactory< + Request, + Response, + HttpApplication +> = new ExpressHttpFactory(); + +const { + request: expressRequest, + response: expressResponse, + query: expressQuery, +} = require('express'); diff --git a/packages/http-server-express/src/index.ts b/packages/http-server-express/src/index.ts new file mode 100644 index 000000000000..a5d9e65a49b4 --- /dev/null +++ b/packages/http-server-express/src/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './http-server-express'; diff --git a/packages/http-server-express/test/integration/certificate.pem b/packages/http-server-express/test/integration/certificate.pem new file mode 100644 index 000000000000..8169c4cd878e --- /dev/null +++ b/packages/http-server-express/test/integration/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCdtKtIFRjO6jANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0Zvc3RlciBDaXR5MRQwEgYDVQQKDAts +b29wYmFjay5pbzAeFw0xODAzMDUwNzExNDhaFw0yODAxMTIwNzExNDhaMEYxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLRm9zdGVyIENpdHkxFDAS +BgNVBAoMC2xvb3BiYWNrLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZgqI7HUKmu1Is +rSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCMZMIsllEsCtWc +eAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XWDaNNCgzrwec+ +aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZcffWYO6edwA6b +POqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQuC+IUpVRc4Jn +TFRasRIPh9oPmAHpe+aeixItpwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBRFmy8 +A3Mfzx2IzvtlPKiEbAryhHxtiGLcN48M/1rRF4mV/iVWxLfckd8Zh91KpGqu9HBG +uHb4oqPw0KFkNnWOl8niE5/QCrqwjgTREQXhDc897Jm4VaWepS8zBK81VdijoSq8 +UowDnr5l5923ltBlfwtg4t5gwIySY/uoQFaQhuW4l6Rpa4lLv4/ardE2o4G3cADe +zANyO7ifT7VWCil4Xg4AVDa40jU/V60z0A8rySCYzhCfrRPG6sCV87cffLn6Yu4o +O/5AXIfS9XF51K5G22vYB5MPwGwm8wClND4AHH/jYJ6dAGNYtw06pHrcKisT5/K3 +2E+lHoiHZPUbDaa0 +-----END CERTIFICATE----- diff --git a/packages/http-server-express/test/integration/http-server-express.test.ts b/packages/http-server-express/test/integration/http-server-express.test.ts new file mode 100644 index 000000000000..02563062f2d6 --- /dev/null +++ b/packages/http-server-express/test/integration/http-server-express.test.ts @@ -0,0 +1,57 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {supertest} from '@loopback/testlab'; + +import * as fs from 'fs'; +import * as path from 'path'; + +import {HTTP_FACTORY, HttpServerConfig, HttpHandler, HttpEndpoint} from '../..'; + +describe('http-server-express (integration)', () => { + const factory = HTTP_FACTORY; + let endpoint: HttpEndpoint; + + afterEach(() => endpoint.stop()); + + it('supports http protocol', async () => { + endpoint = givenEndpoint({port: 0}, async httpCtx => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); + }); + + await endpoint.start(); + await supertest(endpoint.url) + .get('/') + .expect(200, 'Hello'); + }); + + it('supports https protocol', async () => { + const key = fs.readFileSync( + path.join(__dirname, '../../../test/integration/privatekey.pem'), + ); + const cert = fs.readFileSync( + path.join(__dirname, '../../../test/integration/certificate.pem'), + ); + endpoint = givenEndpoint( + {protocol: 'https', httpsServerOptions: {cert, key}, port: 0}, + async httpCtx => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); + }, + ); + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + await endpoint.start(); + await supertest(endpoint.url) + .get('/hello?msg=world') + .expect(200, 'Hello'); + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + }); + + function givenEndpoint(config: HttpServerConfig, handler: HttpHandler) { + return factory.createEndpoint(config || {}, handler); + } +}); diff --git a/packages/http-server-express/test/integration/privatekey.pem b/packages/http-server-express/test/integration/privatekey.pem new file mode 100644 index 000000000000..d7295e1fa8ac --- /dev/null +++ b/packages/http-server-express/test/integration/privatekey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZ +gqI7HUKmu1IsrSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCM +ZMIsllEsCtWceAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XW +DaNNCgzrwec+aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZc +ffWYO6edwA6bPOqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQ +uC+IUpVRc4JnTFRasRIPh9oPmAHpe+aeixItpwIDAQABAoIBABSC1sjbPnnswTkc +19buHVBug6fuKv/lUxwqcV2ELdmuuZcKM6tAynvSFDdpLjl1z4FsQXzdgcUBfQsI +yqMBGeRs580ZADCAXzGM1QthO5KSutBBS3+QNs9/0ToCcnIhqJbOgEYAdNNtVddP +1PKtxQA1bNkTn+tcsPrs8gwZd0XoC8imbJYw9R9nLehmFAM0T+onFn7P0mCh1w9g +2d6bh6nGRacJG7E6QL2KdOzn0Yv1Jg5Ducoru6Gf7QISIBeJ3orfzMJeHhh5vl5X +NLW4kcwX8l1+T1onGD59a2GbdsGmL6m0bfwHiClbkK8ztypioVCsZVaYtB9evCm4 +1eFOGsECgYEAza75ckg2ek8QhSenZ9HI5jrT+oFBghdQzaQU3e3DlKylonKb4Xal +XtSFOnRpA0lNA+J6cliO054rVQMQjFUhFmQWiD3S7Jnz5oiwG7eq7vVt6JauWKxG +GEfoKCH8lrhTP8KEb4bPAK6cRPrk40rRaazYHRPjNVO2/i8mLjbA5MkCgYEAxxS7 +RUpvR++dUgX2WiV5g+6eQxsjRUdWewxM3Xndfx+yPTmIBO/jOCm0dFLb1DMADegE +rab//w9s+RyL8ZyMOOWTLIFD/fgBEP3UcDcForRALpSRrIJz6tfG0/sV8lUBwpLs +ALTs5DeJnlv4GPXhoCGpQPP2hq17L0bANQvw5u8CgYAViGTq9u5lHZPgLMeU0dyT +ZcM9bXy7Ls+xx6S7myGnle99MzxofTBQ3jTYasl3o5vhdTtWbzj8pIlqy/hWiK7/ +FhlZyAcl5/QlxVeSf0bw31bTS7sS424vKo/+a5hy+vcULLwKpPVU3/LSMeX2eaW0 +x3iUirl1or78m1kG64qEKQKBgHIAXDEUq97cxxEGWwlKNminhzdUXgE5FbvG0mlt +dLpsUyweOtbg7BPoRe7q1/mO7vQHrk4muKe9lKCeiUDlbaLTTFELAP15PFsXj8Rm +rbJ7V9mUuEq6NVkBEVmoievIZAahDcZl1NXnO8ZUUiExBHAndn28dqquw0DSWhTG +bsA/AoGAGMxQpBGX2H1vLD532U6WzJfCnrks+tHKMGypspC1q6ChTU1Xo6TJt2/w +ilqnzoYaFsKVT/miD2R1gZnePbGrbYmZGLiEDKttMJKR0UrRX46t7ZIPf7n88ecp +1I4cpDyuCK1AAgG8ZYawMh3SKTkabZ7I1ekFa+e06yg8JYzYKMk= +-----END RSA PRIVATE KEY----- diff --git a/packages/http-server-express/tsconfig.build.json b/packages/http-server-express/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/http-server-express/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} diff --git a/packages/http-server-koa/.npmrc b/packages/http-server-koa/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/http-server-koa/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/http-server-koa/LICENSE b/packages/http-server-koa/LICENSE new file mode 100644 index 000000000000..cbc53c15a55d --- /dev/null +++ b/packages/http-server-koa/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. +Node module: @loopback/http-server +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, koa OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/http-server-koa/README.md b/packages/http-server-koa/README.md new file mode 100644 index 000000000000..a448234484aa --- /dev/null +++ b/packages/http-server-koa/README.md @@ -0,0 +1,20 @@ +# @loopback/http-server-koa + +koa implementation of http server + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See [all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/http-server-koa/docs.json b/packages/http-server-koa/docs.json new file mode 100644 index 000000000000..f5a97f53728f --- /dev/null +++ b/packages/http-server-koa/docs.json @@ -0,0 +1,8 @@ +{ + "content": [ + "index.ts", + "src/http-server-koa.ts", + "src/index.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/http-server-koa/index.d.ts b/packages/http-server-koa/index.d.ts new file mode 100644 index 000000000000..3192ace31738 --- /dev/null +++ b/packages/http-server-koa/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server-koa +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/http-server-koa/index.js b/packages/http-server-koa/index.js new file mode 100644 index 000000000000..91d670c73328 --- /dev/null +++ b/packages/http-server-koa/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server-koa +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/http-server-koa/index.ts b/packages/http-server-koa/index.ts new file mode 100644 index 000000000000..26e1ade5b0c2 --- /dev/null +++ b/packages/http-server-koa/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-koa +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/http-server-koa/package.json b/packages/http-server-koa/package.json new file mode 100644 index 000000000000..f2eb7f8978d9 --- /dev/null +++ b/packages/http-server-koa/package.json @@ -0,0 +1,47 @@ +{ + "name": "@loopback/http-server-koa", + "version": "0.2.0", + "description": "", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-http-server-koa*.tgz dist package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build", + "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-http-server-koa*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^0.2.1", + "@loopback/http-server": "^0.2.0", + "@types/koa": "^2.0.44", + "debug": "^3.1.0", + "koa": "^2.5.0" + }, + "devDependencies": { + "@loopback/build": "^0.2.0", + "@loopback/testlab": "^0.3.0", + "@types/debug": "^0.0.30" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/http-server-koa/src/http-server-koa.ts b/packages/http-server-koa/src/http-server-koa.ts new file mode 100644 index 000000000000..95ce95ca338c --- /dev/null +++ b/packages/http-server-koa/src/http-server-koa.ts @@ -0,0 +1,102 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-koa +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as http from 'http'; +import * as https from 'https'; +import * as debugModule from 'debug'; +const debug = debugModule('loopback:http:server:koa'); + +import { + BaseHttpContext, + BaseHttpHandler, + HttpFactory, + BaseHttpEndpoint, + HttpServerConfig, + DefaultHttpEndpoint, +} from '@loopback/http-server'; + +import * as Koa from 'koa'; + +import {Request, Response} from 'koa'; +export {Request, Response} from 'koa'; + +export { + HttpServerConfig, + HttpRequestListener, + HttpServerLike, +} from '@loopback/http-server'; + +export type HttpApplication = Koa; +export type HttpContext = BaseHttpContext; +export type HttpHandler = BaseHttpHandler; +export type HttpEndpoint = BaseHttpEndpoint; + +function toMiddleware(handler: HttpHandler): Koa.Middleware { + return async (koaCtx, next) => { + debug('Handling request: %s', koaCtx.request.originalUrl); + const httpCtx: HttpContext = { + req: koaCtx.req, + res: koaCtx.res, + request: koaCtx.request, + response: koaCtx.response, + next: next, + }; + await handler(httpCtx); + debug('Finishing request: %s', koaCtx.request.originalUrl); + await next(); + }; +} + +class koaHttpFactory + implements HttpFactory { + createEndpoint(config: HttpServerConfig, handler: HttpHandler) { + // Create an koa representing the server endpoint + const app = new Koa(); + app.use(toMiddleware(handler)); + + const requestHandler = app.callback(); + let server: http.Server | https.Server; + if (config.protocol === 'https') { + server = https.createServer( + config.httpsServerOptions || {}, + requestHandler, + ); + } else { + // default to http + server = http.createServer(requestHandler); + } + + return new KoaHttpEndpoint(config, server, requestHandler, app); + } + + createHttpContext( + req: http.IncomingMessage, + res: http.ServerResponse, + app: HttpApplication, + ): HttpContext { + return app.createContext(req, res); + } + + createApp() { + return new Koa(); + } +} + +class KoaHttpEndpoint extends DefaultHttpEndpoint< + Request, + Response, + HttpApplication +> { + use(handler: HttpHandler) { + const koa = this.app; + koa.use(toMiddleware(handler)); + } +} + +export const HTTP_FACTORY: HttpFactory< + Request, + Response, + HttpApplication +> = new koaHttpFactory(); diff --git a/packages/http-server-koa/src/index.ts b/packages/http-server-koa/src/index.ts new file mode 100644 index 000000000000..174c359c61d5 --- /dev/null +++ b/packages/http-server-koa/src/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server-koa +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './http-server-koa'; diff --git a/packages/http-server-koa/test/integration/certificate.pem b/packages/http-server-koa/test/integration/certificate.pem new file mode 100644 index 000000000000..8169c4cd878e --- /dev/null +++ b/packages/http-server-koa/test/integration/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCdtKtIFRjO6jANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0Zvc3RlciBDaXR5MRQwEgYDVQQKDAts +b29wYmFjay5pbzAeFw0xODAzMDUwNzExNDhaFw0yODAxMTIwNzExNDhaMEYxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLRm9zdGVyIENpdHkxFDAS +BgNVBAoMC2xvb3BiYWNrLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZgqI7HUKmu1Is +rSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCMZMIsllEsCtWc +eAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XWDaNNCgzrwec+ +aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZcffWYO6edwA6b +POqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQuC+IUpVRc4Jn +TFRasRIPh9oPmAHpe+aeixItpwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBRFmy8 +A3Mfzx2IzvtlPKiEbAryhHxtiGLcN48M/1rRF4mV/iVWxLfckd8Zh91KpGqu9HBG +uHb4oqPw0KFkNnWOl8niE5/QCrqwjgTREQXhDc897Jm4VaWepS8zBK81VdijoSq8 +UowDnr5l5923ltBlfwtg4t5gwIySY/uoQFaQhuW4l6Rpa4lLv4/ardE2o4G3cADe +zANyO7ifT7VWCil4Xg4AVDa40jU/V60z0A8rySCYzhCfrRPG6sCV87cffLn6Yu4o +O/5AXIfS9XF51K5G22vYB5MPwGwm8wClND4AHH/jYJ6dAGNYtw06pHrcKisT5/K3 +2E+lHoiHZPUbDaa0 +-----END CERTIFICATE----- diff --git a/packages/http-server-koa/test/integration/http-server-koa.test.ts b/packages/http-server-koa/test/integration/http-server-koa.test.ts new file mode 100644 index 000000000000..1db8649ee806 --- /dev/null +++ b/packages/http-server-koa/test/integration/http-server-koa.test.ts @@ -0,0 +1,57 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {supertest} from '@loopback/testlab'; + +import * as fs from 'fs'; +import * as path from 'path'; + +import {HTTP_FACTORY, HttpServerConfig, HttpHandler, HttpEndpoint} from '../..'; + +describe('http-server-koa (integration)', () => { + const factory = HTTP_FACTORY; + let endpoint: HttpEndpoint; + + afterEach(() => endpoint.stop()); + + it('supports http protocol', async () => { + endpoint = givenEndpoint({port: 0}, async httpCtx => { + httpCtx.response.body = 'Hello'; + }); + + await endpoint.start(); + + await supertest(endpoint.url) + .get('/') + .expect(200, 'Hello'); + }); + + it('supports https protocol', async () => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + const key = fs.readFileSync( + path.join(__dirname, '../../../test/integration/privatekey.pem'), + ); + const cert = fs.readFileSync( + path.join(__dirname, '../../../test/integration/certificate.pem'), + ); + endpoint = givenEndpoint( + {protocol: 'https', httpsServerOptions: {cert, key}, port: 0}, + async httpCtx => { + httpCtx.response.body = 'Hello'; + }, + ); + + await endpoint.start(); + + await supertest(endpoint.url) + .get('/') + .expect(200, 'Hello'); + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + }); + + function givenEndpoint(options: HttpServerConfig, handler: HttpHandler) { + return factory.createEndpoint(options || {}, handler); + } +}); diff --git a/packages/http-server-koa/test/integration/privatekey.pem b/packages/http-server-koa/test/integration/privatekey.pem new file mode 100644 index 000000000000..d7295e1fa8ac --- /dev/null +++ b/packages/http-server-koa/test/integration/privatekey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZ +gqI7HUKmu1IsrSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCM +ZMIsllEsCtWceAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XW +DaNNCgzrwec+aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZc +ffWYO6edwA6bPOqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQ +uC+IUpVRc4JnTFRasRIPh9oPmAHpe+aeixItpwIDAQABAoIBABSC1sjbPnnswTkc +19buHVBug6fuKv/lUxwqcV2ELdmuuZcKM6tAynvSFDdpLjl1z4FsQXzdgcUBfQsI +yqMBGeRs580ZADCAXzGM1QthO5KSutBBS3+QNs9/0ToCcnIhqJbOgEYAdNNtVddP +1PKtxQA1bNkTn+tcsPrs8gwZd0XoC8imbJYw9R9nLehmFAM0T+onFn7P0mCh1w9g +2d6bh6nGRacJG7E6QL2KdOzn0Yv1Jg5Ducoru6Gf7QISIBeJ3orfzMJeHhh5vl5X +NLW4kcwX8l1+T1onGD59a2GbdsGmL6m0bfwHiClbkK8ztypioVCsZVaYtB9evCm4 +1eFOGsECgYEAza75ckg2ek8QhSenZ9HI5jrT+oFBghdQzaQU3e3DlKylonKb4Xal +XtSFOnRpA0lNA+J6cliO054rVQMQjFUhFmQWiD3S7Jnz5oiwG7eq7vVt6JauWKxG +GEfoKCH8lrhTP8KEb4bPAK6cRPrk40rRaazYHRPjNVO2/i8mLjbA5MkCgYEAxxS7 +RUpvR++dUgX2WiV5g+6eQxsjRUdWewxM3Xndfx+yPTmIBO/jOCm0dFLb1DMADegE +rab//w9s+RyL8ZyMOOWTLIFD/fgBEP3UcDcForRALpSRrIJz6tfG0/sV8lUBwpLs +ALTs5DeJnlv4GPXhoCGpQPP2hq17L0bANQvw5u8CgYAViGTq9u5lHZPgLMeU0dyT +ZcM9bXy7Ls+xx6S7myGnle99MzxofTBQ3jTYasl3o5vhdTtWbzj8pIlqy/hWiK7/ +FhlZyAcl5/QlxVeSf0bw31bTS7sS424vKo/+a5hy+vcULLwKpPVU3/LSMeX2eaW0 +x3iUirl1or78m1kG64qEKQKBgHIAXDEUq97cxxEGWwlKNminhzdUXgE5FbvG0mlt +dLpsUyweOtbg7BPoRe7q1/mO7vQHrk4muKe9lKCeiUDlbaLTTFELAP15PFsXj8Rm +rbJ7V9mUuEq6NVkBEVmoievIZAahDcZl1NXnO8ZUUiExBHAndn28dqquw0DSWhTG +bsA/AoGAGMxQpBGX2H1vLD532U6WzJfCnrks+tHKMGypspC1q6ChTU1Xo6TJt2/w +ilqnzoYaFsKVT/miD2R1gZnePbGrbYmZGLiEDKttMJKR0UrRX46t7ZIPf7n88ecp +1I4cpDyuCK1AAgG8ZYawMh3SKTkabZ7I1ekFa+e06yg8JYzYKMk= +-----END RSA PRIVATE KEY----- diff --git a/packages/http-server-koa/tsconfig.build.json b/packages/http-server-koa/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/http-server-koa/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} diff --git a/packages/http-server/.npmrc b/packages/http-server/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/http-server/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/http-server/LICENSE b/packages/http-server/LICENSE new file mode 100644 index 000000000000..e40aabc17e84 --- /dev/null +++ b/packages/http-server/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. +Node module: @loopback/http-server +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/http-server/README.md b/packages/http-server/README.md new file mode 100644 index 000000000000..c431ddabef01 --- /dev/null +++ b/packages/http-server/README.md @@ -0,0 +1,62 @@ +# @loopback/http-server + +This module defines common interfaces/types to create endpoints for http +protocols. + +Interfaces for http server providers to extend or implement: + +- BaseHttpContext: wrapper for Node.js core http req/res and framework specific request/response +- BaseHandleHttp: function to handle http requests/responses +- HttpEndpointFactory: factory to create http endpoints + +Interfaces for `@loopback/rest` and other modules to consume: + +- Request: framework specific http request +- Response: framework specific http response +- HttpContext: http context with framework specific request/response +- HandleHttp: http handler with framework specific request/response + +- HttpServerConfig: configuration for an http/https server +- HttpEndpoint: server/url/... + +To implement the contract for `http-server` using a framework such as `express` +or `koa`, follow the steps below: + +1. Add a new package such as `@loopback/http-server-express`. + +2. Define a class to implement `HttpEndpointFactory` + +3. Export framework specific types: + +```ts +export type Request = ...; // The framework specific Request +export type Response = ...; // The framework specific Response +export type HttpContext = BaseHttpContext; +export type HandleHttp = BaseHandleHttp; +``` + +4. Export `ENDPOINT_FACTORY` as a singleton of `HttpEndpointFactory`. + +```ts +export const ENDPOINT_FACTORY: HttpEndpointFactory< + Request, + Response +> = new CoreHttpEndpointFactory(); +``` + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See [all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/http-server/docs.json b/packages/http-server/docs.json new file mode 100644 index 000000000000..563cee733bd8 --- /dev/null +++ b/packages/http-server/docs.json @@ -0,0 +1,9 @@ +{ + "content": [ + "index.ts", + "src/http-server-core.ts", + "src/types.ts", + "src/index.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/http-server/index.d.ts b/packages/http-server/index.d.ts new file mode 100644 index 000000000000..67040df46f62 --- /dev/null +++ b/packages/http-server/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/http-server/index.js b/packages/http-server/index.js new file mode 100644 index 000000000000..b21de86e1aea --- /dev/null +++ b/packages/http-server/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/http-server/index.ts b/packages/http-server/index.ts new file mode 100644 index 000000000000..f6b5d54546e2 --- /dev/null +++ b/packages/http-server/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/http-server/package.json b/packages/http-server/package.json new file mode 100644 index 000000000000..50aa6f444269 --- /dev/null +++ b/packages/http-server/package.json @@ -0,0 +1,44 @@ +{ + "name": "@loopback/http-server", + "version": "0.2.0", + "description": "", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-http-server*.tgz dist package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build", + "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-http-server*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^0.2.1", + "debug": "^3.1.0" + }, + "devDependencies": { + "@loopback/build": "^0.2.0", + "@loopback/testlab": "^0.3.0", + "@types/debug": "^0.0.30" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/http-server/src/common.ts b/packages/http-server/src/common.ts new file mode 100644 index 000000000000..2cc37f176532 --- /dev/null +++ b/packages/http-server/src/common.ts @@ -0,0 +1,212 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as http from 'http'; +import * as https from 'https'; + +import * as debugModule from 'debug'; +const debug = debugModule('loopback:http:server'); + +/** + * Plain HTTP request listener + */ +export type HttpRequestListener = ( + req: http.IncomingMessage, + res: http.ServerResponse, +) => void; + +/** + * An interface for objects that have a `requestListener` function to handle + * http requests/responses + */ +export interface HttpServerLike { + requestListener: HttpRequestListener; +} + +/** + * Options to configure the http server + */ +export type HttpServerConfig = { + /** + * Protocol, default to `http` + */ + protocol?: 'http' | 'https'; // Will be extended to `http2` in the future + /** + * Port number, default to `0` (ephemeral) + */ + port?: number; + /** + * Host names/addresses to listen on + */ + host?: string; + /** + * Options for https, such as `cert` and `key`. + */ + httpsServerOptions?: https.ServerOptions; +}; + +/** + * Http endpoint + */ +export interface BaseHttpEndpoint extends HttpServerLike { + server: http.Server | https.Server; + url: string; + /** + * Protocol, default to `http` + */ + protocol?: 'http' | 'https'; // Will be extended to `http2` in the future + /** + * Port number, default to `0` (ephemeral) + */ + port?: number; + /** + * Host names/addresses to listen on + */ + host?: string; + app?: APP; // Such as Express or Koa `app` + start(): Promise; + stop(): Promise; + use(handler: BaseHttpHandler): void; +} + +/** + * This interface wraps http request/response and other information. It's + * designed to be used by `http-server-*` modules to provide the concrete + * types for `REQ` and `RES`. + */ +export interface BaseHttpContext { + /** + * The Node.js core http request + */ + req: http.IncomingMessage; + /** + * The Node.js core http response + */ + res: http.ServerResponse; + /** + * Framework specific http request. For example `Express` has its own + * `Request` that extends from `http.IncomingMessage` + */ + request: REQ; + /** + * Framework specific http response. For example `Express` has its own + * `Response` that extends from `http.ServerResponse` + */ + response: RES; + /** + * Next handler + */ + // tslint:disable-next-line:no-any + next?: (() => Promise) | ((err: any) => void); +} + +/** + * Http request/response handler. It's designed to be used by `http-server-*` + * modules to provide the concrete types for `REQ` and `RES`. + */ +export type BaseHttpHandler = ( + httpCtx: BaseHttpContext, +) => Promise; + +/** + * Create an endpoint for the given REST server configuration + */ +export interface HttpFactory { + /** + * Create an http/https endpoint for the configuration and handler. Please + * note the endpoint has not started listening on the port yet. We need to + * call `endpoint.start()` to listen. + * + * @param config The configuration for the http server + * @param handler The http request/response handler + */ + createEndpoint( + config: HttpServerConfig, + handler: BaseHttpHandler, + ): BaseHttpEndpoint; + + /** + * Create a corresponding http context for the plain http request/response + * @param req + * @param res + */ + createHttpContext( + req: http.IncomingMessage, + res: http.ServerResponse, + app: APP, + ): BaseHttpContext; + + createApp(): APP; +} + +export class DefaultHttpEndpoint + implements BaseHttpEndpoint { + url: string; + /** + * Protocol, default to `http` + */ + protocol?: 'http' | 'https'; // Will be extended to `http2` in the future + /** + * Port number, default to `0` (ephemeral) + */ + port?: number; + /** + * Host names/addresses to listen on + */ + host?: string; + + constructor( + private config: HttpServerConfig, + public server: http.Server | https.Server, + public requestListener: HttpRequestListener, + // tslint:disable-next-line:no-any + public app?: any, // Such as Express or Koa `app` + ) { + this.host = config.host; + this.port = config.port; + this.protocol = config.protocol; + } + + use(httpHandler: BaseHttpHandler) { + throw new Error('Middleware is not supported'); + } + + start() { + this.server.listen(this.config.port, this.config.host); + + return new Promise((resolve, reject) => { + this.server.once('listening', () => { + const address = this.server.address(); + this.config.host = this.host = this.host || address.address; + this.config.port = this.port = address.port; + this.config.protocol = this.protocol = this.protocol || 'http'; + if (address.family === 'IPv6') { + this.host = `[${this.host}]`; + } + if (process.env.TRAVIS) { + // Travis CI seems to have trouble connecting to '[::]' or '[::1]' + // Set host to `127.0.0.1` + if (address.address === '::' || address.address === '0.0.0.0') { + this.host = '127.0.0.1'; + } + } + this.url = `${this.protocol}://${this.host}:${this.port}`; + debug('Server is ready at %s', this.url); + resolve(this.url); + }); + this.server.once('error', reject); + }); + } + + stop() { + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + this.server.close((err: any) => { + if (err) reject(err); + else resolve(); + }); + }); + } +} diff --git a/packages/http-server/src/http-server-core.ts b/packages/http-server/src/http-server-core.ts new file mode 100644 index 000000000000..b2ab099f0c80 --- /dev/null +++ b/packages/http-server/src/http-server-core.ts @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as http from 'http'; +import * as https from 'https'; +import { + BaseHttpContext, + BaseHttpHandler, + HttpFactory, + BaseHttpEndpoint, + HttpServerConfig, + HttpRequestListener, + DefaultHttpEndpoint, +} from './common'; +import * as debugModule from 'debug'; +const debug = debugModule('loopback:http:server:core'); + +/** + * Export specific types from this implementation + */ +export type Request = http.IncomingMessage; +export type Response = http.ServerResponse; +export type HttpApplication = undefined; +export type HttpContext = BaseHttpContext; +export type HttpHandler = BaseHttpHandler; +export type HttpEndpoint = BaseHttpEndpoint; + +class CoreHttpFactory + implements HttpFactory { + createEndpoint(config: HttpServerConfig, handler: HttpHandler): HttpEndpoint { + const requestListener: HttpRequestListener = (request, response) => + handler({req: request, res: response, request, response}); + let server: http.Server | https.Server; + if (config.protocol === 'https') { + debug('Creating https server: %s:%d', config.host || '', config.port); + server = https.createServer( + config.httpsServerOptions || {}, + requestListener, + ); + } else { + debug('Creating http server: %s:%d', config.host || '', config.port); + server = http.createServer(requestListener); + } + + return new DefaultHttpEndpoint( + config, + server, + requestListener, + ); + } + + createHttpContext( + req: http.IncomingMessage, + res: http.ServerResponse, + app: HttpApplication, + ): HttpContext { + return { + req, + res, + request: req, + response: res, + }; + } + + createApp() { + return undefined; + } +} + +/** + * A singleton instance of the core http endpoint factory + */ +export const HTTP_FACTORY: HttpFactory< + Request, + Response, + HttpApplication +> = new CoreHttpFactory(); diff --git a/packages/http-server/src/index.ts b/packages/http-server/src/index.ts new file mode 100644 index 000000000000..81c1a5b6f60a --- /dev/null +++ b/packages/http-server/src/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/http-server +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './common'; +export * from './http-server-core'; diff --git a/packages/http-server/test/integration/certificate.pem b/packages/http-server/test/integration/certificate.pem new file mode 100644 index 000000000000..8169c4cd878e --- /dev/null +++ b/packages/http-server/test/integration/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCdtKtIFRjO6jANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0Zvc3RlciBDaXR5MRQwEgYDVQQKDAts +b29wYmFjay5pbzAeFw0xODAzMDUwNzExNDhaFw0yODAxMTIwNzExNDhaMEYxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLRm9zdGVyIENpdHkxFDAS +BgNVBAoMC2xvb3BiYWNrLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZgqI7HUKmu1Is +rSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCMZMIsllEsCtWc +eAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XWDaNNCgzrwec+ +aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZcffWYO6edwA6b +POqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQuC+IUpVRc4Jn +TFRasRIPh9oPmAHpe+aeixItpwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBRFmy8 +A3Mfzx2IzvtlPKiEbAryhHxtiGLcN48M/1rRF4mV/iVWxLfckd8Zh91KpGqu9HBG +uHb4oqPw0KFkNnWOl8niE5/QCrqwjgTREQXhDc897Jm4VaWepS8zBK81VdijoSq8 +UowDnr5l5923ltBlfwtg4t5gwIySY/uoQFaQhuW4l6Rpa4lLv4/ardE2o4G3cADe +zANyO7ifT7VWCil4Xg4AVDa40jU/V60z0A8rySCYzhCfrRPG6sCV87cffLn6Yu4o +O/5AXIfS9XF51K5G22vYB5MPwGwm8wClND4AHH/jYJ6dAGNYtw06pHrcKisT5/K3 +2E+lHoiHZPUbDaa0 +-----END CERTIFICATE----- diff --git a/packages/http-server/test/integration/http-server-core.test.ts b/packages/http-server/test/integration/http-server-core.test.ts new file mode 100644 index 000000000000..ee9e6b833112 --- /dev/null +++ b/packages/http-server/test/integration/http-server-core.test.ts @@ -0,0 +1,57 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {supertest} from '@loopback/testlab'; + +import * as fs from 'fs'; +import * as path from 'path'; + +import {HTTP_FACTORY, HttpServerConfig, HttpHandler, HttpEndpoint} from '../..'; + +describe('http-server-core (integration)', () => { + const factory = HTTP_FACTORY; + let endpoint: HttpEndpoint; + + afterEach(() => endpoint.stop()); + + it('supports http protocol', async () => { + endpoint = givenEndpoint({port: 0}, async httpCtx => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); + }); + + await endpoint.start(); + await supertest(endpoint.url) + .get('/') + .expect(200, 'Hello'); + }); + + it('supports https protocol', async () => { + const key = fs.readFileSync( + path.join(__dirname, '../../../test/integration/privatekey.pem'), + ); + const cert = fs.readFileSync( + path.join(__dirname, '../../../test/integration/certificate.pem'), + ); + endpoint = givenEndpoint( + {protocol: 'https', httpsServerOptions: {cert, key}, port: 0}, + async httpCtx => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); + }, + ); + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + await endpoint.start(); + await supertest(endpoint.url) + .get('/') + .expect(200, 'Hello'); + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + }); + + function givenEndpoint(config: HttpServerConfig, handleHttp: HttpHandler) { + return factory.createEndpoint(config || {}, handleHttp); + } +}); diff --git a/packages/http-server/test/integration/privatekey.pem b/packages/http-server/test/integration/privatekey.pem new file mode 100644 index 000000000000..d7295e1fa8ac --- /dev/null +++ b/packages/http-server/test/integration/privatekey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZ +gqI7HUKmu1IsrSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCM +ZMIsllEsCtWceAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XW +DaNNCgzrwec+aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZc +ffWYO6edwA6bPOqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQ +uC+IUpVRc4JnTFRasRIPh9oPmAHpe+aeixItpwIDAQABAoIBABSC1sjbPnnswTkc +19buHVBug6fuKv/lUxwqcV2ELdmuuZcKM6tAynvSFDdpLjl1z4FsQXzdgcUBfQsI +yqMBGeRs580ZADCAXzGM1QthO5KSutBBS3+QNs9/0ToCcnIhqJbOgEYAdNNtVddP +1PKtxQA1bNkTn+tcsPrs8gwZd0XoC8imbJYw9R9nLehmFAM0T+onFn7P0mCh1w9g +2d6bh6nGRacJG7E6QL2KdOzn0Yv1Jg5Ducoru6Gf7QISIBeJ3orfzMJeHhh5vl5X +NLW4kcwX8l1+T1onGD59a2GbdsGmL6m0bfwHiClbkK8ztypioVCsZVaYtB9evCm4 +1eFOGsECgYEAza75ckg2ek8QhSenZ9HI5jrT+oFBghdQzaQU3e3DlKylonKb4Xal +XtSFOnRpA0lNA+J6cliO054rVQMQjFUhFmQWiD3S7Jnz5oiwG7eq7vVt6JauWKxG +GEfoKCH8lrhTP8KEb4bPAK6cRPrk40rRaazYHRPjNVO2/i8mLjbA5MkCgYEAxxS7 +RUpvR++dUgX2WiV5g+6eQxsjRUdWewxM3Xndfx+yPTmIBO/jOCm0dFLb1DMADegE +rab//w9s+RyL8ZyMOOWTLIFD/fgBEP3UcDcForRALpSRrIJz6tfG0/sV8lUBwpLs +ALTs5DeJnlv4GPXhoCGpQPP2hq17L0bANQvw5u8CgYAViGTq9u5lHZPgLMeU0dyT +ZcM9bXy7Ls+xx6S7myGnle99MzxofTBQ3jTYasl3o5vhdTtWbzj8pIlqy/hWiK7/ +FhlZyAcl5/QlxVeSf0bw31bTS7sS424vKo/+a5hy+vcULLwKpPVU3/LSMeX2eaW0 +x3iUirl1or78m1kG64qEKQKBgHIAXDEUq97cxxEGWwlKNminhzdUXgE5FbvG0mlt +dLpsUyweOtbg7BPoRe7q1/mO7vQHrk4muKe9lKCeiUDlbaLTTFELAP15PFsXj8Rm +rbJ7V9mUuEq6NVkBEVmoievIZAahDcZl1NXnO8ZUUiExBHAndn28dqquw0DSWhTG +bsA/AoGAGMxQpBGX2H1vLD532U6WzJfCnrks+tHKMGypspC1q6ChTU1Xo6TJt2/w +ilqnzoYaFsKVT/miD2R1gZnePbGrbYmZGLiEDKttMJKR0UrRX46t7ZIPf7n88ecp +1I4cpDyuCK1AAgG8ZYawMh3SKTkabZ7I1ekFa+e06yg8JYzYKMk= +-----END RSA PRIVATE KEY----- diff --git a/packages/http-server/tsconfig.build.json b/packages/http-server/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/http-server/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} diff --git a/packages/rest/package.json b/packages/rest/package.json index 9b7dc877a4c7..66acf212ceb8 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -26,6 +26,8 @@ "@loopback/openapi-v3": "^0.5.3", "@loopback/openapi-v3-types": "^0.3.3", "@types/cors": "^2.8.3", + "@types/express": "^4.11.1", + "@loopback/http-server-express": "^0.2.0", "@types/http-errors": "^1.6.1", "body": "^5.1.0", "cors": "^2.8.4", @@ -43,6 +45,7 @@ "@types/debug": "0.0.30", "@types/js-yaml": "^3.11.1", "@types/lodash": "^4.14.106", + "@types/cors": "^2.8.3", "@types/node": "^8.10.4" }, "files": [ diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index 7538a3451521..e1df1caec9bf 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -5,34 +5,25 @@ import {Context} from '@loopback/context'; import {PathsObject, SchemasObject} from '@loopback/openapi-v3-types'; -import {ServerRequest, ServerResponse} from 'http'; import {ControllerSpec} from '@loopback/openapi-v3'; import {SequenceHandler} from './sequence'; import { RoutingTable, - parseRequestUrl, ResolvedRoute, RouteEntry, ControllerClass, ControllerFactory, } from './router/routing-table'; -import {ParsedRequest} from './internal-types'; +import {Request, HttpContext} from './internal-types'; import {RestBindings} from './keys'; -export class HttpHandler { +export class RestHttpHandler { protected _routes: RoutingTable = new RoutingTable(); protected _apiDefinitions: SchemasObject; - public handleRequest: ( - request: ServerRequest, - response: ServerResponse, - ) => Promise; - - constructor(protected _rootContext: Context) { - this.handleRequest = (req, res) => this._handleRequest(req, res); - } + constructor(protected _rootContext: Context) {} registerController( spec: ControllerSpec, @@ -58,30 +49,23 @@ export class HttpHandler { return this._routes.describeApiPaths(); } - findRoute(request: ParsedRequest): ResolvedRoute { + findRoute(request: Request): ResolvedRoute { return this._routes.find(request); } - protected async _handleRequest( - request: ServerRequest, - response: ServerResponse, - ): Promise { - const parsedRequest: ParsedRequest = parseRequestUrl(request); - const requestContext = this._createRequestContext(parsedRequest, response); + async handleRequest(httpCtx: HttpContext): Promise { + const requestContext = this._createRequestContext(httpCtx); const sequence = await requestContext.get( RestBindings.SEQUENCE, ); - await sequence.handle(parsedRequest, response); + await sequence.handle(httpCtx); } - protected _createRequestContext( - req: ParsedRequest, - res: ServerResponse, - ): Context { + protected _createRequestContext(httpCtx: HttpContext): Context { const requestContext = new Context(this._rootContext); - requestContext.bind(RestBindings.Http.REQUEST).to(req); - requestContext.bind(RestBindings.Http.RESPONSE).to(res); + requestContext.bind(RestBindings.Http.REQUEST).to(httpCtx.request); + requestContext.bind(RestBindings.Http.RESPONSE).to(httpCtx.response); requestContext.bind(RestBindings.Http.CONTEXT).to(requestContext); return requestContext; } diff --git a/packages/rest/src/http-server.ts b/packages/rest/src/http-server.ts new file mode 100644 index 000000000000..f5cc4eedf35f --- /dev/null +++ b/packages/rest/src/http-server.ts @@ -0,0 +1,18 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export { + HTTP_FACTORY, + HttpRequestListener, + HttpServerLike, + Request, + Response, + HttpContext, + HttpEndpoint, + HttpServerConfig, + HttpHandler, + NextFunction, + HttpApplication, +} from '@loopback/http-server-express'; diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 450481d764fd..90223fe1203f 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -4,7 +4,6 @@ // License text available at https://opensource.org/licenses/MIT // external dependencies -export {ServerRequest, ServerResponse} from 'http'; export { RouteEntry, @@ -13,7 +12,6 @@ export { ControllerRoute, ResolvedRoute, createResolvedRoute, - parseRequestUrl, ControllerClass, ControllerInstance, ControllerFactory, @@ -34,6 +32,7 @@ export {writeResultToResponse} from './writer'; // http errors export {HttpErrors}; +export * from './http-server'; export * from './http-handler'; export * from './internal-types'; export * from './keys'; diff --git a/packages/rest/src/internal-types.ts b/packages/rest/src/internal-types.ts index 74e9694e9c36..23ccd640ef6a 100644 --- a/packages/rest/src/internal-types.ts +++ b/packages/rest/src/internal-types.ts @@ -4,31 +4,22 @@ // License text available at https://opensource.org/licenses/MIT import {Binding, BoundValue} from '@loopback/context'; -import {ServerRequest, ServerResponse} from 'http'; import {ResolvedRoute, RouteEntry} from './router/routing-table'; -export interface ParsedRequest extends ServerRequest { - // see http://expressjs.com/en/4x/api.html#req.path - path: string; - // see http://expressjs.com/en/4x/api.html#req.query - query: {[key: string]: string | string[]}; - // see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15808 - url: string; - pathname: string; - method: string; -} +import {Request, Response} from './http-server'; +export {Request, Response, HttpContext} from './http-server'; /** * Find a route matching the incoming request. * Throw an error when no route was found. */ -export type FindRoute = (request: ParsedRequest) => ResolvedRoute; +export type FindRoute = (request: Request) => ResolvedRoute; /** * */ export type ParseParams = ( - request: ParsedRequest, + request: Request, route: ResolvedRoute, ) => Promise; @@ -52,7 +43,7 @@ export type InvokeMethod = ( * @param response The response the response to send to. * @param result The operation result to send. */ -export type Send = (response: ServerResponse, result: OperationRetval) => void; +export type Send = (response: Response, result: OperationRetval) => void; /** * Reject the request with an error. @@ -61,11 +52,7 @@ export type Send = (response: ServerResponse, result: OperationRetval) => void; * @param request The request that triggered the error. * @param err The error. */ -export type Reject = ( - response: ServerResponse, - request: ServerRequest, - err: Error, -) => void; +export type Reject = (response: Response, request: Request, err: Error) => void; /** * Log information about a failed request. @@ -77,7 +64,7 @@ export type Reject = ( export type LogError = ( err: Error, statusCode: number, - request: ServerRequest, + request: Request, ) => void; // tslint:disable:no-any diff --git a/packages/rest/src/keys.ts b/packages/rest/src/keys.ts index 6de0b1464614..24bd5e71a75e 100644 --- a/packages/rest/src/keys.ts +++ b/packages/rest/src/keys.ts @@ -3,12 +3,10 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse} from 'http'; import {CoreBindings} from '@loopback/core'; import {BindingKey, Context} from '@loopback/context'; import {OpenApiSpec} from '@loopback/openapi-v3-types'; - -import {HttpHandler} from './http-handler'; +import {RestHttpHandler} from './http-handler'; import {SequenceHandler} from './sequence'; import { BindElement, @@ -16,7 +14,8 @@ import { GetFromContext, InvokeMethod, LogError, - ParsedRequest, + Request, + Response, ParseParams, Reject, Send, @@ -30,8 +29,10 @@ export namespace RestBindings { // RestServer-specific bindings export const CONFIG = CoreBindings.APPLICATION_CONFIG.deepProperty('rest'); export const HOST = BindingKey.create('rest.host'); - export const PORT = BindingKey.create('rest.port'); - export const HANDLER = BindingKey.create('rest.handler'); + export const PORT = BindingKey.create('rest.port'); + export const HANDLER = BindingKey.create('rest.handler'); + export const TRANSPORT = 'rest.transport'; + export const URL = BindingKey.create('rest.url'); export const API_SPEC = BindingKey.create('rest.apiSpec'); export const SEQUENCE = BindingKey.create('rest.sequence'); @@ -63,12 +64,8 @@ export namespace RestBindings { // request-specific bindings export namespace Http { - export const REQUEST = BindingKey.create( - 'rest.http.request', - ); - export const RESPONSE = BindingKey.create( - 'rest.http.response', - ); + export const REQUEST = BindingKey.create('rest.http.request'); + export const RESPONSE = BindingKey.create('rest.http.response'); export const CONTEXT = BindingKey.create( 'rest.http.request.context', ); diff --git a/packages/rest/src/parser.ts b/packages/rest/src/parser.ts index 95d5e0b8f21f..c7c0ce267c75 100644 --- a/packages/rest/src/parser.ts +++ b/packages/rest/src/parser.ts @@ -3,7 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; import { OperationObject, @@ -12,18 +11,14 @@ import { } from '@loopback/openapi-v3-types'; import {REQUEST_BODY_INDEX} from '@loopback/openapi-v3'; import {promisify} from 'util'; -import { - OperationArgs, - ParsedRequest, - PathParameterValues, -} from './internal-types'; +import {OperationArgs, Request, PathParameterValues} from './internal-types'; import {ResolvedRoute} from './router/routing-table'; type HttpError = HttpErrors.HttpError; // tslint:disable-next-line:no-any type MaybeBody = any | undefined; -const parseJsonBody: (req: ServerRequest) => Promise = promisify( +const parseJsonBody: (req: Request) => Promise = promisify( require('body/json'), ); @@ -31,7 +26,7 @@ const parseJsonBody: (req: ServerRequest) => Promise = promisify( * Get the content-type header value from the request * @param req Http request */ -function getContentType(req: ServerRequest): string | undefined { +function getContentType(req: Request): string | undefined { const val = req.headers['content-type']; if (typeof val === 'string') { return val; @@ -51,7 +46,7 @@ function getContentType(req: ServerRequest): string | undefined { * @param pathParams Path parameters in incoming HTTP request */ export async function parseOperationArgs( - request: ParsedRequest, + request: Request, route: ResolvedRoute, ): Promise { const operationSpec = route.spec; @@ -62,7 +57,7 @@ export async function parseOperationArgs( function loadRequestBodyIfNeeded( operationSpec: OperationObject, - request: ServerRequest, + request: Request, ): Promise { if (!operationSpec.requestBody) return Promise.resolve(); @@ -82,7 +77,7 @@ function loadRequestBodyIfNeeded( function buildOperationArguments( operationSpec: OperationObject, - request: ParsedRequest, + request: Request, pathParams: PathParameterValues, body?: MaybeBody, ): OperationArgs { diff --git a/packages/rest/src/providers/find-route.provider.ts b/packages/rest/src/providers/find-route.provider.ts index 0bfc5cb65837..73166bee7032 100644 --- a/packages/rest/src/providers/find-route.provider.ts +++ b/packages/rest/src/providers/find-route.provider.ts @@ -5,22 +5,22 @@ import {Context, inject, Provider} from '@loopback/context'; import {FindRoute} from '../internal-types'; -import {HttpHandler} from '../http-handler'; +import {RestHttpHandler} from '../http-handler'; import {RestBindings} from '../keys'; -import {ParsedRequest} from '../internal-types'; +import {Request} from '../internal-types'; import {ResolvedRoute} from '../router/routing-table'; export class FindRouteProvider implements Provider { constructor( @inject(RestBindings.Http.CONTEXT) protected context: Context, - @inject(RestBindings.HANDLER) protected handler: HttpHandler, + @inject(RestBindings.HANDLER) protected handler: RestHttpHandler, ) {} value(): FindRoute { return request => this.action(request); } - action(request: ParsedRequest): ResolvedRoute { + action(request: Request): ResolvedRoute { const found = this.handler.findRoute(request); found.updateBindings(this.context); return found; diff --git a/packages/rest/src/providers/log-error.provider.ts b/packages/rest/src/providers/log-error.provider.ts index b2aebfb19595..fe66d1c82da5 100644 --- a/packages/rest/src/providers/log-error.provider.ts +++ b/packages/rest/src/providers/log-error.provider.ts @@ -4,15 +4,14 @@ // License text available at https://opensource.org/licenses/MIT import {Provider} from '@loopback/context'; -import {ServerRequest} from 'http'; -import {LogError} from '../internal-types'; +import {LogError, Request} from '../internal-types'; export class LogErrorProvider implements Provider { value(): LogError { return (err, statusCode, req) => this.action(err, statusCode, req); } - action(err: Error, statusCode: number, req: ServerRequest) { + action(err: Error, statusCode: number, req: Request) { if (statusCode < 500) { return; } diff --git a/packages/rest/src/providers/reject.provider.ts b/packages/rest/src/providers/reject.provider.ts index 6aee9ac512e2..8e6127d72b6c 100644 --- a/packages/rest/src/providers/reject.provider.ts +++ b/packages/rest/src/providers/reject.provider.ts @@ -3,9 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {LogError, Reject} from '../internal-types'; +import {LogError, Reject, Request, Response} from '../internal-types'; import {inject, Provider} from '@loopback/context'; -import {ServerResponse, ServerRequest} from 'http'; import {HttpError} from 'http-errors'; import {writeErrorToResponse} from '../writer'; import {RestBindings} from '../keys'; @@ -20,7 +19,7 @@ export class RejectProvider implements Provider { return (response, request, error) => this.action(response, request, error); } - action(response: ServerResponse, request: ServerRequest, error: Error) { + action(response: Response, request: Request, error: Error) { const err = error; const statusCode = err.statusCode || err.status || 500; writeErrorToResponse(response, err); diff --git a/packages/rest/src/rest.application.ts b/packages/rest/src/rest.application.ts index 93c23ab87cc5..8d9d80b5f0e1 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -9,13 +9,14 @@ import {SequenceHandler, SequenceFunction} from './sequence'; import {Binding, Constructor} from '@loopback/context'; import {format} from 'util'; import {RestBindings} from './keys'; -import {RestServer, HttpRequestListener, HttpServerLike} from './rest.server'; +import {RestServer} from './rest.server'; import { RouteEntry, ControllerClass, ControllerFactory, } from './router/routing-table'; import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types'; +import {HttpRequestListener, HttpServerLike} from './http-server'; export const ERR_NO_MULTI_SERVER = format( 'RestApplication does not support multiple servers!', @@ -59,8 +60,8 @@ export class RestApplication extends Application implements HttpServerLike { * @param req The request. * @param res The response. */ - get requestHandler(): HttpRequestListener { - return this.restServer.requestHandler; + get requestListener(): HttpRequestListener { + return this.restServer.requestListener; } constructor(config?: ApplicationConfig) { diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 3652f7947b68..1cd44be72f58 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -15,14 +15,14 @@ import { ControllerInstance, createControllerFactoryForBinding, } from './router/routing-table'; -import {ParsedRequest} from './internal-types'; import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3-types'; -import {ServerRequest, ServerResponse, createServer} from 'http'; -import * as Http from 'http'; import * as cors from 'cors'; +// tslint:disable-next-line:no-unused-variable +import {IncomingMessage, ServerResponse} from 'http'; +import {ServerOptions} from 'https'; import {Application, CoreBindings, Server} from '@loopback/core'; import {getControllerSpec} from '@loopback/openapi-v3'; -import {HttpHandler} from './http-handler'; +import {RestHttpHandler} from './http-handler'; import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; import { FindRoute, @@ -32,15 +32,15 @@ import { ParseParams, } from './internal-types'; import {RestBindings} from './keys'; - -export type HttpRequestListener = ( - req: ServerRequest, - res: ServerResponse, -) => void; - -export interface HttpServerLike { - requestHandler: HttpRequestListener; -} +import { + HTTP_FACTORY, + Request, + Response, + HttpContext, + HttpHandler, + HttpServerLike, + HttpEndpoint, +} from './http-server'; const SequenceActions = RestBindings.SequenceActions; @@ -113,17 +113,16 @@ export class RestServer extends Context implements Server, HttpServerLike { * httpServer.listen(3000); * ``` * - * @param req The request. - * @param res The response. + * @param httpCtx HTTP context */ - public requestHandler: HttpRequestListener; - - protected _httpHandler: HttpHandler; - protected get httpHandler(): HttpHandler { + public httpHandler: HttpHandler; + public readonly options: RestServerConfig; + public endpoint: HttpEndpoint; + protected _restHttpHandler: RestHttpHandler; + protected get restHttpHandler(): RestHttpHandler { this._setupHandlerIfNeeded(); - return this._httpHandler; + return this._restHttpHandler; } - protected _httpServer: Http.Server; /** * @memberof RestServer @@ -142,6 +141,7 @@ export class RestServer extends Context implements Server, HttpServerLike { super(app); options = options || {}; + this.options = options; // Can't check falsiness, 0 is a valid port. if (options.port == null) { @@ -158,24 +158,23 @@ export class RestServer extends Context implements Server, HttpServerLike { this.sequence(options.sequence); } - this.requestHandler = (req: ServerRequest, res: ServerResponse) => { + this.httpHandler = async (httpCtx: HttpContext) => { try { - this._handleHttpRequest(req, res, options!).catch(err => - this._onUnhandledError(req, res, err), - ); + await this._handleHttpRequest(httpCtx, options!); } catch (err) { - this._onUnhandledError(req, res, err); + this._onUnhandledError(httpCtx, err); } }; - this.bind(RestBindings.HANDLER).toDynamicValue(() => this.httpHandler); + this.bind(RestBindings.HANDLER).toDynamicValue(() => this.restHttpHandler); } protected _handleHttpRequest( - request: ServerRequest, - response: ServerResponse, + httpCtx: HttpContext, options: RestServerConfig, ) { + const request = httpCtx.request; + const response = httpCtx.response; // allow CORS support for all endpoints so that users // can test with online SwaggerUI instance @@ -217,9 +216,9 @@ export class RestServer extends Context implements Server, HttpServerLike { request.url && request.url === '/swagger-ui' ) { - return this._redirectToSwaggerUI(request, response, options); + return this._redirectToSwaggerUI(httpCtx, options); } - return this.httpHandler.handleRequest(request, response); + return this.restHttpHandler.handleRequest(httpCtx); } protected _setupHandlerIfNeeded() { @@ -227,9 +226,9 @@ export class RestServer extends Context implements Server, HttpServerLike { // after the app started. The idea is to rebuild the HttpHandler // instance whenever a controller was added/deleted. // See https://github.com/strongloop/loopback-next/issues/433 - if (this._httpHandler) return; + if (this._restHttpHandler) return; - this._httpHandler = new HttpHandler(this); + this._restHttpHandler = new RestHttpHandler(this); for (const b of this.find('controllers.*')) { const controllerName = b.key.replace(/^controllers\./, ''); const ctor = b.valueConstructor; @@ -244,16 +243,22 @@ export class RestServer extends Context implements Server, HttpServerLike { continue; } if (apiSpec.components && apiSpec.components.schemas) { - this._httpHandler.registerApiDefinitions(apiSpec.components.schemas); + this._restHttpHandler.registerApiDefinitions( + apiSpec.components.schemas, + ); } const controllerFactory = createControllerFactoryForBinding(b.key); - this._httpHandler.registerController(apiSpec, ctor, controllerFactory); + this._restHttpHandler.registerController( + apiSpec, + ctor, + controllerFactory, + ); } for (const b of this.find('routes.*')) { // TODO(bajtos) should we support routes defined asynchronously? const route = this.getSync(b.key); - this._httpHandler.registerRoute(route); + this._restHttpHandler.registerRoute(route); } // TODO(bajtos) should we support API spec defined asynchronously? @@ -276,7 +281,7 @@ export class RestServer extends Context implements Server, HttpServerLike { delete spec['x-operation']; const route = new Route(verb, path, spec, handler); - this._httpHandler.registerRoute(route); + this._restHttpHandler.registerRoute(route); return; } @@ -304,7 +309,7 @@ export class RestServer extends Context implements Server, HttpServerLike { ctor, controllerFactory, ); - this._httpHandler.registerRoute(route); + this._restHttpHandler.registerRoute(route); return; } @@ -314,8 +319,8 @@ export class RestServer extends Context implements Server, HttpServerLike { } private async _serveOpenApiSpec( - request: ServerRequest, - response: ServerResponse, + request: Request, + response: Response, options?: OpenApiSpecOptions, ) { options = options || {version: '3.0.0', format: 'json'}; @@ -332,8 +337,7 @@ export class RestServer extends Context implements Server, HttpServerLike { } private async _redirectToSwaggerUI( - request: ServerRequest, - response: ServerResponse, + {request, response}: HttpContext, options: RestServerConfig, ) { response.statusCode = 308; @@ -492,11 +496,11 @@ export class RestServer extends Context implements Server, HttpServerLike { */ getApiSpec(): OpenApiSpec { const spec = this.getSync(RestBindings.API_SPEC); - const defs = this.httpHandler.getApiDefinitions(); + const defs = this.restHttpHandler.getApiDefinitions(); // Apply deep clone to prevent getApiSpec() callers from // accidentally modifying our internal routing data - spec.paths = cloneDeep(this.httpHandler.describeApiPaths()); + spec.paths = cloneDeep(this.restHttpHandler.describeApiPaths()); if (defs) { spec.components = spec.components || {}; spec.components.schemas = cloneDeep(defs); @@ -513,7 +517,7 @@ export class RestServer extends Context implements Server, HttpServerLike { * @inject('send) public send: Send)) { * } * - * public async handle(request: ParsedRequest, response: ServerResponse) { + * public async handle(request: Request, response: Response) { * send(response, 'hello world'); * } * } @@ -552,11 +556,8 @@ export class RestServer extends Context implements Server, HttpServerLike { super(ctx, findRoute, parseParams, invoke, send, reject); } - async handle( - request: ParsedRequest, - response: ServerResponse, - ): Promise { - await Promise.resolve(handlerFn(this, request, response)); + async handle(httpCtx: HttpContext): Promise { + await Promise.resolve(handlerFn(this, httpCtx)); } } @@ -570,26 +571,30 @@ export class RestServer extends Context implements Server, HttpServerLike { * @memberof RestServer */ async start(): Promise { - // Setup the HTTP handler so that we can verify the configuration - // of API spec, controllers and routes at startup time. - this._setupHandlerIfNeeded(); - const httpPort = await this.get(RestBindings.PORT); const httpHost = await this.get(RestBindings.HOST); - this._httpServer = createServer(this.requestHandler); - const httpServer = this._httpServer; + if (httpHost != null) { + this.options.host = httpHost; + } + if (httpPort != null) { + this.options.port = httpPort; + } + // Setup the HTTP handler so that we can verify the configuration + // of API spec, controllers and routes at startup time. + this._setup(); + const url = await this.endpoint.start(); + this.bind(RestBindings.HOST).to(this.endpoint.host); + this.bind(RestBindings.PORT).to(this.endpoint.port); + this.bind(RestBindings.URL).to(url); + } - // TODO(bajtos) support httpHostname too - // See https://github.com/strongloop/loopback-next/issues/434 - httpServer.listen(httpPort, httpHost); + private _setup() { + if (this.endpoint) return; + this._setupHandlerIfNeeded(); - return new Promise((resolve, reject) => { - httpServer.once('listening', () => { - this.bind(RestBindings.PORT).to(httpServer.address().port); - resolve(); - }); - httpServer.once('error', reject); - }); + this.endpoint = HTTP_FACTORY.createEndpoint(this.options, httpCtx => + this.httpHandler(httpCtx), + ); } /** @@ -600,26 +605,18 @@ export class RestServer extends Context implements Server, HttpServerLike { */ async stop() { // Kill the server instance. - const server = this._httpServer; - return new Promise((resolve, reject) => { - server.close((err: Error) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + await (this.endpoint && this.endpoint.stop()); } - protected _onUnhandledError( - req: ServerRequest, - res: ServerResponse, - err: Error, - ) { - if (!res.headersSent) { - res.statusCode = 500; - res.end(); + get requestListener() { + this._setup(); + return this.endpoint && this.endpoint.requestListener; + } + + protected _onUnhandledError({response}: HttpContext, err: Error) { + if (!response.headersSent) { + response.statusCode = 500; + response.end(); } // It's the responsibility of the Sequence to handle any errors. @@ -638,6 +635,8 @@ export class RestServer extends Context implements Server, HttpServerLike { * @interface RestServerConfig */ export interface RestServerConfig { + protocol?: 'http' | 'https'; + httpsServerOptions?: ServerOptions; host?: string; port?: number; cors?: cors.CorsOptions; diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index 8d42f9b9d6c6..c57b2d52cceb 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -16,11 +16,10 @@ import { instantiateClass, ValueOrPromise, } from '@loopback/context'; -import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; import { - ParsedRequest, + Request, PathParameterValues, OperationArgs, OperationRetval, @@ -29,7 +28,6 @@ import { import {ControllerSpec} from '@loopback/openapi-v3'; import * as assert from 'assert'; -import * as url from 'url'; const debug = require('debug')('loopback:core:routing-table'); // TODO(bajtos) Refactor this code to use Trie-based lookup, @@ -38,34 +36,6 @@ const debug = require('debug')('loopback:core:routing-table'); import * as pathToRegexp from 'path-to-regexp'; import {CoreBindings} from '@loopback/core'; -/** - * Parse the URL of the incoming request and set additional properties - * on this request object: - * - `path` - * - `query` - * - * @private - * @param request - */ -export function parseRequestUrl(request: ServerRequest): ParsedRequest { - // TODO(bajtos) The following parsing can be skipped when the router - // is mounted on an express app - const parsedRequest = request as ParsedRequest; - const parsedUrl = url.parse(parsedRequest.url, true); - parsedRequest.path = parsedUrl.pathname || '/'; - // parsedUrl.query cannot be a string as it is parsed with - // parseQueryString = true - if (parsedUrl.query != null && typeof parsedUrl.query !== 'string') { - parsedRequest.query = parsedUrl.query; - } else { - parsedRequest.query = {}; - } - return parsedRequest; -} - -/** - * A controller instance with open properties/methods - */ // tslint:disable-next-line:no-any export type ControllerInstance = {[name: string]: any} & object; @@ -172,7 +142,7 @@ export class RoutingTable { * Map a request to a route * @param request */ - find(request: ParsedRequest): ResolvedRoute { + find(request: Request): ResolvedRoute { for (const entry of this._routes) { const match = entry.match(request); if (match) return match; @@ -205,7 +175,7 @@ export interface RouteEntry { * Map an http request to a route * @param request */ - match(request: ParsedRequest): ResolvedRoute | undefined; + match(request: Request): ResolvedRoute | undefined; /** * Update bindings for the request context @@ -263,7 +233,7 @@ export abstract class BaseRoute implements RouteEntry { }); } - match(request: ParsedRequest): ResolvedRoute | undefined { + match(request: Request): ResolvedRoute | undefined { debug('trying endpoint', this); if (this.verb !== request.method!.toLowerCase()) { debug(' -> verb mismatch'); diff --git a/packages/rest/src/sequence.ts b/packages/rest/src/sequence.ts index e57da1f97430..ec7f4b898429 100644 --- a/packages/rest/src/sequence.ts +++ b/packages/rest/src/sequence.ts @@ -3,16 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -const debug = require('debug')('loopback:core:sequence'); -import {ServerResponse} from 'http'; +const debug = require('debug')('loopback:rest:sequence'); import {inject, Context} from '@loopback/context'; import { FindRoute, InvokeMethod, - ParsedRequest, Send, Reject, ParseParams, + HttpContext, } from './internal-types'; import {RestBindings} from './keys'; @@ -24,8 +23,7 @@ const SequenceActions = RestBindings.SequenceActions; */ export type SequenceFunction = ( sequence: DefaultSequence, - request: ParsedRequest, - response: ServerResponse, + httpCtx: HttpContext, ) => Promise | void; /** @@ -39,7 +37,7 @@ export interface SequenceHandler { * @param request The incoming HTTP request * @param response The HTTP server response where to write the result */ - handle(request: ParsedRequest, response: ServerResponse): Promise; + handle(httpCtx: HttpContext): Promise; } /** @@ -101,16 +99,24 @@ export class DefaultSequence implements SequenceHandler { * @param res HTTP server response with result from Application controller * method invocation */ - async handle(req: ParsedRequest, res: ServerResponse) { + async handle({request, response}: HttpContext) { try { - const route = this.findRoute(req); - const args = await this.parseParams(req, route); + debug('Finding route for %s', request.originalUrl); + const route = this.findRoute(request); + debug('Route found for %s: %s', request.originalUrl, route.describe()); + + debug('Parsing request for %s', request.originalUrl); + const args = await this.parseParams(request, route); + + debug('Invoking target for %s', request.originalUrl); const result = await this.invoke(route, args); + debug('Invocation result for %s: %s', route.describe(), result); - debug('%s result -', route.describe(), result); - this.send(res, result); + debug('Sending response for %s', request.originalUrl); + this.send(response, result); } catch (err) { - this.reject(res, req, err); + debug('Rejecting request for %s: %s', request.originalUrl, err); + this.reject(response, request, err); } } } diff --git a/packages/rest/src/writer.ts b/packages/rest/src/writer.ts index 9c012265cad4..29b34cdf2459 100644 --- a/packages/rest/src/writer.ts +++ b/packages/rest/src/writer.ts @@ -3,8 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse as Response} from 'http'; -import {OperationRetval} from './internal-types'; +import {OperationRetval, Response} from './internal-types'; import {HttpError} from 'http-errors'; import {Readable} from 'stream'; @@ -21,6 +20,9 @@ export function writeResultToResponse( // result returned back from invoking controller method result: OperationRetval, ): void { + if (!response.statusCode) { + response.status(200); + } if (result) { if (result instanceof Readable || typeof result.pipe === 'function') { response.setHeader('Content-Type', 'application/octet-stream'); @@ -61,6 +63,9 @@ export function writeResultToResponse( export function writeErrorToResponse(response: Response, error: Error) { const e = error; const statusCode = (response.statusCode = e.statusCode || e.status || 500); + if (!response.statusCode) { + response.status(statusCode); + } if (e.headers) { // Set response headers for the error for (const h in e.headers) { diff --git a/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts b/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts index 17315f0aa8d4..c98de2577b9d 100644 --- a/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts +++ b/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts @@ -10,9 +10,9 @@ import { RestServer, RestComponent, RestApplication, + HttpContext, } from '../../..'; import {Application} from '@loopback/core'; -import {ServerResponse, ServerRequest} from 'http'; describe('Bootstrapping with RestComponent', () => { context('with a user-defined sequence', () => { @@ -75,10 +75,6 @@ async function startServerCheck(app: Application) { await app.stop(); } -function sequenceHandler( - sequence: DefaultSequence, - request: ServerRequest, - response: ServerResponse, -) { - sequence.send(response, 'hello world'); +function sequenceHandler(sequence: DefaultSequence, httpCtx: HttpContext) { + sequence.send(httpCtx.response, 'hello world'); } diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index 6ee9d6fae8e4..23ba678e5099 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -4,8 +4,6 @@ // License text available at https://opensource.org/licenses/MIT import { - ServerRequest, - ServerResponse, Route, RestBindings, RestServer, @@ -17,6 +15,8 @@ import { ControllerInstance, createControllerFactoryForClass, createControllerFactoryForInstance, + Request, + Response, } from '../../..'; import {api, get, post, param, requestBody} from '@loopback/openapi-v3'; @@ -29,7 +29,7 @@ import { ResponseObject, } from '@loopback/openapi-v3-types'; -import {expect, Client, createClientForHandler} from '@loopback/testlab'; +import {expect, createClientForHandler, Client} from '@loopback/testlab'; import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; import {inject, Context, BindingScope} from '@loopback/context'; @@ -312,12 +312,12 @@ describe('Routing', () => { @api(spec) class StatusController { constructor( - @inject(RestBindings.Http.REQUEST) private request: ServerRequest, - @inject(RestBindings.Http.RESPONSE) private response: ServerResponse, + @inject(RestBindings.Http.REQUEST) private request: Request, + @inject(RestBindings.Http.RESPONSE) private response: Response, ) {} async getStatus(): Promise { - this.response.statusCode = 202; // 202 Accepted + this.response.status(202); // 202 Accepted return this.request.method as string; } } @@ -694,9 +694,9 @@ describe('Routing', () => { it('provides httpHandler compatible with HTTP server API', async () => { const app = new RestApplication(); - app.handler((sequence, req, res) => res.end('hello')); + app.handler((sequence, httpCtx) => httpCtx.res.end('hello')); - await createClientForHandler(app.requestHandler) + await createClientForHandler(app.requestListener) .get('/') .expect(200, 'hello'); }); @@ -731,6 +731,6 @@ describe('Routing', () => { } function whenIMakeRequestTo(serverOrApp: HttpServerLike): Client { - return createClientForHandler(serverOrApp.requestHandler); + return createClientForHandler(serverOrApp.requestListener); } }); diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index f9c49c18fd98..19ae71cb261d 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -4,8 +4,6 @@ // License text available at https://opensource.org/licenses/MIT import { - ServerResponse, - ParsedRequest, FindRoute, InvokeMethod, Send, @@ -18,10 +16,11 @@ import { RestComponent, RestApplication, HttpServerLike, + HttpContext, } from '../../..'; import {api} from '@loopback/openapi-v3'; import {Application} from '@loopback/core'; -import {expect, Client, createClientForHandler} from '@loopback/testlab'; +import {expect, createClientForHandler, Client} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; import {inject, Context} from '@loopback/context'; import { @@ -47,8 +46,8 @@ describe('Sequence', () => { }); it('allows users to define a custom sequence as a function', () => { - server.handler((sequence, request, response) => { - sequence.send(response, 'hello world'); + server.handler((sequence, httpCtx) => { + sequence.send(httpCtx.response, 'hello world'); }); return whenIRequest() .get('/') @@ -59,8 +58,8 @@ describe('Sequence', () => { class MySequence implements SequenceHandler { constructor(@inject(SequenceActions.SEND) private send: Send) {} - async handle(req: ParsedRequest, res: ServerResponse) { - this.send(res, 'hello world'); + async handle(httpCtx: HttpContext) { + this.send(httpCtx.response, 'hello world'); } } // bind user defined sequence @@ -81,11 +80,11 @@ describe('Sequence', () => { @inject(SequenceActions.SEND) protected send: Send, ) {} - async handle(req: ParsedRequest, res: ServerResponse) { - const route = this.findRoute(req); - const args = await this.parseParams(req, route); + async handle(httpCtx: HttpContext) { + const route = this.findRoute(httpCtx.request); + const args = await this.parseParams(httpCtx.request, route); const result = await this.invoke(route, args); - this.send(res, `MySequence ${result}`); + this.send(httpCtx.response, `MySequence ${result}`); } } @@ -100,8 +99,8 @@ describe('Sequence', () => { class MySequence { constructor(@inject(SequenceActions.SEND) protected send: Send) {} - async handle(req: ParsedRequest, res: ServerResponse) { - this.send(res, 'MySequence was invoked.'); + async handle(httpCtx: HttpContext) { + this.send(httpCtx.response, 'MySequence was invoked.'); } } @@ -139,9 +138,9 @@ describe('Sequence', () => { it('makes ctx available in a custom sequence handler function', () => { app.bind('test').to('hello world'); - server.handler((sequence, request, response) => { + server.handler((sequence, httpCtx) => { expect.exists(sequence.ctx); - sequence.send(response, sequence.ctx.getSync('test')); + sequence.send(httpCtx.response, sequence.ctx.getSync('test')); }); return whenIRequest() @@ -163,8 +162,8 @@ describe('Sequence', () => { super(ctx, findRoute, parseParams, invoke, send, reject); } - async handle(req: ParsedRequest, res: ServerResponse) { - this.send(res, this.ctx.getSync('test')); + async handle(httpCtx: HttpContext) { + this.send(httpCtx.response, this.ctx.getSync('test')); } } @@ -219,6 +218,6 @@ describe('Sequence', () => { } function whenIRequest(restServerOrApp: HttpServerLike = server): Client { - return createClientForHandler(restServerOrApp.requestHandler); + return createClientForHandler(restServerOrApp.requestListener); } }); diff --git a/packages/rest/test/integration/certificate.pem b/packages/rest/test/integration/certificate.pem new file mode 100644 index 000000000000..8169c4cd878e --- /dev/null +++ b/packages/rest/test/integration/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCdtKtIFRjO6jANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0Zvc3RlciBDaXR5MRQwEgYDVQQKDAts +b29wYmFjay5pbzAeFw0xODAzMDUwNzExNDhaFw0yODAxMTIwNzExNDhaMEYxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLRm9zdGVyIENpdHkxFDAS +BgNVBAoMC2xvb3BiYWNrLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZgqI7HUKmu1Is +rSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCMZMIsllEsCtWc +eAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XWDaNNCgzrwec+ +aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZcffWYO6edwA6b +POqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQuC+IUpVRc4Jn +TFRasRIPh9oPmAHpe+aeixItpwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBRFmy8 +A3Mfzx2IzvtlPKiEbAryhHxtiGLcN48M/1rRF4mV/iVWxLfckd8Zh91KpGqu9HBG +uHb4oqPw0KFkNnWOl8niE5/QCrqwjgTREQXhDc897Jm4VaWepS8zBK81VdijoSq8 +UowDnr5l5923ltBlfwtg4t5gwIySY/uoQFaQhuW4l6Rpa4lLv4/ardE2o4G3cADe +zANyO7ifT7VWCil4Xg4AVDa40jU/V60z0A8rySCYzhCfrRPG6sCV87cffLn6Yu4o +O/5AXIfS9XF51K5G22vYB5MPwGwm8wClND4AHH/jYJ6dAGNYtw06pHrcKisT5/K3 +2E+lHoiHZPUbDaa0 +-----END CERTIFICATE----- diff --git a/packages/rest/test/integration/certrequest.csr b/packages/rest/test/integration/certrequest.csr new file mode 100644 index 000000000000..6cb36b858ec3 --- /dev/null +++ b/packages/rest/test/integration/certrequest.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICizCCAXMCAQAwRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQH +DAtGb3N0ZXIgQ2l0eTEUMBIGA1UECgwLbG9vcGJhY2suaW8wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCf86wJzz2wp4NoXR6yzwZp4SzqAFU71i0g8if2 +6JYMz7n97BmCojsdQqa7UiytJTFTF5rmv0H+4XqX/69z/XfxleDeRdt94oD47wU+ +8TmBeKumsIxkwiyWUSwK1Zx4BbAVLHIvit6cRfqJX0rEvlCZ2DA0pvXbGVrDn2e5 +MTT79uj7VdYNo00KDOvB5z5pEv1OVd7ACyLExISNh+h4uvpfOj9cqI1m1iTwMED5 +nUFdEYw6plx99Zg7p53ADps86opIwm4E/s4D4PLl2ZEUA4gt0KXlNXIBeWJ0yIH+ +VPPyES4x/BC4L4hSlVFzgmdMVFqxEg+H2g+YAel75p6LEi2nAgMBAAGgADANBgkq +hkiG9w0BAQsFAAOCAQEAReyRaTOtRDijrCqoYp04A7bpOYl6Pe/7ONuhfJGEefZy +ZYxsRstm02knTpy/OtOIH2H95VJNiQJ9tBxvpZ+9VRN7OrTZqXb1+glj3LG4n9Ob +LnFu8/7MD2Ky7NogOyELbLVSxW8h6cRId6c7WO+x7+hEUyalHIJ+k1UYUClZzXiX +8UGu7tLP3bmqCxd9bJQitpOcrySo4uUv0ICpzeqVN717rq8BfjVDzK++atC70PeL +0kdIJdl+85XXAgilTzBioznw8jfgWs5e814A8byWISYM38b1bp8BO9dvZ1cr5xaD +z0LRkNkm/BcGj4QFoVfA8ZL/74Q8bTvx/oMo5voK5w== +-----END CERTIFICATE REQUEST----- diff --git a/packages/rest/test/integration/http-handler.integration.ts b/packages/rest/test/integration/http-handler.integration.ts index c03936de4b4c..5c1376a01508 100644 --- a/packages/rest/test/integration/http-handler.integration.ts +++ b/packages/rest/test/integration/http-handler.integration.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import { - HttpHandler, + RestHttpHandler, DefaultSequence, writeResultToResponse, parseOperationArgs, @@ -12,6 +12,7 @@ import { FindRouteProvider, InvokeMethodProvider, RejectProvider, + HTTP_FACTORY, } from '../..'; import {ControllerSpec, get} from '@loopback/openapi-v3'; import {Context} from '@loopback/context'; @@ -423,7 +424,7 @@ describe('HttpHandler', () => { }); let rootContext: Context; - let handler: HttpHandler; + let handler: RestHttpHandler; function givenHandler() { rootContext = new Context(); rootContext.bind(SequenceActions.FIND_ROUTE).toProvider(FindRouteProvider); @@ -439,7 +440,7 @@ describe('HttpHandler', () => { rootContext.bind(RestBindings.SEQUENCE).toClass(DefaultSequence); - handler = new HttpHandler(rootContext); + handler = new RestHttpHandler(rootContext); rootContext.bind(RestBindings.HANDLER).to(handler); } @@ -459,7 +460,9 @@ describe('HttpHandler', () => { function givenClient() { client = createClientForHandler((req, res) => { - handler.handleRequest(req, res).catch(err => { + const app = HTTP_FACTORY.createApp(); + const httpCtx = HTTP_FACTORY.createHttpContext(req, res, app); + handler.handleRequest(httpCtx).catch(err => { // This should never happen. If we ever get here, // then it means "handler.handlerRequest()" crashed unexpectedly. // We need to make a lot of helpful noise in such case. diff --git a/packages/rest/test/integration/privatekey.pem b/packages/rest/test/integration/privatekey.pem new file mode 100644 index 000000000000..d7295e1fa8ac --- /dev/null +++ b/packages/rest/test/integration/privatekey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAn/OsCc89sKeDaF0ess8GaeEs6gBVO9YtIPIn9uiWDM+5/ewZ +gqI7HUKmu1IsrSUxUxea5r9B/uF6l/+vc/138ZXg3kXbfeKA+O8FPvE5gXirprCM +ZMIsllEsCtWceAWwFSxyL4renEX6iV9KxL5QmdgwNKb12xlaw59nuTE0+/bo+1XW +DaNNCgzrwec+aRL9TlXewAsixMSEjYfoeLr6Xzo/XKiNZtYk8DBA+Z1BXRGMOqZc +ffWYO6edwA6bPOqKSMJuBP7OA+Dy5dmRFAOILdCl5TVyAXlidMiB/lTz8hEuMfwQ +uC+IUpVRc4JnTFRasRIPh9oPmAHpe+aeixItpwIDAQABAoIBABSC1sjbPnnswTkc +19buHVBug6fuKv/lUxwqcV2ELdmuuZcKM6tAynvSFDdpLjl1z4FsQXzdgcUBfQsI +yqMBGeRs580ZADCAXzGM1QthO5KSutBBS3+QNs9/0ToCcnIhqJbOgEYAdNNtVddP +1PKtxQA1bNkTn+tcsPrs8gwZd0XoC8imbJYw9R9nLehmFAM0T+onFn7P0mCh1w9g +2d6bh6nGRacJG7E6QL2KdOzn0Yv1Jg5Ducoru6Gf7QISIBeJ3orfzMJeHhh5vl5X +NLW4kcwX8l1+T1onGD59a2GbdsGmL6m0bfwHiClbkK8ztypioVCsZVaYtB9evCm4 +1eFOGsECgYEAza75ckg2ek8QhSenZ9HI5jrT+oFBghdQzaQU3e3DlKylonKb4Xal +XtSFOnRpA0lNA+J6cliO054rVQMQjFUhFmQWiD3S7Jnz5oiwG7eq7vVt6JauWKxG +GEfoKCH8lrhTP8KEb4bPAK6cRPrk40rRaazYHRPjNVO2/i8mLjbA5MkCgYEAxxS7 +RUpvR++dUgX2WiV5g+6eQxsjRUdWewxM3Xndfx+yPTmIBO/jOCm0dFLb1DMADegE +rab//w9s+RyL8ZyMOOWTLIFD/fgBEP3UcDcForRALpSRrIJz6tfG0/sV8lUBwpLs +ALTs5DeJnlv4GPXhoCGpQPP2hq17L0bANQvw5u8CgYAViGTq9u5lHZPgLMeU0dyT +ZcM9bXy7Ls+xx6S7myGnle99MzxofTBQ3jTYasl3o5vhdTtWbzj8pIlqy/hWiK7/ +FhlZyAcl5/QlxVeSf0bw31bTS7sS424vKo/+a5hy+vcULLwKpPVU3/LSMeX2eaW0 +x3iUirl1or78m1kG64qEKQKBgHIAXDEUq97cxxEGWwlKNminhzdUXgE5FbvG0mlt +dLpsUyweOtbg7BPoRe7q1/mO7vQHrk4muKe9lKCeiUDlbaLTTFELAP15PFsXj8Rm +rbJ7V9mUuEq6NVkBEVmoievIZAahDcZl1NXnO8ZUUiExBHAndn28dqquw0DSWhTG +bsA/AoGAGMxQpBGX2H1vLD532U6WzJfCnrks+tHKMGypspC1q6ChTU1Xo6TJt2/w +ilqnzoYaFsKVT/miD2R1gZnePbGrbYmZGLiEDKttMJKR0UrRX46t7ZIPf7n88ecp +1I4cpDyuCK1AAgG8ZYawMh3SKTkabZ7I1ekFa+e06yg8JYzYKMk= +-----END RSA PRIVATE KEY----- diff --git a/packages/rest/test/integration/rest.server.integration.ts b/packages/rest/test/integration/rest.server.integration.ts index 4edda6e0f817..0bf6c1cb8f89 100644 --- a/packages/rest/test/integration/rest.server.integration.ts +++ b/packages/rest/test/integration/rest.server.integration.ts @@ -4,21 +4,28 @@ // License text available at https://opensource.org/licenses/MIT import {Application, ApplicationConfig} from '@loopback/core'; -import {expect, createClientForHandler} from '@loopback/testlab'; +import { + expect, + createClientForHandler, + createClientForRestServer, +} from '@loopback/testlab'; import {Route, RestBindings, RestServer, RestComponent} from '../..'; import * as yaml from 'js-yaml'; +import * as fs from 'fs'; +import * as path from 'path'; describe('RestServer (integration)', () => { it('updates rest.port binding when listening on ephemeral port', async () => { const server = await givenAServer({rest: {port: 0}}); await server.start(); expect(server.getSync(RestBindings.PORT)).to.be.above(0); + expect(server.options.port).to.be.above(0); await server.stop(); }); it('responds with 500 when Sequence fails with unhandled error', async () => { const server = await givenAServer({rest: {port: 0}}); - server.handler((sequence, request, response) => { + server.handler((sequence, httpCtx) => { return Promise.reject(new Error('unhandled test error')); }); @@ -32,19 +39,19 @@ describe('RestServer (integration)', () => { } }); - return createClientForHandler(server.requestHandler) + return createClientForHandler(server.requestListener) .get('/') .expect(500); }); it('allows cors', async () => { const server = await givenAServer({rest: {port: 0}}); - server.handler((sequence, request, response) => { - response.write('Hello'); - response.end(); + server.handler((sequence, httpCtx) => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); }); - await createClientForHandler(server.requestHandler) + await createClientForHandler(server.requestListener) .get('/') .expect(200, 'Hello') .expect('Access-Control-Allow-Origin', '*') @@ -53,12 +60,12 @@ describe('RestServer (integration)', () => { it('allows cors preflight', async () => { const server = await givenAServer({rest: {port: 0}}); - server.handler((sequence, request, response) => { - response.write('Hello'); - response.end(); + server.handler((sequence, httpCtx) => { + httpCtx.response.write('Hello'); + httpCtx.response.end(); }); - await createClientForHandler(server.requestHandler) + await createClientForHandler(server.requestListener) .options('/') .expect(204) .expect('Access-Control-Allow-Origin', '*') @@ -78,7 +85,7 @@ describe('RestServer (integration)', () => { }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.requestHandler).get( + const response = await createClientForHandler(server.requestListener).get( '/openapi.json', ); expect(response.body).to.containDeep({ @@ -118,7 +125,7 @@ describe('RestServer (integration)', () => { }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.requestHandler).get( + const response = await createClientForHandler(server.requestListener).get( '/openapi.yaml', ); const expected = yaml.safeLoad(` @@ -159,7 +166,7 @@ servers: }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.requestHandler).get( + const response = await createClientForHandler(server.requestListener).get( '/swagger-ui', ); await server.get(RestBindings.PORT); @@ -190,7 +197,7 @@ servers: }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.requestHandler).get( + const response = await createClientForHandler(server.requestListener).get( '/swagger-ui', ); await server.get(RestBindings.PORT); @@ -205,6 +212,31 @@ servers: expect(response.get('Access-Control-Allow-Credentials')).to.equal('true'); }); + it('supports https protocol', async () => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + const key = fs.readFileSync( + path.join(__dirname, '../../../test/integration/privatekey.pem'), + ); + const cert = fs.readFileSync( + path.join(__dirname, '../../../test/integration/certificate.pem'), + ); + const server = await givenAServer({ + rest: {protocol: 'https', httpsServerOptions: {cert, key}, port: 0}, + }); + server.handler((sequence, httpCtx) => { + httpCtx.response.send('Hello'); + }); + + const test = await createClientForRestServer(server); + test.get('/').expect(200, 'Hello'); + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + + expect(server.endpoint).to.be.not.undefined(); + expect(server.endpoint.url).to.be.not.undefined(); + expect(server.endpoint.app).to.be.not.undefined(); + await server.stop(); + }); + async function givenAServer(options?: ApplicationConfig) { const app = new Application(options); app.component(RestComponent); diff --git a/packages/rest/test/unit/parser.unit.ts b/packages/rest/test/unit/parser.unit.ts index a221a4dc08b4..4e67e8202a15 100644 --- a/packages/rest/test/unit/parser.unit.ts +++ b/packages/rest/test/unit/parser.unit.ts @@ -5,8 +5,7 @@ import { parseOperationArgs, - ParsedRequest, - parseRequestUrl, + Request, PathParameterValues, Route, createResolvedRoute, @@ -68,8 +67,8 @@ describe('operationArgsParser', () => { }; } - function givenRequest(options?: ShotRequestOptions): ParsedRequest { - return parseRequestUrl(new ShotRequest(options || {url: '/'})); + function givenRequest(options?: ShotRequestOptions): Request { + return new ShotRequest(options || {url: '/'}); } function givenResolvedRoute( diff --git a/packages/rest/test/unit/rest.component.unit.ts b/packages/rest/test/unit/rest.component.unit.ts index 2e22406243cf..3df9c468ed29 100644 --- a/packages/rest/test/unit/rest.component.unit.ts +++ b/packages/rest/test/unit/rest.component.unit.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect, ShotRequest} from '@loopback/testlab'; +import {expect, ShotRequest, mockResponse} from '@loopback/testlab'; import { Application, ProviderMap, @@ -17,9 +17,9 @@ import { RestServer, RestBindings, RestComponentConfig, - ServerRequest, - HttpHandler, + RestHttpHandler, LogError, + HTTP_FACTORY, } from '../..'; const SequenceActions = RestBindings.SequenceActions; @@ -31,7 +31,7 @@ describe('RestComponent', () => { // Stub constructor requirements for some providers. app.bind(RestBindings.Http.CONTEXT).to(new Context()); - app.bind(RestBindings.HANDLER).to(new HttpHandler(app)); + app.bind(RestBindings.HANDLER).to(new RestHttpHandler(app)); const comp = await app.get('components.RestComponent'); for (const key in comp.providers || {}) { @@ -70,7 +70,7 @@ describe('RestComponent', () => { class CustomLogger implements Provider { value() { - return (err: Error, statusCode: number, request: ServerRequest) => { + return (err: Error, statusCode: number, request: Request) => { lastLog = `${request.url} ${statusCode} ${err.message}`; }; } @@ -80,7 +80,12 @@ describe('RestComponent', () => { app.component(CustomRestComponent); const server = await app.getServer(RestServer); const logError = await server.get(SequenceActions.LOG_ERROR); - logError(new Error('test-error'), 400, new ShotRequest({url: '/'})); + const httpCtx = HTTP_FACTORY.createHttpContext( + new ShotRequest({url: '/'}), + mockResponse().response, + HTTP_FACTORY.createApp(), + ); + logError(new Error('test-error'), 400, httpCtx.request); expect(lastLog).to.equal('/ 400 test-error'); }); diff --git a/packages/rest/test/unit/router/routing-table.unit.ts b/packages/rest/test/unit/router/routing-table.unit.ts index 4507dd055231..b63495bc3cb2 100644 --- a/packages/rest/test/unit/router/routing-table.unit.ts +++ b/packages/rest/test/unit/router/routing-table.unit.ts @@ -3,12 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - ParsedRequest, - parseRequestUrl, - RoutingTable, - ControllerRoute, -} from '../../..'; +import {Request, RoutingTable, ControllerRoute} from '../../..'; import {getControllerSpec, param, get} from '@loopback/openapi-v3'; import {expect, ShotRequestOptions, ShotRequest} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; @@ -107,7 +102,11 @@ describe('RoutingTable', () => { expect(route.describe()).to.equal('TestController.greet'); }); - function givenRequest(options?: ShotRequestOptions): ParsedRequest { - return parseRequestUrl(new ShotRequest(options || {url: '/'})); + function givenRequest(options?: ShotRequestOptions): Request { + const req = new ShotRequest(options || {url: '/'}); + if (!req.path) { + req.path = req.url; + } + return req; } }); diff --git a/packages/rest/test/unit/writer.unit.ts b/packages/rest/test/unit/writer.unit.ts index 77d651789c8e..935401dc8be5 100644 --- a/packages/rest/test/unit/writer.unit.ts +++ b/packages/rest/test/unit/writer.unit.ts @@ -3,12 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse, writeResultToResponse} from '../..'; +import {Response, writeResultToResponse} from '../..'; import {Duplex} from 'stream'; import {expect, mockResponse, ShotObservedResponse} from '@loopback/testlab'; describe('writer', () => { - let response: ServerResponse; + let response: Response; let observedResponse: Promise; beforeEach(setupResponseMock); diff --git a/packages/testlab/package.json b/packages/testlab/package.json index 1fc9342f23c8..ebaf5f661b92 100644 --- a/packages/testlab/package.json +++ b/packages/testlab/package.json @@ -18,6 +18,7 @@ "license": "MIT", "dependencies": { "@types/fs-extra": "^5.0.1", + "@types/express": "^4.11.1", "@types/shot": "^3.4.0", "@types/sinon": "^4.3.0", "@types/supertest": "^2.0.4", diff --git a/packages/testlab/src/client.ts b/packages/testlab/src/client.ts index 1e97f0788687..4b6aa6a4c544 100644 --- a/packages/testlab/src/client.ts +++ b/packages/testlab/src/client.ts @@ -7,7 +7,7 @@ * HTTP client utilities */ -import * as http from 'http'; +import {IncomingMessage, ServerResponse} from 'http'; import supertest = require('supertest'); export {supertest}; @@ -20,29 +20,24 @@ export type Client = supertest.SuperTest; * @param handler */ export function createClientForHandler( - handler: (req: http.ServerRequest, res: http.ServerResponse) => void, + handler: ((req: IncomingMessage, res: ServerResponse) => void), ): Client { - const server = http.createServer(handler); - return supertest(server); + return supertest(handler); } export async function createClientForRestServer( server: RestServer, ): Promise { await server.start(); - const port = - server.options && server.options.http ? server.options.http.port : 3000; - const url = `http://127.0.0.1:${port}`; // TODO(bajtos) Find a way how to stop the server after all tests are done - return supertest(url); + return supertest(server.endpoint.url); } // These interfaces are meant to partially mirror the formats provided // in our other libraries to avoid circular imports. export interface RestServer { start(): Promise; - options?: { - // tslint:disable-next-line:no-any - [prop: string]: any; + endpoint: { + url?: string; }; } diff --git a/packages/testlab/src/shot.ts b/packages/testlab/src/shot.ts index 1d4b10efbfae..a60a0ad86ef9 100644 --- a/packages/testlab/src/shot.ts +++ b/packages/testlab/src/shot.ts @@ -8,7 +8,7 @@ * https://github.com/hapijs/shot */ -import {ServerRequest, ServerResponse} from 'http'; +import {Request, Response} from 'express'; import * as util from 'util'; import { @@ -25,22 +25,20 @@ export const ShotRequest: ShotRequestCtor = require('shot/lib/request'); // tslint:disable-next-line:variable-name export const ShotResponse: ShotResponseCtor = require('shot/lib/response'); -export type ShotRequestCtor = new ( - options: ShotRequestOptions, -) => ServerRequest; +export type ShotRequestCtor = new (options: ShotRequestOptions) => Request; export type ShotCallback = (response: ResponseObject) => void; export type ShotResponseCtor = new ( - request: ServerRequest, + request: Request, onEnd: ShotCallback, -) => ServerResponse; +) => Response; export type ShotObservedResponse = ResponseObject; export interface ShotResponseMock { - request: ServerRequest; - response: ServerResponse; + request: Request; + response: Response; result: Promise; } @@ -48,7 +46,7 @@ export function mockResponse( requestOptions: ShotRequestOptions = {url: '/'}, ): ShotResponseMock { const request = new ShotRequest(requestOptions); - let response: ServerResponse | undefined; + let response: Response | undefined; let result = new Promise(resolve => { response = new ShotResponse(request, resolve); });