среда, 26 февраля 2020 г.

Node.js - TLS Server and Client

1. Overview

This is an example of using module tls in NodeJS to create a client securely connecting to a TLS server.

It is a modified version from documentation about TLS, in which:

- The server is a simple echo one. Clients connect to it, get the same thing back if they send anything to the server.

- The server is a TLS-based server.

- Clients somehow get the server's public key and use it to work securely with the server.

2. Preparation

We need to generate keys & certs for the server. Pay attention to Common Name (e.g. server FQDN or YOUR name) when creating server-csr.pem. It should be your domain name.

$ mkdir tls
$ cd tls
$ openssl genrsa -out server-key.pem 4096
$ openssl req -new -key server-key.pem -out server-csr.pem
$ openssl x509 -req -in server-csr.pem -signkey server-key.pem -out server-cert.pem

For example:

$ openssl req -new -key server-key.pem -out server-csr.pem

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank.
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:VN
State or Province Name (full name) [Some-State]:Hanoi
Locality Name (eg, city) []:Hanoi
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Evolas Technologies
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:evolastech.com
Email Address []:hi@evolastech.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

3. Run the demo

$ node server.js
$ node client.js

You may have following things printed out:

server bound
server connected unauthorized
client connected authorized
welcome!

4. Files

client.js

const tls = require('tls');
const fs = require('fs');

const options = {
    ca: [ fs.readFileSync('server-cert.pem') ]
};

var socket = tls.connect(8000, 'evolastech.com', options, () => {
    console.log('client connected', socket.authorized ? 'authorized' : 'unauthorized');
    process.stdin.pipe(socket);
    process.stdin.resume();
});

socket.setEncoding('utf8');

socket.on('data', (data) => {
    console.log(data);
});

socket.on('end', () => {
    console.log('Ended')
});

sever.js

const tls = require('tls');
const fs = require('fs');

const options = {
    key: fs.readFileSync('server-key.pem'),
    cert: fs.readFileSync('server-cert.pem'),
    rejectUnauthorized: true
};

const server = tls.createServer(options, (socket) => {
    console.log('server connected', socket.authorized ? 'authorized' : 'unauthorized');
    socket.write('welcome!\n');
    socket.setEncoding('utf8');
    socket.pipe(socket);
});

server.listen(8000, () => {
    console.log('server bound');
});

четверг, 8 августа 2019 г.

Remove files after "npm install" command

Файл remove-files.js:

var fs = require('fs');

var doNotRemoveFiles =  [
    'remove-files.js',
    'node_modules',
    '@types',
    'dist',
    'docs',
    'src',
    'tests',
    '.gitignore',
    '.npmignore',
    '.npmrc',
    '.stylelintrc',
    'karma.conf.js',
    'package.json',
    'postcss.config.js',
    'readme.md',
    'tsconfig.json',
    'tslint.json',
    'webpack.config.js',
];

fs.readdir('./', function (error, fileNames) {
    if (error) {throw error;}
    var fileNamesCounter = fileNames.length;
    fileNames.forEach(function (fileName) {
        var len = doNotRemoveFiles.length;
        while (len--) {
            if (doNotRemoveFiles[len] === fileName) {return;}
        }
        fs.stat('./' + fileName, function (error, stats) {
            if (stats.isFile()) {
                fs.unlink('./' + fileName, function (error) {
                    if (error) {throw error;}
                    fileNamesCounter--;
                    if (fileNamesCounter === 0) {
                        console.log('DONE');
                    }
                });
            } else if (stats.isDirectory()) {
                fs.rmdir('./' + fileName, function (error) { // Remove empty "etc" directory.
                    if (error) {throw error;}
                    fileNamesCounter--;
                    if (fileNamesCounter === 0) {
                        console.log('DONE');
                    }
                });
            }
        });
    });
});

Node.js UUID

Файл uuid.js:

var crypto = require('crypto');

function rng () {
    return crypto.randomBytes(16);
}

/**
 * Convert array of 16 byte values to UUID string format of the form:
 * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
 */
var byteToHex = [];
for (var i = 0; i < 256; ++i) {
    byteToHex[i] = (i + 0x100).toString(16).substr(1);
}

function bytesToUuid (buf, offset) {
    var i = offset || 0;
    var bth = byteToHex;
    // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
    return [
        bth[buf[i++]], bth[buf[i++]],
        bth[buf[i++]], bth[buf[i++]], '-',
        bth[buf[i++]], bth[buf[i++]], '-',
        bth[buf[i++]], bth[buf[i++]], '-',
        bth[buf[i++]], bth[buf[i++]], '-',
        bth[buf[i++]], bth[buf[i++]],
        bth[buf[i++]], bth[buf[i++]],
        bth[buf[i++]], bth[buf[i++]]
    ].join('');
}

function uuid (options, buf, offset) {
    var i = buf && offset || 0;

    if (typeof options === 'string') {
        buf = options === 'binary' ? new Array(16) : null;
        options = null;
    }
    options = options || {};

    var rnds = options.random || (options.rng || rng)();

    // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
    rnds[6] = (rnds[6] & 0x0f) | 0x40;
    rnds[8] = (rnds[8] & 0x3f) | 0x80;

    // Copy bytes to buffer, if provided
    if (buf) {
        for (var ii = 0; ii < 16; ++ii) {
            buf[i + ii] = rnds[ii];
        }
    }

    return buf || bytesToUuid(rnds);
}

Файл index.js:

var uuid = require('./uuid');

console.log(uuid());

module.exports = uuid;

Node.js CLI Spinner

Файл spinner.js

function spinner () {
    var sequence = ['|', '/', '-', '\\'];
    var index = 0;
    var intervalId;
    return {
        start: function (doNotBlockNodeJsProcess) {
            index = 0;
            process.stdout.write(sequence[index]);
            intervalId = setInterval(function () {
                process.stdout.write(sequence[index].replace(/./g, '\b'));
                if (index < sequence.length - 1) {
                    index = index + 1;
                } else {
                    index = 0;
                }
                process.stdout.write(sequence[index]);
            }, 100);
           if (doNotBlockNodeJsProcess) {
                intervalId.unref();
           }
        },
        stop: function () {
            clearInterval(intervalId);
            process.stdout.write(sequence[index].replace(/./g, '\b'));
        }
    };
}

module.exports = spinner;

Файл index.js

var spinner = require('spinner')();

spinner.start(true);

setTimeout(function () {
     spinner.stop();
}, 5000);

XHR Test request in browser console

var data = {a: 1, b: 2};
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://site.com/send/data', false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
if (xhr.status !== 200) {
    console.log('ERROR');
} else {
    console.log(xhr.responseText);
}

понедельник, 15 июля 2019 г.

Создание Node.js HTTPS, TLS/SSL сертификатов.

Для работы с HTTPS, TLS/SSL необходимо произвести следующие действия:

1) Создать "центр сертификации" (CA - Certificate Authority), который позволит создавать и проверять "самоподписанные сертфикаты" (Self-Signed SSL Certificate).
2) Создать "приватный ключ" (Private key) для подписи сертификатов.
3) С помощью "приватного ключа" (Private key) создать "запрос на подпись сертификата" (CSR - Certificate signing request).
4) С помощью "приватного ключа" (Private key) и "запроса на подпись сертификата" (CSR - Certificate signing request) создать сам "сертификат" (Cert - Certificate).

Создание "центра сертификации" (Certificate Authority).

Для упрощения процесса скопируйте эти настройки в файл "ca.cnf" и пропишите в них ваши значения.

[ ca ]
default_ca      = CA_default

[ CA_default ]
serial = ca-serial
crl = ca-crl.pem
database = ca-database.txt
name_opt = CA_default
cert_opt = CA_default
default_crl_days = 9999
default_md = md5

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
output_password        = password

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = ca
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = test

Далее в командной строке вашего терминала выполните такую команду:

openssl req -new -x509 -days 9999 -config ca.cnf -keyout ca-key.pem -out ca-crt.pem

В результате вы создадите файлы "центра сертификации": ca-key.pem и ca-crt.pem.

Теперь создадим "приватный ключ" (Private key) для сервера Node.js с помощью следующей команды:

openssl genrsa -out server-key.pem 4096

В результате вы создадите файл server-key.pem.

Теперь создадим "запрос на подпись сертификата" (CSR - Certificate signing request).

Для упрощения процесса скопируйте эти настройки в файл "server.cnf" и пропишите в них ваши значения.

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = localhost
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/
caIssuers;URI.0 = http://example.com/ca.cert

После этого выполните команду:

openssl req -new -config server.cnf -key server-key.pem -out server-csr.pem

В результате вы создадите файл server-csr.pem.

Далее подпишем "запрос на подпись сертификата" (CSR - Certificate signing request) нашим "приватным ключем" (Private key) для создания итогового "сертификата" (Cert - Certificate) нашего Node.js HTTPS, TLS/SSL сервера, выполнив команду:

openssl x509 -req -extfile server.cnf -days 999 -passin "pass:password" -in server-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out server-crt.pem

Обратите внимание на заданный в команде пароль: "pass:password".

В результате вы создадите файл server-crt.pem.

Теперь созданный сертификат готов для использования не сервере.

Создадим файл "server.js" и пропишем в нем данный код для создания HTTPS сервера:

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

var options = {
    key: fs.readFileSync('server-key.pem'),
    cert: fs.readFileSync('server-crt.pem'),
    ca: fs.readFileSync('ca-crt.pem')
};

https.createServer(options, function (req, res) {
    console.log(new Date() + ' ' +  req.connection.remoteAddress + ' ' +  req.method + ' '+ req.url);
    res.writeHead(200);
    res.end("Hello world!");
}).listen(443, '127.0.0.1');

Проверим, как работает наш сервер при получения запроса от браузера, перейдя в браузере по адресу "127.0.0.1:443".

Не забудьте добавить в вашу операционную систему созданный нами "центр сертификации" (CA - Certificate Authority) путем установки файла "ca-crt.pem" и пометки его, как доверенного.

После проверки работы сервера через браузер создадим HTTPS-клиент для Node.js, который мог бы соединяться с нашим HTTPS-сервером c проверкой сертификата сервера через созданный нами "центр сертификации" (CA - Certificate Authority).

Создадим файл "client.js" и пропишем в нем данный код для создания HTTPS клиента:

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

var options = {
    method: 'GET',
    hostname: 'localhost',
    port: 443,
    path: '/',
    ca: fs.readFileSync('ca-crt.pem')
};

var req = https.request(options, function (res) {
    res.on('data', function (data) {
        process.stdout.write(data);
    });
});

req.end();

Обратите внимание, что в options мы добавили свойство "ca", в котором прописан наш файл  "центра сертификации" (CA - Certificate Authority) для того, чтобы наш клиент мог доверять "публичному ключу" (Public key), который он получит от сервера при подключении к нему.

Теперь выполните код подключения клиента к серверу и в консоли командной строки вы увидите надпись: "Hello world!".

Далее создадим свой сертификат для клиента, чтобы он тоже мог с помощью него представляться серверу.

Мы сделаем два сертификата для того, чтобы иметь возможность делать запросы к серверу из двух разных клиентов.

Создадим каждому клиенту свой "приватный ключ" (Private key) командами:

openssl genrsa -out client1-key.pem 4096

openssl genrsa -out client2-key.pem 4096

В результате вы создадите файл client1-key.pem и файл client2-key.pem.

Далее создадим два "запроса на подпись сертификата" (CSR - Certificate signing request).

Для упрощения процесса скопируйте эти настройки в файл "client1.cnf" и пропишите в них ваши значения.

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = client1
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/
caIssuers;URI.0 = http://example.com/ca.cert

А настройки ниже скопируйте в файл "client2.cnf" и пропишите в них ваши значения.

[ req ]
default_bits           = 4096
days                   = 9999
distinguished_name     = req_distinguished_name
attributes             = req_attributes
prompt                 = no
x509_extensions        = v3_ca

[ req_distinguished_name ]
C                      = US
ST                     = MA
L                      = Boston
O                      = Example Co
OU                     = techops
CN                     = client2
emailAddress           = certs@example.com

[ req_attributes ]
challengePassword      = password

[ v3_ca ]
authorityInfoAccess = @issuer_info

[ issuer_info ]
OCSP;URI.0 = http://ocsp.example.com/
caIssuers;URI.0 = http://example.com/ca.cert

"Запросы на подпись сертификата" (CSR - Certificate signing request) создадим с помощью пары команд:

openssl req -new -config client1.cnf -key client1-key.pem -out client1-csr.pem

openssl req -new -config client2.cnf -key client2-key.pem -out client2-csr.pem

В результате вы создадите файл client1-csr.pem и файл client2-csr.pem.

Теперь подпишем "запросы на подпись сертификата" (CSR - Certificate signing request) нашим "приватным ключем" (Private key) для создания итоговых "сертификатов" (Cert - Certificate) наших Node.js HTTPS, TLS/SSL клиентов, выполнив команду:

openssl x509 -req -extfile client1.cnf -days 999 -passin "pass:password" -in client1-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out client1-crt.pem


openssl x509 -req -extfile client2.cnf -days 999 -passin "pass:password" -in client2-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out client2-crt.pem

В результате вы создадите файл client1-crt.pem и файл client2-crt.pem.

Для того, чтобы убедиться, что мы все сделали верно проверим наши сертификаты, выполнив команды:

openssl verify -CAfile ca-crt.pem client1-crt.pem

openssl verify -CAfile ca-crt.pem client2-crt.pem

Такую же проверку вы можете сделать и для сертификата, который вы создали для сервера, выполнив команду:

openssl verify -CAfile ca-crt.pem server-crt.pem

Если вы все сделали верно, то вы получите "OK" в результате проверки.

Теперь напишем код сервера и клиента, использующего каждый свой сертификат.

Добавим в код сервера свойства requestCert и rejectUnauthorized, требующие, чтобы клиент всегда предоставлял серверу свой сертификат, изменив код в файле "server.js" так:

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

var options = {
    key: fs.readFileSync('server-key.pem'),
    cert: fs.readFileSync('server-crt.pem'),
    ca: fs.readFileSync('ca-crt.pem'),
    requestCert: true, 
    rejectUnauthorized: true
};

https.createServer(options, function (req, res) {
    console.log(new Date() + ' '  + req.connection.remoteAddress + ' ' + req.socket.getPeerCertificate().subject.CN +  ' '  + req.method +  ' ' +  req.url);
    res.writeHead(200);
    res.end("Hello world!");
}).listen(443);

Теперь, если вы попробуйте подключиться к серверу из клиента, то подключение клиента будет отвергнуто сервером, поскольку в данный момент клиент еще не умеет передавать серверу свой сертификат.

В итоге Node.js выдаст сообщение об ошибке "socket hangup error" из-за того, что TLS не будет соблюдено.

Поэтому, чтобы все заработало мы добавим в код клиента в файле "client.js" опции key и cert так:

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

var options = {
    method: 'GET',
    hostname: 'localhost',
    port: 443,
    path: '/',
    key: fs.readFileSync('client1-key.pem'), 
    cert: fs.readFileSync('client1-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem')
};

var req = https.request(options, function (res) {
    res.on('data', function(data) {
        process.stdout.write(data);
    });
});

req.on('error', function(e) {
    console.error(e);
});

req.end();

После этого выполните подключение клиента к серверу и сервер в ответ вернет "Hello world!".
В консоли сервера в этом случае вы увидите информации о подключении клиента и его "Common Name" из переданного серверу сертификата.

Поменяйте в коде клиента сертификат client1 на сертификат client2 и вы увидите, что клиент со вторым сертификатом также подключается к серверу.

Теперь проведем "отзыв сертификата" (CR - Certificate Revocation).

Для отзыва второго сертификата клиента мы создадим "список отозванных сертификатов" (CRL - Certificate Revocation List).

Чтобы это сделать создайте пустой текстовый файл "ca-database.txt", символизирующий базу данных.

Далее выполните команду для отзыва второго сертификата клиента client2-crt.pem:

openssl ca -revoke client2-crt.pem -keyfile ca-key.pem -config ca.cnf -cert ca-crt.pem -passin "pass:password"

Обратите внимание на заданный в команде пароль: "pass:password".

После этого выполните команду для обновления "списка отозванных сертификатов" (CRL - Certificate Revocation List):

openssl ca -keyfile ca-key.pem -cert ca-crt.pem -config ca.cnf -gencrl -out ca-crl.pem -passin "pass:password"

Наконец добавьте в опции сервера свойство crl для получения доступа к "списку отозванных сертификатов" (CRL - Certificate Revocation List), изменив код так:

var https = require('https');
var fs = require('fs'); 
var options = { 
    key: fs.readFileSync('server-key.pem'), 
    cert: fs.readFileSync('server-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem'), 
    crl: fs.readFileSync('ca-crl.pem'), 
    requestCert: true, 
    rejectUnauthorized: true 
}; 

https.createServer(options, function (req, res) { 
    console.log(new Date()+ ' ' + req.connection.remoteAddress + ' ' + req.socket.getPeerCertificate().subject.CN + ' ' + req.method + ' ' + req.url); 
    res.writeHead(200); 
    res.end("Hello world!"); 
}).listen(443);

После этого сервер будет учитывать "список отозванных сертификатов" (CRL - Certificate Revocation List) и принимать запрос от клиента с сертификатом client1-crt.pem и отвергать запросы клиента с сертификатом client2-crt.pem.

Таким образом, мы научились создавать самоподписанные сертификаты для сервера и проверять сертификаты, приходящие от клиентов. Дополнительно к этому мы научились отзывать сертификаты.

Поскольку в сертификатах содержится имя клиента Common Name, то мы также можем использовать их для идентификации каждого клиента на нашем сервере.

вторник, 9 июля 2019 г.

Node.js - Simple Stream and Generator Demos

Simple Stream.

const {Readable, Writable} = require('stream');

// Reader
const createCounterReader = () => {
let count = 0;
return new Readable({
objectMode: true,
read () {
count += 1;
console.log('reading:', count);
this.push(count);
}
});
};

const counterReader = createCounterReader();

// Writer
const logWriter = new Writable({
objectMode: true,
write: (chunk, _, done) => {
setTimeout(() => {
console.log('writing:', chunk);
done();
}, 100);
}
});

// Pipe reader to Writer
counterReader.pipe(logWriter);


Stream for async counter.

const {Readable, Writable} = require('stream');

const createCounterReader = (delay) => {
let counter = 0;
const reader = new Readable({
objectMode: true,
read () {}
});
setInterval(() => {
counter += 1;
console.log('reading:', counter);
reader.push(counter);
}, delay);
return reader;
};

const counterReader = createCounterReader(1000);

const logWriter = new Writable({
objectMode: true,
write: (chunk, _, done) => {
console.log('writing:', chunk);
done();
}
});

counterReader.pipe(logWriter);


Stream for async counter writer.

const {Readable, Writable} = require('stream');

const createCounterReader = (delay) => {
let counter = 0;
const reader = new Readable({
objectMode: true,
read () {}
});
setInterval(() => {
counter += 1;
console.log('reading:', counter);
reader.push(counter);
}, delay);
return reader;
};

const counterReader = createCounterReader(1000);

const logWriter = new Writable({
objectMode: true,
write: (chunk, _, done) => {
setTimeout(() => {
console.log('writing:', chunk);
done();
}, 5 * 1000);
}
});

counterReader.pipe(logWriter);


Simple Generator Sync.

function* counterGenerator() {
let count = 0;
while (true) {
count += 1;
console.log('reading:', count);
yield count;
}
}

const counterIterator = counterGenerator();

const logIterator = (iterator) => {
for (const item of iterator) {
console.log('writing:', item);
}
};

logIterator(counterIterator);


Simple Generator Async.

function* counterGenerator() {
let count = 0;
while (true) {
count += 1;
console.log('reading:', count);
yield count;
}
}

const counterIterator = counterGenerator();

const logIterator = async (iterator) => {
for (const item of iterator) {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 100);
});
console.log('writing:', item);
}
};

logIterator(counterIterator);


Async Generator Counter.

async function* counterGenerator(delay) {
let counter = 0;
while (true) {
await new Promise(r => setTimeout(r, delay));
counter += 1;
console.log('reading:', counter);
yield counter;
}
}
const counterIterator = counterGenerator(1000);

const logIterator = async (iterator) => {
for await (const item of iterator) {
console.log('writing:', item);
}
};

logIterator(counterIterator);


Async Generator Counter for Async Interator.

async function* counterGenerator(delay) {
let counter = 0;
while (true) {
await new Promise(r => setTimeout(r, delay));
counter += 1;
console.log('reading:', counter);
yield counter;
}
}
const counterIterator = counterGenerator(1000);

const logIterator = async (iterator) => {
for await (const item of iterator) {
console.log('writing:', item);
await new Promise(r => setTimeout(r, 5 * 1000));
}
};

logIterator(counterIterator);