first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,84 @@
import * as os from 'os';
import { Facility, Severity, SyslogClient, Transport } from '../src';
import { TLS_CERTIFICATE } from './setup';
describe('SyslogClient - Core', () => {
describe('Constructor and defaults', () => {
it('should set default options correctly', () => {
const client = new SyslogClient();
expect(client.target).toBe('127.0.0.1');
expect(client.port).toBe(514);
expect(client.syslogHostname).toBe(os.hostname());
expect(client.tcpTimeout).toBe(10000);
expect(client.transport).toBe(Transport.Udp);
client.close();
});
it('should accept target parameter', () => {
const client = new SyslogClient('127.0.0.2');
expect(client.target).toBe('127.0.0.2');
expect(client.port).toBe(514);
expect(client.syslogHostname).toBe(os.hostname());
client.close();
});
it('should accept custom hostname', () => {
const client = new SyslogClient('127.0.0.2', {
syslogHostname: 'test',
});
expect(client.target).toBe('127.0.0.2');
expect(client.syslogHostname).toBe('test');
client.close();
});
it('should accept custom port and timeout', () => {
const client = new SyslogClient('127.0.0.2', {
syslogHostname: 'test',
port: 5555,
tcpTimeout: 50,
});
expect(client.port).toBe(5555);
expect(client.tcpTimeout).toBe(50);
client.close();
});
it('should accept TCP transport option', () => {
const client = new SyslogClient('127.0.0.2', {
port: 5555,
transport: Transport.Tcp,
});
expect(client.transport).toBe(Transport.Tcp);
client.close();
});
it('should accept TLS transport with certificate', () => {
const client = new SyslogClient('127.0.0.2', {
port: 6514,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
expect(client.transport).toBe(Transport.Tls);
expect(client.tlsCA).toBe(TLS_CERTIFICATE);
client.close();
});
it('should accept facility and severity options', () => {
const client = new SyslogClient('127.0.0.1', {
facility: Facility.Mail,
severity: Severity.Critical,
});
expect(client.facility).toBe(Facility.Mail);
expect(client.severity).toBe(Severity.Critical);
client.close();
});
it('should accept RFC format option', () => {
const client = new SyslogClient('127.0.0.1', {
rfc3164: false,
});
expect(client.rfc3164).toBe(false);
client.close();
});
});
});
+32
View File
@@ -0,0 +1,32 @@
# Test Fixtures
This directory contains test certificates for TLS testing.
## Required Files
These files can be regenerated using the following commands:
- `key.pem` - Private key for test TLS server
- `certificate.pem` - Valid certificate for test TLS server
- `wrong.pem` - Invalid certificate for testing certificate validation
## Generate Test Certificates
You can generate self-signed test certificates using OpenSSL:
```bash
# Generate private key
openssl genrsa -out key.pem 2048
# Generate valid certificate
openssl req -new -x509 -key key.pem -out certificate.pem -days 10000 \
-subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost"
# Generate wrong certificate (different key)
openssl genrsa -out wrong-key.pem 2048
openssl req -new -x509 -key wrong-key.pem -out wrong.pem -days 10000 \
-subj "/C=US/ST=Wrong/L=Wrong/O=Wrong/CN=wrong"
rm wrong-key.pem
```
These certificates are only for testing and should never be used in production.
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfzCCAmegAwIBAgIUEo4pU1hgsKHlV01QIN3hhDaQ6ggwDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
DTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNjAxMDUyMDQ1
NDhaGA8yMDUzMDUyMzIwNDU0OFowTjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRl
c3QxDTALBgNVBAcMBFRlc3QxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMzoP33jcgWYJ/mS
A8NQm9gABgeOVHlc4lEqHsMo9gZy3GfmanI/XFpTr3Nxeb2AMqJ1B9EgXOtJLBDP
J+Z+k3jN9Sv2ZRSClf6ZZUUPFKcjQ6TNYXG/Jgmjoku7k0I9cJluXE8VwlC9mzty
iHbZGo/tRtU4LkbaedmL2eTctcBukX4g99uVMJVKot5l8PWxZ1seVZ6T4IiRcaRo
NcQqL67xspm3KmlrVx/WzlM41BfGj2k+L4Bgenm/bIg1RPQ+MoETViVFGeKgKjkp
kTOjCsZqeK2STCIj1+XI4tLMCxV0B95fw7bcFfEtbnL9XyEjyn+3A33625EJMoiq
OX6bb9MCAwEAAaNTMFEwHQYDVR0OBBYEFEB7f6J6nW90lXS8i4gOR/6rya7qMB8G
A1UdIwQYMBaAFEB7f6J6nW90lXS8i4gOR/6rya7qMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBAIXoFrV9saNREtlDkMp5O66IO+UFhr1NcLYmXvhs
n3H+OFpg/Uo+/p++vHgf5UKNTdx24pO/88FyPujIcq1mfemrAIgcd8t++aclzSlf
G49bZvhYyxsE+qoyl7XWtmYLPydCpYKkL7krLwUVzSeC/f6f/GWEn0vvwWPht5ky
dR/Uze7pg/Ux12gztSLPV77n9gBua6lj5HKhXYGoIFuZSw9n1tWqndT28OQ3VwI7
ZSpFFJmDyevspC2F18NaTE5VmXv9gckeUmqx8nr0zOmWWREVnFt+d4NNvrNLEChj
IJFfkzynUHYTe+1oQkv8Evr0GHU5ocHsqOQ4NqW5gx0XXRA=
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDM6D9943IFmCf5
kgPDUJvYAAYHjlR5XOJRKh7DKPYGctxn5mpyP1xaU69zcXm9gDKidQfRIFzrSSwQ
zyfmfpN4zfUr9mUUgpX+mWVFDxSnI0OkzWFxvyYJo6JLu5NCPXCZblxPFcJQvZs7
coh22RqP7UbVOC5G2nnZi9nk3LXAbpF+IPfblTCVSqLeZfD1sWdbHlWek+CIkXGk
aDXEKi+u8bKZtyppa1cf1s5TONQXxo9pPi+AYHp5v2yINUT0PjKBE1YlRRnioCo5
KZEzowrGanitkkwiI9flyOLSzAsVdAfeX8O23BXxLW5y/V8hI8p/twN9+tuRCTKI
qjl+m2/TAgMBAAECggEARqkwe/EbOOKnpmC7+3aox2v6qFBkwRVO1j54LUTK0p55
czr6Kju/VUbWkjDnknnK/ErvTyah5GFvWLyXPd0YbehYS7jEUrZVk17ClXYF4T0/
7m0E1XzdWSWcqhEdTxJw6fgszPjr8XvxNCbi+FkV19wzOQQOsVBWBLc9hLa31ous
CCMmpmJD+KaaN1D7VKDyLOtMQDPM7xft9VcWTU0qATN86HzvTxRF3+wWi1Z8ChV+
miMfu4yb4Ip/tFSZfCJQqWupxdXYxw+p6HbcYRxSFa8ux4oZoQwvSVIi3pGKFXij
/0oBhx2N1Y0T5LnuUZvne6CjwSyRSCmN68FnUKiogQKBgQD5UuHXcMZiDyZSskpj
6rj//Hc3I9eRvB2CIvxVbDecRoCat3r9SZmZJ+DFxyH9Voy0ClrQnjSjPsXR5T89
Ya+dFELl0zg7scHMw9VVSBsIjPrBhCnqPII1tfZj5ZzKI0oM1P8DwsoBOMotZIjl
eHsUsd3Qw5sogbrwJ5d0cuDcaQKBgQDSZOPLwuEtAc7qyu1lSbifCMYwM4ISac3F
2f116BV43/9L8eQv41XmVl1ELmwebahQdIbTtP5bNGDRHLRyIfbsSWxzBamQkKX9
INzsiaeCENMTEHo4oSXX89DoZkr50iT62y9DKF5TkzNkA+CMvUuKfkh8YZAnLizA
dhrM0NOS2wKBgQD3nJ/ctgzgEEmwMY994fxvhw3i/j62ZswrlZFjSQGFu+M3ROfp
q3HXUGqEIbuY/Z7po6sDq0t9oPcX+QQwctbVOu0hkP81Edn2AbvaYa0vdcwH6rkx
/3wV2axlFAH+IsQFMHhABPFX+02XfVQUCe6649b24X0z4nuEzN028mxtmQKBgC7q
XHmVbUzx+EgXFDTf6ZFdDYS2F60vdrlF0OU36YHYwT8YI4AiO4dvfsCzcVyfq0a1
lVMkKwv/dA9tTTeeJ0etX8eOXa9k8f6uE1WTpIy2X7sCk56JIL01G0KXfOSUXbaq
pbFeERql2nHVA3+evneVjWhfARwbidMpAvTlJCi9AoGAQxFKi/dKzjZQFkikv9xU
QpvtrfjZvYHU56HMDkKA4yIjBpONPWc6TAir5eOroKIKxAF0K+5s/bcQqZagShfZ
WhZSdUid4J+gPCYL2v5F8symu9dxP54xRw1886uJUuqyb7tHhdFCOHRclD5wlLg3
OrfEUwhYRn7d21UZoGDAvYU=
-----END PRIVATE KEY-----
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Generate valid certificate
openssl req -new -x509 -key key.pem -out certificate.pem -days 10000 \
-subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost"
# Generate wrong certificate (different key)
openssl genrsa -out wrong-key.pem 2048
openssl req -new -x509 -key wrong-key.pem -out wrong.pem -days 10000 \
-subj "/C=US/ST=Wrong/L=Wrong/O=Wrong/CN=wrong"
rm wrong-key.pem
+21
View File
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIUfC52wMYMzcoL9c/GY3Ks0beSH2kwDQYJKoZIhvcNAQEL
BQAwTTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVdyb25nMQ4wDAYDVQQHDAVXcm9u
ZzEOMAwGA1UECgwFV3JvbmcxDjAMBgNVBAMMBXdyb25nMCAXDTI2MDEwNTIwNDU0
OFoYDzIwNTMwNTIzMjA0NTQ4WjBNMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFV3Jv
bmcxDjAMBgNVBAcMBVdyb25nMQ4wDAYDVQQKDAVXcm9uZzEOMAwGA1UEAwwFd3Jv
bmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCj1725lupt3pTvMf2O
o931s2l4ObA5qTC6ihlWJX+A7R4RkHrQTlwahEwIgMNSzNURtXOwu3ePyqZE3Luk
pVWGMFdgedNuU+4zNUXXamIyIJfsMuEmw6xrWLpdWqawo0G50Y2RxB4MN6/127PL
EqogXC+0PHdgBUWtso/sJcGRrdQvGpmH9ZXavVNhER4G3q6f/Tcp1eu0xry1UwGV
WN/DghkE5R/9mLm830Lr7gLpEQ6JKUUTnPKaZQW9m9G8gjr6oEXR9QaTcTariHgx
HT0wkGRKO54CC3RdBxqwucdkvgUI7OkaGl8tS46QMI4DwQHcUFLkqx29wXHY/Zhu
mHxDAgMBAAGjUzBRMB0GA1UdDgQWBBT8E+hqfJTcai2AlqT27R4KL/nGNTAfBgNV
HSMEGDAWgBT8E+hqfJTcai2AlqT27R4KL/nGNTAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQCf38uZYe6+cW7/juw2wCoLObSBoomRuJicwpRbDJpo
kzKzlBm4KbAwrz0+2kugZCrqrY1pLZPozQiqqf7UOImNcBi/wCbNk6iksdD9WRpv
tooKb2UHY9Z+1VJ7FqBhEekUJnxAoOlmOnSwPW3qDQNaP+niWZEZjgK+m+3/ZI5d
oW8lU1UGYJs/pZrZDwPUGHRnPj7Sa6q0S5OCVFx4BN+YMKN9ynvVG0z0pkO3QpA3
wOYu/WxINCuFb6Iu469Ov/FoSfnqZRcqZ+PlMDWHr0NCj0DNBP/eGq8VWy6VRabQ
SyRRjByPFe8fK1PAbrwIh8w6QIWz28OQZjAz0TsHnK7D
-----END CERTIFICATE-----
@@ -0,0 +1,285 @@
import { SyslogClient, Transport } from '../src';
import {
awaitUdpMsg,
constructRfc5424Regex,
constructSyslogRegex,
startTestServers,
stopTestServers,
SYSLOG_UDP_PORT,
} from './setup';
beforeAll(async () => {
await startTestServers();
});
afterAll(() => {
stopTestServers();
});
describe('SyslogClient - Message Formats', () => {
describe('RFC 3164 (BSD syslog)', () => {
it('should send RFC 3164 format by default', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('RFC 3164 test');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'RFC 3164 test'));
client.close();
});
it('should send RFC 3164 format when explicitly enabled', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
rfc3164: true,
});
await client.log('RFC 3164 explicit');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'RFC 3164 explicit'));
client.close();
});
it('should send back-dated RFC 3164 messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const backdate = new Date(2017, 2, 1);
await client.log('Back-dated test', { timestamp: backdate });
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Back-dated test', backdate));
client.close();
});
it('should handle messages with custom options', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Custom options', {
rfc3164: true,
msgid: '12345', // msgid is ignored in RFC 3164
});
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Custom options'));
client.close();
});
});
describe('RFC 5424 (modern syslog)', () => {
it('should send RFC 5424 format when enabled', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
rfc3164: false,
});
await client.log('RFC 5424 test');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'RFC 5424 test', '-'));
client.close();
});
it('should use default msgid when not provided', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Default msgid', { rfc3164: false });
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Default msgid', '-'));
client.close();
});
it('should use custom msgid when provided', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Custom msgid', {
rfc3164: false,
msgid: '98765',
});
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Custom msgid', '98765'));
client.close();
});
it('should accept msgid up to 32 characters (RFC 5424 limit)', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const maxLengthMsgid = '550e8400e29b41d4a716446655440000'; // 32 chars (UUID without hyphens)
await client.log('Max length msgid', {
rfc3164: false,
msgid: maxLengthMsgid,
});
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Max length msgid', maxLengthMsgid));
client.close();
});
it('should reject msgid longer than 32 characters (RFC 5424 limit)', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const tooLongMsgid = '550e8400-e29b-41d4-a716-446655440000'; // 36 chars (UUID with hyphens)
await expect(
client.log('Too long msgid', {
rfc3164: false,
msgid: tooLongMsgid,
}),
).rejects.toThrow();
client.close();
});
it('should send back-dated RFC 5424 messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const backdate = new Date(2017, 2, 1);
await client.log('Back-dated RFC 5424', {
rfc3164: false,
msgid: '98765',
timestamp: backdate,
});
const msg = await awaitUdpMsg();
expect(msg).toMatch(
constructRfc5424Regex(134, hostname, 'Back-dated RFC 5424', '98765', backdate),
);
client.close();
});
it('should include process ID in RFC 5424 messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
rfc3164: false,
});
await client.log('Process ID test');
const msg = await awaitUdpMsg();
// Check that message contains a process ID (numeric)
expect(msg).toMatch(/\d+ - - Process ID test/);
client.close();
});
});
describe('Format switching', () => {
it('should allow switching format per message', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
rfc3164: true, // Default to RFC 3164
});
// Send RFC 3164 message (default)
await client.log('Message 1');
const msg1 = await awaitUdpMsg();
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'Message 1'));
// Send RFC 5424 message (override)
await client.log('Message 2', { rfc3164: false });
const msg2 = await awaitUdpMsg();
expect(msg2).toMatch(constructRfc5424Regex(134, hostname, 'Message 2', '-'));
// Send RFC 3164 message again (back to default)
await client.log('Message 3');
const msg3 = await awaitUdpMsg();
expect(msg3).toMatch(constructSyslogRegex(134, hostname, 'Message 3'));
client.close();
});
});
describe('Message content', () => {
it('should preserve newlines in message', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const messageWithNewline = 'Message with\nnewline';
await client.log(messageWithNewline);
const msg = await awaitUdpMsg();
expect(msg).toContain(messageWithNewline);
client.close();
});
it('should handle empty messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, ''));
client.close();
});
});
});
+221
View File
@@ -0,0 +1,221 @@
import * as dgram from 'dgram';
import * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
import * as readline from 'readline';
import * as tls from 'tls';
// Test ports - dynamically assigned by getAvailablePort
export let SYSLOG_UDP_PORT = 0;
export let SYSLOG_TCP_PORT = 0;
export let SYSLOG_TLS_PORT = 0;
/**
* Get an available port by creating and immediately closing a server.
*/
const getAvailablePort = async (): Promise<number> =>
await new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
return reject(new Error('Unable to get port'));
}
const { port } = address;
server.close(() => resolve(port));
});
});
// Test certificates
export const TLS_PRIVATE_KEY = fs.readFileSync(path.join(__dirname, 'fixtures', 'key.pem'), 'utf8');
export const TLS_CERTIFICATE = fs.readFileSync(
path.join(__dirname, 'fixtures', 'certificate.pem'),
'utf8',
);
export const WRONG_CERTIFICATE = fs.readFileSync(
path.join(__dirname, 'fixtures', 'wrong.pem'),
'utf8',
);
// Message queues
const queuedUdpMessages: string[] = [];
const pendingUdpResolvers: Array<(msg: string) => void> = [];
const queuedTcpMessages: string[] = [];
const pendingTcpResolvers: Array<(msg: string) => void> = [];
const queuedTlsMessages: string[] = [];
const pendingTlsResolvers: Array<(msg: string) => void> = [];
// Test servers
let udpServer: dgram.Socket | null = null;
let tcpServer: net.Server | null = null;
let tlsServer: tls.Server | null = null;
/**
* Helper to await syslog messages from UDP server.
*/
export const awaitUdpMsg = async (): Promise<string> =>
await new Promise((resolve) => {
const queued = queuedUdpMessages.shift();
if (queued) return resolve(queued);
pendingUdpResolvers.push(resolve);
});
/**
* Helper to await syslog messages from TCP server.
*/
export const awaitTcpMsg = async (): Promise<string> =>
await new Promise((resolve) => {
const queued = queuedTcpMessages.shift();
if (queued) return resolve(queued);
pendingTcpResolvers.push(resolve);
});
/**
* Helper to await syslog messages from TLS server.
*/
export const awaitTlsMsg = async (): Promise<string> =>
await new Promise((resolve) => {
const queued = queuedTlsMessages.shift();
if (queued) return resolve(queued);
pendingTlsResolvers.push(resolve);
});
/**
* Escape special regex characters.
*/
const escapeRegExp = (text: string): string => text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
/**
* Construct regex to match RFC 3164 syslog message.
*/
export const constructSyslogRegex = (
pri: number,
hostname: string,
msg: string,
timestamp?: Date,
): RegExp => {
let patDate: string;
if (!timestamp) {
// RFC-3164 date format: "MMM dd HH:mm:ss"
// Month is 3 letters, day is space-padded, time is in 24-hour format
patDate = '[A-Z][a-z]{2}\\s{1,2}\\d{1,2}\\s\\d{2}:\\d{2}:\\d{2}';
} else {
const elements = timestamp.toString().split(/\s+/);
const month = elements[1];
let day = elements[2];
const time = elements[4];
// Ensure day is space-padded for single digits as per RFC-3164
if (day.length === 2 && day[0] === '0') {
day = ' ' + day.substring(1);
}
patDate = escapeRegExp(`${month} ${day} ${time}`);
}
// RFC-3164 format: <PRI>TIMESTAMP HOSTNAME MSG
// - PRI is enclosed in angle brackets
// - Single space between timestamp and hostname
// - Single space between hostname and message
// - Optional newline at the end
return new RegExp(
`^<${escapeRegExp(String(pri))}>${patDate}\\s${escapeRegExp(hostname)}\\s${escapeRegExp(msg)}\\n?$`,
);
};
/**
* Construct regex to match RFC 5424 syslog message.
*/
export const constructRfc5424Regex = (
pri: number,
hostname: string,
msg: string,
msgid: string | number,
timestamp?: Date,
): RegExp => {
const patDate = !timestamp
? '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z'
: escapeRegExp(timestamp.toISOString());
return new RegExp(
`^<${escapeRegExp(String(pri))}>\\d+ ${patDate} ${escapeRegExp(hostname)} \\S{1,48} \\d+ ${escapeRegExp(String(msgid))} - ${escapeRegExp(msg)}\\n?$`,
);
};
/**
* Start test syslog servers (UDP, TCP, TLS).
*/
export const startTestServers = async (): Promise<void> => {
// Get available ports
SYSLOG_UDP_PORT = await getAvailablePort();
SYSLOG_TCP_PORT = await getAvailablePort();
SYSLOG_TLS_PORT = await getAvailablePort();
// Setup UDP server
await new Promise<void>((resolve) => {
udpServer = dgram.createSocket('udp4');
udpServer.on('message', (msg) => {
const resolver = pendingUdpResolvers.shift();
if (resolver) return resolver(msg.toString());
queuedUdpMessages.push(msg.toString());
});
udpServer.on('listening', () => {
console.log(`Started UDP syslog server on port ${SYSLOG_UDP_PORT}`);
resolve();
});
udpServer.bind(SYSLOG_UDP_PORT);
});
// Setup TCP server
await new Promise<void>((resolve) => {
tcpServer = net.createServer((socket) => {
const lines = readline.createInterface({ input: socket, output: socket });
lines.on('line', (line) => {
const resolver = pendingTcpResolvers.shift();
if (resolver) return resolver(line);
queuedTcpMessages.push(line);
});
});
tcpServer.listen(SYSLOG_TCP_PORT, () => {
console.log(`Started TCP syslog server on port ${SYSLOG_TCP_PORT}`);
resolve();
});
});
// Setup TLS server
await new Promise<void>((resolve) => {
tlsServer = tls.createServer(
{
key: TLS_PRIVATE_KEY,
cert: TLS_CERTIFICATE,
secureProtocol: 'TLSv1_2_method',
},
(socket) => {
const lines = readline.createInterface({ input: socket, output: socket });
lines.on('line', (line) => {
const resolver = pendingTlsResolvers.shift();
if (resolver) return resolver(line);
queuedTlsMessages.push(line);
});
},
);
tlsServer.listen(SYSLOG_TLS_PORT, () => {
console.log(`Started TLS syslog server on port ${SYSLOG_TLS_PORT}`);
resolve();
});
});
};
/**
* Stop test syslog servers.
*/
export const stopTestServers = (): void => {
udpServer?.close();
tcpServer?.close();
tlsServer?.close();
udpServer = null;
tcpServer = null;
tlsServer = null;
};
@@ -0,0 +1,219 @@
import { Facility, Severity, SyslogClient, Transport } from '../src';
import {
awaitTcpMsg,
constructSyslogRegex,
startTestServers,
stopTestServers,
SYSLOG_TCP_PORT,
} from './setup';
beforeAll(async () => {
await startTestServers();
});
afterAll(() => {
stopTestServers();
});
describe('SyslogClient - TCP Transport', () => {
describe('Basic functionality', () => {
it('should connect and send log messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
await client.log('This is a test');
const msg1 = await awaitTcpMsg();
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
await client.log('This is a second test');
const msg2 = await awaitTcpMsg();
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
client.close();
});
it('should work with promise API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
await client.log('Promise test');
const msg = await awaitTcpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
client.close();
});
it('should work with callback API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
const callbackPromise = new Promise<void>((resolve) => {
void client.log('Callback test', (error) => {
expect(error).toBeUndefined();
resolve();
});
});
const msg = await awaitTcpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
await callbackPromise;
client.close();
});
});
describe('Transport reuse', () => {
it('should reuse TCP transport for multiple messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
await client.log('Transport reuse test');
await awaitTcpMsg();
const transport1 = client['transport_'];
expect(transport1).toBeDefined();
await client.log('Transport reuse test 2');
await awaitTcpMsg();
const transport2 = client['transport_'];
expect(transport2).toBe(transport1);
client.close();
});
});
describe('Close and reconnect', () => {
it('should emit close event', async () => {
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
transport: Transport.Tcp,
});
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
await client.log('Test');
await awaitTcpMsg();
client.close();
await closePromise;
expect(client['transport_']).toBeUndefined();
});
it('should reconnect after close', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
await client.log('Before close');
await awaitTcpMsg();
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
client.close();
await closePromise;
await client.log('After reconnect');
const msg = await awaitTcpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
client.close();
});
});
describe('Log options', () => {
it('should accept options with callback', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
const callbackPromise = new Promise<void>((resolve) => {
void client.log(
'With options',
{
facility: Facility.System,
severity: Severity.Notice,
},
(error) => {
expect(error).toBeUndefined();
resolve();
},
);
});
const msg = await awaitTcpMsg();
expect(msg).toMatch(constructSyslogRegex(29, hostname, 'With options'));
await callbackPromise;
client.close();
});
it('should calculate correct PRI value', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_TCP_PORT,
syslogHostname: hostname,
transport: Transport.Tcp,
});
await client.log('Test', {
facility: Facility.Local0,
severity: Severity.Emergency,
});
const msg = await awaitTcpMsg();
expect(msg).toMatch(constructSyslogRegex(128, hostname, 'Test'));
client.close();
});
});
describe('Error handling', () => {
it('should handle connection timeout', async () => {
const client = new SyslogClient('203.0.113.1', {
port: SYSLOG_TCP_PORT,
tcpTimeout: 500,
transport: Transport.Tcp,
});
const errorPromise = new Promise<Error>((resolve) => {
client.on('error', resolve);
});
await expect(client.log("Shouldn't work")).rejects.toThrow();
const error = await errorPromise;
expect(error).toBeInstanceOf(Error);
});
it('should handle invalid port', () => {
expect(() => {
new SyslogClient('127.0.0.1', {
port: 502342323,
tcpTimeout: 2000,
transport: Transport.Tcp,
});
}).toThrow();
});
});
});
@@ -0,0 +1,187 @@
import { ConnectionError, SyslogClient, Transport } from '../src';
import {
awaitTlsMsg,
constructSyslogRegex,
startTestServers,
stopTestServers,
SYSLOG_TLS_PORT,
TLS_CERTIFICATE,
WRONG_CERTIFICATE,
} from './setup';
beforeAll(async () => {
await startTestServers();
});
afterAll(() => {
stopTestServers();
});
describe('SyslogClient - TLS Transport', () => {
describe('Basic functionality', () => {
it('should connect and send log messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
syslogHostname: hostname,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
await client.log('This is a test');
const msg1 = await awaitTlsMsg();
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
await client.log('This is a second test');
const msg2 = await awaitTlsMsg();
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
client.close();
});
it('should work with promise API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
syslogHostname: hostname,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
await client.log('Promise test');
const msg = await awaitTlsMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
client.close();
});
it('should work with callback API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
syslogHostname: hostname,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
const callbackPromise = new Promise<void>((resolve) => {
void client.log('Callback test', (error) => {
expect(error).toBeUndefined();
resolve();
});
});
const msg = await awaitTlsMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
await callbackPromise;
client.close();
});
});
describe('Transport reuse', () => {
it('should reuse TLS transport for multiple messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
syslogHostname: hostname,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
await client.log('Transport reuse test');
await awaitTlsMsg();
const transport1 = client['transport_'];
expect(transport1).toBeDefined();
await client.log('Transport reuse test 2');
await awaitTlsMsg();
const transport2 = client['transport_'];
expect(transport2).toBe(transport1);
client.close();
});
});
describe('Close and reconnect', () => {
it('should emit close event', async () => {
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
await client.log('Test');
await awaitTlsMsg();
client.close();
await closePromise;
expect(client['transport_']).toBeUndefined();
});
it('should reconnect after close', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
syslogHostname: hostname,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
await client.log('Before close');
await awaitTlsMsg();
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
client.close();
await closePromise;
await client.log('After reconnect');
const msg = await awaitTlsMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
client.close();
});
});
describe('Certificate validation', () => {
it('should reject invalid certificate', async () => {
const client = new SyslogClient('localhost', {
port: SYSLOG_TLS_PORT,
transport: Transport.Tls,
tlsCA: WRONG_CERTIFICATE,
});
const errorPromise = new Promise<Error>((resolve) => {
client.on('error', resolve);
});
await expect(client.log('Should error')).rejects.toBeInstanceOf(ConnectionError);
const error: NodeJS.ErrnoException = await errorPromise;
expect(error.code).toBe('DEPTH_ZERO_SELF_SIGNED_CERT');
});
it('should handle connection timeout', async () => {
const client = new SyslogClient('203.0.113.1', {
port: SYSLOG_TLS_PORT,
tcpTimeout: 500,
transport: Transport.Tls,
tlsCA: TLS_CERTIFICATE,
});
const errorPromise = new Promise<Error>((resolve) => {
client.on('error', resolve);
});
await expect(client.log("Shouldn't work")).rejects.toThrow();
const error = await errorPromise;
expect(error).toBeInstanceOf(Error);
});
});
});
@@ -0,0 +1,182 @@
import { SyslogClient, Transport } from '../src';
import {
awaitUdpMsg,
constructSyslogRegex,
startTestServers,
stopTestServers,
SYSLOG_UDP_PORT,
} from './setup';
beforeAll(async () => {
await startTestServers();
});
afterAll(() => {
stopTestServers();
});
describe('SyslogClient - UDP Transport', () => {
describe('Basic functionality', () => {
it('should connect and send log messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('This is a test');
const msg1 = await awaitUdpMsg();
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
await client.log('This is a second test');
const msg2 = await awaitUdpMsg();
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
client.close();
});
it('should work with promise API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Promise test');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
client.close();
});
it('should work with callback API', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
const callbackPromise = new Promise<void>((resolve) => {
void client.log('Callback test', (error) => {
expect(error).toBeUndefined();
resolve();
});
});
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
await callbackPromise;
client.close();
});
});
describe('Transport reuse', () => {
it('should reuse UDP transport for multiple messages', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Transport reuse test');
await awaitUdpMsg();
const transport1 = client['transport_'];
expect(transport1).toBeDefined();
await client.log('Transport reuse test 2');
await awaitUdpMsg();
const transport2 = client['transport_'];
expect(transport2).toBe(transport1);
client.close();
});
});
describe('UDP bind address', () => {
it('should bind to specific network address', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
udpBindAddress: '127.0.0.1',
});
await client.log('Bind test');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Bind test'));
client.close();
});
it('should handle invalid bind address', () => {
expect(() => {
new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
transport: Transport.Udp,
udpBindAddress: '500.500.500.500',
});
}).toThrow();
});
});
describe('Close and reconnect', () => {
it('should emit close event', async () => {
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
transport: Transport.Udp,
});
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
await client.log('Test');
await awaitUdpMsg();
client.close();
await closePromise;
expect(client['transport_']).toBeUndefined();
});
it('should reconnect after close', async () => {
const hostname = 'testhostname';
const client = new SyslogClient('127.0.0.1', {
port: SYSLOG_UDP_PORT,
syslogHostname: hostname,
transport: Transport.Udp,
});
await client.log('Before close');
await awaitUdpMsg();
const closePromise = new Promise<void>((resolve) => {
client.once('close', resolve);
});
client.close();
await closePromise;
await client.log('After reconnect');
const msg = await awaitUdpMsg();
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
client.close();
});
});
describe('Error handling', () => {
it('should handle invalid port', () => {
expect(() => {
new SyslogClient('127.0.0.1', {
port: 12378726362,
transport: Transport.Udp,
});
}).toThrow();
});
});
});