Skip to content

Commit d0ede2b

Browse files
committedJul 2, 2024
refactor(provider): adds signing to google maps provider requests geocode and reverse
ref #FSMWO-1806
1 parent 3a9cf75 commit d0ede2b

File tree

6 files changed

+86
-8
lines changed

6 files changed

+86
-8
lines changed
 

‎src/provider/google-maps/command/google-maps-geocode.command.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { GeocodeCommand } from '../../../command';
33
import type { GeocodeQuery } from '../../../model';
44
import type { GoogleMapsGeocodeQueryInterface } from '../interface';
55
import { GoogleMapsLocationCommandMixin } from './mixin';
6+
import { urlSign } from '../../../util/url-signing';
67

78
/**
89
* @link {https://developers.google.com/maps/documentation/geocoding/intro#GeocodingRequests}
910
*/
1011
export class GoogleMapsGeocodeCommand extends GoogleMapsLocationCommandMixin(GeocodeCommand)<GoogleMapsGeocodeQueryInterface> {
11-
constructor(httpClient: AxiosInstance, private readonly apiKey: string) {
12-
super(httpClient, apiKey);
12+
constructor(httpClient: AxiosInstance, private readonly apiKey: string, private readonly secret?: string) {
13+
super(httpClient, apiKey, secret);
1314
}
1415

1516
static getUrl(): string {
16-
return 'https://maps.googleapis.com/maps/api/geocode/json';
17+
return 'https://maps.googleapis.com/maps/api/geocode/json'
1718
}
1819

1920
protected async buildQuery(query: GeocodeQuery): Promise<GoogleMapsGeocodeQueryInterface> {
@@ -46,6 +47,10 @@ export class GoogleMapsGeocodeCommand extends GoogleMapsLocationCommandMixin(Geo
4647
providerQuery.region = `.${query.countryCode.toLowerCase()}`;
4748
}
4849

50+
if (this.secret) {
51+
providerQuery.signature = urlSign('/maps/api/geocode/json', providerQuery, this.secret);
52+
}
53+
4954
return providerQuery;
5055
}
5156
}

‎src/provider/google-maps/command/google-maps-reverse.command.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { ReverseCommand } from '../../../command';
33
import type { ReverseQuery } from '../../../model';
44
import type { GoogleMapsReverseQueryInterface } from '../interface';
55
import { GoogleMapsLocationCommandMixin } from './mixin';
6+
import { urlSign } from '../../../util/url-signing';
67

78
/**
89
* TODO implement result_type and location_type
910
* @link {https://developers.google.com/maps/documentation/geocoding/intro#ReverseGeocoding}
1011
*/
1112
export class GoogleMapsReverseCommand extends GoogleMapsLocationCommandMixin(ReverseCommand)<GoogleMapsReverseQueryInterface> {
12-
constructor(httpClient: AxiosInstance, private readonly apiKey: string) {
13-
super(httpClient, apiKey);
13+
constructor(httpClient: AxiosInstance, private readonly apiKey: string, private readonly secret?: string) {
14+
super(httpClient, apiKey, secret);
1415
}
1516

1617
static getUrl(): string {
@@ -30,6 +31,10 @@ export class GoogleMapsReverseCommand extends GoogleMapsLocationCommandMixin(Rev
3031
providerQuery.region = `.${query.countryCode.toLowerCase()}`;
3132
}
3233

34+
if (this.secret) {
35+
providerQuery.signature = urlSign('/maps/api/geocode/json', providerQuery, this.secret);
36+
}
37+
3338
return providerQuery;
3439
}
3540
}

‎src/provider/google-maps/google-maps.provider.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99
} from './command';
1010

1111
export class GoogleMapsProvider extends AbstractHttpProvider {
12-
constructor(httpClient: AxiosInstance, apiKey: string) {
12+
constructor(httpClient: AxiosInstance, apiKey: string, secret?: string) {
1313
super({
14-
geocode: new GoogleMapsGeocodeCommand(httpClient, apiKey),
15-
reverse: new GoogleMapsReverseCommand(httpClient, apiKey),
14+
geocode: new GoogleMapsGeocodeCommand(httpClient, apiKey, secret),
15+
reverse: new GoogleMapsReverseCommand(httpClient, apiKey, secret),
1616
suggest: new GoogleMapsSuggestCommand(httpClient, apiKey),
1717
placeDetails: new GoogleMapsPlaceDetailsCommand(httpClient, apiKey),
1818
distance: new GoogleMapsDistanceCommand(httpClient, apiKey),

‎src/provider/google-maps/interface/google-maps-query.interface.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ export interface GoogleMapsQueryInterface {
77
*/
88
region?: string;
99
language: string;
10+
/**
11+
* Signature for signed request to Google Maps API to bypass the 25k requests/day limit
12+
* https://developers.google.com/maps/documentation/maps-static/digital-signature#server-side-signing
13+
*/
14+
signature?: string;
1015
}

‎src/util/url-signing/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './url-signing';

‎src/util/url-signing/url-signing.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Example from https://googlemaps.github.io/url-signing/urlSigner.js
3+
*/
4+
import crypto from 'crypto';
5+
6+
/**
7+
* Convert from 'web safe' base64 to true base64.
8+
*
9+
* @param {string} safeEncodedString The code you want to translate
10+
* from a web safe form.
11+
* @return {string}
12+
*/
13+
function removeWebSafe(safeEncodedString: string): string {
14+
return safeEncodedString.replace(/-/g, '+').replace(/_/g, '/');
15+
}
16+
17+
/**
18+
* Convert from true base64 to 'web safe' base64
19+
*
20+
* @param {string} encodedString The code you want to translate to a
21+
* web safe form.
22+
* @return {string}
23+
*/
24+
function makeWebSafe(encodedString: string): string {
25+
return encodedString.replace(/\+/g, '-').replace(/\//g, '_');
26+
}
27+
28+
/**
29+
* Takes a base64 code and decodes it.
30+
*
31+
* @param {string} code The encoded data.
32+
* @return {Buffer}
33+
*/
34+
function decodeBase64Hash(code: string): Buffer {
35+
// "new Buffer(...)" is deprecated. Use Buffer.from if it exists.
36+
return Buffer.from ? Buffer.from(code, 'base64') : new Buffer(code, 'base64');
37+
}
38+
39+
/**
40+
* Takes a key and signs the data with it.
41+
*
42+
* @param {string} key Your unique secret key.
43+
* @param {string} data The url to sign.
44+
* @return {string}
45+
*/
46+
function encodeBase64Hash(key: string, data: string): string {
47+
return crypto.createHmac('sha1', key).update(data).digest('base64');
48+
}
49+
50+
/**
51+
* Sign a URL using a secret key.
52+
*
53+
* @param {string} path The url you want to sign.
54+
* @param {string} secret Your unique secret key.
55+
* @param {Object} query Query object
56+
* @return {string}
57+
*/
58+
export function urlSign(path: string, query: any, secret: string, ): string {
59+
const queryString = new URLSearchParams(JSON.stringify(query)).toString();
60+
const safeSecret = decodeBase64Hash(removeWebSafe(secret)).toString('base64');
61+
return makeWebSafe(encodeBase64Hash(safeSecret, path + queryString));
62+
}

0 commit comments

Comments
 (0)