From adb2972d812f4c98b9e94826a7a5f2c06b3e02b1 Mon Sep 17 00:00:00 2001 From: smittythekid <57874528+smittythekid@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:17:47 +0200 Subject: [PATCH] Ordnerstruktur + .gitignore angepasst --- .gitignore | 3 + src/main/java/vs/AsciiChars.java | 87 ++++++++++ src/main/java/vs/StructuredData.java | 180 ++++++++++++++++++++ src/main/java/vs/SyslogMessage.java | 213 ++++++++++++++++++++++++ src/test/java/vs/AsciiChars.java | 87 ++++++++++ src/test/java/vs/AsciiCharsTest.java | 56 +++++++ src/test/java/vs/SyslogMessageTest.java | 93 +++++++++++ 7 files changed, 719 insertions(+) create mode 100644 src/main/java/vs/AsciiChars.java create mode 100644 src/main/java/vs/StructuredData.java create mode 100644 src/main/java/vs/SyslogMessage.java create mode 100644 src/test/java/vs/AsciiChars.java create mode 100644 src/test/java/vs/AsciiCharsTest.java create mode 100644 src/test/java/vs/SyslogMessageTest.java diff --git a/.gitignore b/.gitignore index 9154f4c..bde3447 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ hs_err_pid* replay_pid* +# IDE files +justfile +pom.xml diff --git a/src/main/java/vs/AsciiChars.java b/src/main/java/vs/AsciiChars.java new file mode 100644 index 0000000..bdd005b --- /dev/null +++ b/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/src/main/java/vs/StructuredData.java b/src/main/java/vs/StructuredData.java new file mode 100644 index 0000000..2c54682 --- /dev/null +++ b/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/src/main/java/vs/SyslogMessage.java b/src/main/java/vs/SyslogMessage.java new file mode 100644 index 0000000..156810c --- /dev/null +++ b/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/src/test/java/vs/AsciiChars.java b/src/test/java/vs/AsciiChars.java new file mode 100644 index 0000000..bdd005b --- /dev/null +++ b/src/test/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/src/test/java/vs/AsciiCharsTest.java b/src/test/java/vs/AsciiCharsTest.java new file mode 100644 index 0000000..8aab640 --- /dev/null +++ b/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/src/test/java/vs/SyslogMessageTest.java b/src/test/java/vs/SyslogMessageTest.java new file mode 100644 index 0000000..b60ff0c --- /dev/null +++ b/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\"]" + ); + } +}