1- import { promisify } from 'util' ;
2- import { execFile as _execFile } from 'child_process' ;
31import axios from 'axios' ;
4- import { omit } from 'lodash' ;
52import getLogger from 'common/logger' ;
63
7- const TIMEOUT = 30 * 1000 ;
8-
9- const execFile = promisify ( _execFile ) ;
10-
11- // eslint-disable-next-line @typescript-eslint/no-var-requires
12- const keys = require ( '@root/keys.json' ) ;
13-
144const logger = getLogger ( 'geolocation' ) ;
155
16- type AirportAccessPoint = {
17- ssid : string ;
18- macAddress : string ;
19- signalStrength : number ;
20- channel : number ;
21- } ;
22-
23- function parseAirportAccessPoints ( str : string ) : AirportAccessPoint [ ] {
24- const MAC_RE = / (?: [ \d a - f ] { 2 } [: ] { 1 } ) { 5 } [ \d a - f ] { 2 } / i;
25-
26- return str . split ( '\n' ) . reduce ( ( acc , line ) => {
27- const mac = line . match ( MAC_RE ) ;
28-
29- if ( ! mac ) {
30- return acc ;
31- }
32-
33- const macStart = line . indexOf ( mac [ 0 ] ) ;
34- const [ macAddress , signalStrength , channel ] = line
35- . substr ( macStart )
36- . split ( / [ ] + / )
37- . map ( ( el ) => el . trim ( ) ) ;
38-
39- return [
40- ...acc ,
41- {
42- ssid : line . substr ( 0 , macStart ) . trim ( ) ,
43- macAddress,
44- signalStrength : parseInt ( signalStrength , 10 ) ,
45- channel : parseInt ( channel , 10 ) ,
46- } ,
47- ] ;
48- } , [ ] ) ;
49- }
50-
51- async function getwifiAccessPoints ( ) : Promise < AirportAccessPoint [ ] > {
52- const { stdout } = await execFile (
53- '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport' ,
54- [ '-s' ] ,
55- {
56- timeout : TIMEOUT ,
57- } ,
58- ) ;
59- return parseAirportAccessPoints ( stdout ) ;
60- }
61-
62- type GoogleGeolocationResponse = {
63- location : {
64- lat : number ;
65- lng : number ;
66- } ;
67- accuracy : number ;
68- } ;
69-
70- async function geolocate ( wifiAccessPoints ) : Promise < GoogleGeolocationResponse > {
71- const response = await axios . post < GoogleGeolocationResponse > (
72- 'https://www.googleapis.com/geolocation/v1/geolocate' ,
73- { wifiAccessPoints } ,
74- {
75- params : {
76- key : keys . google ,
77- } ,
78- } ,
79- ) ;
80-
81- return response . data ;
82- }
83-
846export type Location = {
857 latitude : number ;
868 longitude : number ;
879} ;
8810
89- async function getCurrentPosition ( ) : Promise < GeolocationPosition > {
90- const position = await new Promise < GeolocationPosition > ( ( resolve , reject ) => {
91- return navigator . geolocation . getCurrentPosition ( resolve , reject , { timeout : TIMEOUT } ) ;
92- } ) ;
11+ type IPIFYResponse = {
12+ ip : string ;
13+ } ;
14+
15+ type IPAPIResponse = {
16+ status : 'success' | 'fail' ;
17+ lat : number ;
18+ lon : number ;
19+ } ;
9320
94- return position ;
21+ async function getPublicIP ( ) : Promise < string > {
22+ logger . debug ( 'Obtaining public IP address.' ) ;
23+ const response = await axios . get < IPIFYResponse > ( 'https://api.ipify.org?format=json' ) ;
24+ return response . data . ip ;
9525}
9626
97- async function getLocationFromNavigator ( ) : Promise < Location > {
98- logger . debug ( 'Using the default IP-based geolocation.' ) ;
27+ async function geolocatePublicIP ( ) : Promise < Location > {
28+ const ip = await getPublicIP ( ) ;
29+ logger . debug ( `Using the ip-api geolocation with public IP address, ${ ip } .` ) ;
30+ const response = await axios . post < IPAPIResponse > (
31+ `http://ip-api.com/json/${ ip } ?fields=status,lat,lon` ,
32+ ) ;
9933
100- const position = await getCurrentPosition ( ) ;
101- const { latitude, longitude } = position . coords ;
34+ if ( response . data . status != 'success' ) {
35+ throw new GeolocationError (
36+ GeolocationPositionError . POSITION_UNAVAILABLE ,
37+ ) as GeolocationPositionError ;
38+ }
39+
40+ const { lat : latitude , lon : longitude } = response . data ;
10241
10342 return {
10443 latitude,
@@ -107,39 +46,18 @@ async function getLocationFromNavigator(): Promise<Location> {
10746}
10847
10948export async function getLocation ( ) : Promise < Location > {
110- try {
111- // geolocate using available WiFi acces points
112- const wifiAccessPoints = await getwifiAccessPoints ( ) ;
113-
114- if ( wifiAccessPoints . length > 0 ) {
115- const {
116- location : { lat : latitude , lng : longitude } ,
117- accuracy,
118- } = await geolocate (
119- wifiAccessPoints . reduce ( ( acc , wifi ) => [ ...acc , omit ( wifi , [ 'ssid' ] ) ] , [ ] ) ,
120- ) ;
121-
122- logger . debug (
123- `Geolocated using WiFi APs: [${ latitude } , ${ longitude } ], accuracy: ${ accuracy } m.` ,
124- ) ;
125-
126- if ( accuracy < 1000 ) {
127- logger . debug ( 'Location accuracy is < 1km, returning.' ) ;
128-
129- return {
130- latitude,
131- longitude,
132- } as Location ;
133- }
134- }
49+ return geolocatePublicIP ( ) ;
50+ }
13551
136- // fall back to IP geolocation if accurancy is more than 1km radius
137- // fall back to IP geolocation if no WiFi access points
138- return getLocationFromNavigator ( ) ;
139- } catch ( err ) {
140- logger . warn ( err ) ;
52+ class GeolocationError implements GeolocationPositionError {
53+ readonly code : number ;
54+ readonly message : string ;
55+ readonly PERMISSION_DENIED : number = GeolocationPositionError . PERMISSION_DENIED ;
56+ readonly POSITION_UNAVAILABLE : number = GeolocationPositionError . POSITION_UNAVAILABLE ;
57+ readonly TIMEOUT : number = GeolocationPositionError . TIMEOUT ;
14158
142- // fall back to IP geolocation on error
143- return getLocationFromNavigator ( ) ;
59+ constructor ( code : number , message ?: string ) {
60+ this . code = code ;
61+ this . message = message ;
14462 }
14563}
0 commit comments