uNetworking / uSockets

Miniscule cross-platform eventing, networking & crypto for async applications
Apache License 2.0
1.27k stars 260 forks source link

Option to serve multiple certs #81

Closed Napzu closed 4 years ago

Napzu commented 4 years ago

It would be nice to have option to serve multiple certs depending on which domain the request came from.

Maybe something along the lines:

SSLApp({
    sslOptions(){
        ** SOME LOGIC TO GET CORRECT SSL FILES **
        return sllOpts
    }
})
ghost commented 4 years ago

Do you know the corresponding OpenSSL functionality?

Napzu commented 4 years ago

Not really but I know something.

Multiple certification is implemented via Server Name Indication (SNI).

Express uses callback something like this:

SNICallback: function(domain, cb) {
    cb(null, tls.createCredentials({ key: fs.readFileSync('app1.key').toString(), cert: fs.readFileSync('app1.crt').toString() }).context);
}

But in uWebsocket.js context I think this would be the easies from user perspective:

require('uWebSockets.js').SSLApp({
  certs: {
    'domain1.com' : {
      key_file_name : 'domain1_com_key.pem',
      cert_file_name: 'domain1_com_cert.pem',
    },
    'domain2.com' : {
      key_file_name : 'domain2_com_key.pem',
      cert_file_name: 'domain2_com_cert.pem',
    },
  },
  key_file_name : 'fallback_com_key.pem',
  cert_file_name: 'fallback_com_cert.pem',
})
ghost commented 4 years ago

https://en.m.wikipedia.org/wiki/Server_Name_Indication

Okay okay, that sounds like it could be implemented. But this is a typical thing I won't do unless many people want it or someone pays for it

AdrianEddy commented 4 years ago

I implemented it for v0.14 as follows:

auto ctx = uS::TLS::createContext(cert, key);
SSL_CTX_set_tlsext_servername_callback(ctx.getNativeContext(), [](SSL *ssl, int *, void *) -> int {
    if (!ssl) return SSL_TLSEXT_ERR_NOACK;

    const char *hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
    if (!hostname || hostname[0] == '\0') return SSL_TLSEXT_ERR_NOACK;
    if (strstr(hostname, "default.com"))   return SSL_TLSEXT_ERR_OK;

    std::string stdHost(hostname);
    SSL_CTX *native = nullptr;

    static thread_local std::unordered_map<std::string, SSL_CTX *> nativeCtxCache;
    auto cachedNativeCtx = nativeCtxCache.find(stdHost);
    if (cachedNativeCtx != nativeCtxCache.end()) {
        native = cachedNativeCtx->second;
    } else {
        std::string cert = stdHost + ".crt";
        std::string key = stdHost + ".key";

        auto ctx = uS::TLS::createContext(cert, key);
        native = ctx.getNativeContext();

        if (!native) return SSL_TLSEXT_ERR_NOACK;
        nativeCtxCache[stdHost] = native;
    }

    if (SSL_set_SSL_CTX(ssl, native) != native)
        return SSL_TLSEXT_ERR_NOACK;

    return SSL_TLSEXT_ERR_OK;
});

I didn't use v0.15 yet, but I guess it's a good starting point

ghost commented 4 years ago

That's good as a reference but still, this is low priority until more than two wants it

remyrylan commented 4 years ago

@alexhultman my company is releasing an open source framework/CMS for Node.js shortly and we've love to make use of this feature.


Background details:

I've written an efficient HTTP/2 router for our framework that allows serving multiple apps/domains off of one server instance.

Of course I know that websockets + HTTP/2 aren't currently feasible because there isn't even a spec for it yet.

My current plan with our framework is to allow users to deploy an HTTP/2 server that accommodates multiple domains for their front-end app. For their data/API, I'll be using uWS.js to serve GraphQL requests and Websockets via HTTP/1.1 as a separate app/deployment/server on a different domain/subdomain from their front-end apps. It would be phenomenal if users could also serve multiple domains for their data app the same as they can for their front end app.

Then they'd be able to have something like this:

Server 1 (powered by my company's framework):

Server 2 (powered by uWS.js)

This approach of the front-end and data apps being separate is also has the advantage of not having to disconnect and reconnect websocket connected users when a UI-only change is deployed.


Do you think it would be incredibly difficult to make this change? Our team is bootstrapped with no VC cash, but we may be willing to scramble together funds to sponsor the development of this feature.

ghost commented 4 years ago

Yes this could be added but is currently not prioritized

ghost commented 4 years ago

How is this exposed in Express.js? The same as above?

ghost commented 4 years ago

Why would you need to state the domain? It should say in the certificate. You should only need to specify an array of certificates and keys

ghost commented 4 years ago

Basically you want to put all SSL options in an array, so that you can add multiple entire contexts.

remyrylan commented 4 years ago

@alexhultman well, express still requires you to create your own server. Express is more of a route dispatcher that you supply to a Node.js server.

With that in mind, the best syntax example I can think of how it's done in the Node.js https2 server, which extends from the tls module server:

https://nodejs.org/api/tls.html#tls_server_addcontext_hostname_context

const http2 = require('http2');
const server = http2.createSecureServer({});

const contexts = {
  'foobar.com': {
    cert: '...cert...',
    key: '...key...'
  },
  'example.com': {
    cert: '...cert...',
    key: '...key...'
   }
};

Object.entries(contexts).forEach(([hostname, secureContext]) => {
  server.addContext(hostname, secureContext);
});

server.on('request', (req, res) => {
  res.statusCode = 200;
  res.write('Hello world');
  res.end();
});

I would assume that the idea of having the hostname is so that internally, a keyed index could be used to speed up lookups for serving the correct certificate.

Also worth noting that the Node.js TLS server addContext function does allow for wildcards such as *.example.com. That would also be incredibly handy for using the uWebsockets ecosystem to build out multi-tenant apps.

ghost commented 4 years ago

This should be possible with the added us_socket_context_get_native_handle that returns SSL_CTX

ghost commented 4 years ago

This is implemented now

NickFritz1982 commented 3 years ago

This is implemented now

I'm using uWebsockets.js and wondering if there is now the ability to have multiple certificates based on which domain they are accessing from?

We have one device using a self signed which can't be updated which needs to connect and then we want to setup a real SSL for the other people connecting on a separate domain.

I have NGINX with SNI setup to serve the two SSL certificates based on the domain connecting. Just looking for the correct example/structure for SNI when it was added in v18.5.0 to uwebsockets.js, I can't find any docs.

ghost commented 3 years ago

Yes look at ServerName example