OpenSSL NPN in node.js
authorFedor Indutny <fedor.indutny@gmail.com>
Thu, 14 Apr 2011 03:53:39 +0000 (10:53 +0700)
committerRyan Dahl <ry@tinyclouds.org>
Tue, 19 Apr 2011 18:32:26 +0000 (11:32 -0700)
closes #926.

lib/https.js
lib/tls.js
src/node_constants.cc
src/node_crypto.cc
src/node_crypto.h
test/simple/test-tls-npn-server-client.js [new file with mode: 0644]

index bd2e461..7f4aaff 100644 (file)
@@ -23,9 +23,15 @@ var tls = require('tls');
 var http = require('http');
 var inherits = require('util').inherits;
 
+var NPN_ENABLED = process.binding('constants').NPN_ENABLED;
 
 function Server(opts, requestListener) {
   if (!(this instanceof Server)) return new Server(opts, requestListener);
+
+  if (NPN_ENABLED && !opts.NPNProtocols) {
+    opts.NPNProtocols = ['http/1.1', 'http/1.0'];
+  }
+
   tls.Server.call(this, opts, http._connectionListener);
 
   this.httpAllowHalfOpen = false;
@@ -58,6 +64,10 @@ Agent.prototype.defaultPort = 443;
 
 
 Agent.prototype._getConnection = function(host, port, cb) {
+  if (NPN_ENABLED && !this.options.NPNProtocols) {
+    this.options.NPNProtocols = ['http/1.1', 'http/1.0'];
+  }
+
   var s = tls.connect(port, host, this.options, function() {
     // do other checks here?
     if (cb) cb();
index a8f1898..b535697 100644 (file)
@@ -27,6 +27,8 @@ var stream = require('stream');
 var END_OF_FILE = 42;
 var assert = require('assert').ok;
 
+var NPN_ENABLED = process.binding('constants').NPN_ENABLED;
+
 var debug;
 if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {
   debug = function(a) { console.error('TLS:', a); };
@@ -38,10 +40,36 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {
 var Connection = null;
 try {
   Connection = process.binding('crypto').Connection;
+  exports.NPN_ENABLED = NPN_ENABLED;
 } catch (e) {
   throw new Error('node.js not compiled with openssl crypto support.');
 }
 
+// Convert protocols array into valid OpenSSL protocols list
+// ("\x06spdy/2\x08http/1.1\x08http/1.0")
+function convertNPNProtocols(NPNProtocols, out) {
+  // If NPNProtocols is Array - translate it into buffer
+  if (Array.isArray(NPNProtocols)) {
+    var buff = new Buffer(NPNProtocols.reduce(function(p, c) {
+      return p + 1 + Buffer.byteLength(c);
+    }, 0));
+
+    NPNProtocols.reduce(function(offset, c) {
+      var clen = Buffer.byteLength(c);
+      buff[offset] = clen;
+      buff.write(c, offset + 1);
+
+      return offset + 1 + clen;
+    }, 0);
+
+    NPNProtocols = buff;
+  }
+
+  // If it's already a Buffer - store it
+  if (Buffer.isBuffer(NPNProtocols)) {
+    out.NPNProtocols = NPNProtocols;
+  }
+};
 
 // Base class of both CleartextStream and EncryptedStream
 function CryptoStream(pair) {
@@ -437,12 +465,14 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) {
  * Provides a pair of streams to do encrypted communication.
  */
 
-function SecurePair(credentials, isServer, requestCert, rejectUnauthorized) {
+function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
+                    NPNProtocols) {
   if (!(this instanceof SecurePair)) {
     return new SecurePair(credentials,
                           isServer,
                           requestCert,
-                          rejectUnauthorized);
+                          rejectUnauthorized,
+                          NPNProtocols);
   }
 
   var self = this;
@@ -478,6 +508,10 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized) {
                              this._requestCert,
                              this._rejectUnauthorized);
 
+  if (NPN_ENABLED && NPNProtocols) {
+    this._ssl.setNPNProtocols(NPNProtocols);
+    this.npnProtocol = null;
+  }
 
   /* Acts as a r/w stream to the cleartext side of the stream. */
   this.cleartext = new CleartextStream(this);
@@ -588,6 +622,10 @@ SecurePair.prototype._cycle = function(depth) {
 
 SecurePair.prototype._maybeInitFinished = function() {
   if (this._ssl && !this._secureEstablished && this._ssl.isInitFinished()) {
+    if (NPN_ENABLED) {
+      this.npnProtocol = this._ssl.getNegotiatedProtocol();
+    }
+
     this._secureEstablished = true;
     debug('secure established');
     this.emit('secure');
@@ -745,13 +783,15 @@ function Server(/* [options], listener */) {
     var pair = new SecurePair(creds,
                               true,
                               self.requestCert,
-                              self.rejectUnauthorized);
+                              self.rejectUnauthorized,
+                              self.NPNProtocols);
 
     var cleartext = pipe(pair, socket);
     cleartext._controlReleased = false;
 
     pair.on('secure', function() {
       pair.cleartext.authorized = false;
+      pair.cleartext.npnProtocol = pair.npnProtocol;
       if (!self.requestCert) {
         cleartext._controlReleased = true;
         self.emit('secureConnection', pair.cleartext, pair.encrypted);
@@ -812,6 +852,7 @@ Server.prototype.setOptions = function(options) {
   if (options.ciphers) this.ciphers = options.ciphers;
   if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
   if (options.secureOptions) this.secureOptions = options.secureOptions;
+  if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this);
 };
 
 
@@ -854,7 +895,9 @@ exports.connect = function(port /* host, options, cb */) {
   var sslcontext = crypto.createCredentials(options);
   //sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA');
 
-  var pair = new SecurePair(sslcontext, false);
+  convertNPNProtocols(options.NPNProtocols, this);
+  var pair = new SecurePair(sslcontext, false, true, false,
+                            this.NPNProtocols);
 
   var cleartext = pipe(pair, socket);
 
@@ -863,6 +906,8 @@ exports.connect = function(port /* host, options, cb */) {
   pair.on('secure', function() {
     var verifyError = pair._ssl.verifyError();
 
+    cleartext.npnProtocol = pair.npnProtocol;
+
     if (verifyError) {
       cleartext.authorized = false;
       cleartext.authorizationError = verifyError;
index 5cad3c3..7d316ff 100644 (file)
@@ -912,6 +912,11 @@ void DefineConstants(Handle<Object> target) {
 #ifdef SSL_OP_CRYPTOPRO_TLSEXT_BUG
   NODE_DEFINE_CONSTANT(target, SSL_OP_CRYPTOPRO_TLSEXT_BUG);
 #endif
+
+#ifdef OPENSSL_NPN_NEGOTIATED
+#define NPN_ENABLED 1
+  NODE_DEFINE_CONSTANT(target, NPN_ENABLED);
+#endif
 }
 
 }  // namespace node
index 03024c3..2d82ff0 100644 (file)
@@ -565,6 +565,11 @@ void Connection::Initialize(Handle<Object> target) {
   NODE_SET_PROTOTYPE_METHOD(t, "receivedShutdown", Connection::ReceivedShutdown);
   NODE_SET_PROTOTYPE_METHOD(t, "close", Connection::Close);
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+  NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", Connection::GetNegotiatedProto);
+  NODE_SET_PROTOTYPE_METHOD(t, "setNPNProtocols", Connection::SetNPNProtocols);
+#endif
+
   target->Set(String::NewSymbol("Connection"), t->GetFunction());
 }
 
@@ -614,6 +619,76 @@ static int VerifyCallback(int preverify_ok, X509_STORE_CTX *ctx) {
   return 1;
 }
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+
+int Connection::AdvertiseNextProtoCallback_(SSL *s,
+                                            const unsigned char **data,
+                                            unsigned int *len,
+                                            void *arg) {
+
+  Connection *p = static_cast<Connection*>(SSL_get_app_data(s));
+
+  if (p->npnProtos_.IsEmpty()) {
+    // No initialization - no NPN protocols
+    *data = reinterpret_cast<const unsigned char*>("");
+    *len = 0;
+  } else {
+    *data = reinterpret_cast<const unsigned char*>(Buffer::Data(p->npnProtos_));
+    *len = Buffer::Length(p->npnProtos_);
+  }
+
+  return SSL_TLSEXT_ERR_OK;
+}
+
+int Connection::SelectNextProtoCallback_(SSL *s,
+                             unsigned char **out, unsigned char *outlen,
+                             const unsigned char* in,
+                             unsigned int inlen, void *arg) {
+  Connection *p = static_cast<Connection*> SSL_get_app_data(s);
+
+  // Release old protocol handler if present
+  if (!p->selectedNPNProto_.IsEmpty()) {
+    p->selectedNPNProto_.Dispose();
+  }
+
+  if (p->npnProtos_.IsEmpty()) {
+    // We should at least select one protocol
+    // If server is using NPN
+    *out = reinterpret_cast<unsigned char*>(const_cast<char*>("http/1.1"));
+    *outlen = 8;
+
+    // set status unsupported
+    p->selectedNPNProto_ = Persistent<Value>::New(False());
+
+    return SSL_TLSEXT_ERR_OK;
+  }
+
+  const unsigned char* npnProtos =
+      reinterpret_cast<const unsigned char*>(Buffer::Data(p->npnProtos_));
+
+  int status = SSL_select_next_proto(out, outlen, in, inlen, npnProtos,
+                                     Buffer::Length(p->npnProtos_));
+
+  switch (status) {
+    case OPENSSL_NPN_UNSUPPORTED:
+      p->selectedNPNProto_ = Persistent<Value>::New(Null());
+      break;
+    case OPENSSL_NPN_NEGOTIATED:
+      p->selectedNPNProto_ = Persistent<Value>::New(String::New(
+                                 reinterpret_cast<const char*>(*out), *outlen
+                             ));
+      break;
+    case OPENSSL_NPN_NO_OVERLAP:
+      p->selectedNPNProto_ = Persistent<Value>::New(False());
+      break;
+    default:
+      break;
+  }
+
+  return SSL_TLSEXT_ERR_OK;
+}                                  
+#endif
+
 
 Handle<Value> Connection::New(const Arguments& args) {
   HandleScope scope;
@@ -633,6 +708,23 @@ Handle<Value> Connection::New(const Arguments& args) {
   p->ssl_ = SSL_new(sc->ctx_);
   p->bio_read_ = BIO_new(BIO_s_mem());
   p->bio_write_ = BIO_new(BIO_s_mem());
+
+#ifdef OPENSSL_NPN_NEGOTIATED
+  SSL_set_app_data(p->ssl_, p);
+  if (is_server) {
+    // Server should advertise NPN protocols
+    SSL_CTX_set_next_protos_advertised_cb(sc->ctx_,
+                                          AdvertiseNextProtoCallback_,
+                                          NULL);
+  } else {
+    // Client should select protocol from advertised
+    // If server supports NPN
+    SSL_CTX_set_next_proto_select_cb(sc->ctx_,
+                                     SelectNextProtoCallback_,
+                                     NULL);
+  }
+#endif
+
   SSL_set_bio(p->ssl_, p->bio_read_, p->bio_write_);
 
 #ifdef SSL_MODE_RELEASE_BUFFERS
@@ -1184,6 +1276,48 @@ Handle<Value> Connection::Close(const Arguments& args) {
   return True();
 }
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+Handle<Value> Connection::GetNegotiatedProto(const Arguments& args) {
+  HandleScope scope;
+
+  Connection *ss = Connection::Unwrap(args);
+
+  if (ss->is_server_) {
+    const unsigned char *npn_proto;
+    unsigned int npn_proto_len;
+
+    SSL_get0_next_proto_negotiated(ss->ssl_, &npn_proto, &npn_proto_len);
+
+    if (!npn_proto) {
+      return False();
+    }
+
+    return String::New((const char*) npn_proto, npn_proto_len);
+  } else {
+    return ss->selectedNPNProto_;
+  }
+}
+
+Handle<Value> Connection::SetNPNProtocols(const Arguments& args) {
+  HandleScope scope;
+
+  Connection *ss = Connection::Unwrap(args);
+
+  if (args.Length() < 1 || !Buffer::HasInstance(args[0])) {
+    return ThrowException(Exception::Error(String::New(
+           "Must give a Buffer as first argument")));
+  }
+
+  // Release old handle
+  if (!ss->npnProtos_.IsEmpty()) {
+    ss->npnProtos_.Dispose();
+  }
+  ss->npnProtos_ = Persistent<Object>::New(args[0]->ToObject());
+
+  return True();
+};
+#endif
+
 
 static void HexEncode(unsigned char *md_value,
                       int md_len,
index 2cbffc2..6432654 100644 (file)
@@ -23,6 +23,7 @@
 #define SRC_NODE_CRYPTO_H_
 
 #include <node.h>
+
 #include <node_object_wrap.h>
 #include <v8.h>
 
 #include <openssl/x509.h>
 #include <openssl/hmac.h>
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+#include <node_buffer.h>
+#endif
+
 #define EVP_F_EVP_DECRYPTFINAL 101
 
 
@@ -94,6 +99,11 @@ class Connection : ObjectWrap {
  public:
   static void Initialize(v8::Handle<v8::Object> target);
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+  v8::Persistent<v8::Object> npnProtos_;
+  v8::Persistent<v8::Value> selectedNPNProto_;
+#endif
+
  protected:
   static v8::Handle<v8::Value> New(const v8::Arguments& args);
   static v8::Handle<v8::Value> EncIn(const v8::Arguments& args);
@@ -111,6 +121,20 @@ class Connection : ObjectWrap {
   static v8::Handle<v8::Value> Start(const v8::Arguments& args);
   static v8::Handle<v8::Value> Close(const v8::Arguments& args);
 
+#ifdef OPENSSL_NPN_NEGOTIATED
+  // NPN
+  static v8::Handle<v8::Value> GetNegotiatedProto(const v8::Arguments& args);
+  static v8::Handle<v8::Value> SetNPNProtocols(const v8::Arguments& args);
+  static int AdvertiseNextProtoCallback_(SSL *s,
+                                         const unsigned char **data,
+                                         unsigned int *len,
+                                         void *arg);
+  static int SelectNextProtoCallback_(SSL *s,
+                                      unsigned char **out, unsigned char *outlen,
+                                      const unsigned char* in,
+                                      unsigned int inlen, void *arg);
+#endif
+
   int HandleBIOError(BIO *bio, const char* func, int rv);
   int HandleSSLError(const char* func, int rv);
 
@@ -139,6 +163,7 @@ class Connection : ObjectWrap {
   BIO *bio_read_;
   BIO *bio_write_;
   SSL *ssl_;
+  
   bool is_server_; /* coverity[member_decl] */
 };
 
diff --git a/test/simple/test-tls-npn-server-client.js b/test/simple/test-tls-npn-server-client.js
new file mode 100644 (file)
index 0000000..0c3d257
--- /dev/null
@@ -0,0 +1,101 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var NPN_ENABLED = process.binding('constants').NPN_ENABLED;
+
+if (!process.versions.openssl || !NPN_ENABLED) {
+  console.error("Skipping because node compiled without OpenSSL or " +
+                "with old OpenSSL version.");
+  process.exit(0);
+}
+
+var common = require('../common'),
+    assert = require('assert'),
+    fs = require('fs'),
+    tls = require('tls');
+
+function filenamePEM(n) {
+  return require('path').join(common.fixturesDir, 'keys', n + ".pem");
+}
+
+function loadPEM(n) {
+  return fs.readFileSync(filenamePEM(n));
+}
+
+var serverOptions = {
+  key: loadPEM('agent2-key'),
+  cert: loadPEM('agent2-cert'),
+  crl: loadPEM('ca2-crl'),
+  NPNProtocols: ['a', 'b', 'c']
+};
+
+var clientsOptions = [{
+  key: serverOptions.key,
+  cert: serverOptions.cert,
+  crl: serverOptions.crl,
+  NPNProtocols: ['a', 'b', 'c']
+},{
+  key: serverOptions.key,
+  cert: serverOptions.cert,
+  crl: serverOptions.crl,
+  NPNProtocols: ['c', 'b', 'e']
+},{
+  key: serverOptions.key,
+  cert: serverOptions.cert,
+  crl: serverOptions.crl,
+  NPNProtocols: ['first-priority-unsupported', 'x', 'y']
+}];
+
+var serverPort = common.PORT;
+
+var serverResults = [],
+    clientsResults = [];
+
+var server = tls.createServer(serverOptions, function(c) {
+  serverResults.push(c.npnProtocol);
+});
+server.listen(serverPort, startTest);
+
+function startTest() {
+  function connectClient(options, callback) {
+    var client = tls.connect(serverPort, 'localhost', options, function() {
+      clientsResults.push(client.npnProtocol);
+      client.destroy();
+
+      callback();
+    });
+  };
+
+  connectClient(clientsOptions[0], function() {
+    connectClient(clientsOptions[1], function() {
+      connectClient(clientsOptions[2], function() {
+        server.close();
+      });
+    });
+  });
+};
+
+process.on('exit', function() {
+  assert.equal(serverResults[0], clientsResults[0]);
+  assert.equal(serverResults[1], clientsResults[1]);
+  assert.equal(serverResults[2], 'first-priority-unsupported');
+  assert.equal(clientsResults[2], false);
+});