├── .nvmrc ├── .gitignore ├── index.js ├── .travis.yml ├── .eslintrc.js ├── .eslintrc.json ├── package.json ├── .github └── workflows │ └── npm-publish.yml ├── README.md ├── LICENSE ├── bin └── index.js ├── src ├── Checker.js └── formatters │ └── ResultFormatter.js └── test ├── Checker.test.js └── formatters └── ResultFormatter.test.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/Checker'); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | git: 5 | depth: 10 6 | branches: 7 | only: 8 | - master 9 | before_install: 10 | - "nvm use $TRAVIS_NODE_VERSION" 11 | - "npm config set loglevel error" 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es2020': true, 4 | 'node': true, 5 | }, 6 | 'extends': [ 7 | 'google', 8 | ], 9 | 'parserOptions': { 10 | 'ecmaVersion': 11, 11 | 'sourceType': 'module', 12 | }, 13 | 'rules': { 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 11, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssl-date-checker", 3 | "version": "2.2.0", 4 | "description": "Library to check and report on the start and expiration date of a given SSL certificate for a given domain.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint" 9 | }, 10 | "keywords": [ 11 | "SSL", 12 | "checker", 13 | "Certificate", 14 | "expiry", 15 | "Secure Socket Layer", 16 | "TLS" 17 | ], 18 | "author": "Ray Hammond (https://geeksretreat.wordpress.com/)", 19 | "license": "MIT", 20 | "preferGlobal": true, 21 | "bin": { 22 | "ssl-date-checker": "bin/index.js" 23 | }, 24 | "repository": "https://github.com/rheh/ssl-date-checker", 25 | "devDependencies": { 26 | "eslint": "^8.6.0", 27 | "eslint-config-google": "^0.14.0", 28 | "jest": "^27.4.7" 29 | }, 30 | "dependencies": { 31 | "yargs": "^17.3.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssl-date-checker v2.0.6 2 | 3 | Nodejs Library to check and report on the issues on and expiration date of a given SSL certificate for a given domain. 4 | 5 | Usage: 6 | 7 | `$ ssl-date-checker host [-f text|json] [-p port]` 8 | 9 | Example results: 10 | 11 | `$ ssl-date-checker npm.org` 12 | 13 | ``` 14 | Certification for npm.org 15 | Issue On: Dec 13 00:51:56 2014 GMT 16 | Expires On: Jan 13 11:24:29 2017 GMT 17 | Expires in 536 days 18 | ``` 19 | 20 | `$ ssl-date-checker npm.org -f json` 21 | 22 | Example results: 23 | 24 | ``` 25 | { 26 | "valid_from": "Oct 15 09:57:00 2014 GMT", 27 | "valid_to": "Oct 16 09:57:00 2015 GMT", 28 | "expires": 69, 29 | "expired": false, 30 | "host": "iptorrents.com" 31 | } 32 | ``` 33 | 34 | ## Installing 35 | 36 | `npm install -g ssl-date-checker` 37 | 38 | ## Contributors 39 | 40 | One-man-band at the moment. Contact me at twitter on @rayhammond, or, via my blog here http://geeksretreat.wordpress.com if you are interest in getting involved. 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ray Hammond 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | /* eslint-disable require-jsdoc */ 3 | 4 | const checker = require('../index.js'); 5 | const ResultFormatter = require('../src/formatters/ResultFormatter'); 6 | const argv = require('yargs') 7 | .usage('Usage: $0: [-f format] [-p port]') 8 | .command('json', 'Out format json') 9 | .command('text', 'Out format textual') 10 | .demand(1, 'You must supply a host name') 11 | .argv; 12 | 13 | async function check(chosenHost, chosenPort) { 14 | try { 15 | const dateInfo = await checker(chosenHost, chosenPort); 16 | const formatter = new ResultFormatter(format); 17 | console.log(formatter.format(chosenHost, dateInfo)); 18 | 19 | process.exit(0); 20 | } catch (error) { 21 | const code = error.code; 22 | 23 | if (code === 'ENOTFOUND') { 24 | console.log('The domain that you are trying to reach is unavailable or malformed.'); 25 | } else if (code === 'ECONNREFUSED') { 26 | console.log('The domain that you are trying cannot be reach on specified port.'); 27 | } else { 28 | console.log(error); 29 | } 30 | 31 | process.exit(-1); 32 | } 33 | } 34 | 35 | const host = argv._[0]; 36 | const format = argv.f === true ? 'text' : argv.f || 'text'; 37 | const port = argv.p === true ? 443 : argv.p || 443; 38 | 39 | check(host, port); 40 | -------------------------------------------------------------------------------- /src/Checker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | const https = require('https'); 3 | 4 | function checkHost(newHost) { 5 | if (!newHost) { 6 | throw new Error('Invalid host'); 7 | } 8 | 9 | return true; 10 | } 11 | 12 | function checkPort(newPort) { 13 | const portVal = newPort || 443; 14 | const numericPort = (!isNaN(parseFloat(portVal)) && isFinite(portVal)); 15 | 16 | if (numericPort === false) { 17 | throw new Error('Invalid port'); 18 | } 19 | 20 | return true; 21 | } 22 | 23 | async function checker(host, port) { 24 | if (host === null || port === null) { 25 | throw new Error('Invalid host or port'); 26 | } 27 | 28 | checkHost(host); 29 | checkPort(port); 30 | 31 | return new Promise((resolve, reject) => { 32 | const options = { 33 | host, 34 | port, 35 | method: 'GET', 36 | rejectUnauthorized: false, 37 | }; 38 | 39 | const req = https.request(options, function(res) { 40 | res.on('data', (d) => { 41 | // process.stdout.write(d); 42 | }); 43 | 44 | const certificateInfo = res.socket.getPeerCertificate(); 45 | 46 | console.log(certificateInfo); 47 | 48 | const dateInfo = { 49 | valid_from: certificateInfo.valid_from, 50 | valid_to: certificateInfo.valid_to, 51 | serialNumber: certificateInfo.serialNumber, 52 | fingerprint : certificateInfo.fingerprint 53 | }; 54 | 55 | resolve(dateInfo); 56 | }); 57 | 58 | req.on('error', (e) => { 59 | reject(e); 60 | }); 61 | 62 | req.end(); 63 | }); 64 | } 65 | 66 | module.exports = checker; 67 | -------------------------------------------------------------------------------- /test/Checker.test.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const checker = require('../src/Checker'); 3 | 4 | jest.mock('https'); 5 | 6 | describe('#checker enforces valid host and port', () => { 7 | let testContext; 8 | 9 | beforeAll(() => { 10 | testContext = {}; 11 | }); 12 | 13 | describe('Data validation', () => { 14 | it('throws an Exception when passing nothing', () => { 15 | expect(async function() { 16 | await checker(null, null).rejects.toMatch('Invalid host'); 17 | }); 18 | }); 19 | 20 | it('throws an Exception when passing undefined host', () => { 21 | expect(async function() { 22 | await checker(undefined, 443).rejects.toMatch('Invalid host'); 23 | }); 24 | }); 25 | 26 | it('throws an Exception when setting host to nothing', () => { 27 | expect(async function() { 28 | await checker(null, 'google.com').rejects.toMatch('Invalid host'); 29 | }); 30 | }); 31 | 32 | it('throws an Exception when setting port to a string', () => { 33 | expect(async function() { 34 | checker('google.com', 'fred').rejects.toMatch('Invalid port'); 35 | }); 36 | }); 37 | 38 | it('throws an Exception when setting port to a object', () => { 39 | expect(async function() { 40 | await checker('google.com', {}).rejects.toMatch('Invalid port'); 41 | }); 42 | }); 43 | 44 | it('throws an Exception when setting port to a function', () => { 45 | expect(async function() { 46 | await checker('google.com').rejects.toMatch('Invalid port'); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/formatters/ResultFormatter.js: -------------------------------------------------------------------------------- 1 | const TEXT_FORMAT = 'text'; 2 | const JSON_FORMAT = 'json'; 3 | 4 | const ResultFormatter = function(format) { 5 | this.chosenFormat = format || 'text'; 6 | this.setFormat(this.chosenFormat); 7 | }; 8 | 9 | ResultFormatter.prototype.setFormat = function(format) { 10 | this.chosenFormat = format; 11 | }; 12 | 13 | ResultFormatter.prototype.getFormat = function() { 14 | return this.chosenFormat; 15 | }; 16 | 17 | ResultFormatter.prototype.format = function(host, dateInfo, mockNow) { 18 | if ((this.chosenFormat === TEXT_FORMAT || this.chosenFormat === JSON_FORMAT) === false) { 19 | throw new Error('Invalid format, options text or json'); 20 | } 21 | 22 | let formattedResult = null; 23 | 24 | // eslint-disable-next-line require-jsdoc 25 | function dhm(t) { 26 | const cd = 24 * 60 * 60 * 1000; 27 | const ch = 60 * 60 * 1000; 28 | let d = Math.floor(t / cd); 29 | let h = Math.floor( (t - d * cd) / ch); 30 | let m = Math.round( (t - d * cd - h * ch) / 60000); 31 | 32 | const pad = function(n) { 33 | return n < 10 ? '0' + n : n; 34 | }; 35 | 36 | if (m === 60) { 37 | h++; 38 | m = 0; 39 | } 40 | 41 | if (h === 24) { 42 | d++; 43 | h = 0; 44 | } 45 | 46 | return [d, pad(h), pad(m)]; 47 | } 48 | 49 | const expires = new Date(dateInfo.valid_to); 50 | const now = mockNow ? new Date(mockNow) : new Date(); 51 | const days = dhm(expires - now)[0]; 52 | 53 | if (this.chosenFormat === TEXT_FORMAT) { 54 | const daysText = days <= 0 ? 'This has expired!' : 'Expires in ' + days + ' days'; 55 | 56 | formattedResult = `Certification for ${host}\n` + 57 | `Issue On: ${dateInfo.valid_from}\n`+ 58 | `Expires On: ${dateInfo.valid_to}\n` + 59 | `${daysText}\n`; 60 | } else if (this.chosenFormat === JSON_FORMAT) { 61 | dateInfo.expires = dhm(expires - new Date())[0]; 62 | dateInfo.expired = days <= 0; 63 | dateInfo.host = host; 64 | 65 | formattedResult = JSON.stringify(dateInfo, null, 4); 66 | } 67 | 68 | return formattedResult; 69 | }; 70 | 71 | module.exports = ResultFormatter; 72 | -------------------------------------------------------------------------------- /test/formatters/ResultFormatter.test.js: -------------------------------------------------------------------------------- 1 | const ResultFormatter = require('../../src/formatters/ResultFormatter'); 2 | 3 | describe('#SSL Formatter functions ', () => { 4 | const mock = { 5 | valid_from: new Date(), 6 | valid_to: new Date() + 1, 7 | }; 8 | 9 | const host = 'google.com'; 10 | 11 | describe('Formatter input validation', () => { 12 | it('throws an Exception when setting invalid format', () => { 13 | expect(function() { 14 | const formatter = new ResultFormatter('XML'); 15 | formatter.format(); 16 | }).toThrow(Error, 'Invalid format'); 17 | }); 18 | }); 19 | 20 | describe('Setters and Getters work', () => { 21 | it('Constructor variables stored and recalled', () => { 22 | const formatter = new ResultFormatter('text'); 23 | expect(formatter.getFormat()).toBe('text'); 24 | }); 25 | 26 | it('Setters and getters stored and recalled', () => { 27 | const formatter = new ResultFormatter('text'); 28 | formatter.setFormat('json'); 29 | expect(formatter.getFormat()).toBe('json'); 30 | }); 31 | }); 32 | 33 | describe('Formatters work', () => { 34 | it('JSON format works', () => { 35 | const formatter = new ResultFormatter('json'); 36 | expect(formatter.format(host, mock)).toBe(JSON.stringify(mock, null, 4)); 37 | }); 38 | 39 | it('Text format works when valid', () => { 40 | const mock = { 41 | valid_from: 'Mon Oct 23 2016 19:59:39 GMT+0100 (BST)', 42 | valid_to: 'Mon Mar 23 2018 19:59:39 GMT+0100 (BST)', 43 | }; 44 | 45 | const expected = 'Certification for ' + host + '\n' + 46 | 'Issue On: ' + mock.valid_from + '\n' + 47 | 'Expires On: ' + mock.valid_to + '\n' + 48 | 'Expires in 150 days\n'; 49 | 50 | const mockNow = 'Tue Oct 24 2017 11:59:39 GMT+0100 (BST)'; 51 | const formatter = new ResultFormatter('text'); 52 | expect(formatter.format(host, mock, mockNow)).toBe(expected); 53 | }); 54 | 55 | it('Text format works when expired', () => { 56 | const mock = { 57 | valid_from: 'Mon Oct 23 2011 19:59:39 GMT+0100 (BST)', 58 | valid_to: 'Mon Mar 23 2012 19:59:39 GMT+0100 (BST)', 59 | }; 60 | 61 | const expected = 'Certification for ' + host + '\n' + 62 | 'Issue On: ' + mock.valid_from + '\n' + 63 | 'Expires On: ' + mock.valid_to + '\n' + 64 | 'This has expired!\n'; 65 | 66 | const formatter = new ResultFormatter('text'); 67 | expect(formatter.format(host, mock)).toBe(expected); 68 | }); 69 | }); 70 | }); 71 | --------------------------------------------------------------------------------