From b7f5dbc7fbd28d1b76d288d04345527cad96b44e Mon Sep 17 00:00:00 2001 From: daniboy Date: Wed, 7 Oct 2026 17:10:28 +0200 Subject: [PATCH] Initial commit: syslog server and test client --- .gitignore | 2 + syslog_client.py | 144 ++++++++++++ vs.pu01/.gitignore | 10 + vs.pu01/.idea/.gitignore | 10 + vs.pu01/.idea/compiler.xml | 13 ++ vs.pu01/.idea/encodings.xml | 7 + vs.pu01/.idea/jarRepositories.xml | 20 ++ vs.pu01/.idea/misc.xml | 12 + vs.pu01/justfile | 12 + vs.pu01/pom.xml | 60 +++++ vs.pu01/src/main/java/vs/AsciiChars.java | 87 +++++++ vs.pu01/src/main/java/vs/Main.java | 197 ++++++++++++++++ vs.pu01/src/main/java/vs/StructuredData.java | 180 +++++++++++++++ vs.pu01/src/main/java/vs/SyslogMessage.java | 213 ++++++++++++++++++ vs.pu01/src/test/java/vs/AsciiCharsTest.java | 56 +++++ .../src/test/java/vs/SyslogMessageTest.java | 93 ++++++++ 16 files changed, 1116 insertions(+) create mode 100644 .gitignore create mode 100644 syslog_client.py create mode 100644 vs.pu01/.gitignore create mode 100644 vs.pu01/.idea/.gitignore create mode 100644 vs.pu01/.idea/compiler.xml create mode 100644 vs.pu01/.idea/encodings.xml create mode 100644 vs.pu01/.idea/jarRepositories.xml create mode 100644 vs.pu01/.idea/misc.xml create mode 100644 vs.pu01/justfile create mode 100644 vs.pu01/pom.xml create mode 100644 vs.pu01/src/main/java/vs/AsciiChars.java create mode 100644 vs.pu01/src/main/java/vs/Main.java create mode 100644 vs.pu01/src/main/java/vs/StructuredData.java create mode 100644 vs.pu01/src/main/java/vs/SyslogMessage.java create mode 100644 vs.pu01/src/test/java/vs/AsciiCharsTest.java create mode 100644 vs.pu01/src/test/java/vs/SyslogMessageTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/syslog_client.py b/syslog_client.py new file mode 100644 index 0000000..492a7fb --- /dev/null +++ b/syslog_client.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Test client for the RFC 5424 syslog server (UDP only). + +Service discovery: broadcast an empty datagram to 255.255.255.255:8888, +the server's reply source address is the syslog server IP. +Then log messages go to :514 (RFC 5424 format, no structured data). + +Examples: + python syslog_client.py "hello world" + python syslog_client.py -s 4 -f 1 --app myapp "disk almost full" + python syslog_client.py --server 192.168.0.10 "skip discovery" + python syslog_client.py --oversize 2000 "too long on purpose" + python syslog_client.py --raw "garbage, not RFC 5424" +""" +import argparse +import os +import socket +import sys +from datetime import datetime, timezone + +DISCOVERY_PORT = 8888 +SYSLOG_PORT = 514 +BROADCAST = "255.255.255.255" + + +def discover(timeout, retries, discovery_port, broadcast=BROADCAST): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.settimeout(timeout) + try: + for attempt in range(1, retries + 1): + print(f"[discovery] broadcast {attempt}/{retries} -> {broadcast}:{discovery_port}") + sock.sendto(b"", (broadcast, discovery_port)) + try: + _, (ip, port) = sock.recvfrom(1024) + print(f"[discovery] reply from {ip}:{port}") + return ip + except socket.timeout: + continue + finally: + sock.close() + return None + + +def build_message(facility, severity, app, procid, msgid, text, hostname): + pri = facility * 8 + severity + # RFC 5424 timestamp: full-date "T" full-time, UTC with Z + ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + # HEADER SP STRUCTURED-DATA SP MSG ; SD is NILVALUE "-" + return f"<{pri}>1 {ts} {hostname} {app} {procid} {msgid} - {text}" + + +def run_demo(server, port, hostname, procid): + bm = lambda fac, sev, app, msgid, text, host=hostname: build_message( + fac, sev, app, procid, msgid, text, host).encode("utf-8") + ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + cases = [ + ("valid: user.info", bm(1, 6, "demo", "ID1", "hello from demo client")), + ("valid: kernel.emerg", bm(0, 0, "kernel", "BOOT", "system on fire")), + ("valid: local7.debug", bm(23, 7, "myapp", "-", "debug details")), + ("valid: nil fields", f"<14>1 {ts} - - - - - only nil header".encode()), + ("valid: UTF-8 text with BOM", f"<14>1 {ts} {hostname} demo {procid} U1 - ".encode() + + b"\xef\xbb\xbf" + "Grüße, 世界".encode("utf-8")), + ("valid: no message body", f"<14>1 {ts} {hostname} demo {procid} E1 -".encode()), + ("valid: ~480 bytes", bm(1, 6, "demo", "LEN", "A" * 400)), + ("INVALID: too long (3000 B)", bm(1, 6, "demo", "LEN", "B" * 3000)), + ("INVALID: APP-NAME 49 chars", bm(1, 6, "a" * 49, "ID1", "app name too long")), + ("INVALID: MSGID 33 chars", bm(1, 6, "demo", "m" * 33, "msgid too long")), + ("INVALID: no PRI", f"1 {ts} {hostname} demo {procid} ID1 - no pri".encode()), + ("INVALID: PRI out of range <192>", f"<192>1 {ts} {hostname} demo {procid} ID1 - bad pri".encode()), + ("INVALID: garbage", b"this is not syslog"), + ("INVALID: empty datagram", b""), + ] + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + for label, data in cases: + sock.sendto(data, (server, port)) + print(f"[demo] {label:34} {len(data):5} B {data[:60]!r}") + finally: + sock.close() + return 0 + + +def main(): + p = argparse.ArgumentParser(description="Syslog (RFC 5424, UDP) test client") + p.add_argument("message", nargs="?", help="log message text (not needed with --demo)") + p.add_argument("--demo", action="store_true", + help="discover server, then send a set of example packets (valid and invalid)") + p.add_argument("-s", "--severity", type=int, default=6, choices=range(8), + help="0=emerg .. 7=debug (default 6=info)") + p.add_argument("-f", "--facility", type=int, default=1, choices=range(24), + help="0..23 (default 1=user)") + p.add_argument("--app", default="testclient", help="APP-NAME") + p.add_argument("--msgid", default="-", help="MSGID") + p.add_argument("--hostname", default=socket.gethostname().replace(" ", "_") or "-") + p.add_argument("-n", "--count", type=int, default=1, help="send message N times") + p.add_argument("--server", help="server IP, skips auto-discovery") + p.add_argument("--port", type=int, default=SYSLOG_PORT, help="syslog port (default 514)") + p.add_argument("--discovery-port", type=int, default=DISCOVERY_PORT) + p.add_argument("--broadcast", default=BROADCAST, metavar="ADDR", + help="discovery target (default 255.255.255.255; use 127.255.255.255 " + "or 127.0.0.1 to stay on localhost)") + p.add_argument("--timeout", type=float, default=2.0, help="discovery wait per try (s)") + p.add_argument("--retries", type=int, default=3) + p.add_argument("--oversize", type=int, metavar="BYTES", + help="pad message to this many bytes total (test server length check)") + p.add_argument("--raw", action="store_true", + help="send MESSAGE verbatim, no RFC 5424 header (test server validation)") + args = p.parse_args() + + server = args.server or discover(args.timeout, args.retries, args.discovery_port, args.broadcast) + if not server: + print("[error] no server found", file=sys.stderr) + return 1 + + procid = str(os.getpid()) + if args.demo: + return run_demo(server, args.port, args.hostname, procid) + if not args.message: + p.error("message required unless --demo") + if args.raw: + data = args.message.encode("utf-8") + else: + data = build_message(args.facility, args.severity, args.app, procid, + args.msgid, args.message, args.hostname).encode("utf-8") + if args.oversize and len(data) < args.oversize: + data += b"X" * (args.oversize - len(data)) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + for i in range(args.count): + try: + sock.sendto(data, (server, args.port)) + except OSError as e: + print(f"[error] send failed ({len(data)} bytes): {e}", file=sys.stderr) + return 1 + print(f"[send] {i + 1}/{args.count} {len(data)} bytes -> {server}:{args.port}") + finally: + sock.close() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vs.pu01/.gitignore b/vs.pu01/.gitignore new file mode 100644 index 0000000..0ab4aa3 --- /dev/null +++ b/vs.pu01/.gitignore @@ -0,0 +1,10 @@ +**/.DS_Store +**/*.class +/bin/ +/.project +/.classpath +/.factorypath +/.settings/ +/target/ +/.apt_generated/ +/.apt_generated_tests/ diff --git a/vs.pu01/.idea/.gitignore b/vs.pu01/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/vs.pu01/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/vs.pu01/.idea/compiler.xml b/vs.pu01/.idea/compiler.xml new file mode 100644 index 0000000..b9e1a22 --- /dev/null +++ b/vs.pu01/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vs.pu01/.idea/encodings.xml b/vs.pu01/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/vs.pu01/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vs.pu01/.idea/jarRepositories.xml b/vs.pu01/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/vs.pu01/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vs.pu01/.idea/misc.xml b/vs.pu01/.idea/misc.xml new file mode 100644 index 0000000..1b4a03b --- /dev/null +++ b/vs.pu01/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/vs.pu01/justfile b/vs.pu01/justfile new file mode 100644 index 0000000..2143089 --- /dev/null +++ b/vs.pu01/justfile @@ -0,0 +1,12 @@ +default: + mvn clean compile test +exec class args: compile + mvn exec:java -Dexec.mainClass={{class}} -Dexec.args={{args}} +clean: + mvn clean +compile: + mvn compile +test: compile + mvn test +javadoc: + mvn javadoc:javadoc diff --git a/vs.pu01/pom.xml b/vs.pu01/pom.xml new file mode 100644 index 0000000..a5a6559 --- /dev/null +++ b/vs.pu01/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + vs + vs.pu01 + 1.0-SNAPSHOT + jar + + + 10 + UTF-8 + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.projectlombok + lombok + 1.18.48 + provided + + + net.jcip + jcip-annotations + 1.0 + provided + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.9.0 + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + private + en_US + + + + + diff --git a/vs.pu01/src/main/java/vs/AsciiChars.java b/vs.pu01/src/main/java/vs/AsciiChars.java new file mode 100644 index 0000000..bdd005b --- /dev/null +++ b/vs.pu01/src/main/java/vs/AsciiChars.java @@ -0,0 +1,87 @@ +package vs; + +/** + * helper class for RFC 5424 (https://datatracker.ietf.org/doc/html/rfc5424) + * compliant log messages as immutable Java objects - representation of a subset + * of printable strings of specific length + * + * @author Sandro Leuchter + * + */ +public abstract class AsciiChars { + + private final String value; + + public String value() { + return this.value; + } + + protected AsciiChars(int length, String value) { + if (value != null) { + if (value.length() > length) { + throw new IllegalArgumentException( + "Stringlänge = " + value.length() + " > " + length + ); + } + for (int c : value.getBytes()) { + if (c < 33 || c > 126) { + throw new IllegalArgumentException( + "Stringinhalt nicht printable US-ASCII ohne Space" + ); + } + } + } + this.value = value; + } + + @Override + public String toString() { + if (value() == null || value().length() == 0) { + return "-"; + } else { + return value(); + } + } + + public static final class L004 extends AsciiChars { + + public L004(String value) { + super(4, value); + } + } + + public static final class L012 extends AsciiChars { + + public L012(String value) { + super(12, value); + } + } + + public static final class L032 extends AsciiChars { + + public L032(String value) { + super(32, value); + } + } + + public static final class L048 extends AsciiChars { + + public L048(String value) { + super(48, value); + } + } + + public static final class L128 extends AsciiChars { + + public L128(String value) { + super(128, value); + } + } + + public static final class L255 extends AsciiChars { + + public L255(String value) { + super(255, value); + } + } +} diff --git a/vs.pu01/src/main/java/vs/Main.java b/vs.pu01/src/main/java/vs/Main.java new file mode 100644 index 0000000..ae40d65 --- /dev/null +++ b/vs.pu01/src/main/java/vs/Main.java @@ -0,0 +1,197 @@ +package vs; + +import com.sun.jdi.InternalException; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; + +public class Main { + + static DatagramSocket syslogSocket; + static DatagramSocket broadcastSocket; + + + + public static void main(String[] args){ + + try { + syslogSocket = new DatagramSocket(514); + } catch (SocketException e) { + System.err.println("Couldnt bind to port 514, shutting down"); + throw new RuntimeException(e); + } + + try { + broadcastSocket = new DatagramSocket(8888); + } catch (SocketException e) { + System.err.println("Couldnt bind to port 8888, shutting down"); + throw new RuntimeException(e); + } + + try { + broadcastSocket.setSoTimeout(200); + syslogSocket.setSoTimeout(200); + } catch (SocketException e) { + throw new RuntimeException(e); + } + while (true){ + + byte[] buffer = new byte[2048]; + + DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); + + //broadcast read and reply + try { + broadcastSocket.receive(packet); + + //we received braodcast message, answer from syslog socket so they know where we are + DatagramPacket answer = new DatagramPacket(new byte[1],0,1,packet.getSocketAddress()); + syslogSocket.send(answer); + }catch (SocketTimeoutException e){ + //nop, just empty + } catch (IOException e) { + throw new RuntimeException(e); + } + + //syslog logic + + packet = new DatagramPacket(buffer,0,buffer.length); + + try { + syslogSocket.receive(packet); + }catch (SocketTimeoutException e){ + //nop, just empty + continue; + } catch (IOException e) { + throw new RuntimeException(e); + } + + try { + SyslogMessage msg = parseSysMessage(packet.getData()); + System.out.println("Log received from "+packet.getAddress().getHostName() +":"+ packet.getPort()+ " : "+msg.toString()); + }catch (Exception e){ + System.err.println("msg couldnt be parsed, "+e.getMessage()); + e.printStackTrace(); + } + + + } + } + + static SyslogMessage parseSysMessage(byte[] buff) throws InternalException{ + if (buff[0] != 60){ + throw new InternalException("msg not well formed"); + } + SyslogMessage.Facility fac; + SyslogMessage.Severity sev; + + int step = 1; + + String prival_str = ""; + for(;step < 4;step++){ + if (buff[step] == 62) break; + prival_str += (char) buff[step]; + } + if (buff[step] != 62) throw new InternalException("PRIVAL malformed"); + if (prival_str.isEmpty()) throw new InternalException("PRIVAL not there"); + + int primval = Integer.parseInt(prival_str); + fac = SyslogMessage.Facility.values()[primval/8]; + sev = SyslogMessage.Severity.values()[primval%8]; + + //version is always 1 + if (buff[++step] != 49) throw new InternalException("VERSION NOT 1"); + + //skip whitespace and verion + ++step; + ++step; + + //TIMESTAMP (apparently we skip this) + while(buff[step] != 32) + step++; + + //skip whitespace + step++; + + //HOSTNAME + StringBuilder hostnameBuilder = new StringBuilder(); + while(buff[step] != 32){ + hostnameBuilder.append((char) buff[step]); + step++; + } + + + AsciiChars.L255 hostname = new AsciiChars.L255(hostnameBuilder.toString()); + + //skip whitespace + step++; + + //APP-NAME + StringBuilder appNameBuilder = new StringBuilder(); + while (buff[step] != 32){ + appNameBuilder.append((char) buff[step]); + step++; + } + + AsciiChars.L048 appName = new AsciiChars.L048(appNameBuilder.toString()); + + //skip whitespace + step++; + + //PROCID + StringBuilder procIDBuilder = new StringBuilder(); + while (buff[step] != 32){ + procIDBuilder.append((char) buff[step]); + step++; + } + + AsciiChars.L128 procID = new AsciiChars.L128(procIDBuilder.toString()); + + //skip whitespace + step++; + + //MSGID + StringBuilder msgIDBuilder = new StringBuilder(); + while (buff[step] != 32){ + msgIDBuilder.append((char) buff[step]); + step++; + } + + AsciiChars.L032 msgID = new AsciiChars.L032(msgIDBuilder.toString()); + + //skip whitespace + step++; + + + //skip structured data + while (buff[step] != 32){ + step++; + } + + //MESSAGE + + + String end = new String(buff,step,buff.length-step-1); + SyslogMessage.Message msg; + + if (end.startsWith("\u00EF\u00BB\u00BF")){ + msg = new SyslogMessage.TextMessage(end); + + } + else { + Byte[] msgBuff = new Byte[buff.length-step]; + for (int i = 0;i < msgBuff.length;i++){ + msgBuff[i] = buff[i+step]; + } + msg = new SyslogMessage.BinaryMessage(msgBuff); + } + + return new SyslogMessage(fac,sev,hostname,appName,procID,msgID,null,msg); + + + } +} diff --git a/vs.pu01/src/main/java/vs/StructuredData.java b/vs.pu01/src/main/java/vs/StructuredData.java new file mode 100644 index 0000000..2c54682 --- /dev/null +++ b/vs.pu01/src/main/java/vs/StructuredData.java @@ -0,0 +1,180 @@ +package vs; + +import java.util.ArrayList; +import java.util.List; + +/** + * helper class for RFC 5424 (https://datatracker.ietf.org/doc/html/rfc5424) + * compliant log messages as immutable Java objects - structured data (set of + * key/value-pairs) with some predefined sets according to the standard + * + * @author Sandro Leuchter + * + */ +public class StructuredData { + + public static class Element { + + private final String name; + private List parameters; + + public static Element newTimeQuality( + boolean tzKnown, + boolean isSynced + ) { + return newTimeQuality(tzKnown, isSynced, null); + } + + public static Element newTimeQuality( + boolean tzKnown, + boolean isSynced, + Integer syncAccuracy + ) { + var e = new Element("timeQuality"); + e.add(new Param("tzKnown", (tzKnown) ? "1" : "0")); + e.add(new Param("isSynced", (isSynced) ? "1" : "0")); + if (syncAccuracy != null && !isSynced) { + e.add(new Param("syncAccuracy", String.valueOf(syncAccuracy))); + } + return e; + } + + public static Element newOrigin( + String enterpriseId, + String software, + String swVersion + ) { + return newOrigin( + new String[] {}, + enterpriseId, + software, + swVersion + ); + } + + public static Element newOrigin( + String ip, + String enterpriseId, + String software, + String swVersion + ) { + return newOrigin( + new String[] { ip }, + enterpriseId, + software, + swVersion + ); + } + + public static Element newOrigin( + String[] ip, + String enterpriseId, + String software, + String swVersion + ) { + var e = new Element("origin"); + for (var p : ip) { + e = e.add(new Param("ip", p)); + } + if (enterpriseId != null && !enterpriseId.equals("")) { + e = e.add(new Param("enterpriseId", enterpriseId)); + } + if (software != null && !software.equals("")) { + e = e.add(new Param("software", software)); + } + if (swVersion != null && !swVersion.equals("")) { + e = e.add(new Param("swVersion", swVersion)); + } + return e; + } + + public static Element newMeta( + Integer sequenceId, + Integer sysUpTime, + String language + ) { + var e = new Element("meta"); + if (sequenceId != null && sequenceId > 0) { + e = e.add( + new Param( + "sequenceId", + String.valueOf(sequenceId % 2147483647) + ) + ); + } + if (sysUpTime != null && sysUpTime >= 0) { + e = e.add(new Param("sysUpTime", String.valueOf(sysUpTime))); + } + if (language != null && !language.equals("")) { + e = e.add(new Param("language", language)); + } + return e; + } + + public Element(String name) { + this.name = name; + this.parameters = new ArrayList<>(); + } + + public Element add(Param parameter) { + var e = new Element(this.name); + e.parameters = this.parameters; + e.parameters.add(parameter); + return e; + } + + @Override + public String toString() { + var str = "[" + this.name; + for (var p : this.parameters) { + str = str + " " + p.toString(); + } + return str + "]"; + } + } + + public static class Param { + + private final String name; + // name: printable US-ASCII string ^[@=\]\"\s]+ + // "@" + private enterpise number "@\d+(\.\d+)*" + private final String value; + + public Param(String name, String value) { + this.name = name; // 7-Bit ASCII + this.value = value; // UTF-8 + } + + @Override + public String toString() { + return this.name + "=\"" + this.value + "\""; + } + } + + private List params; + + public StructuredData() { + this.params = new ArrayList<>(); + } + + public StructuredData(List params) { + this.params = params; + } + + public String toString() { + if (this.params.size() == 0) { + return "-"; + } + var str = ""; + for (var p : this.params) { + str = str + p.toString(); + } + return str; + } + + public StructuredData add(Element e) { + var p = this.params; + p.add(e); + return new StructuredData(p); + } +} diff --git a/vs.pu01/src/main/java/vs/SyslogMessage.java b/vs.pu01/src/main/java/vs/SyslogMessage.java new file mode 100644 index 0000000..156810c --- /dev/null +++ b/vs.pu01/src/main/java/vs/SyslogMessage.java @@ -0,0 +1,213 @@ +package vs; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * RFC 5424 (https://datatracker.ietf.org/doc/html/rfc5424) compliant log + * messages as immutable Java objects + * + * @author Sandro Leuchter + * + */ +public class SyslogMessage implements Serializable { + + private static final long serialVersionUID = -5895573029109990861L; + private final Facility fac; + private final Severity sev; + private final AsciiChars.L255 host; + private final AsciiChars.L048 appName; + private final AsciiChars.L128 procId; + private final AsciiChars.L032 msgId; + private final StructuredData data; + private final Message message; + + public SyslogMessage( + Facility fac, + Severity sev, + AsciiChars.L255 host, + AsciiChars.L048 appName, + AsciiChars.L128 procId, + AsciiChars.L032 msgId, + StructuredData data, + Message message + ) { + this.fac = fac; + this.sev = sev; + this.host = host; + this.appName = appName; + this.procId = procId; + this.msgId = msgId; + this.data = data; + this.message = message; + } + + public Facility fac() { + return this.fac; + } + + public Severity sev() { + return sev; + } + + public AsciiChars.L255 host() { + return host; + } + + public AsciiChars.L048 appName() { + return appName; + } + + public AsciiChars.L128 procId() { + return procId; + } + + public AsciiChars.L032 msgId() { + return msgId; + } + + public StructuredData data() { + return data; + } + + public Message message() { + return message; + } + + public static int version() { + return VERSION; + } + + public static enum Facility { + KERNEL, + USER, + MAIL_SYSTEM, + SYS_DAEMON, + SECURITY1, + INTERNAL, + PRINTER, + NEWS, + UUCP, + CLOCK1, + SECURITY2, + FTP, + NTP, + AUDIT, + ALERT, + CLOCK2, + LOCAL0, + LOCAL1, + LOCAL2, + LOCAL3, + LOCAL4, + LOCAL5, + LOCAL6, + LOCAL7, + } + + public static enum Severity { + EMERGENCY, + ALERT, + CRITICAL, + ERROR, + WARNING, + NOTICE, + INFORMATIONAL, + DEBUG, + } + + public static interface Message { + public Object message(); + + public int length(); + } + + public static class BinaryMessage implements Message { + + private Byte[] message; + + public BinaryMessage(Byte[] message) { + this.message = message; + } + + @Override + public String toString() { + return message.toString(); + } + + @Override + public Object message() { + return this.message; + } + + @Override + public int length() { + return this.message.length; + } + } + + public static class TextMessage implements Message { + + private String message; // UTF8 + + public TextMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return "\u00EF\u00BB\u00BF" + message.toString(); + } + + @Override + public Object message() { + return this.message; + } + + @Override + public int length() { + return this.message.length(); + } + } + + static final int VERSION = 1; // RFC 5424, Mar 2009 + + @Override + public String toString() { + var prival = String.valueOf(fac().ordinal() * 8 + sev().ordinal()); + var d = ""; + if (data() != null) { + d = " " + data(); + } + var m = ""; + if ( + message() != null && + message().message() != null && + message().length() > 0 + ) { + m = " " + message(); + } + var timestamp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format( + new Date() + ); + return ( + "<" + + prival + + ">" + + VERSION + + " " + + timestamp + + " " + + host().toString() + + " " + + appName().toString() + + " " + + procId().toString() + + " " + + msgId().toString() + + d + + m + ); + } +} diff --git a/vs.pu01/src/test/java/vs/AsciiCharsTest.java b/vs.pu01/src/test/java/vs/AsciiCharsTest.java new file mode 100644 index 0000000..8aab640 --- /dev/null +++ b/vs.pu01/src/test/java/vs/AsciiCharsTest.java @@ -0,0 +1,56 @@ +package vs; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AsciiCharsTest { + + @Test + void nullValue() { + var ac = new AsciiChars.L004(null); + assertEquals(ac.toString(), "-"); + } + + @Test + void emptyValue() { + var ac = new AsciiChars.L004(""); + assertEquals(ac.toString(), "-"); + } + + @Test + void longValue() { + var ac = new AsciiChars.L004("1234"); + assertEquals(ac.toString(), "1234"); + } + + @Test + void longerValue() { + var thrown = assertThrows(IllegalArgumentException.class, () -> { + new AsciiChars.L004("12345"); + }); + assertEquals("Stringlänge = 5 > 4", thrown.getMessage()); + } + + @Test + void space() { + var thrown = assertThrows(IllegalArgumentException.class, () -> { + new AsciiChars.L004("1 1"); + }); + assertEquals( + "Stringinhalt nicht printable US-ASCII ohne Space", + thrown.getMessage() + ); + } + + @Test + void special() { + var thrown = assertThrows(IllegalArgumentException.class, () -> { + new AsciiChars.L004("ä"); + }); + assertEquals( + "Stringinhalt nicht printable US-ASCII ohne Space", + thrown.getMessage() + ); + } +} diff --git a/vs.pu01/src/test/java/vs/SyslogMessageTest.java b/vs.pu01/src/test/java/vs/SyslogMessageTest.java new file mode 100644 index 0000000..b60ff0c --- /dev/null +++ b/vs.pu01/src/test/java/vs/SyslogMessageTest.java @@ -0,0 +1,93 @@ +package vs; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import vs.StructuredData.*; +import vs.SyslogMessage.*; + +class SyslogMessageTest { + + @Test + void testToString() { + var m1 = new SyslogMessage( + // + Facility.SECURITY1, // + Severity.CRITICAL, // + new AsciiChars.L255("mymachine.example.com"), // + new AsciiChars.L048("su"), // + new AsciiChars.L128(""), // + new AsciiChars.L032("ID47"), // + new StructuredData() // + .add(Element.newTimeQuality(true, true)) + .add( + new Element("exampleSDID@32473") + .add(new Param("iut", "3")) + .add(new Param("eventSource", "Application")) + .add(new Param("eventID", "1011")) + ) + .add( + new Element("examplePriority@32473").add( + new Param("class", "high") + ) + ), + new TextMessage("'su root' failed for lonvick on /dev/pts/8") + ); + var s = m1.toString(); + assertEquals(s.substring(0, 6), "<34>1 "); + assertEquals( + s.substring(s.length() - 221, s.length()), + " mymachine.example.com su - ID47 [timeQuality tzKnown=\"1\" isSynced=\"1\"][exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"][examplePriority@32473 class=\"high\"] 'su root' failed for lonvick on /dev/pts/8" + ); + } + + @Test + void test2() { + var m1 = new SyslogMessage( + // + Facility.SECURITY1, // + Severity.CRITICAL, // + new AsciiChars.L255("mymachine.example.com"), // + new AsciiChars.L048("su"), // + new AsciiChars.L128(""), // + new AsciiChars.L032("ID47"), // + null, // + new TextMessage("'su root' failed for lonvick on /dev/pts/8") + ); + var s = m1.toString(); + assertEquals(s.substring(0, 6), "<34>1 "); + assertEquals( + s.substring(s.length() - 78, s.length()), + " mymachine.example.com su - ID47 'su root' failed for lonvick on /dev/pts/8" + ); + } + + @Test + void test3() { + var m1 = new SyslogMessage( + // + Facility.SECURITY1, // + Severity.CRITICAL, // + new AsciiChars.L255("mymachine.example.com"), // + new AsciiChars.L048("su"), // + new AsciiChars.L128(""), // + new AsciiChars.L032("ID47"), // + new StructuredData() // + .add(Element.newTimeQuality(true, true)) + .add( + Element.newOrigin( + new String[] { "0.0.8.8", "8.8.8.8" }, + null, + null, + null + ) + ) + .add(Element.newMeta(null, 32, "de")), + new BinaryMessage(null) + ); + assertEquals( + m1.data().toString(), + "[timeQuality tzKnown=\"1\" isSynced=\"1\"][origin ip=\"0.0.8.8\" ip=\"8.8.8.8\"][meta sysUpTime=\"32\" language=\"de\"]" + ); + } +}