From 34d8bac90693211c0ff6b2a5427a5de2c6a6c404 Mon Sep 17 00:00:00 2001 From: John Hentges Date: Thu, 26 Jun 2025 06:10:27 -0700 Subject: [PATCH 1/3] Add encryption toggle checkboxes --- .gitignore | 2 + .../DirectIpNet.Native.csproj | 11 + csharp/DirectIpNet.Native/Exports.cs | 56 +++++ csharp/DirectIpNet.Native/README.md | 35 +++ csharp/DirectIpNet.Native/directip.h | 28 +++ csharp/DirectIpNet/Confirmation.cs | 38 ++++ csharp/DirectIpNet/DirectIpClient.cs | 29 +++ csharp/DirectIpNet/MOMessage.cs | 59 +++++ csharp/DirectIpNet/MTHeader.cs | 36 +++ csharp/DirectIpNet/MTMessage.cs | 85 +++++++ csharp/DirectIpNet/MTPayload.cs | 23 ++ csharp/DirectIpNet/MessageStatus.cs | 22 ++ csharp/DirectIpNet/README.md | 38 ++++ csharp/DirectIpNet/SBDDirectIP.csproj | 7 + csharp/SBDDirectIP.App/MainForm.Designer.cs | 146 ++++++++++++ csharp/SBDDirectIP.App/MainForm.cs | 210 ++++++++++++++++++ csharp/SBDDirectIP.App/Program.cs | 15 ++ .../Properties/ApplicationConfiguration.cs | 14 ++ csharp/SBDDirectIP.App/Properties/Settings.cs | 50 +++++ csharp/SBDDirectIP.App/README.md | 19 ++ csharp/SBDDirectIP.App/SBDDirectIP.App.csproj | 15 ++ 21 files changed, 938 insertions(+) create mode 100644 csharp/DirectIpNet.Native/DirectIpNet.Native.csproj create mode 100644 csharp/DirectIpNet.Native/Exports.cs create mode 100644 csharp/DirectIpNet.Native/README.md create mode 100644 csharp/DirectIpNet.Native/directip.h create mode 100644 csharp/DirectIpNet/Confirmation.cs create mode 100644 csharp/DirectIpNet/DirectIpClient.cs create mode 100644 csharp/DirectIpNet/MOMessage.cs create mode 100644 csharp/DirectIpNet/MTHeader.cs create mode 100644 csharp/DirectIpNet/MTMessage.cs create mode 100644 csharp/DirectIpNet/MTPayload.cs create mode 100644 csharp/DirectIpNet/MessageStatus.cs create mode 100644 csharp/DirectIpNet/README.md create mode 100644 csharp/DirectIpNet/SBDDirectIP.csproj create mode 100644 csharp/SBDDirectIP.App/MainForm.Designer.cs create mode 100644 csharp/SBDDirectIP.App/MainForm.cs create mode 100644 csharp/SBDDirectIP.App/Program.cs create mode 100644 csharp/SBDDirectIP.App/Properties/ApplicationConfiguration.cs create mode 100644 csharp/SBDDirectIP.App/Properties/Settings.cs create mode 100644 csharp/SBDDirectIP.App/README.md create mode 100644 csharp/SBDDirectIP.App/SBDDirectIP.App.csproj diff --git a/.gitignore b/.gitignore index ea8c4bf..51d6621 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +bin +obj diff --git a/csharp/DirectIpNet.Native/DirectIpNet.Native.csproj b/csharp/DirectIpNet.Native/DirectIpNet.Native.csproj new file mode 100644 index 0000000..66195dc --- /dev/null +++ b/csharp/DirectIpNet.Native/DirectIpNet.Native.csproj @@ -0,0 +1,11 @@ + + + net8.0 + enable + Library + true + + + + + diff --git a/csharp/DirectIpNet.Native/Exports.cs b/csharp/DirectIpNet.Native/Exports.cs new file mode 100644 index 0000000..044c6d4 --- /dev/null +++ b/csharp/DirectIpNet.Native/Exports.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using SBDDirectIP; + +namespace SBDDirectIP.Native; + +public static unsafe class Exports +{ + [StructLayout(LayoutKind.Sequential)] + public struct ConfirmationInfo + { + public uint ClientMsgId; + public uint IdReference; + public MessageStatus Status; + } + + [UnmanagedCallersOnly(EntryPoint = "directip_send_mt")] + public static int SendMt(byte* host, int port, uint clientMsgId, byte* imei, byte* payload, int payloadLen, ConfirmationInfo* confirmation) + { + try + { + var hostStr = Marshal.PtrToStringUTF8((IntPtr)host)!; + var imeiStr = Marshal.PtrToStringUTF8((IntPtr)imei)!; + var data = new byte[payloadLen]; + if (payload != null && payloadLen > 0) + { + Marshal.Copy((IntPtr)payload, data, 0, payloadLen); + } + + var client = new DirectIpClient(hostStr, port); + var msg = MTMessage.Builder() + .ClientMsgId(clientMsgId) + .Imei(imeiStr) + .Payload(data) + .Build(); + + Task task = client.SendAsync(msg); + task.Wait(); + Confirmation conf = task.Result; + + if (confirmation != null) + { + confirmation->ClientMsgId = conf.ClientMsgId; + confirmation->IdReference = conf.IdReference; + confirmation->Status = conf.Status; + } + + return 0; + } + catch + { + return -1; + } + } +} diff --git a/csharp/DirectIpNet.Native/README.md b/csharp/DirectIpNet.Native/README.md new file mode 100644 index 0000000..5c450da --- /dev/null +++ b/csharp/DirectIpNet.Native/README.md @@ -0,0 +1,35 @@ +# DirectIpNet.Native + +This project exposes the `SBDDirectIP` API as a native C library. It uses +`UnmanagedCallersOnly` so the resulting binary can be consumed from any language +that can call C functions. + +## Building + +Use `dotnet publish` with NativeAOT enabled to produce a shared library: + +```shell +cd csharp/DirectIpNet.Native +dotnet publish -c Release -r linux-x64 +``` + +The compiled library will be placed under `bin/Release/net8.0/linux-x64/native`. + +## Usage + +Include `directip.h` in your C or C++ project and link against the produced +library. Example: + +```c +#include "directip.h" + +int main() { + ConfirmationInfo conf; + const char payload[] = "Hello"; + int rc = directip_send_mt("127.0.0.1", 10800, 1, + "012345678901234", + (const uint8_t*)payload, sizeof(payload)-1, + &conf); + return rc; +} +``` diff --git a/csharp/DirectIpNet.Native/directip.h b/csharp/DirectIpNet.Native/directip.h new file mode 100644 index 0000000..d85a8df --- /dev/null +++ b/csharp/DirectIpNet.Native/directip.h @@ -0,0 +1,28 @@ +#ifndef DIRECTIP_H +#define DIRECTIP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint32_t ClientMsgId; + uint32_t IdReference; + int32_t Status; +} ConfirmationInfo; + +int directip_send_mt(const char* host, + int port, + uint32_t client_msg_id, + const char* imei, + const uint8_t* payload, + int payload_len, + ConfirmationInfo* confirmation); + +#ifdef __cplusplus +} +#endif + +#endif // DIRECTIP_H diff --git a/csharp/DirectIpNet/Confirmation.cs b/csharp/DirectIpNet/Confirmation.cs new file mode 100644 index 0000000..c4f719e --- /dev/null +++ b/csharp/DirectIpNet/Confirmation.cs @@ -0,0 +1,38 @@ +using System; +using System.Buffers.Binary; + +namespace SBDDirectIP; + +public class Confirmation +{ + public uint ClientMsgId { get; } + public string Imei { get; } + public uint IdReference { get; } + public MessageStatus Status { get; } + + public Confirmation(uint clientMsgId, string imei, uint idReference, MessageStatus status) + { + ClientMsgId = clientMsgId; + Imei = imei; + IdReference = idReference; + Status = status; + } + + public static Confirmation FromArray(ReadOnlySpan data) + { + int offset = 0; + if (data[offset++] != 0x44) throw new ArgumentException("Invalid confirmation element"); + ushort len = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset,2)); + offset += 2; + if (len != 25) throw new ArgumentException("Unexpected confirmation length"); + uint clientId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset,4)); + offset += 4; + string imei = System.Text.Encoding.ASCII.GetString(data.Slice(offset,15)); + offset += 15; + uint refId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset,4)); + offset += 4; + short statusCode = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset,2)); + var status = (MessageStatus)statusCode; + return new Confirmation(clientId, imei, refId, status); + } +} diff --git a/csharp/DirectIpNet/DirectIpClient.cs b/csharp/DirectIpNet/DirectIpClient.cs new file mode 100644 index 0000000..5441f09 --- /dev/null +++ b/csharp/DirectIpNet/DirectIpClient.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace SBDDirectIP; + +public class DirectIpClient +{ + private readonly string _host; + private readonly int _port; + + public DirectIpClient(string host, int port) + { + _host = host; + _port = port; + } + + public async Task SendAsync(MTMessage message) + { + using var client = new TcpClient(); + await client.ConnectAsync(_host, _port); + using var stream = client.GetStream(); + var data = message.ToArray(); + await stream.WriteAsync(data, 0, data.Length); + var buffer = new byte[56]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + return Confirmation.FromArray(buffer.AsSpan(3)); + } +} diff --git a/csharp/DirectIpNet/MOMessage.cs b/csharp/DirectIpNet/MOMessage.cs new file mode 100644 index 0000000..4e48714 --- /dev/null +++ b/csharp/DirectIpNet/MOMessage.cs @@ -0,0 +1,59 @@ +using System; +using System.Buffers.Binary; + +namespace SBDDirectIP; + +/// +/// Represents a Mobile Originated (MO) message received from a remote device. +/// Only the header fields required to locate the payload are parsed. +/// +public class MOMessage +{ + public string Imei { get; } + public byte[] Payload { get; } + + private MOMessage(string imei, byte[] payload) + { + Imei = imei; + Payload = payload; + } + + /// + /// Parse a raw DirectIP MO message. + /// + public static MOMessage FromArray(ReadOnlySpan data) + { + if (data.Length < 3) + throw new ArgumentException("Invalid data length", nameof(data)); + if (data[0] != 1) + throw new ArgumentException("Unsupported protocol version", nameof(data)); + + ushort length = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(1, 2)); + if (data.Length - 3 < length) + throw new ArgumentException("Incomplete message", nameof(data)); + + int offset = 3; + if (data[offset++] != 0x01) + throw new ArgumentException("Missing MO-Header", nameof(data)); + ushort hlen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); + offset += 2; + if (hlen != 28) + throw new ArgumentException("Unexpected header length", nameof(data)); + + offset += 4; // CDR reference + string imei = System.Text.Encoding.ASCII.GetString(data.Slice(offset, 15)); + offset += 15; + offset += 1; // session status + offset += 2; // MOMSN + offset += 2; // MTMSN + offset += 4; // time of session + + if (data[offset++] != 0x02) + throw new ArgumentException("Missing MO-Payload", nameof(data)); + ushort plen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); + offset += 2; + byte[] payload = data.Slice(offset, plen).ToArray(); + + return new MOMessage(imei, payload); + } +} diff --git a/csharp/DirectIpNet/MTHeader.cs b/csharp/DirectIpNet/MTHeader.cs new file mode 100644 index 0000000..289fe0e --- /dev/null +++ b/csharp/DirectIpNet/MTHeader.cs @@ -0,0 +1,36 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Text; + +namespace SBDDirectIP; + +internal class MTHeader +{ + public uint ClientMsgId { get; } + public string Imei { get; } + public ushort DispositionFlags { get; } + + public MTHeader(uint clientMsgId, string imei, ushort dispositionFlags) + { + if (imei.Length != 15) throw new ArgumentException("IMEI must be 15 digits", nameof(imei)); + ClientMsgId = clientMsgId; + Imei = imei; + DispositionFlags = dispositionFlags; + } + + public byte[] ToArray() + { + var buffer = new byte[24]; + int offset = 0; + buffer[offset++] = 0x41; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), 21); + offset += 2; + BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(offset), ClientMsgId); + offset += 4; + Encoding.ASCII.GetBytes(Imei).CopyTo(buffer.AsSpan(offset)); + offset += 15; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), DispositionFlags); + return buffer; + } +} diff --git a/csharp/DirectIpNet/MTMessage.cs b/csharp/DirectIpNet/MTMessage.cs new file mode 100644 index 0000000..70a1dc2 --- /dev/null +++ b/csharp/DirectIpNet/MTMessage.cs @@ -0,0 +1,85 @@ +using System.Buffers.Binary; + +namespace SBDDirectIP; + +public class MTMessage +{ + private readonly MTHeader _header; + private readonly MTPayload _payload; + + private MTMessage(MTHeader header, MTPayload payload) + { + _header = header; + _payload = payload; + } + + public static MTMessageBuilder Builder() => new MTMessageBuilder(); + + public byte[] ToArray() + { + var headerBytes = _header.ToArray(); + var payloadBytes = _payload.ToArray(); + var length = (ushort)(headerBytes.Length + payloadBytes.Length); + + var buffer = new byte[3 + length]; + int offset = 0; + buffer[offset++] = 1; // protocol version + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), length); + offset += 2; + headerBytes.CopyTo(buffer.AsSpan(offset)); + offset += headerBytes.Length; + payloadBytes.CopyTo(buffer.AsSpan(offset)); + return buffer; + } + + public static MTMessage FromArray(ReadOnlySpan data) + { + if (data.Length < 3) throw new ArgumentException("Invalid message"); + if (data[0] != 1) throw new ArgumentException("Unsupported protocol version"); + ushort len = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(1, 2)); + if (data.Length - 3 < len) throw new ArgumentException("Incomplete message"); + int offset = 3; + // parse header + if (data[offset] != 0x41) throw new ArgumentException("Missing header"); + offset += 1; + ushort hLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset,2)); + offset += 2; + uint clientMsgId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset,4)); + offset += 4; + string imei = System.Text.Encoding.ASCII.GetString(data.Slice(offset,15)); + offset += 15; + ushort flags = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset,2)); + offset += 2; + var header = new MTHeader(clientMsgId, imei, flags); + // parse payload + if (data[offset] != 0x42) throw new ArgumentException("Missing payload"); + offset += 1; + ushort pLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset,2)); + offset += 2; + var payload = new byte[pLen]; + data.Slice(offset, pLen).CopyTo(payload); + var pl = new MTPayload(payload); + return new MTMessage(header, pl); + } + + public class MTMessageBuilder + { + private uint _clientMsgId; + private string? _imei; + private byte[] _payload = Array.Empty(); + private ushort _flags = 0; + + public MTMessageBuilder ClientMsgId(uint id) { _clientMsgId = id; return this; } + public MTMessageBuilder Imei(string imei) { _imei = imei; return this; } + public MTMessageBuilder Payload(byte[] data) { _payload = data ?? Array.Empty(); return this; } + public MTMessageBuilder DispositionFlags(ushort flags) { _flags = flags; return this; } + + public MTMessage Build() + { + if (_imei == null) throw new InvalidOperationException("IMEI missing"); + var header = new MTHeader(_clientMsgId, _imei, _flags); + var payload = new MTPayload(_payload); + return new MTMessage(header, payload); + } + } +} diff --git a/csharp/DirectIpNet/MTPayload.cs b/csharp/DirectIpNet/MTPayload.cs new file mode 100644 index 0000000..3ced372 --- /dev/null +++ b/csharp/DirectIpNet/MTPayload.cs @@ -0,0 +1,23 @@ +using System.Buffers.Binary; + +namespace SBDDirectIP; + +internal class MTPayload +{ + public byte[] Data { get; } + public MTPayload(byte[] data) + { + Data = data ?? System.Array.Empty(); + } + + public byte[] ToArray() + { + var buffer = new byte[3 + Data.Length]; + int offset = 0; + buffer[offset++] = 0x42; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), (ushort)Data.Length); + offset += 2; + Data.CopyTo(buffer.AsSpan(offset)); + return buffer; + } +} diff --git a/csharp/DirectIpNet/MessageStatus.cs b/csharp/DirectIpNet/MessageStatus.cs new file mode 100644 index 0000000..d161773 --- /dev/null +++ b/csharp/DirectIpNet/MessageStatus.cs @@ -0,0 +1,22 @@ +namespace SBDDirectIP; + +/// +/// Status of a confirmation message returned by the gateway. +/// This mirrors the enum defined in the original Rust implementation. +/// +public enum MessageStatus +{ + SuccessfulQueueOrder = 0, + InvalidImei = -1, + UnknownImei = -2, + PayloadOversized = -3, + PayloadMissing = -4, + MtQueueFull = -5, + MtResourcesUnavailable = -6, + ProtocolViolation = -7, + RingAlertsDisabled = -8, + SsdNotAttached = -9, + SourceAddressRejected = -10, + MtmsnOutOfRange = -11, + CertificateRejected = -12 +} diff --git a/csharp/DirectIpNet/README.md b/csharp/DirectIpNet/README.md new file mode 100644 index 0000000..b396044 --- /dev/null +++ b/csharp/DirectIpNet/README.md @@ -0,0 +1,38 @@ +# SBDDirectIP + +This is a lightweight C# implementation of Iridium's Short Burst Data Direct-IP protocol. +It provides the ability to create and send Mobile Terminated (MT) messages, +parse confirmation responses from the gateway and decode Mobile Originated (MO) +messages. The code mirrors the functionality of the Rust library contained in +the repository but targets .NET applications. + +## Building + +``` +dotnet build +``` + +## Usage + +``` +var client = new SBDDirectIP.DirectIpClient("127.0.0.1", 10800); +var msg = MTMessage.Builder() + .ClientMsgId(123) + .Imei("012345678901234") + .Payload(System.Text.Encoding.ASCII.GetBytes("Hello")) + .Build(); +var confirmation = await client.SendAsync(msg); +Console.WriteLine(confirmation.Status); +``` + +MO messages received from a device can be parsed using `MOMessage.FromArray` to +extract the encrypted payload: + +``` +var mo = MOMessage.FromArray(data); +byte[] payload = mo.Payload; +``` + +This library can be referenced from any .NET language. For integration with +native applications, see the `DirectIpNet.Native` project which exports a C API +around this library. diff --git a/csharp/DirectIpNet/SBDDirectIP.csproj b/csharp/DirectIpNet/SBDDirectIP.csproj new file mode 100644 index 0000000..0661bde --- /dev/null +++ b/csharp/DirectIpNet/SBDDirectIP.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + SBDDirectIP + + diff --git a/csharp/SBDDirectIP.App/MainForm.Designer.cs b/csharp/SBDDirectIP.App/MainForm.Designer.cs new file mode 100644 index 0000000..486700f --- /dev/null +++ b/csharp/SBDDirectIP.App/MainForm.Designer.cs @@ -0,0 +1,146 @@ +using System.Windows.Forms; + +namespace SBDDirectIP.App; + +partial class MainForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer? components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.imeiLabel = new Label(); + this.imeiTextBox = new TextBox(); + this.keyLabel = new Label(); + this.keyTextBox = new TextBox(); + this.serverLabel = new Label(); + this.serverTextBox = new TextBox(); + this.payloadLabel = new Label(); + this.payloadTextBox = new TextBox(); + this.sendButton = new Button(); + this.outputTextBox = new TextBox(); + this.encryptCheckBox = new CheckBox(); + this.decryptCheckBox = new CheckBox(); + this.SuspendLayout(); + + // imeiLabel + this.imeiLabel.AutoSize = true; + this.imeiLabel.Location = new System.Drawing.Point(10, 15); + this.imeiLabel.Name = "imeiLabel"; + this.imeiLabel.Size = new System.Drawing.Size(130, 15); + this.imeiLabel.Text = "IMEI"; + + // imeiTextBox + this.imeiTextBox.Location = new System.Drawing.Point(150, 10); + this.imeiTextBox.Name = "imeiTextBox"; + this.imeiTextBox.Size = new System.Drawing.Size(440, 23); + + // keyLabel + this.keyLabel.AutoSize = true; + this.keyLabel.Location = new System.Drawing.Point(10, 45); + this.keyLabel.Name = "keyLabel"; + this.keyLabel.Size = new System.Drawing.Size(130, 15); + this.keyLabel.Text = "AES Key (hex)"; + + // keyTextBox + this.keyTextBox.Location = new System.Drawing.Point(150, 40); + this.keyTextBox.Name = "keyTextBox"; + this.keyTextBox.Size = new System.Drawing.Size(440, 23); + + // serverLabel + this.serverLabel.AutoSize = true; + this.serverLabel.Location = new System.Drawing.Point(10, 75); + this.serverLabel.Name = "serverLabel"; + this.serverLabel.Size = new System.Drawing.Size(130, 15); + this.serverLabel.Text = "Gateway host:port"; + + // serverTextBox + this.serverTextBox.Location = new System.Drawing.Point(150, 70); + this.serverTextBox.Name = "serverTextBox"; + this.serverTextBox.Size = new System.Drawing.Size(240, 23); + this.serverTextBox.Text = "directip.sbd.iridium.com:10800"; + + // payloadLabel + this.payloadLabel.AutoSize = true; + this.payloadLabel.Location = new System.Drawing.Point(10, 105); + this.payloadLabel.Name = "payloadLabel"; + this.payloadLabel.Size = new System.Drawing.Size(130, 15); + this.payloadLabel.Text = "Payload (hex)"; + + // payloadTextBox + this.payloadTextBox.Location = new System.Drawing.Point(150, 100); + this.payloadTextBox.Name = "payloadTextBox"; + this.payloadTextBox.Size = new System.Drawing.Size(240, 23); + + // sendButton + this.sendButton.Location = new System.Drawing.Point(150, 130); + this.sendButton.Name = "sendButton"; + this.sendButton.Size = new System.Drawing.Size(75, 23); + this.sendButton.Text = "Send"; + this.sendButton.UseVisualStyleBackColor = true; + this.sendButton.Click += new System.EventHandler(this.OnSend); + + // encryptCheckBox + this.encryptCheckBox.Location = new System.Drawing.Point(240, 130); + this.encryptCheckBox.Name = "encryptCheckBox"; + this.encryptCheckBox.Size = new System.Drawing.Size(80, 24); + this.encryptCheckBox.Text = "Encrypt"; + this.encryptCheckBox.Checked = true; + this.encryptCheckBox.AutoSize = true; + + // decryptCheckBox + this.decryptCheckBox.Location = new System.Drawing.Point(330, 130); + this.decryptCheckBox.Name = "decryptCheckBox"; + this.decryptCheckBox.Size = new System.Drawing.Size(80, 24); + this.decryptCheckBox.Text = "Decrypt"; + this.decryptCheckBox.Checked = true; + this.decryptCheckBox.AutoSize = true; + + // outputTextBox + this.outputTextBox.Location = new System.Drawing.Point(10, 160); + this.outputTextBox.Multiline = true; + this.outputTextBox.Name = "outputTextBox"; + this.outputTextBox.ReadOnly = true; + this.outputTextBox.ScrollBars = ScrollBars.Vertical; + this.outputTextBox.Size = new System.Drawing.Size(580, 390); + + // MainForm + this.ClientSize = new System.Drawing.Size(620, 600); + this.Controls.AddRange(new Control[] { this.imeiLabel, this.imeiTextBox, this.keyLabel, this.keyTextBox, this.serverLabel, this.serverTextBox, this.payloadLabel, this.payloadTextBox, this.sendButton, this.encryptCheckBox, this.decryptCheckBox, this.outputTextBox }); + this.Name = "MainForm"; + this.Text = "SBD DirectIP Client"; + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private Label imeiLabel; + private Label keyLabel; + private Label serverLabel; + private Label payloadLabel; + private CheckBox encryptCheckBox; + private CheckBox decryptCheckBox; +} diff --git a/csharp/SBDDirectIP.App/MainForm.cs b/csharp/SBDDirectIP.App/MainForm.cs new file mode 100644 index 0000000..3e42793 --- /dev/null +++ b/csharp/SBDDirectIP.App/MainForm.cs @@ -0,0 +1,210 @@ +using System; +using System.Windows.Forms; +using SBDDirectIP; +using System.Security.Cryptography; + +namespace SBDDirectIP.App; + +public partial class MainForm : Form +{ + private TextBox imeiTextBox = null!; + private TextBox keyTextBox = null!; + private TextBox serverTextBox = null!; + private TextBox payloadTextBox = null!; + private Button sendButton = null!; + private TextBox outputTextBox = null!; + private CheckBox encryptCheckBox = null!; + private CheckBox decryptCheckBox = null!; + private System.Threading.CancellationTokenSource? listenerCts; + + public MainForm() + { + InitializeComponent(); + Load += (_, _) => StartListener(); + FormClosing += (_, _) => listenerCts?.Cancel(); + + imeiTextBox.Text = Properties.Settings.Default.IMEI; + keyTextBox.Text = Properties.Settings.Default.AESKey; + serverTextBox.Text = Properties.Settings.Default.ServerHostPort; + encryptCheckBox.Checked = Properties.Settings.Default.EncryptOnSend; + decryptCheckBox.Checked = Properties.Settings.Default.DecryptOnReceive; + } + + private async void OnSend(object? sender, EventArgs e) + { + try + { + sendButton.Enabled = false; + Log("Preparing message"); + string imei = imeiTextBox.Text.Trim(); + string keyHex = keyTextBox.Text.Trim(); + string server = serverTextBox.Text.Trim(); + string payloadHex = payloadTextBox.Text.Trim(); + var tokens = payloadHex.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0 || tokens.Length > 30) + { + Log("Invalid payload length"); + MessageBox.Show("Payload must be space separated hex bytes (max 30 bytes)."); + return; + } + byte[] payload = new byte[tokens.Length]; + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i].Length != 2 || !byte.TryParse(tokens[i], System.Globalization.NumberStyles.HexNumber, null, out payload[i])) + { + Log("Invalid payload format"); + MessageBox.Show("Payload must be space separated hex bytes like 'AA 55'."); + return; + } + } + byte[] key = Array.Empty(); + if (encryptCheckBox.Checked) + { + try { key = Convert.FromHexString(keyHex); } catch { } + if (key.Length != 32) + { + Log("Invalid AES key length"); + MessageBox.Show("AES Key must be 32 bytes (64 hex characters, no spaces)."); + return; + } + Log($"Encrypting {payload.Length} bytes"); + using var aes = Aes.Create(); + aes.Key = key; + aes.IV = new byte[16]; + using var encryptor = aes.CreateEncryptor(); + payload = encryptor.TransformFinalBlock(payload, 0, payload.Length); + } + + var settings = Properties.Settings.Default; + settings.IMEI = imei; + settings.AESKey = keyHex; + settings.ServerHostPort = server; + settings.EncryptOnSend = encryptCheckBox.Checked; + settings.DecryptOnReceive = decryptCheckBox.Checked; + settings.Save(); + + string host = server; int port = 10800; + if (server.Contains(":")) + { + var parts = server.Split(':',2); + host = parts[0]; + port = int.Parse(parts[1]); + } + + Log($"Connecting to {host}:{port}"); + var client = new DirectIpClient(host, port); + var msg = MTMessage.Builder() + .ClientMsgId(1) + .Imei(imei) + .Payload(payload) + .Build(); + Log("Sending message"); + var conf = await client.SendAsync(msg); + Log($"Status: {conf.Status} Ref: {conf.IdReference}"); + outputTextBox.Text = $"Status: {conf.Status}"; + } + catch (Exception ex) + { + Log($"Error: {ex.Message}"); + MessageBox.Show(ex.Message); + } + finally + { + sendButton.Enabled = true; + } + } + + private void Log(string message) + { + if (InvokeRequired) + Invoke(new Action(() => Log(message))); + else + outputTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}"); + } + + private void StartListener() + { + listenerCts = new System.Threading.CancellationTokenSource(); + var ct = listenerCts.Token; + System.Threading.Tasks.Task.Run(async () => + { + var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 10800); + listener.Start(); + Log("Listening on port 10800"); + try + { + while (!ct.IsCancellationRequested) + { + var client = await listener.AcceptTcpClientAsync(ct); + _ = HandleClientAsync(client, ct); + } + } + catch (OperationCanceledException) { } + finally + { + listener.Stop(); + } + }, ct); + } + + private async System.Threading.Tasks.Task HandleClientAsync(System.Net.Sockets.TcpClient client, System.Threading.CancellationToken ct) + { + using (client) + { + try + { + var stream = client.GetStream(); + byte[] prefix = new byte[3]; + if (await ReadExactAsync(stream, prefix.AsMemory(), ct) != 3) return; + ushort len = System.Buffers.Binary.BinaryPrimitives.ReadUInt16BigEndian(prefix.AsSpan(1)); + byte[] buffer = new byte[3 + len]; + prefix.CopyTo(buffer, 0); + if (await ReadExactAsync(stream, buffer.AsMemory(3, len), ct) != len) return; + + var mo = MOMessage.FromArray(buffer); + string keyHex = ""; + bool decrypt = true; + this.Invoke(new Action(() => + { + keyHex = keyTextBox.Text.Trim(); + decrypt = decryptCheckBox.Checked; + })); + + byte[] plain = mo.Payload; + if (decrypt) + { + byte[] key = Array.Empty(); + try { key = Convert.FromHexString(keyHex); } catch { } + if (key.Length != 32) + { + Invoke(new Action(() => Log("Received message but key invalid"))); + return; + } + using var aes = Aes.Create(); + aes.Key = key; + aes.IV = new byte[16]; + using var decryptor = aes.CreateDecryptor(); + plain = decryptor.TransformFinalBlock(mo.Payload, 0, mo.Payload.Length); + } + string hex = BitConverter.ToString(plain).Replace("-", " "); + Invoke(new Action(() => Log($"RX from {mo.Imei}: {hex}"))); + } + catch (Exception ex) + { + Invoke(new Action(() => Log($"Receive error: {ex.Message}"))); + } + } + } + + private static async System.Threading.Tasks.Task ReadExactAsync(System.Net.Sockets.NetworkStream stream, Memory buffer, System.Threading.CancellationToken ct) + { + int read = 0; + while (read < buffer.Length) + { + int n = await stream.ReadAsync(buffer.Slice(read), ct); + if (n == 0) break; + read += n; + } + return read; + } +} diff --git a/csharp/SBDDirectIP.App/Program.cs b/csharp/SBDDirectIP.App/Program.cs new file mode 100644 index 0000000..e3ef8dc --- /dev/null +++ b/csharp/SBDDirectIP.App/Program.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Forms; + +namespace SBDDirectIP.App +{ + internal static class Program + { + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} diff --git a/csharp/SBDDirectIP.App/Properties/ApplicationConfiguration.cs b/csharp/SBDDirectIP.App/Properties/ApplicationConfiguration.cs new file mode 100644 index 0000000..8d63fd7 --- /dev/null +++ b/csharp/SBDDirectIP.App/Properties/ApplicationConfiguration.cs @@ -0,0 +1,14 @@ +using System; +using System.Windows.Forms; + +namespace SBDDirectIP.App; + +internal static class ApplicationConfiguration +{ + public static void Initialize() + { + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + } +} diff --git a/csharp/SBDDirectIP.App/Properties/Settings.cs b/csharp/SBDDirectIP.App/Properties/Settings.cs new file mode 100644 index 0000000..1760920 --- /dev/null +++ b/csharp/SBDDirectIP.App/Properties/Settings.cs @@ -0,0 +1,50 @@ +using System.Configuration; + +namespace SBDDirectIP.App.Properties; + +internal sealed partial class Settings : ApplicationSettingsBase +{ + private static readonly Settings defaultInstance = (Settings)Synchronized(new Settings()); + + public static Settings Default => defaultInstance; + + [UserScopedSetting] + [DefaultSettingValue("")] + public string IMEI + { + get => (string)this[nameof(IMEI)]; + set => this[nameof(IMEI)] = value; + } + + [UserScopedSetting] + [DefaultSettingValue("")] + public string AESKey + { + get => (string)this[nameof(AESKey)]; + set => this[nameof(AESKey)] = value; + } + + [UserScopedSetting] + [DefaultSettingValue("directip.sbd.iridium.com:10800")] + public string ServerHostPort + { + get => (string)this[nameof(ServerHostPort)]; + set => this[nameof(ServerHostPort)] = value; + } + + [UserScopedSetting] + [DefaultSettingValue("True")] + public bool EncryptOnSend + { + get => (bool)this[nameof(EncryptOnSend)]; + set => this[nameof(EncryptOnSend)] = value; + } + + [UserScopedSetting] + [DefaultSettingValue("True")] + public bool DecryptOnReceive + { + get => (bool)this[nameof(DecryptOnReceive)]; + set => this[nameof(DecryptOnReceive)] = value; + } +} diff --git a/csharp/SBDDirectIP.App/README.md b/csharp/SBDDirectIP.App/README.md new file mode 100644 index 0000000..24127b9 --- /dev/null +++ b/csharp/SBDDirectIP.App/README.md @@ -0,0 +1,19 @@ +# SBDDirectIP Windows App + +This WinForms application demonstrates how to send MT messages using `SBDDirectIP.dll`. + +## Building + +``` +dotnet build +``` + +## Usage + +Fill in the IMEI, AES key (64 hex characters), gateway host and port, +and the payload as space‑separated hexadecimal byte values (for example +`AA 55 10 01`) with a maximum of 30 bytes. Use the **Encrypt** and **Decrypt** +checkboxes to toggle AES256 processing. The states of these options are saved. +Press **Send** to transmit the message. Incoming MO messages are accepted on +port 10800 and displayed in the log box; payloads are decrypted only when the +**Decrypt** option is enabled. diff --git a/csharp/SBDDirectIP.App/SBDDirectIP.App.csproj b/csharp/SBDDirectIP.App/SBDDirectIP.App.csproj new file mode 100644 index 0000000..7c0138b --- /dev/null +++ b/csharp/SBDDirectIP.App/SBDDirectIP.App.csproj @@ -0,0 +1,15 @@ + + + WinExe + net8.0-windows + true + enable + enable + SBDDirectIP.App + x64 + + + + + + From f290823ceca74f17c61b22a7385bf7871d465c30 Mon Sep 17 00:00:00 2001 From: John Hentges Date: Thu, 26 Jun 2025 06:57:35 -0700 Subject: [PATCH 2/3] Add Send ASCII option --- csharp/SBDDirectIP.App/MainForm.Designer.cs | 11 +++- csharp/SBDDirectIP.App/MainForm.cs | 50 ++++++++++++++----- csharp/SBDDirectIP.App/Properties/Settings.cs | 8 +++ csharp/SBDDirectIP.App/README.md | 14 +++--- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/csharp/SBDDirectIP.App/MainForm.Designer.cs b/csharp/SBDDirectIP.App/MainForm.Designer.cs index 486700f..d2d9ae9 100644 --- a/csharp/SBDDirectIP.App/MainForm.Designer.cs +++ b/csharp/SBDDirectIP.App/MainForm.Designer.cs @@ -43,6 +43,7 @@ private void InitializeComponent() this.outputTextBox = new TextBox(); this.encryptCheckBox = new CheckBox(); this.decryptCheckBox = new CheckBox(); + this.asciiCheckBox = new CheckBox(); this.SuspendLayout(); // imeiLabel @@ -118,6 +119,13 @@ private void InitializeComponent() this.decryptCheckBox.Checked = true; this.decryptCheckBox.AutoSize = true; + // asciiCheckBox + this.asciiCheckBox.Location = new System.Drawing.Point(420, 130); + this.asciiCheckBox.Name = "asciiCheckBox"; + this.asciiCheckBox.Size = new System.Drawing.Size(85, 24); + this.asciiCheckBox.Text = "Send ASCII"; + this.asciiCheckBox.AutoSize = true; + // outputTextBox this.outputTextBox.Location = new System.Drawing.Point(10, 160); this.outputTextBox.Multiline = true; @@ -128,7 +136,7 @@ private void InitializeComponent() // MainForm this.ClientSize = new System.Drawing.Size(620, 600); - this.Controls.AddRange(new Control[] { this.imeiLabel, this.imeiTextBox, this.keyLabel, this.keyTextBox, this.serverLabel, this.serverTextBox, this.payloadLabel, this.payloadTextBox, this.sendButton, this.encryptCheckBox, this.decryptCheckBox, this.outputTextBox }); + this.Controls.AddRange(new Control[] { this.imeiLabel, this.imeiTextBox, this.keyLabel, this.keyTextBox, this.serverLabel, this.serverTextBox, this.payloadLabel, this.payloadTextBox, this.sendButton, this.encryptCheckBox, this.decryptCheckBox, this.asciiCheckBox, this.outputTextBox }); this.Name = "MainForm"; this.Text = "SBD DirectIP Client"; this.ResumeLayout(false); @@ -143,4 +151,5 @@ private void InitializeComponent() private Label payloadLabel; private CheckBox encryptCheckBox; private CheckBox decryptCheckBox; + private CheckBox asciiCheckBox; } diff --git a/csharp/SBDDirectIP.App/MainForm.cs b/csharp/SBDDirectIP.App/MainForm.cs index 3e42793..70cc1d2 100644 --- a/csharp/SBDDirectIP.App/MainForm.cs +++ b/csharp/SBDDirectIP.App/MainForm.cs @@ -15,6 +15,7 @@ public partial class MainForm : Form private TextBox outputTextBox = null!; private CheckBox encryptCheckBox = null!; private CheckBox decryptCheckBox = null!; + private CheckBox asciiCheckBox = null!; private System.Threading.CancellationTokenSource? listenerCts; public MainForm() @@ -28,6 +29,7 @@ public MainForm() serverTextBox.Text = Properties.Settings.Default.ServerHostPort; encryptCheckBox.Checked = Properties.Settings.Default.EncryptOnSend; decryptCheckBox.Checked = Properties.Settings.Default.DecryptOnReceive; + asciiCheckBox.Checked = Properties.Settings.Default.SendAsAscii; } private async void OnSend(object? sender, EventArgs e) @@ -39,23 +41,37 @@ private async void OnSend(object? sender, EventArgs e) string imei = imeiTextBox.Text.Trim(); string keyHex = keyTextBox.Text.Trim(); string server = serverTextBox.Text.Trim(); - string payloadHex = payloadTextBox.Text.Trim(); - var tokens = payloadHex.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0 || tokens.Length > 30) + string payloadInput = payloadTextBox.Text.Trim(); + byte[] payload; + if (asciiCheckBox.Checked) { - Log("Invalid payload length"); - MessageBox.Show("Payload must be space separated hex bytes (max 30 bytes)."); - return; + payload = System.Text.Encoding.ASCII.GetBytes(payloadInput); + if (payload.Length == 0 || payload.Length > 30) + { + Log("Invalid ASCII payload length"); + MessageBox.Show("ASCII payload must be 1-30 bytes."); + return; + } } - byte[] payload = new byte[tokens.Length]; - for (int i = 0; i < tokens.Length; i++) + else { - if (tokens[i].Length != 2 || !byte.TryParse(tokens[i], System.Globalization.NumberStyles.HexNumber, null, out payload[i])) + var tokens = payloadInput.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0 || tokens.Length > 30) { - Log("Invalid payload format"); - MessageBox.Show("Payload must be space separated hex bytes like 'AA 55'."); + Log("Invalid payload length"); + MessageBox.Show("Payload must be space separated hex bytes (max 30 bytes)."); return; } + payload = new byte[tokens.Length]; + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i].Length != 2 || !byte.TryParse(tokens[i], System.Globalization.NumberStyles.HexNumber, null, out payload[i])) + { + Log("Invalid payload format"); + MessageBox.Show("Payload must be space separated hex bytes like 'AA 55'."); + return; + } + } } byte[] key = Array.Empty(); if (encryptCheckBox.Checked) @@ -81,6 +97,7 @@ private async void OnSend(object? sender, EventArgs e) settings.ServerHostPort = server; settings.EncryptOnSend = encryptCheckBox.Checked; settings.DecryptOnReceive = decryptCheckBox.Checked; + settings.SendAsAscii = asciiCheckBox.Checked; settings.Save(); string host = server; int port = 10800; @@ -187,7 +204,8 @@ private async System.Threading.Tasks.Task HandleClientAsync(System.Net.Sockets.T plain = decryptor.TransformFinalBlock(mo.Payload, 0, mo.Payload.Length); } string hex = BitConverter.ToString(plain).Replace("-", " "); - Invoke(new Action(() => Log($"RX from {mo.Imei}: {hex}"))); + string ascii = ToPrintableString(plain); + Invoke(new Action(() => Log($"RX from {mo.Imei}: {hex} | {ascii}"))); } catch (Exception ex) { @@ -196,6 +214,14 @@ private async System.Threading.Tasks.Task HandleClientAsync(System.Net.Sockets.T } } + private static string ToPrintableString(ReadOnlySpan data) + { + var sb = new System.Text.StringBuilder(data.Length); + foreach (byte b in data) + sb.Append(b >= 32 && b < 127 ? (char)b : '.'); + return sb.ToString(); + } + private static async System.Threading.Tasks.Task ReadExactAsync(System.Net.Sockets.NetworkStream stream, Memory buffer, System.Threading.CancellationToken ct) { int read = 0; diff --git a/csharp/SBDDirectIP.App/Properties/Settings.cs b/csharp/SBDDirectIP.App/Properties/Settings.cs index 1760920..e2a46fe 100644 --- a/csharp/SBDDirectIP.App/Properties/Settings.cs +++ b/csharp/SBDDirectIP.App/Properties/Settings.cs @@ -47,4 +47,12 @@ public bool DecryptOnReceive get => (bool)this[nameof(DecryptOnReceive)]; set => this[nameof(DecryptOnReceive)] = value; } + + [UserScopedSetting] + [DefaultSettingValue("False")] + public bool SendAsAscii + { + get => (bool)this[nameof(SendAsAscii)]; + set => this[nameof(SendAsAscii)] = value; + } } diff --git a/csharp/SBDDirectIP.App/README.md b/csharp/SBDDirectIP.App/README.md index 24127b9..95c2b09 100644 --- a/csharp/SBDDirectIP.App/README.md +++ b/csharp/SBDDirectIP.App/README.md @@ -11,9 +11,11 @@ dotnet build ## Usage Fill in the IMEI, AES key (64 hex characters), gateway host and port, -and the payload as space‑separated hexadecimal byte values (for example -`AA 55 10 01`) with a maximum of 30 bytes. Use the **Encrypt** and **Decrypt** -checkboxes to toggle AES256 processing. The states of these options are saved. -Press **Send** to transmit the message. Incoming MO messages are accepted on -port 10800 and displayed in the log box; payloads are decrypted only when the -**Decrypt** option is enabled. +and the payload. By default the payload must be space‑separated hexadecimal +byte values (for example `AA 55 10 01`) with a maximum of 30 bytes. Enable the +**Send ASCII** option to send the payload as literal ASCII text instead. +Use the **Encrypt** and **Decrypt** checkboxes to toggle AES256 processing. The +state of all checkboxes is stored in the user settings. Press **Send** to +transmit the message. Incoming MO messages are accepted on port 10800 and +displayed in the log box in both hex and ASCII forms; payloads are decrypted +only when the **Decrypt** option is enabled. From df11519c20a649b5bcf3fbcca3ca87cbcf8c51e2 Mon Sep 17 00:00:00 2001 From: John Hentges Date: Thu, 26 Jun 2025 06:57:43 -0700 Subject: [PATCH 3/3] Combine library classes into single MTMessage file --- csharp/DirectIpNet/Confirmation.cs | 38 ------- csharp/DirectIpNet/MOMessage.cs | 59 ----------- csharp/DirectIpNet/MTHeader.cs | 36 ------- csharp/DirectIpNet/MTMessage.cs | 156 ++++++++++++++++++++++++++++ csharp/DirectIpNet/MTPayload.cs | 23 ---- csharp/DirectIpNet/MessageStatus.cs | 22 ---- 6 files changed, 156 insertions(+), 178 deletions(-) delete mode 100644 csharp/DirectIpNet/Confirmation.cs delete mode 100644 csharp/DirectIpNet/MOMessage.cs delete mode 100644 csharp/DirectIpNet/MTHeader.cs delete mode 100644 csharp/DirectIpNet/MTPayload.cs delete mode 100644 csharp/DirectIpNet/MessageStatus.cs diff --git a/csharp/DirectIpNet/Confirmation.cs b/csharp/DirectIpNet/Confirmation.cs deleted file mode 100644 index c4f719e..0000000 --- a/csharp/DirectIpNet/Confirmation.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Buffers.Binary; - -namespace SBDDirectIP; - -public class Confirmation -{ - public uint ClientMsgId { get; } - public string Imei { get; } - public uint IdReference { get; } - public MessageStatus Status { get; } - - public Confirmation(uint clientMsgId, string imei, uint idReference, MessageStatus status) - { - ClientMsgId = clientMsgId; - Imei = imei; - IdReference = idReference; - Status = status; - } - - public static Confirmation FromArray(ReadOnlySpan data) - { - int offset = 0; - if (data[offset++] != 0x44) throw new ArgumentException("Invalid confirmation element"); - ushort len = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset,2)); - offset += 2; - if (len != 25) throw new ArgumentException("Unexpected confirmation length"); - uint clientId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset,4)); - offset += 4; - string imei = System.Text.Encoding.ASCII.GetString(data.Slice(offset,15)); - offset += 15; - uint refId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset,4)); - offset += 4; - short statusCode = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset,2)); - var status = (MessageStatus)statusCode; - return new Confirmation(clientId, imei, refId, status); - } -} diff --git a/csharp/DirectIpNet/MOMessage.cs b/csharp/DirectIpNet/MOMessage.cs deleted file mode 100644 index 4e48714..0000000 --- a/csharp/DirectIpNet/MOMessage.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Buffers.Binary; - -namespace SBDDirectIP; - -/// -/// Represents a Mobile Originated (MO) message received from a remote device. -/// Only the header fields required to locate the payload are parsed. -/// -public class MOMessage -{ - public string Imei { get; } - public byte[] Payload { get; } - - private MOMessage(string imei, byte[] payload) - { - Imei = imei; - Payload = payload; - } - - /// - /// Parse a raw DirectIP MO message. - /// - public static MOMessage FromArray(ReadOnlySpan data) - { - if (data.Length < 3) - throw new ArgumentException("Invalid data length", nameof(data)); - if (data[0] != 1) - throw new ArgumentException("Unsupported protocol version", nameof(data)); - - ushort length = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(1, 2)); - if (data.Length - 3 < length) - throw new ArgumentException("Incomplete message", nameof(data)); - - int offset = 3; - if (data[offset++] != 0x01) - throw new ArgumentException("Missing MO-Header", nameof(data)); - ushort hlen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); - offset += 2; - if (hlen != 28) - throw new ArgumentException("Unexpected header length", nameof(data)); - - offset += 4; // CDR reference - string imei = System.Text.Encoding.ASCII.GetString(data.Slice(offset, 15)); - offset += 15; - offset += 1; // session status - offset += 2; // MOMSN - offset += 2; // MTMSN - offset += 4; // time of session - - if (data[offset++] != 0x02) - throw new ArgumentException("Missing MO-Payload", nameof(data)); - ushort plen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); - offset += 2; - byte[] payload = data.Slice(offset, plen).ToArray(); - - return new MOMessage(imei, payload); - } -} diff --git a/csharp/DirectIpNet/MTHeader.cs b/csharp/DirectIpNet/MTHeader.cs deleted file mode 100644 index 289fe0e..0000000 --- a/csharp/DirectIpNet/MTHeader.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.IO; -using System.Text; - -namespace SBDDirectIP; - -internal class MTHeader -{ - public uint ClientMsgId { get; } - public string Imei { get; } - public ushort DispositionFlags { get; } - - public MTHeader(uint clientMsgId, string imei, ushort dispositionFlags) - { - if (imei.Length != 15) throw new ArgumentException("IMEI must be 15 digits", nameof(imei)); - ClientMsgId = clientMsgId; - Imei = imei; - DispositionFlags = dispositionFlags; - } - - public byte[] ToArray() - { - var buffer = new byte[24]; - int offset = 0; - buffer[offset++] = 0x41; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), 21); - offset += 2; - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(offset), ClientMsgId); - offset += 4; - Encoding.ASCII.GetBytes(Imei).CopyTo(buffer.AsSpan(offset)); - offset += 15; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), DispositionFlags); - return buffer; - } -} diff --git a/csharp/DirectIpNet/MTMessage.cs b/csharp/DirectIpNet/MTMessage.cs index 70a1dc2..95e9b02 100644 --- a/csharp/DirectIpNet/MTMessage.cs +++ b/csharp/DirectIpNet/MTMessage.cs @@ -1,4 +1,6 @@ +using System; using System.Buffers.Binary; +using System.Text; namespace SBDDirectIP; @@ -83,3 +85,157 @@ public MTMessage Build() } } } + +/// +/// Status of a confirmation message returned by the gateway. +/// Mirrors the enum from the original Rust implementation. +/// +public enum MessageStatus +{ + SuccessfulQueueOrder = 0, + InvalidImei = -1, + UnknownImei = -2, + PayloadOversized = -3, + PayloadMissing = -4, + MtQueueFull = -5, + MtResourcesUnavailable = -6, + ProtocolViolation = -7, + RingAlertsDisabled = -8, + SsdNotAttached = -9, + SourceAddressRejected = -10, + MtmsnOutOfRange = -11, + CertificateRejected = -12 +} + +internal class MTHeader +{ + public uint ClientMsgId { get; } + public string Imei { get; } + public ushort DispositionFlags { get; } + + public MTHeader(uint clientMsgId, string imei, ushort dispositionFlags) + { + if (imei.Length != 15) throw new ArgumentException("IMEI must be 15 digits", nameof(imei)); + ClientMsgId = clientMsgId; + Imei = imei; + DispositionFlags = dispositionFlags; + } + + public byte[] ToArray() + { + var buffer = new byte[24]; + int offset = 0; + buffer[offset++] = 0x41; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), 21); + offset += 2; + BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(offset), ClientMsgId); + offset += 4; + Encoding.ASCII.GetBytes(Imei).CopyTo(buffer.AsSpan(offset)); + offset += 15; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), DispositionFlags); + return buffer; + } +} + +internal class MTPayload +{ + public byte[] Data { get; } + public MTPayload(byte[] data) + { + Data = data ?? Array.Empty(); + } + + public byte[] ToArray() + { + var buffer = new byte[3 + Data.Length]; + int offset = 0; + buffer[offset++] = 0x42; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), (ushort)Data.Length); + offset += 2; + Data.CopyTo(buffer.AsSpan(offset)); + return buffer; + } +} + +public class Confirmation +{ + public uint ClientMsgId { get; } + public string Imei { get; } + public uint IdReference { get; } + public MessageStatus Status { get; } + + public Confirmation(uint clientMsgId, string imei, uint idReference, MessageStatus status) + { + ClientMsgId = clientMsgId; + Imei = imei; + IdReference = idReference; + Status = status; + } + + public static Confirmation FromArray(ReadOnlySpan data) + { + int offset = 0; + if (data[offset++] != 0x44) throw new ArgumentException("Invalid confirmation element"); + ushort len = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); + offset += 2; + if (len != 25) throw new ArgumentException("Unexpected confirmation length"); + uint clientId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)); + offset += 4; + string imei = Encoding.ASCII.GetString(data.Slice(offset, 15)); + offset += 15; + uint refId = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)); + offset += 4; + short statusCode = BinaryPrimitives.ReadInt16BigEndian(data.Slice(offset, 2)); + var status = (MessageStatus)statusCode; + return new Confirmation(clientId, imei, refId, status); + } +} + +public class MOMessage +{ + public string Imei { get; } + public byte[] Payload { get; } + + private MOMessage(string imei, byte[] payload) + { + Imei = imei; + Payload = payload; + } + + public static MOMessage FromArray(ReadOnlySpan data) + { + if (data.Length < 3) + throw new ArgumentException("Invalid data length", nameof(data)); + if (data[0] != 1) + throw new ArgumentException("Unsupported protocol version", nameof(data)); + + ushort length = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(1, 2)); + if (data.Length - 3 < length) + throw new ArgumentException("Incomplete message", nameof(data)); + + int offset = 3; + if (data[offset++] != 0x01) + throw new ArgumentException("Missing MO-Header", nameof(data)); + ushort hlen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); + offset += 2; + if (hlen != 28) + throw new ArgumentException("Unexpected header length", nameof(data)); + + offset += 4; // CDR reference + string imei = Encoding.ASCII.GetString(data.Slice(offset, 15)); + offset += 15; + offset += 1; // session status + offset += 2; // MOMSN + offset += 2; // MTMSN + offset += 4; // time of session + + if (data[offset++] != 0x02) + throw new ArgumentException("Missing MO-Payload", nameof(data)); + ushort plen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(offset, 2)); + offset += 2; + byte[] payload = data.Slice(offset, plen).ToArray(); + + return new MOMessage(imei, payload); + } +} + diff --git a/csharp/DirectIpNet/MTPayload.cs b/csharp/DirectIpNet/MTPayload.cs deleted file mode 100644 index 3ced372..0000000 --- a/csharp/DirectIpNet/MTPayload.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Buffers.Binary; - -namespace SBDDirectIP; - -internal class MTPayload -{ - public byte[] Data { get; } - public MTPayload(byte[] data) - { - Data = data ?? System.Array.Empty(); - } - - public byte[] ToArray() - { - var buffer = new byte[3 + Data.Length]; - int offset = 0; - buffer[offset++] = 0x42; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), (ushort)Data.Length); - offset += 2; - Data.CopyTo(buffer.AsSpan(offset)); - return buffer; - } -} diff --git a/csharp/DirectIpNet/MessageStatus.cs b/csharp/DirectIpNet/MessageStatus.cs deleted file mode 100644 index d161773..0000000 --- a/csharp/DirectIpNet/MessageStatus.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace SBDDirectIP; - -/// -/// Status of a confirmation message returned by the gateway. -/// This mirrors the enum defined in the original Rust implementation. -/// -public enum MessageStatus -{ - SuccessfulQueueOrder = 0, - InvalidImei = -1, - UnknownImei = -2, - PayloadOversized = -3, - PayloadMissing = -4, - MtQueueFull = -5, - MtResourcesUnavailable = -6, - ProtocolViolation = -7, - RingAlertsDisabled = -8, - SsdNotAttached = -9, - SourceAddressRejected = -10, - MtmsnOutOfRange = -11, - CertificateRejected = -12 -}