@logtape/syslog 1.3.0-dev.388 → 1.3.0-dev.398

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/syslog.ts CHANGED
@@ -3,6 +3,7 @@ import { createSocket } from "node:dgram";
3
3
  import { Socket } from "node:net";
4
4
  import { hostname } from "node:os";
5
5
  import process from "node:process";
6
+ import * as tls from "node:tls";
6
7
 
7
8
  /**
8
9
  * Syslog protocol type.
@@ -84,6 +85,26 @@ const SEVERITY_LEVELS: Record<LogLevel, number> = {
84
85
  trace: 7, // Debug: debug-level messages (same as debug)
85
86
  };
86
87
 
88
+ /**
89
+ * TLS options for secure TCP connections.
90
+ * @since 1.3.0
91
+ */
92
+ export interface SyslogTlsOptions {
93
+ /**
94
+ * Whether to reject connections with invalid certificates.
95
+ * Setting this to `false` disables certificate validation, which makes
96
+ * the connection vulnerable to man-in-the-middle attacks.
97
+ * @default `true`
98
+ */
99
+ readonly rejectUnauthorized?: boolean;
100
+
101
+ /**
102
+ * Custom CA certificates to trust. If not provided, the default system
103
+ * CA certificates are used.
104
+ */
105
+ readonly ca?: string | readonly string[];
106
+ }
107
+
87
108
  /**
88
109
  * Options for the syslog sink.
89
110
  * @since 0.12.0
@@ -107,6 +128,21 @@ export interface SyslogSinkOptions {
107
128
  */
108
129
  readonly protocol?: SyslogProtocol;
109
130
 
131
+ /**
132
+ * Whether to use TLS for TCP connections.
133
+ * This option is ignored for UDP connections.
134
+ * @default `false`
135
+ * @since 1.3.0
136
+ */
137
+ readonly secure?: boolean;
138
+
139
+ /**
140
+ * TLS options for secure TCP connections.
141
+ * This option is only used when `secure` is `true` and `protocol` is `"tcp"`.
142
+ * @since 1.3.0
143
+ */
144
+ readonly tlsOptions?: SyslogTlsOptions;
145
+
110
146
  /**
111
147
  * The syslog facility to use for all messages.
112
148
  * @default "local0"
@@ -411,46 +447,97 @@ export class NodeUdpSyslogConnection implements SyslogConnection {
411
447
  * @since 0.12.0
412
448
  */
413
449
  export class DenoTcpSyslogConnection implements SyslogConnection {
414
- private connection?: Deno.TcpConn;
450
+ private connection?: Deno.TcpConn | Deno.TlsConn;
415
451
  private encoder = new TextEncoder();
416
452
 
417
453
  constructor(
418
454
  private hostname: string,
419
455
  private port: number,
420
456
  private timeout: number,
457
+ private secure: boolean,
458
+ private tlsOptions?: SyslogTlsOptions,
421
459
  ) {}
422
460
 
423
461
  async connect(): Promise<void> {
424
462
  try {
425
- if (this.timeout > 0) {
426
- // Use AbortController for proper timeout handling
427
- const controller = new AbortController();
428
- const timeoutId = setTimeout(() => {
429
- controller.abort();
430
- }, this.timeout);
431
-
432
- try {
433
- this.connection = await Deno.connect({
434
- hostname: this.hostname,
435
- port: this.port,
436
- transport: "tcp",
437
- signal: controller.signal,
463
+ const connectOptions: Deno.ConnectOptions = {
464
+ hostname: this.hostname,
465
+ port: this.port,
466
+ transport: "tcp",
467
+ };
468
+
469
+ if (this.secure) {
470
+ const tlsConnectOptions: Deno.ConnectTlsOptions = {
471
+ hostname: this.hostname,
472
+ port: this.port,
473
+ caCerts: this.tlsOptions?.ca
474
+ ? (Array.isArray(this.tlsOptions.ca)
475
+ ? [...this.tlsOptions.ca]
476
+ : [this.tlsOptions.ca])
477
+ : undefined,
478
+ };
479
+ const connectPromise = Deno.connectTls(tlsConnectOptions);
480
+ if (this.timeout > 0) {
481
+ let timeoutId: number;
482
+ let timedOut = false;
483
+ const timeoutPromise = new Promise<never>((_, reject) => {
484
+ timeoutId = setTimeout(() => {
485
+ timedOut = true;
486
+ reject(new Error("TCP connection timeout"));
487
+ }, this.timeout);
438
488
  });
439
- clearTimeout(timeoutId);
440
- } catch (error) {
441
- clearTimeout(timeoutId);
442
- if (controller.signal.aborted) {
443
- throw new Error("TCP connection timeout");
489
+
490
+ try {
491
+ this.connection = await Promise.race([
492
+ connectPromise,
493
+ timeoutPromise,
494
+ ]);
495
+ } catch (error) {
496
+ // If timed out, clean up the connection when it eventually completes
497
+ if (timedOut) {
498
+ connectPromise
499
+ .then((conn) => {
500
+ try {
501
+ conn.close();
502
+ } catch {
503
+ // Ignore close errors
504
+ }
505
+ })
506
+ .catch(() => {
507
+ // Ignore connection errors
508
+ });
509
+ }
510
+ throw error;
511
+ } finally {
512
+ clearTimeout(timeoutId!);
444
513
  }
445
- throw error;
514
+ } else {
515
+ this.connection = await connectPromise;
516
+ }
517
+ } else { // Insecure TCP connection
518
+ if (this.timeout > 0) {
519
+ // Use AbortController for proper timeout handling
520
+ const controller = new AbortController();
521
+ const timeoutId = setTimeout(() => {
522
+ controller.abort();
523
+ }, this.timeout);
524
+
525
+ try {
526
+ this.connection = await Deno.connect({
527
+ ...connectOptions,
528
+ signal: controller.signal,
529
+ });
530
+ clearTimeout(timeoutId);
531
+ } catch (error) {
532
+ clearTimeout(timeoutId);
533
+ if (controller.signal.aborted) {
534
+ throw new Error("TCP connection timeout");
535
+ }
536
+ throw error;
537
+ }
538
+ } else {
539
+ this.connection = await Deno.connect(connectOptions);
446
540
  }
447
- } else {
448
- // No timeout
449
- this.connection = await Deno.connect({
450
- hostname: this.hostname,
451
- port: this.port,
452
- transport: "tcp",
453
- });
454
541
  }
455
542
  } catch (error) {
456
543
  throw new Error(`Failed to connect to syslog server: ${error}`);
@@ -502,19 +589,35 @@ export class DenoTcpSyslogConnection implements SyslogConnection {
502
589
  * @since 0.12.0
503
590
  */
504
591
  export class NodeTcpSyslogConnection implements SyslogConnection {
505
- private connection?: Socket;
592
+ private connection?: Socket | tls.TLSSocket;
506
593
  private encoder = new TextEncoder();
507
594
 
508
595
  constructor(
509
596
  private hostname: string,
510
597
  private port: number,
511
598
  private timeout: number,
599
+ private secure: boolean,
600
+ private tlsOptions?: SyslogTlsOptions,
512
601
  ) {}
513
602
 
514
603
  connect(): Promise<void> {
515
604
  try {
516
605
  return new Promise<void>((resolve, reject) => {
517
- const socket = new Socket();
606
+ const connectionOptions: tls.ConnectionOptions = {
607
+ port: this.port,
608
+ host: this.hostname,
609
+ timeout: this.timeout,
610
+ rejectUnauthorized: this.tlsOptions?.rejectUnauthorized ?? true,
611
+ ca: this.tlsOptions?.ca
612
+ ? (Array.isArray(this.tlsOptions.ca)
613
+ ? [...this.tlsOptions.ca]
614
+ : [this.tlsOptions.ca])
615
+ : undefined,
616
+ };
617
+
618
+ const socket: Socket | tls.TLSSocket = this.secure
619
+ ? tls.connect(connectionOptions)
620
+ : new Socket();
518
621
 
519
622
  const timeout = setTimeout(() => {
520
623
  socket.destroy();
@@ -532,7 +635,10 @@ export class NodeTcpSyslogConnection implements SyslogConnection {
532
635
  reject(error);
533
636
  });
534
637
 
535
- socket.connect(this.port, this.hostname);
638
+ // For non-TLS sockets, explicitly call connect
639
+ if (!this.secure) {
640
+ (socket as Socket).connect(this.port, this.hostname);
641
+ }
536
642
  });
537
643
  } catch (error) {
538
644
  throw new Error(`Failed to connect to syslog server: ${error}`);
@@ -671,6 +777,8 @@ export function getSyslogSink(
671
777
  const hostname = options.hostname ?? "localhost";
672
778
  const port = options.port ?? 514;
673
779
  const protocol = options.protocol ?? "udp";
780
+ const secure = options.secure ?? false;
781
+ const tlsOptions = options.tlsOptions;
674
782
  const facility = options.facility ?? "local0";
675
783
  const appName = options.appName ?? "logtape";
676
784
  const syslogHostname = options.syslogHostname ?? getSystemHostname();
@@ -693,12 +801,24 @@ export function getSyslogSink(
693
801
  if (typeof Deno !== "undefined") {
694
802
  // Deno runtime
695
803
  return protocol === "tcp"
696
- ? new DenoTcpSyslogConnection(hostname, port, timeout)
804
+ ? new DenoTcpSyslogConnection(
805
+ hostname,
806
+ port,
807
+ timeout,
808
+ secure,
809
+ tlsOptions,
810
+ )
697
811
  : new DenoUdpSyslogConnection(hostname, port, timeout);
698
812
  } else {
699
813
  // Node.js runtime (and Bun, which uses Node.js APIs)
700
814
  return protocol === "tcp"
701
- ? new NodeTcpSyslogConnection(hostname, port, timeout)
815
+ ? new NodeTcpSyslogConnection(
816
+ hostname,
817
+ port,
818
+ timeout,
819
+ secure,
820
+ tlsOptions,
821
+ )
702
822
  : new NodeUdpSyslogConnection(hostname, port, timeout);
703
823
  }
704
824
  })();
@@ -730,5 +850,11 @@ export function getSyslogSink(
730
850
  isConnected = false;
731
851
  };
732
852
 
853
+ // Expose for testing purposes
854
+ Object.defineProperty(sink, "_internal_lastPromise", {
855
+ get: () => lastPromise,
856
+ enumerable: false,
857
+ });
858
+
733
859
  return sink;
734
860
  }