Skip to content

Commit

Permalink
Migrate url shortener service (#50896)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Dec 11, 2019
1 parent 489b39c commit b6ea699
Show file tree
Hide file tree
Showing 16 changed files with 504 additions and 89 deletions.
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# App
/x-pack/legacy/plugins/lens/ @elastic/kibana-app
/x-pack/legacy/plugins/graph/ @elastic/kibana-app
/src/plugins/share/ @elastic/kibana-app
/src/legacy/server/url_shortening/ @elastic/kibana-app
/src/legacy/server/sample_data/ @elastic/kibana-app

# App Architecture
Expand All @@ -14,7 +16,6 @@
/src/plugins/kibana_react/ @elastic/kibana-app-arch
/src/plugins/kibana_utils/ @elastic/kibana-app-arch
/src/plugins/navigation/ @elastic/kibana-app-arch
/src/plugins/share/ @elastic/kibana-app-arch
/src/plugins/ui_actions/ @elastic/kibana-app-arch
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
Expand All @@ -28,7 +29,6 @@
/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch
/src/legacy/server/index_patterns/ @elastic/kibana-app-arch
/src/legacy/server/url_shortening/ @elastic/kibana-app-arch

# APM
/x-pack/legacy/plugins/apm/ @elastic/apm-ui
Expand Down
2 changes: 0 additions & 2 deletions src/legacy/server/url_shortening/routes/create_routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@

import { shortUrlLookupProvider } from './lib/short_url_lookup';
import { createGotoRoute } from './goto';
import { createShortenUrlRoute } from './shorten_url';


export function createRoutes(server) {
const shortUrlLookup = shortUrlLookupProvider(server);

server.route(createGotoRoute({ server, shortUrlLookup }));
server.route(createShortenUrlRoute({ shortUrlLookup }));
}
8 changes: 1 addition & 7 deletions src/legacy/server/url_shortening/routes/goto.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,12 @@ import { shortUrlAssertValid } from './lib/short_url_assert_valid';

export const createGotoRoute = ({ server, shortUrlLookup }) => ({
method: 'GET',
path: '/goto/{urlId}',
path: '/goto_LP/{urlId}',
handler: async function (request, h) {
try {
const url = await shortUrlLookup.getUrl(request.params.urlId, request);
shortUrlAssertValid(url);

const uiSettings = request.getUiSettingsService();
const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage');
if (!stateStoreInSessionStorage) {
return h.redirect(request.getBasePath() + url);
}

const app = server.getHiddenUiAppById('stateSessionStorageRedirect');
return h.renderApp(app, {
redirectUrl: url,
Expand Down
24 changes: 0 additions & 24 deletions src/legacy/server/url_shortening/routes/lib/short_url_lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* under the License.
*/

import crypto from 'crypto';
import { get } from 'lodash';

export function shortUrlLookupProvider(server) {
Expand All @@ -34,29 +33,6 @@ export function shortUrlLookupProvider(server) {
}

return {
async generateUrlId(url, req) {
const id = crypto.createHash('md5').update(url).digest('hex');
const savedObjectsClient = req.getSavedObjectsClient();
const { isConflictError } = savedObjectsClient.errors;

try {
const doc = await savedObjectsClient.create('url', {
url,
accessCount: 0,
createDate: new Date(),
accessDate: new Date()
}, { id });

return doc.id;
} catch (error) {
if (isConflictError(error)) {
return id;
}

throw error;
}
},

async getUrl(id, req) {
const doc = await req.getSavedObjectsClient().get('url', id);
updateMetadata(doc, req);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,43 +48,6 @@ describe('shortUrlLookupProvider', () => {
sandbox.restore();
});

describe('generateUrlId', () => {
it('returns the document id', async () => {
const id = await shortUrl.generateUrlId(URL, req);
expect(id).toEqual(ID);
});

it('provides correct arguments to savedObjectsClient', async () => {
await shortUrl.generateUrlId(URL, req);

sinon.assert.calledOnce(savedObjectsClient.create);
const [type, attributes, options] = savedObjectsClient.create.getCall(0).args;

expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']);
expect(attributes.url).toEqual(URL);
expect(options.id).toEqual(ID);
});

it('passes persists attributes', async () => {
await shortUrl.generateUrlId(URL, req);

sinon.assert.calledOnce(savedObjectsClient.create);
const [type, attributes] = savedObjectsClient.create.getCall(0).args;

expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']);
expect(attributes.url).toEqual(URL);
});

it('gracefully handles version conflict', async () => {
const error = savedObjectsClient.errors.decorateConflictError(new Error());
savedObjectsClient.create.throws(error);
const id = await shortUrl.generateUrlId(URL, req);
expect(id).toEqual(ID);
});
});

describe('getUrl', () => {
beforeEach(() => {
const attributes = { accessCount: 2, url: URL };
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/share/kibana.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "share",
"version": "kibana",
"server": false,
"server": true,
"ui": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,9 @@
* under the License.
*/

import { handleShortUrlError } from './lib/short_url_error';
import { shortUrlAssertValid } from './lib/short_url_assert_valid';
import { PluginInitializerContext } from '../../../core/server';
import { SharePlugin } from './plugin';

export const createShortenUrlRoute = ({ shortUrlLookup }) => ({
method: 'POST',
path: '/api/shorten_url',
handler: async function (request) {
try {
shortUrlAssertValid(request.payload.url);
const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request);
return { urlId };
} catch (err) {
throw handleShortUrlError(err);
}
}
});
export function plugin(initializerContext: PluginInitializerContext) {
return new SharePlugin(initializerContext);
}
37 changes: 37 additions & 0 deletions src/plugins/share/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
import { createRoutes } from './routes/create_routes';

export class SharePlugin implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}

public async setup(core: CoreSetup) {
createRoutes(core, this.initializerContext.logger.get());
}

public start() {
this.initializerContext.logger.get().debug('Starting plugin');
}

public stop() {
this.initializerContext.logger.get().debug('Stopping plugin');
}
}
32 changes: 32 additions & 0 deletions src/plugins/share/server/routes/create_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CoreSetup, Logger } from 'kibana/server';

import { shortUrlLookupProvider } from './lib/short_url_lookup';
import { createGotoRoute } from './goto';
import { createShortenUrlRoute } from './shorten_url';

export function createRoutes({ http }: CoreSetup, logger: Logger) {
const shortUrlLookup = shortUrlLookupProvider({ logger });
const router = http.createRouter();

createGotoRoute({ router, shortUrlLookup, http });
createShortenUrlRoute({ router, shortUrlLookup });
}
64 changes: 64 additions & 0 deletions src/plugins/share/server/routes/goto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CoreSetup, IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';

import { shortUrlAssertValid } from './lib/short_url_assert_valid';
import { ShortUrlLookupService } from './lib/short_url_lookup';

export const createGotoRoute = ({
router,
shortUrlLookup,
http,
}: {
router: IRouter;
shortUrlLookup: ShortUrlLookupService;
http: CoreSetup['http'];
}) => {
router.get(
{
path: '/goto/{urlId}',
validate: {
params: schema.object({ urlId: schema.string() }),
},
},
router.handleLegacyErrors(async function(context, request, response) {
const url = await shortUrlLookup.getUrl(request.params.urlId, {
savedObjects: context.core.savedObjects.client,
});
shortUrlAssertValid(url);

const uiSettings = context.core.uiSettings.client;
const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage');
if (!stateStoreInSessionStorage) {
return response.redirected({
headers: {
location: http.basePath.prepend(url),
},
});
}
return response.redirected({
headers: {
location: http.basePath.prepend('/goto_LP/' + request.params.urlId),
},
});
})
);
};
63 changes: 63 additions & 0 deletions src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { shortUrlAssertValid } from './short_url_assert_valid';

describe('shortUrlAssertValid()', () => {
const invalid = [
['protocol', 'http://localhost:5601/app/kibana'],
['protocol', 'https://localhost:5601/app/kibana'],
['protocol', 'mailto:foo@bar.net'],
['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url
['hostname', 'localhost/app/kibana'],
['hostname and port', 'local.host:5601/app/kibana'],
['hostname and auth', 'user:pass@localhost.net/app/kibana'],
['path traversal', '/app/../../not-kibana'],
['deep path', '/app/kibana/foo'],
['deep path', '/app/kibana/foo/bar'],
['base path', '/base/app/kibana'],
];

invalid.forEach(([desc, url]) => {
it(`fails when url has ${desc}`, () => {
try {
shortUrlAssertValid(url);
throw new Error(`expected assertion to throw`);
} catch (err) {
if (!err || !err.isBoom) {
throw err;
}
}
});
});

const valid = [
'/app/kibana',
'/app/monitoring#angular/route',
'/app/text#document-id',
'/app/some?with=query',
'/app/some?with=query#and-a-hash',
];

valid.forEach(url => {
it(`allows ${url}`, () => {
shortUrlAssertValid(url);
});
});
});
Loading

0 comments on commit b6ea699

Please sign in to comment.