diff --git a/src/provider/google-maps/command/google-maps-geocode.command.ts b/src/provider/google-maps/command/google-maps-geocode.command.ts index 1881f3a..50dad54 100644 --- a/src/provider/google-maps/command/google-maps-geocode.command.ts +++ b/src/provider/google-maps/command/google-maps-geocode.command.ts @@ -3,17 +3,18 @@ import { GeocodeCommand } from '../../../command'; import type { GeocodeQuery } from '../../../model'; import type { GoogleMapsGeocodeQueryInterface } from '../interface'; import { GoogleMapsLocationCommandMixin } from './mixin'; +import { urlSign } from '../../../util/url-signing'; /** * @link {https://developers.google.com/maps/documentation/geocoding/intro#GeocodingRequests} */ export class GoogleMapsGeocodeCommand extends GoogleMapsLocationCommandMixin(GeocodeCommand) { - constructor(httpClient: AxiosInstance, private readonly apiKey: string) { - super(httpClient, apiKey); + constructor(httpClient: AxiosInstance, private readonly apiKey: string, private readonly secret?: string) { + super(httpClient, apiKey, secret); } static getUrl(): string { - return 'https://maps.googleapis.com/maps/api/geocode/json'; + return 'https://maps.googleapis.com/maps/api/geocode/json' } protected async buildQuery(query: GeocodeQuery): Promise { @@ -46,6 +47,10 @@ export class GoogleMapsGeocodeCommand extends GoogleMapsLocationCommandMixin(Geo providerQuery.region = `.${query.countryCode.toLowerCase()}`; } + if (this.secret) { + providerQuery.signature = urlSign('/maps/api/geocode/json', providerQuery, this.secret); + } + return providerQuery; } } diff --git a/src/provider/google-maps/command/google-maps-reverse.command.ts b/src/provider/google-maps/command/google-maps-reverse.command.ts index ee275a0..668776e 100644 --- a/src/provider/google-maps/command/google-maps-reverse.command.ts +++ b/src/provider/google-maps/command/google-maps-reverse.command.ts @@ -3,14 +3,15 @@ import { ReverseCommand } from '../../../command'; import type { ReverseQuery } from '../../../model'; import type { GoogleMapsReverseQueryInterface } from '../interface'; import { GoogleMapsLocationCommandMixin } from './mixin'; +import { urlSign } from '../../../util/url-signing'; /** * TODO implement result_type and location_type * @link {https://developers.google.com/maps/documentation/geocoding/intro#ReverseGeocoding} */ export class GoogleMapsReverseCommand extends GoogleMapsLocationCommandMixin(ReverseCommand) { - constructor(httpClient: AxiosInstance, private readonly apiKey: string) { - super(httpClient, apiKey); + constructor(httpClient: AxiosInstance, private readonly apiKey: string, private readonly secret?: string) { + super(httpClient, apiKey, secret); } static getUrl(): string { @@ -30,6 +31,10 @@ export class GoogleMapsReverseCommand extends GoogleMapsLocationCommandMixin(Rev providerQuery.region = `.${query.countryCode.toLowerCase()}`; } + if (this.secret) { + providerQuery.signature = urlSign('/maps/api/geocode/json', providerQuery, this.secret); + } + return providerQuery; } } diff --git a/src/provider/google-maps/google-maps.provider.ts b/src/provider/google-maps/google-maps.provider.ts index e3327c8..6874252 100644 --- a/src/provider/google-maps/google-maps.provider.ts +++ b/src/provider/google-maps/google-maps.provider.ts @@ -9,10 +9,10 @@ import { } from './command'; export class GoogleMapsProvider extends AbstractHttpProvider { - constructor(httpClient: AxiosInstance, apiKey: string) { + constructor(httpClient: AxiosInstance, apiKey: string, secret?: string) { super({ - geocode: new GoogleMapsGeocodeCommand(httpClient, apiKey), - reverse: new GoogleMapsReverseCommand(httpClient, apiKey), + geocode: new GoogleMapsGeocodeCommand(httpClient, apiKey, secret), + reverse: new GoogleMapsReverseCommand(httpClient, apiKey, secret), suggest: new GoogleMapsSuggestCommand(httpClient, apiKey), placeDetails: new GoogleMapsPlaceDetailsCommand(httpClient, apiKey), distance: new GoogleMapsDistanceCommand(httpClient, apiKey), diff --git a/src/provider/google-maps/interface/google-maps-query.interface.ts b/src/provider/google-maps/interface/google-maps-query.interface.ts index 3a39d9a..9dac4ce 100644 --- a/src/provider/google-maps/interface/google-maps-query.interface.ts +++ b/src/provider/google-maps/interface/google-maps-query.interface.ts @@ -7,4 +7,9 @@ export interface GoogleMapsQueryInterface { */ region?: string; language: string; + /** + * Signature for signed request to Google Maps API to bypass the 25k requests/day limit + * https://developers.google.com/maps/documentation/maps-static/digital-signature#server-side-signing + */ + signature?: string; } diff --git a/src/util/url-signing/index.ts b/src/util/url-signing/index.ts new file mode 100644 index 0000000..f1d27ad --- /dev/null +++ b/src/util/url-signing/index.ts @@ -0,0 +1 @@ +export * from './url-signing'; diff --git a/src/util/url-signing/url-signing.ts b/src/util/url-signing/url-signing.ts new file mode 100644 index 0000000..1357184 --- /dev/null +++ b/src/util/url-signing/url-signing.ts @@ -0,0 +1,59 @@ +import crypto from 'crypto'; + +/** + * Convert from 'web safe' base64 to true base64. + * + * @param {string} safeEncodedString The code you want to translate + * from a web safe form. + * @return {string} + */ +function removeWebSafe(safeEncodedString: string): string { + return safeEncodedString.replace(/-/g, '+').replace(/_/g, '/'); +} + +/** + * Convert from true base64 to 'web safe' base64 + * + * @param {string} encodedString The code you want to translate to a + * web safe form. + * @return {string} + */ +function makeWebSafe(encodedString: string): string { + return encodedString.replace(/\+/g, '-').replace(/\//g, '_'); +} + +/** + * Takes a base64 code and decodes it. + * + * @param {string} code The encoded data. + * @return {string} + */ +function decodeBase64Hash(code: string): Buffer { + // "new Buffer(...)" is deprecated. Use Buffer.from if it exists. + return Buffer.from ? Buffer.from(code, 'base64') : new Buffer(code, 'base64'); +} + +/** + * Takes a key and signs the data with it. + * + * @param {string} key Your unique secret key. + * @param {string} data The url to sign. + * @return {string} + */ +function encodeBase64Hash(key: string, data: string): string { + return crypto.createHmac('sha1', key).update(data).digest('base64'); +} + +/** + * Sign a URL using a secret key. + * + * @param {string} path The url you want to sign. + * @param {string} secret Your unique secret key. + * @param {string} query Query object + * @return {string} + */ +export function urlSign(path: string, query: any, secret: string, ): string { + const queryString = new URLSearchParams(JSON.parse(JSON.stringify(query))).toString(); + const safeSecret = decodeBase64Hash(removeWebSafe(secret)).toString(); + return makeWebSafe(encodeBase64Hash(safeSecret, path + queryString)); +}