Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rest): remove support for HTTP redirect to an external API Explorer #6290

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions docs/site/Binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,19 @@ dependency is not needed, `toDynamicValue` can be used instead.
#### An alias

An alias is the key with optional path to resolve the value from another
binding. For example, if we want to get options from RestServer for the API
explorer, we can configure the `apiExplorer.options` to be resolved from
`servers.RestServer.options#apiExplorer`.
binding. For example, if we want to get options from RestServer for the OpenAPI
Spec endpoints, we can configure the `openApiSpec.options` to be resolved from
`servers.RestServer.options#openApiSpec`.

```ts
ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}});
ctx
.bind('apiExplorer.options')
.toAlias('servers.RestServer.options#apiExplorer');
const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'}
.bind('servers.RestServer.options')
.to({openApiSpec: {setServersFromRequest: true}});
ctx
.bind('openApiSpec.options')
.toAlias('servers.RestServer.options#openApiSpec');
const openApiSpecOptions = await ctx.get('openApiSpec.options');
// => {setServersFromRequest: true}
```

### Configure the scope
Expand Down
55 changes: 9 additions & 46 deletions docs/site/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,54 +76,17 @@ const app = new RestApplication({

### Configure the API Explorer

LoopBack allows externally hosted API Explorer UI to render the OpenAPI
endpoints for a REST server. Such URLs can be specified with `rest.apiExplorer`:

- url: URL for the hosted API Explorer UI, default to
`https://explorer.loopback.io`.
- httpUrl: URL for the API explorer served over plain http to deal with mixed
content security imposed by browsers as the spec is exposed over `http` by
default. See https://github.com/strongloop/loopback-next/issues/1603. Default
to the value of `url`.

```ts
const app = new RestApplication({
rest: {
apiExplorer: {
url: 'https://petstore.swagger.io',
httpUrl: 'http://petstore.swagger.io',
},
},
});
```

#### Disable redirect to API Explorer

To disable redirect to the externally hosted API Explorer, set the config option
`rest.apiExplorer.disabled` to `true`.

```ts
const app = new RestApplication({
rest: {
apiExplorer: {
disabled: true,
},
},
});
```

{% include note.html content="To completely disable API Explorer, we also need
to [disable the self-hosted REST API Explorer extension](./Self-hosted-REST-API-Explorer.md#disable-self-hosted-api-explorer)." %}

### Use a self-hosted API Explorer

Hosting the API Explorer at an external URL has a few downsides, for example a
working internet connection is required to explore the API. As a recommended
alternative, LoopBack comes with an extension that provides a self-hosted
Explorer UI. Please refer to
Starting from `@loopback/rest` version `7.0.0`, LoopBack no longer supports
automatic redirect to an externally hosted API Explorer UI. Instead, we provide
an extension that implements a self-hosted API Explorer UI. Please refer to
[Self-hosted REST API Explorer](./Self-hosted-REST-API-Explorer.md) for more
details.

By default, applications scaffolded using `lb4 app` are coming with a
pre-configured API Explorer. See
[disable the self-hosted REST API Explorer extension](./Self-hosted-REST-API-Explorer.md#disable-self-hosted-api-explorer)
for instructions on how to disable this pre-configured API Explorer.

### Customize CORS

[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) is enabled
Expand Down Expand Up @@ -247,7 +210,7 @@ for more details.
| cors | CorsOptions | Specify the CORS options. |
| sequence | SequenceHandler | Use a custom SequenceHandler to change the behavior of the RestServer for the request-response lifecycle. |
| openApiSpec | OpenApiSpecOptions | Customize how OpenAPI spec is served |
| apiExplorer | ApiExplorerOptions | Customize how API explorer is served |
| apiExplorer | ApiExplorerOptions | **(DEPRECATED)** Customize how API explorer is served |
| requestBodyParser | RequestBodyParserOptions | Customize how request body is parsed |
| router | RouterOptions | Customize how trailing slashes are used for routing |
| listenOnStart | boolean (default to true) | Control if the server should listen on http/https when it's started |
Expand Down
176 changes: 14 additions & 162 deletions packages/rest/src/__tests__/integration/rest.server.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
import {OpenAPIObject} from '@loopback/openapi-v3';
import {
createClientForHandler,
createRestAppClient,
expect,
givenHttpServerConfig,
httpsGetAsync,
Expand All @@ -34,13 +33,13 @@ import {
Request,
requestBody,
RequestContext,
RestApplication,
RestBindings,
RestComponent,
RestServer,
RestServerConfig,
} from '../..';
import {RestTags} from '../../keys';
import {ApiExplorerOptions} from '../../rest.server';
const readFileAsync = util.promisify(fs.readFile);

const FIXTURES = path.resolve(__dirname, '../../../fixtures');
Expand Down Expand Up @@ -608,7 +607,7 @@ paths:
).to.throw(/already configured/);
});

it('exposes "GET /explorer" endpoint', async () => {
it('no longer exposes "GET /explorer" endpoint', async () => {
const app = new Application();
app.component(RestComponent);
const server = await app.getServer(RestServer);
Expand All @@ -622,20 +621,9 @@ paths:
};
server.route('get', '/greet', greetSpec, function greet() {});

const response = await createClientForHandler(server.requestHandler).get(
'/explorer',
);
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'http://explorer.loopback.io',
'\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json',
].join(''),
);
expect(response.status).to.equal(302);
expect(response.get('Location')).match(expectedUrl);
expect(response.get('Access-Control-Allow-Origin')).to.equal('*');
expect(response.get('Access-Control-Allow-Credentials')).to.equal('true');
await createClientForHandler(server.requestHandler)
.get('/explorer')
.expect(404);
});

it('can be configured to disable "GET /explorer"', async () => {
Expand All @@ -650,125 +638,17 @@ paths:
await request.get('/explorer').expect(404);
});

it('honors "x-forwarded-*" headers', async () => {
const app = new Application();
app.component(RestComponent);
const server = await app.getServer(RestServer);

const response = await createClientForHandler(server.requestHandler)
.get('/explorer')
.set('x-forwarded-proto', 'https')
.set('x-forwarded-host', 'example.com')
.set('x-forwarded-port', '8080');
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'https://explorer.loopback.io',
'\\?url=https://example.com:8080/openapi.json',
].join(''),
);
expect(response.get('Location')).match(expectedUrl);
});

it('honors "x-forwarded-host" headers', async () => {
const app = new Application();
app.component(RestComponent);
const server = await app.getServer(RestServer);

const response = await createClientForHandler(server.requestHandler)
.get('/explorer')
.set('x-forwarded-proto', 'http')
.set('x-forwarded-host', 'example.com:8080,my.example.com:9080');
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'http://explorer.loopback.io',
'\\?url=http://example.com:8080/openapi.json',
].join(''),
);
expect(response.get('Location')).match(expectedUrl);
});

it('skips port if it is the default for http or https', async () => {
const app = new Application();
app.component(RestComponent);
const server = await app.getServer(RestServer);

const response = await createClientForHandler(server.requestHandler)
.get('/explorer')
.set('x-forwarded-proto', 'https')
.set('x-forwarded-host', 'example.com')
.set('x-forwarded-port', '443');
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'https://explorer.loopback.io',
'\\?url=https://example.com/openapi.json',
].join(''),
);
expect(response.get('Location')).match(expectedUrl);
});

it('handles requests with missing Host header', async () => {
const app = new RestApplication({
rest: {port: 0, host: '127.0.0.1'},
});
await app.start();
const port = await app.restServer.get(RestBindings.PORT);

const response = await createRestAppClient(app)
.get('/explorer')
.set('host', '');
await app.stop();
const expectedUrl = new RegExp(`\\?url=http://127.0.0.1:${port}`);
expect(response.get('Location')).match(expectedUrl);
});

it('exposes "GET /explorer" endpoint with apiExplorer.url', async () => {
const server = await givenAServer({
rest: {
apiExplorer: {
url: 'https://petstore.swagger.io',
},
},
});

const response = await createClientForHandler(server.requestHandler).get(
'/explorer',
);
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'https://petstore.swagger.io',
'\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json',
].join(''),
);
expect(response.status).to.equal(302);
expect(response.get('Location')).match(expectedUrl);
});

it('exposes "GET /explorer" endpoint with apiExplorer.urlForHttp', async () => {
const server = await givenAServer({
rest: {
apiExplorer: {
url: 'https://petstore.swagger.io',
httpUrl: 'http://petstore.swagger.io',
it('throws an error when apiExplorer config is missing "disable" flag', () => {
return expect(
givenAServer({
rest: {
...givenHttpServerConfig(),
apiExplorer: {} as ApiExplorerOptions,
},
},
});

const response = await createClientForHandler(server.requestHandler).get(
'/explorer',
);
await server.get(RestBindings.PORT);
const expectedUrl = new RegExp(
[
'http://petstore.swagger.io',
'\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json',
].join(''),
}),
).to.be.rejectedWith(
/Externally hosted API Explorer is no longer supported/,
);
expect(response.status).to.equal(302);
expect(response.get('Location')).match(expectedUrl);
});

it('supports HTTPS protocol with key and certificate files', async () => {
Expand Down Expand Up @@ -825,34 +705,6 @@ paths:
await server.stop();
});

// https://github.com/strongloop/loopback-next/issues/1623
skipOnTravis(it, 'handles IPv6 address for API Explorer UI', async () => {
const keyPath = path.join(FIXTURES, 'key.pem');
const certPath = path.join(FIXTURES, 'cert.pem');
const server = await givenAServer({
rest: {
port: 0,
host: '::1',
protocol: 'https',
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
},
});
server.handler(dummyRequestHandler);
await server.start();
const serverUrl = server.getSync(RestBindings.URL);

// The `Location` header should be something like
// https://explorer.loopback.io?url=https://[::1]:58470/openapi.json
const res = await httpsGetAsync(serverUrl + '/explorer');
const location = res.headers['location'];
expect(location).to.match(/\[\:\:1\]\:\d+\/openapi.json/);
expect(location).to.equal(
`https://explorer.loopback.io?url=${serverUrl}/openapi.json`,
);
await server.stop();
});

it('honors HTTPS config binding after instantiation', async () => {
const keyPath = path.join(FIXTURES, 'key.pem');
const certPath = path.join(FIXTURES, 'cert.pem');
Expand Down
Loading