35703

Remote host closed connection during handshake with Nest API

Question:

Trying out the Nest API, I got the OAuth flow working without problems, made the first API call (to https://developer-api.nest.com/devices.json), got the 307 redirect as expected, but then my call to the redirect location fails with Remote host closed connection during handshake. I went to the Nest developer event in San Francisco last night, and <strong>Lev Stesin</strong> told me to post a full log here and mention his name.

Code (Apex, running on Force.com):

public with sharing virtual class NestController { public class OAuthResponse { public String access_token; public String token_type; public Integer expires_in; public String refresh_token; public String error; } public static OAuthResponse parse(String json) { return (OAuthResponse) System.JSON.deserialize(json, OAuthResponse.class); } public String accessToken {get; set;} public String output {get; set;} private String getAll(String accessToken) { String url = 'https://developer-api.nest.com/devices.json?auth='+accessToken+'&print=pretty'; HttpRequest req = new HttpRequest(); req.setEndpoint(url); req.setMethod('GET'); req.setTimeout(60*1000); Http h = new Http(); String resp; HttpResponse res = h.send(req); resp = res.getBody(); if (res.getStatusCode() == 307) { url = res.getHeader('Location'); System.debug('Redirect to: '+url); req = new HttpRequest(); req.setEndpoint(url); req.setMethod('GET'); req.setTimeout(60*1000); h = new Http(); res = h.send(req); resp = res.getBody(); } System.debug('Get returns: '+resp); return resp; } public virtual PageReference login() { String clientId = '989360fb-9a1f-4d13-929e-0b40111c725a'; String clientSecret = 'SECRET'; String sessionId = null; String state = 'wow'; // Get a URL for the page without any query params String url = ApexPages.currentPage().getUrl().split('\\?')[0]; System.debug('url is '+url); // note: connect url in fb application connect setting should be: https://c.na3.visual.force.com/apex/ // you need the trailing slash even though it bitches about it String rediruri = 'https://'+ApexPages.currentPage().getHeaders().get('Host')+url; System.debug('rediruri is:'+rediruri); String authuri = 'https://home.nest.com/login/oauth2'+ '?client_id='+clientId+ '&state='+state; // No session PageReference pageRef; if (ApexPages.currentPage().getParameters().containsKey('error')) { // Initial step of OAuth - redirect to OAuth service System.debug('Error:' + ApexPages.currentPage().getParameters().get('error')); return null; } if (! ApexPages.currentPage().getParameters().containsKey('code')) { // Initial step of OAuth - redirect to OAuth service System.debug('Nest OAuth Step 1'); return new PageReference(authuri); } // Second step of OAuth - get token from OAuth service String code = ApexPages.currentPage().getParameters().get('code'); System.debug('Nest OAuth Step 2 - code:'+code); String tokenuri = 'https://api.home.nest.com/oauth2/access_token'; String body = 'code='+code+ '&client_id='+clientId+ '&client_secret='+clientSecret+ '&grant_type=authorization_code'; System.debug('body is:'+body); HttpRequest req = new HttpRequest(); req.setEndpoint(tokenuri); req.setMethod('POST'); req.setTimeout(60*1000); req.setBody(body); Http h = new Http(); String resp; if (code.equals('TEST')) { resp = 'access_token=TEST&expires=3600'; } else { HttpResponse res = h.send(req); resp = res.getBody(); } System.debug('FINAL RESP IS:'+resp); OAuthResponse oauth = parse(resp); if (oauth.error != null) { // Error getting token - probably reusing code - start again return new PageReference(authuri); } accessToken = oauth.access_token; output = getAll(accessToken); return null; } }

Initial OAuth Redirect:

https://home.nest.com/login/oauth2?client_id=989360fb-9a1f-4d13-929e-0b40111c725a&state=wow

User authorizes app to access thermostats, Nest redirects back to my app:

https://c.na9.visual.force.com/apex/Nest?state=wow&code=6F3GV6WQ35NGLYB2

I successfully exchange the code for an access token:

POST to https://api.home.nest.com/oauth2/access_token with body

code=6F3GV6WQ35NGLYB2&client_id=989360fb-9a1f-4d13-929e-0b40111c725a&client_secret=SECRET&grant_type=authorization_code

Response:

{"access_token":"c.eDzTiwBeVak0Jq7RWVjBJPXrZT8kI5Hh4rgnYG7eDvzytZbqTJbMsnGBHLUcKOSZ7xjk8NR4oNAE4iUh1EBtkHllg55C0Ckb29jsSqL5VwdMxSUoTSBDkKt8QzMAoUCD3Ru8iSo7XYpPc8qU","expires_in":315360000}

(I revoked the token from home.nest.com, so it's safe for me to post here!)

So I do a GET on

https://developer-api.nest.com/devices.json?auth=c.eDzTiwBeVak0Jq7RWVjBJPXrZT8kI5Hh4rgnYG7eDvzytZbqTJbMsnGBHLUcKOSZ7xjk8NR4oNAE4iUh1EBtkHllg55C0Ckb29jsSqL5VwdMxSUoTSBDkKt8QzMAoUCD3Ru8iSo7XYpPc8qU&print=pretty

and receive the expected 307 redirect, with location

https://firebase-apiserver01-tah01-iad01.dapi.production.nest.com:9553/devices.json?auth=c.eDzTiwBeVak0Jq7RWVjBJPXrZT8kI5Hh4rgnYG7eDvzytZbqTJbMsnGBHLUcKOSZ7xjk8NR4oNAE4iUh1EBtkHllg55C0Ckb29jsSqL5VwdMxSUoTSBDkKt8QzMAoUCD3Ru8iSo7XYpPc8qU&print=pretty

Now, when I GET that URL in my Apex code running on Force.com, it fails with

System.CalloutException: Remote host closed connection during handshake

But if I do the same GET from curl on the command line, it succeeds, returning the expected JSON response.

So it looks like there may be some incompatibility in the SSL handshake. I'll investigate at the Force.com end; it would be good if someone at Nest could check the logs at their end - there should be enough detail here.

<strong>EDIT</strong> - Here's the output from curl -v to that URL:

$ curl -v 'https://firebase-apiserver01-tah01-iad01.dapi.production.nest.com:9553/devices.json?auth=c.dPHNEweWehQ47tzSm0sf13o8rX1isO9IdEG1HFwoAmeA2FtBLH1fTiksRtN9DGcPAOyEI3VINz2fD3CFma5ozSNbpqUIwGDGc8ixD1etjiIW6TmXN0Rd0p5VzEtk6sDwIe8j10NH1hKDhevX&print=pretty' * About to connect() to firebase-apiserver01-tah01-iad01.dapi.production.nest.com port 9553 (#0) * Trying 54.196.205.148... * connected * Connected to firebase-apiserver01-tah01-iad01.dapi.production.nest.com (54.196.205.148) port 9553 (#0) * SSLv3, TLS handshake, Client hello (1): * SSLv3, TLS handshake, Server hello (2): * SSLv3, TLS handshake, CERT (11): * SSLv3, TLS handshake, Server key exchange (12): * SSLv3, TLS handshake, Server finished (14): * SSLv3, TLS handshake, Client key exchange (16): * SSLv3, TLS change cipher, Client hello (1): * SSLv3, TLS handshake, Finished (20): * SSLv3, TLS change cipher, Client hello (1): * SSLv3, TLS handshake, Finished (20): * SSL connection using EDH-RSA-DES-CBC3-SHA * Server certificate: * subject: OU=Domain Control Validated; CN=*.dapi.production.nest.com * start date: 2014-05-28 22:31:28 GMT * expire date: 2015-05-28 22:31:28 GMT * subjectAltName: firebase-apiserver01-tah01-iad01.dapi.production.nest.com matched * issuer: C=US; ST=Arizona; L=Scottsdale; O=GoDaddy.com, Inc.; OU=http://certs.godaddy.com/repository/; CN=Go Daddy Secure Certificate Authority - G2 * SSL certificate verify ok. > GET /devices.json?auth=c.dPHNEweWehQ47tzSm0sf13o8rX1isO9IdEG1HFwoAmeA2FtBLH1fTiksRtN9DGcPAOyEI3VINz2fD3CFma5ozSNbpqUIwGDGc8ixD1etjiIW6TmXN0Rd0p5VzEtk6sDwIe8j10NH1hKDhevX&print=pretty HTTP/1.1 > User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8y zlib/1.2.5 > Host: firebase-apiserver01-tah01-iad01.dapi.production.nest.com:9553 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json; charset=UTF-8 < Access-Control-Allow-Origin: * < Cache-Control: private, no-cache, max-age=0 < Content-Length: 2218 < { "thermostats" : { "pYo-lbpXuVm_DctuTckA_HdEswRgRkbx" : { "locale" : "en-US", "temperature_scale" : "F", "is_using_emergency_heat" : false, "has_fan" : true, "software_version" : "4.2.3", "has_leaf" : true, "device_id" : "pYo-lbpXuVm_DctuTckA_HdEswRgRkbx", "name" : "Downstairs", "can_heat" : true, "can_cool" : true, "hvac_mode" : "off", "target_temperature_c" : 24.5, "target_temperature_f" : 76, "target_temperature_high_c" : 24.0, "target_temperature_high_f" : 75, "target_temperature_low_c" : 20.0, "target_temperature_low_f" : 68, "ambient_temperature_c" : 25.0, "ambient_temperature_f" : 78, "away_temperature_high_c" : 24.0, "away_temperature_high_f" : 76, "away_temperature_low_c" : 15.5, "away_temperature_low_f" : 60, "structure_id" : "HqSZlH08Jc3CtBNIS4OLPdiWLpcfW5o6dP2DvSox7hcGVpBGOH9cQA", "fan_timer_active" : false, "name_long" : "Downstairs Thermostat", "is_online" : true, "last_connection" : "2014-06-26T23:16:24.341Z" }, "pYo-lbpXuVncrx7IdGTWyXdEswRgRkbx" : { "locale" : "en-US", "temperature_scale" : "F", "is_using_emergency_heat" : false, "has_fan" : true, "software_version" : "4.2.3", "has_leaf" : true, "device_id" : "pYo-lbpXuVncrx7IdGTWyXdEswRgRkbx", "name" : "Upstairs", "can_heat" : true, "can_cool" : true, "hvac_mode" : "off", "target_temperature_c" : 24.0, "target_temperature_f" : 76, "target_temperature_high_c" : 24.0, "target_temperature_high_f" : 75, "target_temperature_low_c" : 20.0, "target_temperature_low_f" : 68, "ambient_temperature_c" : 25.0, "ambient_temperature_f" : 78, "away_temperature_high_c" : 24.0, "away_temperature_high_f" : 76, "away_temperature_low_c" : 15.5, "away_temperature_low_f" : 60, "structure_id" : "HqSZlH08Jc3CtBNIS4OLPdiWLpcfW5o6dP2DvSox7hcGVpBGOH9cQA", "fan_timer_active" : false, "name_long" : "Upstairs Thermostat", "is_online" : true, "last_connection" : "2014-06-26T23:16:27.849Z" } } * Connection #0 to host firebase-apiserver01-tah01-iad01.dapi.production.nest.com left intact }* Closing connection #0 * SSLv3, TLS alert, Client hello (1):

Answer1:

I don't think the server supports SSLv3. Try using --tlsv1 and see if that works.

Answer2:

The same callout from Salesforce works just fine now. I guess Nest or Force.com must have tweaked some SSL config.

Recommend

  • Fill column values by matching values in each row in two dataframe
  • How to execute in first a function and in second canActivate()
  • How to match/merge data from two different files in R?
  • How can I get the url from an ng-resource?
  • Find the last row in a data frame that meets certain criteria
  • Updating and Deleting Records in Salesforce using REST API
  • Getting Unsupported AMF version error while using AMF Connection to send flex message
  • “mean” within “by” throwing warning in R
  • Optimizing Google Sheets .appendRow()
  • How to restart Apache Apex application?
  • How invoke APEX method from custom button
  • How to reload Oracle APEX 5.0 Tree dynamically
  • not able to create VC++ project, with VS11
  • Visual studio 2015 keystroke with mouse button
  • R h2o.glm - issue with max_active_predictors
  • Python: Split a String Field into 3 Separate Fields using Lambda
  • how can I compare dates in array to find the earliest one?
  • Divide a $1 by 3 and adjusting 1 cent
  • IE6 changes DOCTYPE to a bad one
  • How to pass solution folder as parameter in command line arguments (for debug)?
  • Doctrine/Symfony entity generator and generating entity from one table
  • How do I shift the decimal place in Python?
  • Default parameter as generic type
  • Dynamically switching connect in Modelica
  • ASP.NET MVC 2 Preview 2 - display directory list rather than home/index
  • nonblocking BIO_do_connect blocked when there is no internet connected
  • Redux Form - Not able to type anything in input
  • Get history of file changes from TFS to implement custom “blame”-behaviour of exceptions
  • WPF - CanExecute dosn't fire when raising Commands from a UserControl
  • C# - Serializing and deserializing static member
  • Sending data from AppleScript to FileMaker records
  • Validaiting emails with Net.Mail MailAddress
  • Which linear programming package should I use for high numbers of constraints and “warm starts” [clo
  • 'TypeError' while using NSGA2 to solve Multi-objective prob. from pyopt-sparse in OpenMDAO
  • Javascript + PHP Encryption with pidCrypt
  • How to include full .NET prerequisite for Wix Burn installer
  • How get height of the a view with gone visibility and height defined as wrap_content in xml?
  • How to CLICK on IE download dialog box i.e.(Open, Save, Save As…)
  • Linking SubReports Without LinkChild/LinkMaster
  • Is it possible to post an object from jquery to bottle.py?