Investigating restarts concerning Error: [400] jwk and kid header fields are mutually exclusive #23

Open
opened 2019-12-29 13:38:00 +00:00 by Ghost · 4 comments

Since I updated to the most recent version, I'm getting less restarts from my proxy handling certificates.

No cert for the IP should be fine:

debug: ignoring servername "82.165.x.x"
(it's probably either missing from your config, or a bot)
servername_unknown { servername: '82.165.x.x' }

This is probably a bot too, do I have to handle this to prevent restarts (pm2 managed)?

error Error: [400] jwk and kid header fields are mutually exclusive
11|proxy   |     at /home/ias/pm2deployed/source/proxy/node_modules/@root/acme/utils.js:118:8
11|proxy   |     at runMicrotasks (<anonymous>)
11|proxy   |     at processTicksAndRejections (internal/process/task_queues.js:93:5) {
11|proxy   |   status: 400,
11|proxy   |   code: 'E_ACME',
11|proxy   |   detail: 'jwk and kid header fields are mutually exclusive',
11|proxy   |   urn: 'urn:ietf:params:acme:error:malformed',
11|proxy   |   uri: undefined,
11|proxy   |   _rawError: {
11|proxy   |     type: 'urn:ietf:params:acme:error:malformed',
11|proxy   |     detail: 'jwk and kid header fields are mutually exclusive'
11|proxy   |   },
11|proxy   |   _rawBody: {
11|proxy   |     type: 'urn:ietf:params:acme:error:malformed',
11|proxy   |     detail: 'jwk and kid header fields are mutually exclusive',
11|proxy   |     status: 400
11|proxy   |   },
11|proxy   |   toJSON: [Function: errorToJSON],
11|proxy   |   context: 'cert_order',
11|proxy   |   subject: 'mydomain.app',
11|proxy   |   servername: 'sub.mydomain.app',
11|proxy   |   _site: {
11|proxy   |     subject: 'mydomain.app',
11|proxy   |     altnames: [
11|proxy   |       'sub.mydomain.app'
11|proxy   |     ],
11|proxy   |     renewAt: 1580037708516
11|proxy   |   }
11|proxy   | }
Since I updated to the most recent version, I'm getting less restarts from my proxy handling certificates. No cert for the IP should be fine: ``` debug: ignoring servername "82.165.x.x" (it's probably either missing from your config, or a bot) servername_unknown { servername: '82.165.x.x' } ``` This is probably a bot too, do I have to handle this to prevent restarts (pm2 managed)? ``` error Error: [400] jwk and kid header fields are mutually exclusive 11|proxy | at /home/ias/pm2deployed/source/proxy/node_modules/@root/acme/utils.js:118:8 11|proxy | at runMicrotasks (<anonymous>) 11|proxy | at processTicksAndRejections (internal/process/task_queues.js:93:5) { 11|proxy | status: 400, 11|proxy | code: 'E_ACME', 11|proxy | detail: 'jwk and kid header fields are mutually exclusive', 11|proxy | urn: 'urn:ietf:params:acme:error:malformed', 11|proxy | uri: undefined, 11|proxy | _rawError: { 11|proxy | type: 'urn:ietf:params:acme:error:malformed', 11|proxy | detail: 'jwk and kid header fields are mutually exclusive' 11|proxy | }, 11|proxy | _rawBody: { 11|proxy | type: 'urn:ietf:params:acme:error:malformed', 11|proxy | detail: 'jwk and kid header fields are mutually exclusive', 11|proxy | status: 400 11|proxy | }, 11|proxy | toJSON: [Function: errorToJSON], 11|proxy | context: 'cert_order', 11|proxy | subject: 'mydomain.app', 11|proxy | servername: 'sub.mydomain.app', 11|proxy | _site: { 11|proxy | subject: 'mydomain.app', 11|proxy | altnames: [ 11|proxy | 'sub.mydomain.app' 11|proxy | ], 11|proxy | renewAt: 1580037708516 11|proxy | } 11|proxy | } ```
Owner
jwk and kid header fields are mutually exclusive

Hmm... that may point to a bug in ACME.js or Greenlock, but unfortunately there is zero useful information in that stack trace. It looks like it's being swallowed by some internal node task queue handler...

Do you have any idea how to reproduce it? Or some clues about when it occurs? Or what happens just before?

The default error handler will just print everything to the screen. There may be some internal errors that aren't caught and cause a restart... but if so that's a bug.

``` jwk and kid header fields are mutually exclusive ``` Hmm... that may point to a bug in ACME.js or Greenlock, but unfortunately there is zero useful information in that stack trace. It looks like it's being swallowed by some internal node task queue handler... Do you have any idea how to reproduce it? Or some clues about when it occurs? Or what happens just before? The default error handler will just print everything to the screen. There may be some internal errors that aren't caught and cause a restart... but if so that's a bug.
Author

Can't tell yet, I was hoping you could explain the error?

Don't understand this one either:

 Error: read ECONNRESET
11|proxy   |     at TLSWrap.onStreamRead (internal/stream_base_commons.js:201:27)
11|proxy   | Error: getaddrinfo ENOTFOUND api.rootprojects.org
11|proxy   |     at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26)
11|proxy   | (node:81940) UnhandledPromiseRejectionWarning: Error: getaddrinfo ENOTFOUND api.rootprojects.org
11|proxy   |     at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26)

Can't tell yet, I was hoping you could explain the error? Don't understand this one either: ``` Error: read ECONNRESET 11|proxy | at TLSWrap.onStreamRead (internal/stream_base_commons.js:201:27) 11|proxy | Error: getaddrinfo ENOTFOUND api.rootprojects.org 11|proxy | at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26) 11|proxy | (node:81940) UnhandledPromiseRejectionWarning: Error: getaddrinfo ENOTFOUND api.rootprojects.org 11|proxy | at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:60:26) ```
Author

To clarify, pm2 is restarting the process every few hours because it shut down for some reason, not sure!

Don't have an unhandledRejection handler (yet) because I'd like to handle them ;)

All my code (sorry for all the litter still in it):


'use strict';

//
// custom proxy server for config based subdomain routing
//

const http = require('http');
const httpProxy = require('http-proxy');
const greenlock = require('./greenlock');

const decodeCommaSeparatedMapping = m => !m ? '' :
  m.split(',').reduce((apps, app) => {
    const [praefix, port] = app.split(':');
    apps[praefix] = port;
    return apps;
  }, {});

const NODE_ENV = process.env.NODE_ENV || 'development';
const PORT = process.env.PORT || 80;
//const PORT_SSL = process.env.PORT_SSL || 443;
const ENABLE_SSL = process.env.ENABLE_SSL || false;

const HTTP_BEHIND_HTTPS = !ENABLE_SSL || process.env.HTTP_BEHIND_HTTPS !== "false";

const TARGET_HOST = process.env.TARGET_HOST || '127.0.0.1';
const ORIGIN = process.env.TARGET_SERVER_HOST || process.env.ORIGIN || 'mydomain.app';

console.log(`Proxying [${NODE_ENV}] *. ${ORIGIN}:${PORT} to ${TARGET_HOST}`);
if (!ENABLE_SSL) {
  console.log('- SSL disabled, HTTP only -');
}

const includeStaging = true;
const servers = require('../config/servers')(includeStaging);
// const hosts = require('../config/proxy.config');
const hosts = servers.reduce((m, {prefix, port}) => {
  m[prefix] = port;
  return m;
});

const mapping = decodeCommaSeparatedMapping(process.env.mapping || '') || hosts;
const DEFAULT_TARGET_HOST = TARGET_HOST + ':' + mapping['default'];
console.log('Proxy Config: default', DEFAULT_TARGET_HOST);
console.log('Proxy Subdomain routing:')
console.log(Object.keys(mapping).map(host => host + ' - ' + mapping[host]).join('\n'));

const DOMAINS = Object.keys(mapping).map(host => host + '.' + ORIGIN);

const targetServer = (host = '', url = '') => {
  //
  // route subdomains to ports
  //
  const [_, praefix] = url.split('/');
  const [sub] = host.split('.');
  if (mapping[praefix]) {
    return TARGET_HOST + ':' + mapping[praefix];
  }
  if (mapping[sub]) {
    return TARGET_HOST + ':' + mapping[sub];
  }
  if (hosts[host]) {
    return hosts[host];
  }
  return DEFAULT_TARGET_HOST;
};

// prepare ssl
const altnames = [ORIGIN, 'www.' + ORIGIN, ...DOMAINS]; 
console.log('SSL altnames', altnames.join(','));
function find(options) {
  var subject = options.subject;
  // may include wildcard
  // var altnames = options.altnames;
  var wildname = options.wildname; // *.example.com
  console.log('find', subject, wildname);
  return Promise.resolve([
    {
      altnames,
      subject: ORIGIN,
      challenges: {
        'http-01': { module: 'acme-http-01-standalone' }
        // TODO need acme-dns for wildcard certificate
        //'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' }
      }
    }
  ]);
}

// TODO error logging
const notify = null;

//
// create http proxy
//
const proxy = httpProxy.createServer({
  ws: true,
  secure: !HTTP_BEHIND_HTTPS
});
proxy.on('error', function (error) {
  console.log('General Proxy Error!', error);
});

//
// proxy delegating request handlers
//

function requestHandler(req, res) {
  const host = req.headers.host;
  const protocol = HTTP_BEHIND_HTTPS ? 'http://' : 'https://';
  const target = protocol + targetServer(host, req.url);
  // console.log('Proxying', host, '=>', target)
  proxy.web(req, res, {
    target
  }, (e) => {
    console.log('HTTP Error!', e);
  });
}

function setupUpgradeHandler(server) {
  server.on('upgrade', (req, res) => {
    const host = req.headers.host;
    const protocol = HTTP_BEHIND_HTTPS ? 'http://' : 'https://';
    const target = protocol + targetServer(host, req.url);
    proxy.ws(req, res, {
      target, ws: true
    }, (e) => {
      console.log('Socket Error!', e); //, req);
    });
  });
}

if (ENABLE_SSL) {
  //
  // create greenlock proxy server
  //
  greenlock(find, notify, glx => {
    glx.serveApp(requestHandler);
    const server = glx.httpsServer();
    setupUpgradeHandler(server, proxy);
    console.log('\greenlock proxy is running!');
  });
} else {
  const server = http.createServer({}, requestHandler);
  setupUpgradeHandler(server, proxy);
  
  //
  // start HTTP proxy server
  //
  server.listen(PORT);
  console.log('\proxy running on port', PORT);
}

//--------------------------------------------------
//greenlock.js
'use strict';

var pkg = require('./package.json');

module.exports = function greenlockExpressServer(find, notify, callback) {
  require('@root/greenlock-express')
    .init(function () {
      // This object will be passed to Greenlock.create()

      var options = {
        // some options, like cluster, are special to Greenlock Express
        cluster: false,
        // The rest are the same as for Greenlock
        // name & version for ACME client user agent
        // packageAgent: pkg.name + '/' + pkg.version,
        // contact for security and critical bug notices
        maintainerEmail: pkg.author.email,
        // where to find .greenlockrc and set default paths
        packageRoot: __dirname,
        // error logging
        notify: notify || function (ev, args) {
          console.info(ev, args);
        },
        // TODO add find,
        // In the simplest case you can ignore all incoming options
        // and return a single site config in the same format as the config file

        // function find(options) {
        //   var servername = options.servername; // www.example.com
        //   var wildname = options.wildname; // *.example.com
        //   return Promise.resolve([
        //       { subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
        //   ]);
        // }
      };

      return options;
    })
    .serve(callback); //function (glx) {
      // will start servers on port 80 and 443
      //glx.serveApp(function (req, res) {
      //  res.end('Hello, Encrypted World!');
      //});
      // you can get access to the raw server (i.e. for websockets)
      //glx.httpsServer(); // returns raw server object
    //});
};

To clarify, pm2 is restarting the process every few hours because it shut down for some reason, not sure! Don't have an unhandledRejection handler (yet) because I'd like to handle them ;) All my code (sorry for all the litter still in it): ```javascript 'use strict'; // // custom proxy server for config based subdomain routing // const http = require('http'); const httpProxy = require('http-proxy'); const greenlock = require('./greenlock'); const decodeCommaSeparatedMapping = m => !m ? '' : m.split(',').reduce((apps, app) => { const [praefix, port] = app.split(':'); apps[praefix] = port; return apps; }, {}); const NODE_ENV = process.env.NODE_ENV || 'development'; const PORT = process.env.PORT || 80; //const PORT_SSL = process.env.PORT_SSL || 443; const ENABLE_SSL = process.env.ENABLE_SSL || false; const HTTP_BEHIND_HTTPS = !ENABLE_SSL || process.env.HTTP_BEHIND_HTTPS !== "false"; const TARGET_HOST = process.env.TARGET_HOST || '127.0.0.1'; const ORIGIN = process.env.TARGET_SERVER_HOST || process.env.ORIGIN || 'mydomain.app'; console.log(`Proxying [${NODE_ENV}] *. ${ORIGIN}:${PORT} to ${TARGET_HOST}`); if (!ENABLE_SSL) { console.log('- SSL disabled, HTTP only -'); } const includeStaging = true; const servers = require('../config/servers')(includeStaging); // const hosts = require('../config/proxy.config'); const hosts = servers.reduce((m, {prefix, port}) => { m[prefix] = port; return m; }); const mapping = decodeCommaSeparatedMapping(process.env.mapping || '') || hosts; const DEFAULT_TARGET_HOST = TARGET_HOST + ':' + mapping['default']; console.log('Proxy Config: default', DEFAULT_TARGET_HOST); console.log('Proxy Subdomain routing:') console.log(Object.keys(mapping).map(host => host + ' - ' + mapping[host]).join('\n')); const DOMAINS = Object.keys(mapping).map(host => host + '.' + ORIGIN); const targetServer = (host = '', url = '') => { // // route subdomains to ports // const [_, praefix] = url.split('/'); const [sub] = host.split('.'); if (mapping[praefix]) { return TARGET_HOST + ':' + mapping[praefix]; } if (mapping[sub]) { return TARGET_HOST + ':' + mapping[sub]; } if (hosts[host]) { return hosts[host]; } return DEFAULT_TARGET_HOST; }; // prepare ssl const altnames = [ORIGIN, 'www.' + ORIGIN, ...DOMAINS]; console.log('SSL altnames', altnames.join(',')); function find(options) { var subject = options.subject; // may include wildcard // var altnames = options.altnames; var wildname = options.wildname; // *.example.com console.log('find', subject, wildname); return Promise.resolve([ { altnames, subject: ORIGIN, challenges: { 'http-01': { module: 'acme-http-01-standalone' } // TODO need acme-dns for wildcard certificate //'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' } } } ]); } // TODO error logging const notify = null; // // create http proxy // const proxy = httpProxy.createServer({ ws: true, secure: !HTTP_BEHIND_HTTPS }); proxy.on('error', function (error) { console.log('General Proxy Error!', error); }); // // proxy delegating request handlers // function requestHandler(req, res) { const host = req.headers.host; const protocol = HTTP_BEHIND_HTTPS ? 'http://' : 'https://'; const target = protocol + targetServer(host, req.url); // console.log('Proxying', host, '=>', target) proxy.web(req, res, { target }, (e) => { console.log('HTTP Error!', e); }); } function setupUpgradeHandler(server) { server.on('upgrade', (req, res) => { const host = req.headers.host; const protocol = HTTP_BEHIND_HTTPS ? 'http://' : 'https://'; const target = protocol + targetServer(host, req.url); proxy.ws(req, res, { target, ws: true }, (e) => { console.log('Socket Error!', e); //, req); }); }); } if (ENABLE_SSL) { // // create greenlock proxy server // greenlock(find, notify, glx => { glx.serveApp(requestHandler); const server = glx.httpsServer(); setupUpgradeHandler(server, proxy); console.log('\greenlock proxy is running!'); }); } else { const server = http.createServer({}, requestHandler); setupUpgradeHandler(server, proxy); // // start HTTP proxy server // server.listen(PORT); console.log('\proxy running on port', PORT); } //-------------------------------------------------- //greenlock.js 'use strict'; var pkg = require('./package.json'); module.exports = function greenlockExpressServer(find, notify, callback) { require('@root/greenlock-express') .init(function () { // This object will be passed to Greenlock.create() var options = { // some options, like cluster, are special to Greenlock Express cluster: false, // The rest are the same as for Greenlock // name & version for ACME client user agent // packageAgent: pkg.name + '/' + pkg.version, // contact for security and critical bug notices maintainerEmail: pkg.author.email, // where to find .greenlockrc and set default paths packageRoot: __dirname, // error logging notify: notify || function (ev, args) { console.info(ev, args); }, // TODO add find, // In the simplest case you can ignore all incoming options // and return a single site config in the same format as the config file // function find(options) { // var servername = options.servername; // www.example.com // var wildname = options.wildname; // *.example.com // return Promise.resolve([ // { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } // ]); // } }; return options; }) .serve(callback); //function (glx) { // will start servers on port 80 and 443 //glx.serveApp(function (req, res) { // res.end('Hello, Encrypted World!'); //}); // you can get access to the raw server (i.e. for websockets) //glx.httpsServer(); // returns raw server object //}); }; ```
Author

I get the same error message every few days:

Error: [400] jwk and kid header fields are mutually exclusive\n at /home/sero/activi/node_modules/@root/acme/utils.js:118:8\n at runMicrotasks ()\n at processTicksAndRejections (internal/process/task_queues.js:93:5)\n at async Object.greenlock._order (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:445:23)\n at async Object.greenlock._renew (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:335:9)\n at async Object.greenlock.get (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:212:23)

I get the same error message every few days: ` Error: [400] jwk and kid header fields are mutually exclusive\n at /home/sero/activi/node_modules/@root/acme/utils.js:118:8\n at runMicrotasks ()\n at processTicksAndRejections (internal/process/task_queues.js:93:5)\n at async Object.greenlock._order (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:445:23)\n at async Object.greenlock._renew (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:335:9)\n at async Object.greenlock.get (/home/sero/activi/node_modules/@root/greenlock/greenlock.js:212:23) `
Sign in to join this conversation.
No Label
No Milestone
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: root/greenlock-express.js#23
No description provided.