diff --git a/examples/hello-world/src/application.ts b/examples/hello-world/src/application.ts index bfa570c88383..2972649f8f42 100644 --- a/examples/hello-world/src/application.ts +++ b/examples/hello-world/src/application.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {RestApplication, RestServer} from '@loopback/rest'; +import { RestApplication, RestServer } from '@loopback/rest'; export class HelloWorldApplication extends RestApplication { constructor() { @@ -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/hello-world/src/index.ts b/examples/hello-world/src/index.ts index 5d46ddb70020..e2ecdbdc56b0 100644 --- a/examples/hello-world/src/index.ts +++ b/examples/hello-world/src/index.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {HelloWorldApplication} from './application'; +import { HelloWorldApplication } from './application'; export async function main() { const app = new HelloWorldApplication(); diff --git a/examples/log-extension/src/providers/log-action.provider.ts b/examples/log-extension/src/providers/log-action.provider.ts index 9d477ba219a4..8a2b0d31cedf 100644 --- a/examples/log-extension/src/providers/log-action.provider.ts +++ b/examples/log-extension/src/providers/log-action.provider.ts @@ -3,11 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject, Provider, Constructor, Getter} from '@loopback/context'; -import {CoreBindings} from '@loopback/core'; -import {OperationArgs, ParsedRequest} from '@loopback/rest'; -import {getLogMetadata} from '../decorators'; -import {EXAMPLE_LOG_BINDINGS, LOG_LEVEL} from '../keys'; +import { inject, Provider, Constructor, Getter } from '@loopback/context'; +import { CoreBindings } from '@loopback/core'; +import { OperationArgs, Request } from '@loopback/rest'; +import { getLogMetadata } from '../decorators/log.decorator'; +import { EXAMPLE_LOG_BINDINGS, LOG_LEVEL } from '../keys'; import { LogFn, TimerFn, @@ -19,10 +19,10 @@ import chalk from 'chalk'; export class LogActionProvider implements Provider { // LogWriteFn is an optional dependency and it falls back to `logToConsole` - @inject(EXAMPLE_LOG_BINDINGS.LOGGER, {optional: true}) + @inject(EXAMPLE_LOG_BINDINGS.LOGGER, { optional: true }) writeLog: LogWriterFn = logToConsole; - @inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL, {optional: true}) + @inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL, { optional: true }) logLevel: LOG_LEVEL = LOG_LEVEL.WARN; constructor( @@ -31,11 +31,11 @@ export class LogActionProvider implements Provider { @inject.getter(CoreBindings.CONTROLLER_METHOD_NAME) private readonly getMethod: Getter, @inject(EXAMPLE_LOG_BINDINGS.TIMER) public timer: TimerFn, - ) {} + ) { } 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..b94d6e41a0b0 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, @@ -25,7 +25,7 @@ export interface LogFn { /** * Log level metadata */ -export type LevelMetadata = {level: number}; +export type LevelMetadata = { level: number }; /** * High resolution time as [seconds, nanoseconds]. Used by process.hrtime(). diff --git a/examples/log-extension/test/acceptance/log.extension.acceptance.ts b/examples/log-extension/test/acceptance/log.extension.acceptance.ts index 5cc752dc4337..d375961fa8b5 100644 --- a/examples/log-extension/test/acceptance/log.extension.acceptance.ts +++ b/examples/log-extension/test/acceptance/log.extension.acceptance.ts @@ -12,10 +12,9 @@ import { InvokeMethod, Send, Reject, - ParsedRequest, - ServerResponse, + HttpContext, } from '@loopback/rest'; -import {get, param} from '@loopback/openapi-v3'; +import { get, param } from '@loopback/openapi-v3'; import { LogMixin, LOG_LEVEL, @@ -31,19 +30,19 @@ import { createClientForHandler, expect, } from '@loopback/testlab'; -import {Context, inject} from '@loopback/context'; +import { Context, inject } from '@loopback/context'; import chalk from 'chalk'; const SequenceActions = RestBindings.SequenceActions; -import {createLogSpy, restoreLogSpy} from '../log-spy'; -import {logToMemory, resetLogs} from '../in-memory-logger'; +import { createLogSpy, restoreLogSpy } from '../log-spy'; +import { logToMemory, resetLogs } from '../in-memory-logger'; describe('log extension acceptance test', () => { let app: LogApp; let spy: SinonSpy; - class LogApp extends LogMixin(RestApplication) {} + class LogApp extends LogMixin(RestApplication) { } const debugMatch: string = chalk.white( 'DEBUG: /debug :: MyController.debug() => debug called', @@ -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(); @@ -212,9 +211,9 @@ describe('log extension acceptance test', () => { @inject(SequenceActions.SEND) protected send: Send, @inject(SequenceActions.REJECT) protected reject: Reject, @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..4ed7976e3a59 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 @@ -3,8 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {sinon} from '@loopback/testlab'; -import {ParsedRequest} from '@loopback/rest'; +import { sinon } from '@loopback/testlab'; +import { Request } from '@loopback/rest'; import { LogActionProvider, LogFn, @@ -15,13 +15,13 @@ import { } from '../../..'; import chalk from 'chalk'; -import {createLogSpy, restoreLogSpy, createConsoleStub} from '../../log-spy'; -import {logToMemory} from '../../in-memory-logger'; +import { createLogSpy, restoreLogSpy, createConsoleStub } from '../../log-spy'; +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(); @@ -99,7 +99,7 @@ describe('LogActionProvider with default logger', () => { async function getLogger(logWriter?: LogWriterFn) { class TestClass { @log(LOG_LEVEL.ERROR) - test() {} + test() { } } const provider = new LogActionProvider( diff --git a/examples/todo/src/sequence.ts b/examples/todo/src/sequence.ts index 55f2eb029e03..ee835c7ef0f4 100644 --- a/examples/todo/src/sequence.ts +++ b/examples/todo/src/sequence.ts @@ -3,18 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context, inject} from '@loopback/context'; +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; @@ -26,16 +25,16 @@ export class MySequence implements SequenceHandler { @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, @inject(SequenceActions.SEND) public send: Send, @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..f394dba33cca 100644 --- a/examples/todo/test/acceptance/application.acceptance.ts +++ b/examples/todo/test/acceptance/application.acceptance.ts @@ -3,12 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; -import {RestServer} from '@loopback/rest'; -import {TodoListApplication} from '../../src/application'; -import {TodoRepository} from '../../src/repositories/'; -import {givenTodo} from '../helpers'; -import {Todo} from '../../src/models/'; +import { createClientForHandler, expect, supertest } from '@loopback/testlab'; +import { RestServer } from '@loopback/rest'; +import { TodoListApplication } from '../../src/application'; +import { TodoRepository } from '../../src/repositories/'; +import { givenTodo } from '../helpers'; +import { Todo } from '../../src/models/'; describe('Application', () => { let app: TodoListApplication; @@ -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/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/.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 26acbc7b01bc..f87a4bca682e 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -25,9 +25,11 @@ "dependencies": { "@loopback/context": "^0.10.1", "@loopback/core": "^0.8.1", + "@loopback/http-server-express": "^0.2.0", "@loopback/openapi-v3": "^0.10.1", "@loopback/openapi-v3-types": "^0.7.1", "@types/cors": "^2.8.3", + "@types/express": "^4.11.1", "@types/http-errors": "^1.6.1", "body": "^5.1.0", "cors": "^2.8.4", diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index a3e0dffa3a38..f4f0c7d30b73 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -3,36 +3,27 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context} from '@loopback/context'; -import {PathObject, SchemasObject} from '@loopback/openapi-v3-types'; -import {ServerRequest, ServerResponse} from 'http'; -import {ControllerSpec} from '@loopback/openapi-v3'; +import { Context } from '@loopback/context'; +import { PathObject, SchemasObject } from '@loopback/openapi-v3-types'; +import { ControllerSpec } from '@loopback/openapi-v3'; -import {SequenceHandler} from './sequence'; +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'; +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..04b886212957 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, @@ -29,11 +27,12 @@ import * as HttpErrors from 'http-errors'; export * from './parser'; -export {writeResultToResponse} from './writer'; +export { writeResultToResponse } from './writer'; // http errors -export {HttpErrors}; +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..b053b7cbe997 100644 --- a/packages/rest/src/internal-types.ts +++ b/packages/rest/src/internal-types.ts @@ -3,15 +3,18 @@ // This file is licensed under the MIT License. // 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'; +import { Binding, BoundValue } from '@loopback/context'; +import { ServerRequest, ServerResponse } from 'http'; +import { ResolvedRoute, RouteEntry } from './router/routing-table'; + +import { Request, Response } from './http-server'; +export { Request, Response, HttpContext } from './http-server'; 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[]}; + query: { [key: string]: string | string[] }; // see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15808 url: string; pathname: string; @@ -22,13 +25,13 @@ export interface ParsedRequest extends ServerRequest { * 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; @@ -81,7 +84,7 @@ export type LogError = ( ) => void; // tslint:disable:no-any -export type PathParameterValues = {[key: string]: any}; +export type PathParameterValues = { [key: string]: any }; export type OperationArgs = any[]; /** diff --git a/packages/rest/src/keys.ts b/packages/rest/src/keys.ts index 87648bd2714b..337f108d5659 100644 --- a/packages/rest/src/keys.ts +++ b/packages/rest/src/keys.ts @@ -3,20 +3,19 @@ // 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 {SequenceHandler} from './sequence'; +import { CoreBindings } from '@loopback/core'; +import { BindingKey, Context } from '@loopback/context'; +import { OpenApiSpec } from '@loopback/openapi-v3-types'; +import { RestHttpHandler } from './http-handler'; +import { SequenceHandler } from './sequence'; import { BindElement, FindRoute, GetFromContext, InvokeMethod, LogError, - ParsedRequest, + Request, + Response, ParseParams, Reject, Send, @@ -24,7 +23,7 @@ import { // NOTE(bajtos) The following import is required to satisfy TypeScript compiler // tslint:disable-next-line:no-unused-variable -import {OpenAPIObject} from '@loopback/openapi-v3-types'; +import { OpenAPIObject } from '@loopback/openapi-v3-types'; /** * RestServer-specific bindings @@ -41,11 +40,15 @@ export namespace RestBindings { /** * Binding key for setting and injecting the port number of RestServer */ - export const PORT = BindingKey.create('rest.port'); + export const PORT = BindingKey.create('rest.port'); /** * Internal binding key for http-handler */ - export const HANDLER = BindingKey.create('rest.handler'); + export const HANDLER = BindingKey.create('rest.handler'); + /** + * Internal binding key for URL + */ + export const URL = BindingKey.create('rest.url'); /** * Binding key for setting and injecting an OpenAPI spec @@ -116,13 +119,13 @@ export namespace RestBindings { /** * Binding key for setting and injecting the http request */ - export const REQUEST = BindingKey.create( + export const REQUEST = BindingKey.create( 'rest.http.request', ); /** * Binding key for setting and injecting the http response */ - export const RESPONSE = BindingKey.create( + export const RESPONSE = BindingKey.create( 'rest.http.response', ); /** diff --git a/packages/rest/src/providers/find-route.provider.ts b/packages/rest/src/providers/find-route.provider.ts index 0bfc5cb65837..63149be21369 100644 --- a/packages/rest/src/providers/find-route.provider.ts +++ b/packages/rest/src/providers/find-route.provider.ts @@ -3,24 +3,24 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context, inject, Provider} from '@loopback/context'; -import {FindRoute} from '../internal-types'; -import {HttpHandler} from '../http-handler'; -import {RestBindings} from '../keys'; -import {ParsedRequest} from '../internal-types'; -import {ResolvedRoute} from '../router/routing-table'; +import { Context, inject, Provider } from '@loopback/context'; +import { FindRoute } from '../internal-types'; +import { RestHttpHandler } from '../http-handler'; +import { RestBindings } from '../keys'; +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/rest.application.ts b/packages/rest/src/rest.application.ts index 93c23ab87cc5..b71e98bc1fe3 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -3,19 +3,20 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Application, ApplicationConfig, Server} from '@loopback/core'; -import {RestComponent} from './rest.component'; -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 { Application, ApplicationConfig, Server } from '@loopback/core'; +import { RestComponent } from './rest.component'; +import { SequenceHandler, SequenceFunction } from './sequence'; +import { Binding, Constructor } from '@loopback/context'; +import { format } from 'util'; +import { RestBindings } from './keys'; +import { RestServer } from './rest.server'; import { RouteEntry, ControllerClass, ControllerFactory, } from './router/routing-table'; -import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types'; +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..d40910dc4295 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -3,9 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {AssertionError} from 'assert'; -import {safeDump} from 'js-yaml'; -import {Binding, Context, Constructor, inject} from '@loopback/context'; +import { AssertionError } from 'assert'; +import { safeDump } from 'js-yaml'; +import { Binding, Context, Constructor, inject } from '@loopback/context'; import { Route, ControllerRoute, @@ -15,15 +15,15 @@ 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 { OpenApiSpec, OperationObject } from '@loopback/openapi-v3-types'; import * as cors from 'cors'; -import {Application, CoreBindings, Server} from '@loopback/core'; -import {getControllerSpec} from '@loopback/openapi-v3'; -import {HttpHandler} from './http-handler'; -import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; +// 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 { RestHttpHandler } from './http-handler'; +import { DefaultSequence, SequenceHandler, SequenceFunction } from './sequence'; import { FindRoute, InvokeMethod, @@ -31,16 +31,16 @@ import { Reject, ParseParams, } from './internal-types'; -import {RestBindings} from './keys'; - -export type HttpRequestListener = ( - req: ServerRequest, - res: ServerResponse, -) => void; - -export interface HttpServerLike { - requestHandler: HttpRequestListener; -} +import { RestBindings } from './keys'; +import { + HTTP_FACTORY, + Request, + Response, + HttpContext, + HttpHandler, + HttpServerLike, + HttpEndpoint, +} from './http-server'; const SequenceActions = RestBindings.SequenceActions; @@ -61,9 +61,9 @@ interface OpenApiSpecOptions { format?: string; } -const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecOptions} = { - '/openapi.json': {version: '3.0.0', format: 'json'}, - '/openapi.yaml': {version: '3.0.0', format: 'yaml'}, +const OPENAPI_SPEC_MAPPING: { [key: string]: OpenApiSpecOptions } = { + '/openapi.json': { version: '3.0.0', format: 'json' }, + '/openapi.yaml': { version: '3.0.0', format: 'yaml' }, }; /** @@ -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 @@ -192,7 +191,7 @@ export class RestServer extends Context implements Server, HttpServerLike { // at https://github.com/expressjs/cors/blob/master/lib/index.js only uses // http.ServerRequest/ServerResponse // tslint:disable-next-line:no-any - cors(corsOptions)(request as any, response as any, () => {}); + cors(corsOptions)(request as any, response as any, () => { }); if (request.method === 'OPTIONS') { return Promise.resolve(); } @@ -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,11 +319,11 @@ 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'}; + options = options || { version: '3.0.0', format: 'json' }; let specObj = this.getApiSpec(); if (options.format === 'json') { const spec = JSON.stringify(specObj, null, 2); @@ -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 8c166925ed4c..f06e635062f7 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -16,58 +16,28 @@ import { instantiateClass, ValueOrPromise, } from '@loopback/context'; -import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; import { - ParsedRequest, + Request, PathParameterValues, OperationArgs, OperationRetval, } from '../internal-types'; -import {ControllerSpec} from '@loopback/openapi-v3'; +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, // e.g. via wayfarer/trie or find-my-way // See https://github.com/strongloop/loopback-next/issues/98 import * as pathToRegexp from 'path-to-regexp'; -import {CoreBindings} from '@loopback/core'; +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; +export type ControllerInstance = { [name: string]: any } & object; /** * A factory function to create controller instances synchronously or @@ -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'); @@ -374,9 +344,9 @@ export class ControllerRoute extends BaseRoute { if (!methodName) { throw new Error( 'methodName must be provided either via the ControllerRoute argument ' + - 'or via "x-operation-name" extension field in OpenAPI spec. ' + - `Operation: "${verb} ${path}" ` + - `Controller: ${controllerName}.`, + 'or via "x-operation-name" extension field in OpenAPI spec. ' + + `Operation: "${verb} ${path}" ` + + `Controller: ${controllerName}.`, ); } diff --git a/packages/rest/src/sequence.ts b/packages/rest/src/sequence.ts index e57da1f97430..350565f56df3 100644 --- a/packages/rest/src/sequence.ts +++ b/packages/rest/src/sequence.ts @@ -4,8 +4,8 @@ // License text available at https://opensource.org/licenses/MIT const debug = require('debug')('loopback:core:sequence'); -import {ServerResponse} from 'http'; -import {inject, Context} from '@loopback/context'; +import { ServerResponse } from 'http'; +import { inject, Context } from '@loopback/context'; import { FindRoute, InvokeMethod, @@ -13,8 +13,9 @@ import { Send, Reject, ParseParams, + HttpContext, } from './internal-types'; -import {RestBindings} from './keys'; +import { RestBindings } from './keys'; const SequenceActions = RestBindings.SequenceActions; @@ -24,8 +25,7 @@ const SequenceActions = RestBindings.SequenceActions; */ export type SequenceFunction = ( sequence: DefaultSequence, - request: ParsedRequest, - response: ServerResponse, + httpCtx: HttpContext, ) => Promise | void; /** @@ -39,7 +39,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; } /** @@ -83,34 +83,42 @@ export class DefaultSequence implements SequenceHandler { @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, @inject(SequenceActions.SEND) public send: Send, @inject(SequenceActions.REJECT) public reject: Reject, - ) {} + ) { } /** - * Runs the default sequence. Given a request and response, running the - * sequence will produce a response or an error. - * - * Default sequence executes these steps - * - Finds the appropriate controller method, swagger spec - * and args for invocation - * - Parses HTTP request to get API argument list - * - Invokes the API which is defined in the Application Controller - * - Writes the result from API into the HTTP response - * - Error is caught and logged using 'logError' if any of the above steps - * in the sequence fails with an error. - * @param req Parsed incoming HTTP request - * @param res HTTP server response with result from Application controller - * method invocation - */ - async handle(req: ParsedRequest, res: ServerResponse) { + * Runs the default sequence. Given a request and response, running the + * sequence will produce a response or an error. + * + * Default sequence executes these steps + * - Finds the appropriate controller method, swagger spec + * and args for invocation + * - Parses HTTP request to get API argument list + * - Invokes the API which is defined in the Application Controller + * - Writes the result from API into the HTTP response + * - Error is caught and logged using 'logError' if any of the above steps + * in the sequence fails with an error. + * @param req Parsed incoming HTTP request + * @param res HTTP server response with result from Application controller + * method invocation + */ + 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/test/acceptance/bootstrapping/rest.acceptance.ts b/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts old mode 100644 new mode 100755 index 17315f0aa8d4..c98de2577b9d --- 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/module-exporting/feature.md b/packages/rest/test/acceptance/module-exporting/feature.md old mode 100644 new mode 100755 diff --git a/packages/rest/test/acceptance/routing/feature.md b/packages/rest/test/acceptance/routing/feature.md old mode 100644 new mode 100755 diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts old mode 100644 new mode 100755 index 6ee9d6fae8e4..23ba678e5099 --- 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 old mode 100644 new mode 100755 index f9c49c18fd98..19ae71cb261d --- 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/helpers.ts b/packages/rest/test/helpers.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/integration/certificate.pem b/packages/rest/test/integration/certificate.pem new file mode 100755 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 100755 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 old mode 100644 new mode 100755 index c03936de4b4c..5c1376a01508 --- 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 100755 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 old mode 100644 new mode 100755 index 4edda6e0f817..0bf6c1cb8f89 --- 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 old mode 100644 new mode 100755 index a221a4dc08b4..4e67e8202a15 --- 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/re-export.unit.ts b/packages/rest/test/unit/re-export.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/rest.application/rest.application.unit.ts b/packages/rest/test/unit/rest.application/rest.application.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/rest.component.unit.ts b/packages/rest/test/unit/rest.component.unit.ts old mode 100644 new mode 100755 index 2e22406243cf..3df9c468ed29 --- 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/rest.server/rest.server.controller.unit.ts b/packages/rest/test/unit/rest.server/rest.server.controller.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts b/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts old mode 100644 new mode 100755 index c114129737fd..681507244cd6 --- a/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts +++ b/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts @@ -3,17 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect, validateApiSpec} from '@loopback/testlab'; -import {Application} from '@loopback/core'; +import { expect, validateApiSpec } from '@loopback/testlab'; +import { Application } from '@loopback/core'; import { RestServer, Route, RestComponent, createControllerFactoryForClass, } from '../../..'; -import {get, post, requestBody} from '@loopback/openapi-v3'; -import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; -import {model, property} from '@loopback/repository'; +import { get, post, requestBody } from '@loopback/openapi-v3'; +import { anOpenApiSpec } from '@loopback/openapi-spec-builder'; +import { model, property } from '@loopback/repository'; describe('RestServer.getApiSpec()', () => { let app: Application; @@ -31,7 +31,7 @@ describe('RestServer.getApiSpec()', () => { title: 'Test API', version: '1.0.0', }, - servers: [{url: 'example.com:8080/api'}], + servers: [{ url: 'example.com:8080/api' }], paths: {}, 'x-foo': 'bar', }); @@ -43,16 +43,16 @@ describe('RestServer.getApiSpec()', () => { title: 'Test API', version: '1.0.0', }, - servers: [{url: 'example.com:8080/api'}], + servers: [{ url: 'example.com:8080/api' }], paths: {}, 'x-foo': 'bar', }); }); it('binds a route via app.route(route)', () => { - function greet() {} + function greet() { } const binding = server.route( - new Route('get', '/greet', {responses: {}}, greet), + new Route('get', '/greet', { responses: {} }, greet), ); expect(binding.key).to.eql('routes.get %2Fgreet'); expect(binding.tagNames).containEql('route'); @@ -60,13 +60,13 @@ describe('RestServer.getApiSpec()', () => { it('binds a route via app.route(..., Controller, method)', () => { class MyController { - greet() {} + greet() { } } const binding = server.route( 'get', '/greet.json', - {responses: {}}, + { responses: {} }, MyController, createControllerFactoryForClass(MyController), 'greet', @@ -76,8 +76,8 @@ describe('RestServer.getApiSpec()', () => { }); it('returns routes registered via app.route(route)', () => { - function greet() {} - server.route(new Route('get', '/greet', {responses: {}}, greet)); + function greet() { } + server.route(new Route('get', '/greet', { responses: {} }, greet)); const spec = server.getApiSpec(); expect(spec.paths).to.eql({ @@ -91,13 +91,13 @@ describe('RestServer.getApiSpec()', () => { it('returns routes registered via app.route(..., Controller, method)', () => { class MyController { - greet() {} + greet() { } } server.route( 'get', '/greet', - {responses: {}}, + { responses: {} }, MyController, createControllerFactoryForClass(MyController), 'greet', @@ -118,8 +118,8 @@ describe('RestServer.getApiSpec()', () => { it('honors tags in the operation spec', () => { class MyController { - @get('/greet', {responses: {}, tags: ['MyTag']}) - greet() {} + @get('/greet', { responses: {}, tags: ['MyTag'] }) + greet() { } } app.controller(MyController); @@ -139,7 +139,7 @@ describe('RestServer.getApiSpec()', () => { it('returns routes registered via app.controller()', () => { class MyController { @get('/greet') - greet() {} + greet() { } } app.controller(MyController); @@ -163,7 +163,7 @@ describe('RestServer.getApiSpec()', () => { } class MyController { @post('/foo') - createFoo(@requestBody() foo: MyModel) {} + createFoo(@requestBody() foo: MyModel) { } } app.controller(MyController); @@ -181,7 +181,7 @@ describe('RestServer.getApiSpec()', () => { }); it('preserves routes specified in app.api()', () => { - function status() {} + function status() { } server.api( anOpenApiSpec() .withOperation('get', '/status', { @@ -191,8 +191,8 @@ describe('RestServer.getApiSpec()', () => { .build(), ); - function greet() {} - server.route(new Route('get', '/greet', {responses: {}}, greet)); + function greet() { } + server.route(new Route('get', '/greet', { responses: {} }, greet)); const spec = server.getApiSpec(); expect(spec.paths).to.eql({ diff --git a/packages/rest/test/unit/rest.server/rest.server.unit.ts b/packages/rest/test/unit/rest.server/rest.server.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/router/controller-factory.test.ts b/packages/rest/test/unit/router/controller-factory.test.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/router/controller-route.unit.ts b/packages/rest/test/unit/router/controller-route.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/router/reject.provider.unit.ts b/packages/rest/test/unit/router/reject.provider.unit.ts old mode 100644 new mode 100755 diff --git a/packages/rest/test/unit/router/routing-table.unit.ts b/packages/rest/test/unit/router/routing-table.unit.ts old mode 100644 new mode 100755 index 4507dd055231..b63495bc3cb2 --- 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 old mode 100644 new mode 100755 index 77d651789c8e..935401dc8be5 --- 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);