简体   繁体   中英

Using a signed Google Maps API Geocoding request from a .NET Command Line Application

So I'm writing an application to cache geocoding data as I import records. I've got it working fine when I use an unsigned request, however I can't seem to figure out what's wrong when I try to use my company's clientid and signature. I always get a 403 Forbidden.

Here's my URL builder:

    private const string _googleUri = "http://maps.googleapis.com/maps/api/geocode/xml?address=";
    private const string _googleClientId = "XXXXXXXX";
    private const string _googleSignature = "XXXXXXXXXXXXXXXXXXXXXXXX";

//RESOLVED
    private static String GetGeocodeUri(string address)
    {
        ASCIIEncoding encoding = new ASCIIEncoding();
        string url = String.Format("{0}{1}&client={2}&sensor=false"
                                   , _googleUri
                                   , HttpUtility.UrlEncode(address)
                                   , _googleClientId);

        // converting key to bytes will throw an exception, need to replace '-' and '_' characters first.
        string usablePrivateKey = _googleSignature.Replace("-", "+").Replace("_", "/");
        byte[] privateKeyBytes = Convert.FromBase64String(usablePrivateKey);

        Uri uri = new Uri(url);
        byte[] encodedPathAndQueryBytes = encoding.GetBytes( uri.LocalPath + uri.Query );

        // compute the hash
        HMACSHA1 algorithm = new HMACSHA1(privateKeyBytes);
        byte[] hash = algorithm.ComputeHash(encodedPathAndQueryBytes);

        // convert the bytes to string and make url-safe by replacing '+' and '/' characters
        string signature = Convert.ToBase64String(hash).Replace("+", "-").Replace("/", "_");

        // Add the signature to the existing URI.
        return uri.Scheme + "://" + uri.Host + uri.LocalPath + uri.Query + "&signature=" + signature;

    } 

Here's the Program:

public static AddressClass GetResponseAddress(string address)
    {
        AddressClass GoogleAddress = new AddressClass();
        XmlDocument doc = new XmlDocument();
        String myUri = GetGeocodeUri(address);

        try
        {
            doc.Load(myUri);
            XmlNode root = doc.DocumentElement;
            if (root.SelectSingleNode("/GeocodeResponse/status").InnerText == "OK")
            {
                GoogleAddress.Latitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat").InnerText);
                GoogleAddress.Longitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat").InnerText);

            }
         }
         catch (Exception ex)
         {
            Console.WriteLine("Exception <" + ex.Message + ">");

         }           

        return GoogleAddress;
    }

Now, my initial reaction to it not working was that Google must be missing the referer domain because they must be registered. So I tried it with HttpWebRequest and set the referer to my domain, but still no dice.

//Not needed, Just an alternate method
public static AddressClass GetResponseAddress(string address)
    {
        AddressClass GoogleAddress = new AddressClass();
        WebClient client = new WebClient();
        XmlDocument doc = new XmlDocument();
        Uri myUri = new Uri(GetGeocodeUri(address));
        HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(myUri);
        myRequest.Referer = "http://www.myDomain.com/";

        //I've even tried pretending to be Chrome
        //myRequest.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7";

        try
        {
            doc.Load(myRequest.GetResponse().GetResponseStream());
            XmlNode root = doc.DocumentElement;
            if (root.SelectSingleNode("/GeocodeResponse/status").InnerText == "OK")
            {
                GoogleAddress.Latitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat").InnerText);
                GoogleAddress.Longitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat").InnerText);
            }
         }
         catch (Exception ex)
         {
              Console.WriteLine("Exception <" + ex.Message + ">");

         }

        return GoogleAddress;
    }

Any help would be much appreciated.

const String gmeClientID = "gme-myClientId";
const String key = "myGoogleKey";

var urlRequest = String.Format("/maps/api/geocode/json?latlng={0},{1}&sensor=false&client={2}",Latitude,Longitude,gmeClientID);

HMACSHA1 myhmacsha1 = new HMACSHA1();
myhmacsha1.Key = Convert.FromBase64String(key); 
var hash = myhmacsha1.ComputeHash(Encoding.ASCII.GetBytes(urlRequest));

var url = String.Format("http://maps.googleapis.com{0}&signature={1}", urlRequest, Convert.ToBase64String(hash).Replace("+", "-").Replace("/", "_"));

var request = (HttpWebRequest)HttpWebRequest.Create(url);

URL-encoding is sometimes necessary (see below) but not enough. Your problem is that you are not, in fact, signing your requests.

The value in your _googleSignature constant is not a signature, but your private cryptographic key , which is bad. Your private cryptographic key should never, ever be part of any request by itself.

Instead, you need to use it to generate a new signature for every unique request. Please see the Maps API for Business Authentication documentation, it also includes an example for Signing a URL in Java :)

When signing requests to the Google Maps API Web Services with your Maps API for Business client id and your private cryptographic key, the Referer header and source IP address are totally irrelevant ;)

URL-encoding is only necessary on the address parameter, as part of Building a Valid URL . You should never URL-encode your signature as it's already URL-safe by using the modified Base64 for URLs.

You probably need to properly URL-encode the parameters before you substitute them into the query string. You can use HttpUtility.UrlEncode if you're willing to import the System.Web assembly (and not use a client .NET profile) or you can include or borrow code from Microsoft's Web Protection Library to do this.

address = HttpUtility.UrlEncode(address); // better than Replace(" ", "+");

return String.Format("{0}{1}&client={2}&sensor=false&signature={3}",
                 _googleUri, address,
                 HttpUtility.UrlEncode(_googleClientId),
                 HttpUtility.UrlEncode(_googleSignature));

I think they will check if the Ip the request comes from matches the domain the signature was registered for.

Can you try to send the request from your webserver?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM