From b1c4fc0e8ab275b59c70a967495f823389c6a95c Mon Sep 17 00:00:00 2001 From: Dimitrios Date: Sat, 11 Apr 2026 18:08:52 +0200 Subject: [PATCH] Copy src from Leuchter; create SyslogClient and Server; modify justfile and gitignore --- .gitignore | 10 ++ README.md | 29 +++- justfile | 16 ++ pom.xml | 60 +++++++ src/helloworld.java | 5 - src/main/java/vs/AsciiChars.java | 87 ++++++++++ src/main/java/vs/StructuredData.java | 180 ++++++++++++++++++++ src/main/java/vs/SyslogClient.java | 56 +++++++ src/main/java/vs/SyslogMessage.java | 213 ++++++++++++++++++++++++ src/main/java/vs/SyslogServer.java | 102 ++++++++++++ src/test/java/vs/AsciiCharsTest.java | 56 +++++++ src/test/java/vs/SyslogMessageTest.java | 93 +++++++++++ 12 files changed, 901 insertions(+), 6 deletions(-) create mode 100644 justfile create mode 100644 pom.xml delete mode 100644 src/helloworld.java 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/SyslogClient.java create mode 100644 src/main/java/vs/SyslogMessage.java create mode 100644 src/main/java/vs/SyslogServer.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..c129e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,13 @@ hs_err_pid* replay_pid* +**/.DS_Store +**/*.class +/bin/ +/.project +/.classpath +/.factorypath +/.settings/ +/target/ +/.apt_generated/ +/.apt_generated_tests/ diff --git a/README.md b/README.md index e78b6a8..28fa698 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # Verteilte_Systeme -Bearbeitung Prüfungsvorleistung für Verteilte Systeme \ No newline at end of file +Bearbeitung Prüfungsvorleistung für Verteilte Systeme + +# Syslog-Logging-System (RFC 5424) + +Dieses Projekt implementiert einen Syslog-Server und einen Test-Client auf Basis von UDP. Es beinhaltet eine automatische Service-Discovery. + +## Projektstruktur & Klassen + +* **`pom.xml`**: Die Maven-Konfigurationsdatei. Sie verwaltet Abhängigkeiten und die Java-Version. +* **`justfile`**: Automatisierungsskript für Windows (PowerShell) und Linux, um Befehle wie `just clean` oder `just exec` zu nutzen. +* **`vs.SyslogServer`**: Der Hauptdienst. Er startet zwei parallele Threads: + * **Syslog-Service**: Empfängt Log-Nachrichten auf Port 514. + * **Discovery-Service**: Antwortet auf Broadcast-Anfragen auf Port 8888. +* **`vs.SyslogClient`**: Test-Client, der den Server via Broadcast sucht und eine standardkonforme Nachricht sendet. +* **`vs.SyslogMessage`**: Repräsentiert eine Nachricht gemäß RFC 5424. +* **`vs.AsciiChars` / `vs.StructuredData`**: Hilfsklassen zur Validierung und Formatierung von Zeichenketten gemäß Spezifikation. + +## Funktionsablauf (Service Discovery) + +1. **Broadcast**: Der Client sendet ein UDP-Paket an `255.255.255.255:8888`. +2. **Antwort**: Der Server empfängt den Broadcast und sendet ein leeres UDP-Paket an den Client zurück. +3. **Identifikation**: Der Client liest die IP-Adresse des Absenders aus dem Antwort-Paket aus. +4. **Logging**: Der Client sendet die eigentliche Syslog-Nachricht direkt an die ermittelte IP auf Port 514. + +## Ausführung +* Server starten: `just exec SyslogServer` +* Client starten: `just exec SyslogClient` +* Aufräumen: `just clean` \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..f5abb79 --- /dev/null +++ b/justfile @@ -0,0 +1,16 @@ +# Konfiguration für Windows (PowerShell). +# Falls Sie Linux/macOS nutzen, bitte diese Zeile in ["sh", "-c"] ändern: +set shell := ["powershell.exe", "-c"] + +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 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..47d6394 --- /dev/null +++ b/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.30 + 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/src/helloworld.java b/src/helloworld.java deleted file mode 100644 index 6994211..0000000 --- a/src/helloworld.java +++ /dev/null @@ -1,5 +0,0 @@ -public class helloworld { - public static void main(String[] args) { - System.out.println("Hello World!"); - } -} 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/SyslogClient.java b/src/main/java/vs/SyslogClient.java new file mode 100644 index 0000000..4a3af1d --- /dev/null +++ b/src/main/java/vs/SyslogClient.java @@ -0,0 +1,56 @@ +package vs; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; + +public class SyslogClient { + + public static void main(String[] args) throws Exception { + DatagramSocket socket = new DatagramSocket(); + socket.setBroadcast(true); + + // 🔍 Discovery (Unverändert) + byte[] discoverMsg = "DISCOVER".getBytes(); + DatagramPacket packet = new DatagramPacket( + discoverMsg, discoverMsg.length, + InetAddress.getByName("255.255.255.255"), 8888 + ); + socket.send(packet); + + // 📥 Antwort empfangen (Unverändert) + byte[] buffer = new byte[256]; + DatagramPacket response = new DatagramPacket(buffer, buffer.length); + socket.receive(response); + System.out.println("Server gefunden: " + response.getAddress()); + + // 📨 ECHTE RFC 5424 Syslog-Nachricht aus DEINEN Klassen bauen! + SyslogMessage msg = new SyslogMessage( + SyslogMessage.Facility.USER, + SyslogMessage.Severity.INFORMATIONAL, + new AsciiChars.L255("mein-laptop"), + new AsciiChars.L048("SyslogClient"), + new AsciiChars.L128("PID1234"), + new AsciiChars.L032("MSG-01"), + new StructuredData().add(StructuredData.Element.newTimeQuality(true, true)), + new SyslogMessage.TextMessage("Das ist eine standardkonforme Nachricht!") + ); + + // Die Klasse wandelt alles in den perfekten String um (inklusive Zeitstempel) + String logString = msg.toString(); + System.out.println("Sende generierten String: " + logString); + + // Nachricht als UTF-8 Bytes senden + byte[] data = logString.getBytes(StandardCharsets.UTF_8); + + DatagramPacket syslogPacket = new DatagramPacket( + data, data.length, response.getAddress(), 514 + ); + + socket.send(syslogPacket); + System.out.println("Log gesendet!"); + + socket.close(); + } +} \ No newline at end of file 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/main/java/vs/SyslogServer.java b/src/main/java/vs/SyslogServer.java new file mode 100644 index 0000000..42b6c69 --- /dev/null +++ b/src/main/java/vs/SyslogServer.java @@ -0,0 +1,102 @@ +package vs; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; + +public class SyslogServer { + + private static final int SYSLOG_PORT = 514; + private static final int DISCOVERY_PORT = 8888; + private static final int MAX_MESSAGE_LENGTH = 1024; // konservativ (UDP safe) + + public static void main(String[] args) { + System.out.println("Starting Syslog Server..."); + + // Thread für Syslog + new Thread(() -> runSyslogServer()).start(); + + // Thread für Discovery + new Thread(() -> runDiscoveryServer()).start(); + } + + private static void runSyslogServer() { + try (DatagramSocket socket = new DatagramSocket(SYSLOG_PORT)) { + // Puffergröße 2048 gemäß RFC 5424 Empfehlung. + // Ermöglicht den Empfang kompletter Netzwerkpakete (MTU), um zu lange + // Nachrichten aktiv zu erkennen und abzulehnen, statt sie nur abzuschneiden. + byte[] buffer = new byte[2048]; + + System.out.println("Syslog Server läuft auf Port " + SYSLOG_PORT); + + while (true) { + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + socket.receive(packet); + + int length = packet.getLength(); + + // Länge prüfen + if (length > MAX_MESSAGE_LENGTH) { + System.out.println("Nachricht zu lang von " + packet.getAddress()); + continue; + } + + String message = new String(packet.getData(), 0, length, StandardCharsets.UTF_8); + + // 📡 Ausgabe mit Client-IP + System.out.println( + "[SYSLOG] Von " + + packet.getAddress().getHostAddress() + + ":" + + packet.getPort() + + " -> " + + message + ); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void runDiscoveryServer() { + try (DatagramSocket socket = new DatagramSocket(DISCOVERY_PORT)) { + byte[] buffer = new byte[256]; + + System.out.println("Discovery Service läuft auf Port " + DISCOVERY_PORT); + + while (true) { + DatagramPacket request = new DatagramPacket(buffer, buffer.length); + socket.receive(request); + + InetAddress clientAddress = request.getAddress(); + int clientPort = request.getPort(); + + System.out.println( + "[DISCOVERY] Anfrage von " + + clientAddress.getHostAddress() + ); + + // Antwort (leer reicht laut Aufgabe) + byte[] responseData = new byte[0]; + + DatagramPacket response = new DatagramPacket( + responseData, + responseData.length, + clientAddress, + clientPort + ); + + socket.send(response); + + System.out.println( + "[DISCOVERY] Antwort gesendet an " + + clientAddress.getHostAddress() + ); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file 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\"]" + ); + } +}