twitter-archive / CocoaSPDY

SPDY for iOS and OS X
Apache License 2.0
2.39k stars 233 forks source link

How to run against localhost? #134

Closed plivesey closed 8 years ago

plivesey commented 8 years ago

I'm trying to run this library against localhost, but I cannot get past the TLS handshake. I have tried the following: The base case:

NSURL *url = [NSURL URLWithString:@"https://localhost:8888"];

    // USING NSURLSession defaultSessionConfiguration];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.protocolClasses = @[[SPDYURLSessionProtocol class]];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    id task = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)

This as expected, fails the TLS handshake. So, I tried to avoid it with:

SPDYConfiguration *configuration = [SPDYConfiguration defaultConfiguration];
    NSMutableDictionary *tlsSettings = [NSMutableDictionary dictionary];
    [tlsSettings setObject:(NSNumber *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain];
    [configuration setValue:tlsSettings forKey:@"tlsSettings"];

    [SPDYURLConnectionProtocol setConfiguration:configuration];

This fails with: 2015-10-03 14:31:03.688 SimpleSPDYExample[7271:1288578] SPDY [ERROR] socket closing with error: Error Domain=SPDYSocketErrorDomain Code=6 "Unexpected end of stream." UserInfo={NSLocalizedDescription=Unexpected end of stream.}

Finally, I tried adding my certificate as a profile to the simulator (without the config code above), but this didn't help. If I go to safari in the simulator, and go to https://localhost:8888, it has an untrusted cert error. But after I added the cert as a root certificate on the simulator, there was no problem. But there was still a problem with the sample app with the code above.

Note: The above code works if I just run it over NSURLSession without the SPDY protocol by adding:

// This gets around the fact that the sample server running on the sample domain is using a self-signed certificate.
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }

How can I successfully run against localhost without TLS errors?

ps. Here is my server code in node.js (very simple):

var spdy = require('spdy');
var fs = require('fs');

var options = {
  key: fs.readFileSync(__dirname + '/keys/key.pem'),
  cert: fs.readFileSync(__dirname + '/keys/cert.pem')
};

var server = spdy.createServer(options, function(req, res) {
  var message = "No SPDY for you!"
  if (req.isSpdy){
    message = "YAY! SPDY Works!"
  }
  res.end('hello world! ' + message);
});

server.listen(8888);

Thanks for any help you can give me.

plivesey commented 8 years ago

I'm still struggling to get a hello world example up and running for development. I've also taken a look at this example: https://github.com/ohsc/NonNPN-SPDY-Test/issues/1 but cannot get it to work.

Are there any server/client examples that currently work?

Thanks.

kgoodier commented 8 years ago

Just guessing here, but I'd imagine the node.js / spdy server is expecting NPN. CocoaSPDY is just going to start speaking SPDY as soon as the TLS session is established, while the server is likely expecting HTTP due to the lack of NPN. Seems a very likely reason for the server to terminate the TCP connection. You've probably seen it, but the README for CocoaSPDY has a section "A note on NPN".

I don't know much about node.js + spdy... is it possible to force it to speak SPDY? Or can you intercept & examine the first few bytes in order to look for either an HTTP verb or a SPDY frame header?

kgoodier commented 8 years ago

Sorry, missed it earlier -- just read the link to ohsc's project. I'd assume that's the right setup for the server. Another idea, in your example, you aren't overriding the TLS trust evaluator. Call [SPDYProtocol setTLSTrustEvaluator] and set it to your delegate, and in your evaluateServerTest:forHost: implementation (see SPDYTLSTrustEvaluator.h), just return YES. I think adding the cert like you did would negate the need for this, but can't hurt.

In addition, ensure you have logging cranked up by calling "[SPDYProtocol setLoggerLevel:SPDYLogLevelDebug];". It's already set to that level for debug builds, but just in case...

plivesey commented 8 years ago

I'm using node-spdy (https://github.com/indutny/node-spdy). It does expect NPN, but you can set a plain flag which is defined as follows:

plain - if defined, server will ignore NPN and ALPN data and choose whether to use spdy or plain http by looking at first data packet.

I thought that this was the point of the SETTINGS packet so was assuming this would work. Let me try to intercept the first bytes.

plivesey commented 8 years ago

@kgoodier Yeah, I've switched over to using [SPDYURLConnectionProtocol setTLSTrustEvaluator:self];

Sorry, the code above is a little out dated. Now I just have:

    [SPDYURLConnectionProtocol setTLSTrustEvaluator:self];
    [SPDYCommonLogger setLoggerLevel:SPDYLogLevelInfo];

- (BOOL)evaluateServerTrust:(SecTrustRef)trust forHost:(NSString *)host {
    return YES;
}
plivesey commented 8 years ago

Huh...I think I may have worked it out... It looks like it succeeds if I do both:

    SPDYConfiguration *configuration = [SPDYConfiguration defaultConfiguration];
    NSMutableDictionary *tlsSettings = [NSMutableDictionary dictionary];
    [tlsSettings setObject:(NSNumber *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain];
    [configuration setValue:tlsSettings forKey:@"tlsSettings"];

    [SPDYURLConnectionProtocol setConfiguration:configuration];
    [SPDYURLConnectionProtocol setTLSTrustEvaluator:self];
plivesey commented 8 years ago

With just setTLSTrustEvaluator:self

2015-10-09 13:49:19.289 SimpleSPDYExample[24104:256965] SPDY [WARNING] loaded DEBUG build of SPDY framework
2015-10-09 13:49:19.305 SimpleSPDYExample[24104:257068] SPDY [INFO] start loading https://localhost:8888/
2015-10-09 13:49:19.308 SimpleSPDYExample[24104:257068] SPDY [INFO] queueing request: https://localhost:8888/
2015-10-09 13:49:19.309 SimpleSPDYExample[24104:257068] SPDY [INFO] Proxy: added direct endpoint <SPDYOriginEndpoint: localhost:8888 origin:<SPDYOrigin: https://localhost:8888>>
2015-10-09 13:49:19.309 SimpleSPDYExample[24104:257068] SPDY [INFO] socket attempting connection to <SPDYOriginEndpoint: localhost:8888 origin:<SPDYOrigin: https://localhost:8888>>
2015-10-09 13:49:19.310 SimpleSPDYExample[24104:257068] SPDY [INFO] <SPDYSession: 0x7fd915a2ee80> connecting to <SPDYOrigin: https://localhost:8888>
2015-10-09 13:49:19.312 SimpleSPDYExample[24104:257068] SPDY [INFO] socket connected to <SPDYOriginEndpoint: localhost:8888 origin:<SPDYOrigin: https://localhost:8888>>
2015-10-09 13:49:19.312 SimpleSPDYExample[24104:257068] SPDY [INFO] <SPDYSession: 0x7fd915a2ee80> connected to <SPDYOrigin: https://localhost:8888> (127.0.0.1:8888)
2015-10-09 13:49:19.315 SimpleSPDYExample[24104:257068] CFNetwork SSLHandshake failed (-9807)
2015-10-09 13:49:19.315 SimpleSPDYExample[24104:257068] SPDY [INFO] socket closing with error: Error Domain=NSOSStatusErrorDomain Code=-9807 "(null)"
2015-10-09 13:49:19.315 SimpleSPDYExample[24104:257068] SPDY [INFO] <SPDYSession: 0x7fd915a2ee80> connection error: Error Domain=NSOSStatusErrorDomain Code=-9807 "(null)"
2015-10-09 13:49:19.316 SimpleSPDYExample[24104:257068] SPDY [INFO] <SPDYSession: 0x7fd915a2ee80> connection closed
2015-10-09 13:49:19.316 SimpleSPDYExample[24104:257068] SPDY [INFO] <SPDYSession: 0x7fd915a2ee80> connection closed
2015-10-09 13:49:19.316 SimpleSPDYExample[24104:257027] Data task completed
2015-10-09 13:49:19.316 SimpleSPDYExample[24104:257068] SPDY [INFO] stop loading https://localhost:8888/
2015-10-09 13:49:19.316 SimpleSPDYExample[24104:257027] Error: Error Domain=NSOSStatusErrorDomain Code=-9807 "(null)" UserInfo={x-spdy-metadata-identifier=466116559.316003/7fd913539dc0}

With the custom configuration it works. Do you think this is problem with the library? Or is this expected behavior?

kgoodier commented 8 years ago

I tried the same setup locally and found 2 issues, but got it to work finally.

  1. The spdy.js library has changed how its options are configured from what ohsc used. The bare minimum you need is:

    var options = { spdy: { plain: true } };

As far as I can tell, the "ssl" option is unused. You need the "plain" option in order to enable the library to look at the first few bytes to determine the protocol. This is why you don't need to configure any protocols, but seting "plain: true" seems to create a non-SSL server only. This isn't clear in their docs and is potentially a bug (?). That leads to the second point:

  1. Your client should make plain HTTP, not HTTPS, calls. i.e., to "http://127.0.0.1:3232/". Because the server is not an HTTPS server, the protocol eval code will see the first few TLS handshake bytes and fallback to 'http/1.1' default protocol... which clearly isn't right.

For local development, I actually think this is better. You don't have to worry about any certificates or TLS trust issues, and you can easily inspect the payloads on the wire. No need to set tlsSettings or tlsTrustEvaluator in CocoaSPDY.

kgoodier commented 8 years ago

To answer your previous question: if using an SSL server, you would indeed need both the tlsSetting and the trust evaluator override. I tried that too, and was still getting an error due to the server being plaintext, not TLS.

Checked the spdy.js code again, and it seems you can't force both plain and https server using just options and "spdy.createServer(options, function...)". Though I may be missing something. Using "spdy.createServer(https.Server, options, function...)" seems to be the way to force plain AND https. I'd still lean towards non-encrypted for localhost testing.

plivesey commented 8 years ago

Ahhh. So me trying to force TLS was making my server think I wanted HTTP/1.1 but the client was still expecting SPDY.

Funnily enough, when I try the options above (just spdy.plain = true), I get this error:

tls.js:1124 throw new Error('Missing PFX or certificate + private key.'); ^ Error: Missing PFX or certificate + private key. at Server (tls.js:1124:11) at Server (https.js:35:14) at Server._init (/Users/plivesey/Repo/SPDY-Example/spdy-test/node_modules/spdy/lib/spdy/server.js:77:10) at new Server (/Users/plivesey/Repo/SPDY-Example/spdy-test/node_modules/spdy/lib/spdy/server.js:21:10) at Object.create as createServer at Object. (/Users/plivesey/Repo/SPDY-Example/spdy-test/index.js:19:19) at Module._compile (module.js:456:26) at Object.Module._extensions..js (module.js:474:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:312:12)

Even this doesn't work: { plain: true }. But this does: { plain: true, ssl: false }. Seems strange to me, but not going to debug that now...leaving it for someone else if they stumble across this thread.

And yeah, I agree that going without TLS is better. I thought you couldn't run SPDY without TLS, but I presume that's just in production (and who would do plain anyway).

Anyway, I'll close this ticket for now, and try to get a push example up and running and post it on github next time I get a chance to work on this.

plivesey commented 8 years ago

FYI, reason I'm interested in push is because I want to try to create a proof of concept for a push based JSON API. Client makes original request, but for all dependent resources, they are pushed instead of inlined.