diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d86405 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Snipe-IT bPAC Daemon + +## Usage + +Install the [bPAC Client Component](https://support.brother.com/g/s/es/dev/en/bpac/download/index.html#client) on the server that runs this daemon. + +(TODO: Write detailed guide) + +## Start with Windows + +Create a shortcut in `shell:startup` folder. + +## Start minimized + +Just pass `-m` to the argument list. + +# labels.blade.php + +Replace `resources/views/hardware/labels.blade.php` with content below: + +Specifically, if you're using docker, mount it to `/var/www/html/resources/views/hardware/labels.blade.php`. + +```php + 'ID: ' . $asset->id, + 'name' => empty($asset->name) ? '' : 'N: ' . $asset->name, + 'serial' => empty($asset->serial) ? '' : 'S: ' . $asset->serial, + 'model' => empty($asset->model->name) ? '' : 'M: ' . $asset->model->name, + 'company' => $asset->company === null ? null : 'C: ' . $asset->company->name, + 'asset_tag' => $asset->asset_tag, + ]; +} +?> + + + + + + + Labels Print + + + +
+ Labels Data: +
+ +
+ +
+ + + + +``` diff --git a/SnipeIT-bPAC.sln b/SnipeIT-bPAC.sln new file mode 100644 index 0000000..435a2b4 --- /dev/null +++ b/SnipeIT-bPAC.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32804.467 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SnipeIT-bPAC", "SnipeIT-bPAC\SnipeIT-bPAC.csproj", "{4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}.Debug|Any CPU.ActiveCfg = Debug|x64 + {4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}.Debug|x86.ActiveCfg = Debug|x86 + {4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}.Debug|x86.Build.0 = Debug|x86 + {4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}.Release|Any CPU.ActiveCfg = Release|x64 + {4FC36674-2D6F-4DDD-AB2E-5DA6A1B13B26}.Release|x86.ActiveCfg = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {84AD9696-89C8-4A3B-AFBD-30C6889879DB} + EndGlobalSection +EndGlobal diff --git a/SnipeIT-bPAC/App.config b/SnipeIT-bPAC/App.config new file mode 100644 index 0000000..c95444a --- /dev/null +++ b/SnipeIT-bPAC/App.config @@ -0,0 +1,21 @@ + + + + +
+ + + + + + + + + + + + 8000 + + + + \ No newline at end of file diff --git a/SnipeIT-bPAC/HttpListenerContextExtension.cs b/SnipeIT-bPAC/HttpListenerContextExtension.cs new file mode 100644 index 0000000..f77714c --- /dev/null +++ b/SnipeIT-bPAC/HttpListenerContextExtension.cs @@ -0,0 +1,25 @@ +using System.Net; +using System.Text; +using System.Text.Json; + +namespace SnipeIT_bPAC +{ + public static class HttpListenerContextExtension + { + public static void SendJson(this HttpListenerContext ctx, object? data) + { + var resp = ctx.Response; + resp.StatusCode = 200; + resp.ContentType = "application/json"; + + resp.AppendHeader("Access-Control-Allow-Origin", "*"); + resp.AppendHeader("Access-Control-Allow-Headers", "authorization"); + + if (data != null) + { + resp.OutputStream.Write(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); + } + resp.OutputStream.Close(); + } + } +} diff --git a/SnipeIT-bPAC/Interop.bpac.dll b/SnipeIT-bPAC/Interop.bpac.dll new file mode 100644 index 0000000..bfe58df Binary files /dev/null and b/SnipeIT-bPAC/Interop.bpac.dll differ diff --git a/SnipeIT-bPAC/Program.cs b/SnipeIT-bPAC/Program.cs new file mode 100644 index 0000000..084a3f2 --- /dev/null +++ b/SnipeIT-bPAC/Program.cs @@ -0,0 +1,17 @@ +namespace SnipeIT_bPAC +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new ServerForm(args)); + } + } +} \ No newline at end of file diff --git a/SnipeIT-bPAC/Properties/Settings.Designer.cs b/SnipeIT-bPAC/Properties/Settings.Designer.cs new file mode 100644 index 0000000..afaff68 --- /dev/null +++ b/SnipeIT-bPAC/Properties/Settings.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace SnipeIT_bPAC.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.3.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string AccessKey { + get { + return ((string)(this["AccessKey"])); + } + set { + this["AccessKey"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string TemplateFile { + get { + return ((string)(this["TemplateFile"])); + } + set { + this["TemplateFile"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("8000")] + public ushort ServerPort { + get { + return ((ushort)(this["ServerPort"])); + } + set { + this["ServerPort"] = value; + } + } + } +} diff --git a/SnipeIT-bPAC/Properties/Settings.settings b/SnipeIT-bPAC/Properties/Settings.settings new file mode 100644 index 0000000..46dc61a --- /dev/null +++ b/SnipeIT-bPAC/Properties/Settings.settings @@ -0,0 +1,15 @@ + + + + + + + + + + + + 8000 + + + \ No newline at end of file diff --git a/SnipeIT-bPAC/ServerForm.Designer.cs b/SnipeIT-bPAC/ServerForm.Designer.cs new file mode 100644 index 0000000..566117c --- /dev/null +++ b/SnipeIT-bPAC/ServerForm.Designer.cs @@ -0,0 +1,228 @@ +namespace SnipeIT_bPAC +{ + partial class ServerForm + { + /// + /// 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.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.textBox_accesskey = new System.Windows.Forms.TextBox(); + this.textBox_template = new System.Windows.Forms.TextBox(); + this.checkBox1 = new System.Windows.Forms.CheckBox(); + this.button_selectFile = new System.Windows.Forms.Button(); + this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components); + this.textBox_log = new System.Windows.Forms.TextBox(); + this.label3 = new System.Windows.Forms.Label(); + this.button_start = new System.Windows.Forms.Button(); + this.button_stop = new System.Windows.Forms.Button(); + this.numericUpDown_port = new System.Windows.Forms.NumericUpDown(); + this.linkLabel1 = new System.Windows.Forms.LinkLabel(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown_port)).BeginInit(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 47); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(75, 17); + this.label1.TabIndex = 0; + this.label1.Text = "Access Key:"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 78); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(88, 17); + this.label2.TabIndex = 1; + this.label2.Text = "Template File:"; + // + // textBox_accesskey + // + this.textBox_accesskey.Location = new System.Drawing.Point(106, 44); + this.textBox_accesskey.Name = "textBox_accesskey"; + this.textBox_accesskey.PasswordChar = '*'; + this.textBox_accesskey.Size = new System.Drawing.Size(343, 23); + this.textBox_accesskey.TabIndex = 2; + this.textBox_accesskey.Leave += new System.EventHandler(this.SaveSettings); + // + // textBox_template + // + this.textBox_template.Location = new System.Drawing.Point(106, 75); + this.textBox_template.Name = "textBox_template"; + this.textBox_template.Size = new System.Drawing.Size(318, 23); + this.textBox_template.TabIndex = 3; + this.textBox_template.Leave += new System.EventHandler(this.SaveSettings); + // + // checkBox1 + // + this.checkBox1.AutoSize = true; + this.checkBox1.Location = new System.Drawing.Point(455, 49); + this.checkBox1.Name = "checkBox1"; + this.checkBox1.Size = new System.Drawing.Size(15, 14); + this.checkBox1.TabIndex = 4; + this.checkBox1.UseVisualStyleBackColor = true; + this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged); + // + // button_selectFile + // + this.button_selectFile.Location = new System.Drawing.Point(430, 75); + this.button_selectFile.Name = "button_selectFile"; + this.button_selectFile.Size = new System.Drawing.Size(40, 23); + this.button_selectFile.TabIndex = 5; + this.button_selectFile.Text = "..."; + this.button_selectFile.UseVisualStyleBackColor = true; + this.button_selectFile.Click += new System.EventHandler(this.button_selectFile_Click); + // + // notifyIcon1 + // + this.notifyIcon1.Text = "SnipeIT bPAC Daemon"; + this.notifyIcon1.MouseClick += new System.Windows.Forms.MouseEventHandler(this.notifyIcon1_MouseClick); + // + // textBox_log + // + this.textBox_log.Location = new System.Drawing.Point(12, 116); + this.textBox_log.Multiline = true; + this.textBox_log.Name = "textBox_log"; + this.textBox_log.ReadOnly = true; + this.textBox_log.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.textBox_log.Size = new System.Drawing.Size(458, 183); + this.textBox_log.TabIndex = 6; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(12, 15); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(76, 17); + this.label3.TabIndex = 7; + this.label3.Text = "Server Port:"; + // + // button_start + // + this.button_start.Location = new System.Drawing.Point(314, 12); + this.button_start.Name = "button_start"; + this.button_start.Size = new System.Drawing.Size(75, 23); + this.button_start.TabIndex = 9; + this.button_start.Text = "Start"; + this.button_start.UseVisualStyleBackColor = true; + this.button_start.Click += new System.EventHandler(this.button_start_Click); + // + // button_stop + // + this.button_stop.Enabled = false; + this.button_stop.Location = new System.Drawing.Point(395, 12); + this.button_stop.Name = "button_stop"; + this.button_stop.Size = new System.Drawing.Size(75, 23); + this.button_stop.TabIndex = 10; + this.button_stop.Text = "Stop"; + this.button_stop.UseVisualStyleBackColor = true; + this.button_stop.Click += new System.EventHandler(this.button_stop_Click); + // + // numericUpDown_port + // + this.numericUpDown_port.Location = new System.Drawing.Point(106, 12); + this.numericUpDown_port.Maximum = new decimal(new int[] { + 65535, + 0, + 0, + 0}); + this.numericUpDown_port.Minimum = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.numericUpDown_port.Name = "numericUpDown_port"; + this.numericUpDown_port.Size = new System.Drawing.Size(202, 23); + this.numericUpDown_port.TabIndex = 11; + this.numericUpDown_port.Value = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.numericUpDown_port.ValueChanged += new System.EventHandler(this.SaveSettings); + // + // linkLabel1 + // + this.linkLabel1.AutoSize = true; + this.linkLabel1.Location = new System.Drawing.Point(235, 302); + this.linkLabel1.Name = "linkLabel1"; + this.linkLabel1.Size = new System.Drawing.Size(235, 17); + this.linkLabel1.TabIndex = 12; + this.linkLabel1.TabStop = true; + this.linkLabel1.Text = "https://github.com/xWTF/SnipeIT-bPAC"; + this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked); + // + // ServerForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(482, 328); + this.Controls.Add(this.linkLabel1); + this.Controls.Add(this.numericUpDown_port); + this.Controls.Add(this.button_stop); + this.Controls.Add(this.button_start); + this.Controls.Add(this.label3); + this.Controls.Add(this.textBox_log); + this.Controls.Add(this.button_selectFile); + this.Controls.Add(this.checkBox1); + this.Controls.Add(this.textBox_template); + this.Controls.Add(this.textBox_accesskey); + this.Controls.Add(this.label2); + this.Controls.Add(this.label1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "ServerForm"; + this.Text = "Snipe-IT bPAC Daemon"; + this.Load += new System.EventHandler(this.ServerForm_Load); + this.SizeChanged += new System.EventHandler(this.ServerForm_SizeChanged); + this.VisibleChanged += new System.EventHandler(this.ServerForm_VisibleChanged); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDown_port)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private Label label1; + private Label label2; + private TextBox textBox_accesskey; + private TextBox textBox_template; + private CheckBox checkBox1; + private Button button_selectFile; + private NotifyIcon notifyIcon1; + private TextBox textBox_log; + private Label label3; + private Button button_start; + private Button button_stop; + private NumericUpDown numericUpDown_port; + private LinkLabel linkLabel1; + } +} \ No newline at end of file diff --git a/SnipeIT-bPAC/ServerForm.cs b/SnipeIT-bPAC/ServerForm.cs new file mode 100644 index 0000000..6258d64 --- /dev/null +++ b/SnipeIT-bPAC/ServerForm.cs @@ -0,0 +1,227 @@ +using bpac; +using System.Diagnostics; +using System.Net; +using System.Text; +using System.Text.Json; + +namespace SnipeIT_bPAC +{ + public partial class ServerForm : Form + { + public HttpListener? Listener; + public DocumentClass Document = new DocumentClass(); + + private bool StartMinimized = false; + + public ServerForm(string[] args) + { + if (args.Contains("-m")) + { + StartMinimized = true; + } + + InitializeComponent(); + + notifyIcon1.Icon = Icon; + textBox_log.BackColor = Color.White; + + numericUpDown_port.DataBindings.Add("Value", Properties.Settings.Default, "ServerPort"); + textBox_accesskey.DataBindings.Add("Text", Properties.Settings.Default, "AccessKey"); + textBox_template.DataBindings.Add("Text", Properties.Settings.Default, "TemplateFile"); + } + + protected override void SetVisibleCore(bool value) + { + if (StartMinimized) + { + StartMinimized = false; + base.SetVisibleCore(false); + return; + } + base.SetVisibleCore(value); + } + + private void Log(string data) => Invoke(() => + { + textBox_log.AppendText("[" + DateTime.Now.ToString("G") + "] " + data + Environment.NewLine); + }); + + private void ServerForm_Load(object sender, EventArgs e) + { + button_start.PerformClick(); + } + + private void ServerForm_VisibleChanged(object sender, EventArgs e) => notifyIcon1.Visible = !Visible; + + private void ServerForm_SizeChanged(object sender, EventArgs e) + { + if (WindowState == FormWindowState.Minimized) + { + Hide(); + } + } + + private void notifyIcon1_MouseClick(object sender, MouseEventArgs e) + { + Show(); + WindowState = FormWindowState.Normal; + BringToFront(); + } + + private void checkBox1_CheckedChanged(object sender, EventArgs e) + { + textBox_accesskey.PasswordChar = checkBox1.Checked ? '\0' : '*'; + } + + private void button_selectFile_Click(object sender, EventArgs e) + { + var dialog = new OpenFileDialog() + { + Title = "Select Template File", + Filter = "Template Files|*.lbx|All Files|*.*", + CheckFileExists = true, + }; + if (dialog.ShowDialog() == DialogResult.OK) + { + Properties.Settings.Default.TemplateFile = textBox_template.Text = dialog.FileName; + SaveSettings(); + } + } + + private void button_start_Click(object sender, EventArgs e) + { + try + { + var port = (ushort)numericUpDown_port.Value; + + Listener = new HttpListener(); + Listener.Prefixes.Add($"http://127.0.0.1:{port}/"); + + Listener.Start(); + BeginGetContext(); + + Log("Server started on port: " + port); + } + catch (Exception ex) + { + Log("Unable to start server: " + ex); + return; + } + numericUpDown_port.Enabled = button_start.Enabled = false; + button_stop.Enabled = true; + } + + private void button_stop_Click(object sender, EventArgs e) + { + Listener?.Stop(); + Listener = null; + Log("Server stopped"); + + numericUpDown_port.Enabled = button_start.Enabled = true; + button_stop.Enabled = false; + } + + private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) => Process.Start(new ProcessStartInfo + { + FileName = "https://github.com/xWTF/SnipeIT-bPAC", + UseShellExecute = true, + }); + + private void SaveSettings(object? sender = null, EventArgs? e = null) => Properties.Settings.Default.Save(); + + private void HandleRequest(HttpListenerContext ctx) + { + var req = ctx.Request; + if (req.Url?.AbsolutePath != "/print") + { + ctx.SendJson(new { code = 404 }); + return; + } + + if (req.HttpMethod == "OPTIONS") + { + ctx.SendJson(null); + return; + } + else if (req.HttpMethod != "POST") + { + ctx.SendJson(new { code = 403, msg = "bad method" }); + return; + } + + var auth = req.Headers["Authorization"]?.Split(" "); + if (auth == null || auth.Length != 2 || auth[0].ToLower() != "bearer" || auth[1] != Invoke(() => textBox_accesskey.Text)) + { + Log("Access denied: " + req.RemoteEndPoint.ToString()); + ctx.SendJson(new { code = 403, msg = "access denied" }); + return; + } + + using var reader = new StreamReader(req.InputStream, Encoding.UTF8); + var requests = JsonSerializer.Deserialize[]>(reader.ReadToEnd()); + if (requests == null) + { + ctx.SendJson(new { code = 400, msg = "Unable to deserialize the JSON" }); + return; + } + + try + { + if (!Document.Open(Invoke(() => textBox_template.Text))) + { + Log("Print error: Unable to open template"); + ctx.SendJson(new { code = 500, msg = "Unable to open template" }); + return; + } + + Document.StartPrint(requests.Length + " Labels", PrintOptionConstants.bpoDefault); + foreach (var r in requests) + { + foreach (var kv in r) + { + var obj = Document.GetObject(kv.Key); + if (obj != null) + { + obj.Text = kv.Value.ToString(); + } + } + Document.PrintOut(1, PrintOptionConstants.bpoDefault); + } + Document.EndPrint(); + Document.Close(); + + Log("Printed " + requests.Length + " labels from " + req.RemoteEndPoint.ToString()); + } + catch (Exception ex) + { + Log("Print error: " + ex); + ctx.SendJson(new { code = 500, msg = "Unknown error occured" }); + return; + } + ctx.SendJson(new { code = 200 }); + } + + private void BeginGetContext() => Listener?.BeginGetContext(new AsyncCallback(ListenerCallback), Listener); + + private void ListenerCallback(IAsyncResult result) + { + try + { + if (Listener == null) + { + return; + } + HandleRequest(Listener.EndGetContext(result)); + } + catch (ObjectDisposedException) + { + return; + } + catch (Exception ex) + { + Log("Unknown error:" + ex); + } + BeginGetContext(); + } + } +} diff --git a/SnipeIT-bPAC/ServerForm.resx b/SnipeIT-bPAC/ServerForm.resx new file mode 100644 index 0000000..50794d4 --- /dev/null +++ b/SnipeIT-bPAC/ServerForm.resx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/SnipeIT-bPAC/SnipeIT-bPAC.csproj b/SnipeIT-bPAC/SnipeIT-bPAC.csproj new file mode 100644 index 0000000..e16884b --- /dev/null +++ b/SnipeIT-bPAC/SnipeIT-bPAC.csproj @@ -0,0 +1,43 @@ + + + + WinExe + net6.0-windows + SnipeIT_bPAC + enable + true + enable + x64;x86 + xWTF + Convenient bPAC print daemon for Snipe-IT asset management software + Copyright (c) 2023 xWTF + + + + + tlbimp + 3 + 1 + 90359d74-b7d9-467f-b938-3883f4cab582 + 0 + false + False + + + + + + True + True + Settings.settings + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + \ No newline at end of file