Browse Source

commit: tftp OK; todo: timeout and reduct output for speed up

HOME 5 years ago
parent
commit
eca4c228af

+ 6 - 0
BootServer.sln

@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DhcpServer", "DhcpServer\Dh
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TftpServer", "TftpServer\TftpServer.csproj", "{21F3E98E-0210-4118-84F1-8DA4D90BCB0B}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{369BBF56-A30E-478F-AE99-8427BC7415E8}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
 		{21F3E98E-0210-4118-84F1-8DA4D90BCB0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{21F3E98E-0210-4118-84F1-8DA4D90BCB0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{21F3E98E-0210-4118-84F1-8DA4D90BCB0B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{369BBF56-A30E-478F-AE99-8427BC7415E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{369BBF56-A30E-478F-AE99-8427BC7415E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{369BBF56-A30E-478F-AE99-8427BC7415E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{369BBF56-A30E-478F-AE99-8427BC7415E8}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 3 - 1
BootServer.sln.DotSettings

@@ -1,4 +1,6 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=blksize/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Bootp/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Dhcp/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/UserDictionary/Words/=Tftp/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Tftp/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=tsize/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 5 - 1
DhcpServer/App/DhcpPacket.cs

@@ -13,7 +13,11 @@ namespace DhcpServer
             if (entry.SubNetMask != null) SubNetMask = IPAddress.Parse(entry.SubNetMask);
             if (entry.GateWay != null) Router = IPAddress.Parse(entry.GateWay);
             if (entry.DnsServer != null) DnsServer = IPAddress.Parse(entry.DnsServer);
-            if (entry.TftpServer != null) TftpServer = entry.TftpServer;
+            if (entry.TftpServer != null)
+            {
+                TftpServer = entry.TftpServer;
+                NextServerIpAddress = IPAddress.Parse(entry.TftpServer);
+            }
             if (entry.BootFileName != null) BootFileName = entry.BootFileName;
         }
     }

+ 6 - 1
DhcpServer/DhcpServer.csproj

@@ -64,7 +64,6 @@
     <Compile Include="Protocol\DhcpPacket.cs" />
     <Compile Include="DhcpProgram.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="Utils\ExtensionMethods.cs" />
   </ItemGroup>
   <ItemGroup>
     <None Include="App.config" />
@@ -74,5 +73,11 @@
       <LastGenOutput>Settings.Designer.cs</LastGenOutput>
     </None>
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Utils\Utils.csproj">
+      <Project>{369BBF56-A30E-478F-AE99-8427BC7415E8}</Project>
+      <Name>Utils</Name>
+    </ProjectReference>
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 2 - 2
DhcpServer/Protocol/DhcpPacket.cs

@@ -1,9 +1,9 @@
-using DhcpServer.Utils;
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Text;
+using Utils;
 
 namespace DhcpServer
 {

+ 1 - 1
TftpServer/App.config

@@ -14,7 +14,7 @@
                 <value>0.0.0.0</value>
             </setting>
             <setting name="Root" serializeAs="String">
-                <value>Root</value>
+                <value>E:\PxeWorkingSpace\sample1</value>
             </setting>
         </TftpServer.Properties.Settings>
     </applicationSettings>

+ 33 - 0
TftpServer/App/BlockReader.cs

@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+
+namespace TftpServer
+{
+    internal class BlockReader : IDisposable
+    {
+        public DateTime LastActive { get; set; }
+
+        private readonly FileStream _fileStream;
+
+        public BlockReader(string path)
+        {
+            _fileStream = File.OpenRead(path);
+            LastActive = DateTime.Now;
+        }
+
+        /// <param name="blockNumber">Start from 1</param>
+        /// <param name="buffer">Length is block size</param>
+        /// <returns>Bytes read</returns>
+        public int ReadBlock(int blockNumber, byte[] buffer)
+        {
+            LastActive = DateTime.Now;
+            _fileStream.Position = (blockNumber - 1) * buffer.Length;
+            return _fileStream.Read(buffer, 0, buffer.Length);
+        }
+
+        public void Dispose()
+        {
+            _fileStream?.Dispose();
+        }
+    }
+}

+ 35 - 0
TftpServer/App/TftpTransferSession.cs

@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+
+namespace TftpServer
+{
+    internal class TftpTransferSession
+    {
+        public DateTime LastActive { get; set; }
+
+        public string FilePath { get; }
+
+        public long FileLength { get; }
+
+        public byte[] BlockBuffer { get; }
+
+        public int BlockCount { get; }
+
+        public TftpTransferSession(string filePath, int blockSize)
+        {
+            FilePath = filePath;
+            FileLength = new FileInfo(filePath).Length;
+            BlockBuffer = new byte[blockSize];
+
+            BlockCount = (int)Math.Ceiling(FileLength / (double)blockSize);
+
+            LastActive = DateTime.Now;
+        }
+
+        public int Read(int blockNumber, BlockReader reader)
+        {
+            LastActive = DateTime.Now;
+            return reader.ReadBlock(blockNumber, BlockBuffer);
+        }
+    }
+}

+ 15 - 0
TftpServer/Protocol/TftpErrorNumber.cs

@@ -0,0 +1,15 @@
+namespace TftpServer
+{
+    public enum TftpErrorNumber : short
+    {
+        SeeMessage = 0,
+        FileNoFound = 1,
+        AccessViolation = 2,
+        DiskFullOrAllocationExceeded = 3,
+        IllegalOperation = 4,
+        UnknownTransferId = 5,
+        FileAlreadyExists = 6,
+        NoSuchUser = 7,
+        TerminateTransferDueToOptionNegotiation = 8
+    }
+}

+ 1 - 0
TftpServer/Protocol/TftpOpCode.cs

@@ -7,5 +7,6 @@
         Data = 3,
         Ack = 4,
         Error = 5,
+        OptionAck = 6,
     }
 }

+ 103 - 17
TftpServer/Protocol/TftpPacket.cs

@@ -1,48 +1,134 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Text;
 using TftpServer.Utils;
+using Utils;
 
 namespace TftpServer
 {
     public class TftpPacket
     {
-        public TftpOpCode OpCode { get; set; }
+        public TftpOpCode OpCode { get; }
 
-        public string FileName { get; set; }
-        public string Mode { get; set; }
+        public string FileName { get; }
+        public string Mode { get; }
 
-        public short BlockNumber { get; set; }
-        public byte[] Data { get; set; }
+        public short BlockNumber { get; }
+        public byte[] DataBuffer { get; }
+        public int DataLength { get; }
 
-        public short ErrorNumber { get; set; }
-        public string ErrorMessage { get; set; }
+        public TftpErrorNumber ErrorNumber { get; }
+        public string ErrorMessage { get; }
 
-        public Dictionary<string, byte> Options { get; set; }
+        public int BlockSize
+        {
+            get => Options.MapValue("blksize", int.Parse, () => 512);
+            set => Options["blksize"] = value.ToString();
+        }
+
+        public int? TransferSize
+        {
+            get => Options.MapValue("tsize", s => int.Parse(s), () => (int?)null);
+            set => Options["tsize"] = value.ToString();
+        }
+
+        public Dictionary<string, string> Options { get; set; } = new Dictionary<string, string>();
 
         public TftpPacket(byte[] buffer, int length)
         {
-            using var stream = new MemoryStream(buffer);
+            using var stream = new MemoryStream(buffer, 0, length, true, true);
             using var reader = new BinaryReader(stream);
 
             OpCode = (TftpOpCode)IPAddress.NetworkToHostOrder(reader.ReadInt16());
 
             switch (OpCode)
             {
-                default: throw new InvalidDataException($"Unknown OpCode: {OpCode} (0x{OpCode:X4})");
+                default: throw new NotImplementedException($"OpCode: {OpCode} (0x{(short)OpCode:X4})");
+
                 case TftpOpCode.Read:
-                    var len = buffer.ScanNullTerminated(stream.Position);
-                    FileName = Encoding.ASCII.GetString(buffer, (int)stream.Position, len);
-                    stream.Position += len;
-                    len = buffer.ScanNullTerminated(stream.Position);
-                    Mode = Encoding.ASCII.GetString(buffer, (int)stream.Position, len);
-                    stream.Position += len;
+                    FileName = stream.ReadNullTerminatedString();
+                    Mode = stream.ReadNullTerminatedString();
                     break;
+
                 case TftpOpCode.Ack:
                     BlockNumber = IPAddress.NetworkToHostOrder(reader.ReadInt16());
                     break;
+
+                case TftpOpCode.Error:
+                    ErrorNumber = (TftpErrorNumber)IPAddress.NetworkToHostOrder(reader.ReadInt16());
+                    ErrorMessage = stream.ReadNullTerminatedString();
+                    break;
+            }
+
+            while (stream.Position < length)
+            {
+                var key = stream.ReadNullTerminatedString();
+                if (stream.Position >= length) continue;
+                var val = stream.ReadNullTerminatedString();
+                Options[key] = val;
+            }
+        }
+
+        public TftpPacket(string errorMessage, TftpErrorNumber errorNumber = TftpErrorNumber.SeeMessage) : this(TftpOpCode.Error)
+        {
+            ErrorMessage = errorMessage;
+            ErrorNumber = errorNumber;
+        }
+
+        public TftpPacket(short blockNumber, byte[] dataBuffer, int dataLength) : this(TftpOpCode.Data)
+        {
+            BlockNumber = blockNumber;
+            DataBuffer = dataBuffer;
+            DataLength = dataLength;
+        }
+
+        public TftpPacket(TftpOpCode opCode)
+        {
+            OpCode = opCode;
+        }
+
+        public int WriteToBuffer(byte[] buffer)
+        {
+            using var stream = new MemoryStream(buffer);
+            using var writer = new BinaryWriter(stream);
+
+            writer.Write(IPAddress.HostToNetworkOrder((short)OpCode));
+
+            switch (OpCode)
+            {
+                default: throw new NotImplementedException($"OpCode: {OpCode} (0x{(short)OpCode:X4})");
+
+                case TftpOpCode.OptionAck:
+                    break;
+
+                case TftpOpCode.Data:
+                    writer.Write(IPAddress.HostToNetworkOrder(BlockNumber));
+                    if (0 == DataLength) writer.Write(DataBuffer);
+                    else writer.Write(DataBuffer, 0, DataLength);
+                    break;
+
+                case TftpOpCode.Error:
+
+                    writer.Write(IPAddress.HostToNetworkOrder((short)ErrorNumber));
+
+                    if (null != ErrorMessage)
+                    {
+                        var len = Encoding.ASCII.GetBytes(ErrorMessage, 0, ErrorMessage.Length, buffer, (int)stream.Position);
+                        stream.Position += len;
+                    }
+                    writer.Write((byte)0);
+                    break;
             }
+
+            foreach (var option in Options)
+            {
+                stream.WriteNullTerminatedString(option.Key);
+                stream.WriteNullTerminatedString(option.Value);
+            }
+
+            return (int)stream.Position;
         }
     }
 }

+ 140 - 30
TftpServer/TftpProgram.cs

@@ -1,9 +1,9 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Net.Sockets;
 using System.Threading;
-using TftpServer.Utils;
 
 namespace TftpServer
 {
@@ -11,24 +11,32 @@ namespace TftpServer
     {
         private static bool _isRunning;
         private static string Root;
+        private static int TimeOutSecond = 10;
+
         private static void Main(string[] args)
         {
             Console.WriteLine("Starting...");
 
             Root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Properties.Settings.Default.Root);
+            if (false == Directory.Exists(Root))
+            {
+                Console.WriteLine("Error: Root path no exist");
+            }
+            else
+            {
+                var tWorker = new Thread(Working);
+                _isRunning = true;
+                tWorker.Start();
 
-            var tWorker = new Thread(Working);
-            _isRunning = true;
-            tWorker.Start();
-
-            Console.WriteLine("Press ENTER to Stop.");
-            Console.ReadLine();
+                Console.WriteLine("Press ENTER to Stop.");
+                Console.ReadLine();
 
-            Console.Write("Shutting down...");
-            _isRunning = false;
-            tWorker.Join();
+                Console.Write("Shutting down...");
+                _isRunning = false;
+                tWorker.Join();
 
-            Console.Write("Stopped.");
+                Console.Write("Stopped.");
+            }
 
             Console.WriteLine();
             Console.Write("Press ENTER to Exit.");
@@ -46,48 +54,150 @@ namespace TftpServer
             };
             socket.Bind(listenEndPoint);
 
+            var dicSessions = new Dictionary<EndPoint, TftpTransferSession>();
+            var dicBlockReaders = new Dictionary<string, BlockReader>();
+
+            BlockReader GetBlockReader(string path)
+            {
+                if (dicBlockReaders.TryGetValue(path, out var br))
+                {
+                    return br;
+                }
+
+                br = new BlockReader(path);
+                dicBlockReaders[path] = br;
+                return br;
+            }
+
             var upTime = DateTime.Now;
 
             Console.WriteLine($"TFTP Server started, listing on: {listenEndPoint}");
 
             var buffer = new byte[65536];
             EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 68);
-            var polling = DateTime.Now;
+            var pollingTime = DateTime.Now;
             while (_isRunning)
             {
                 Console.CursorLeft = 0;
+
+                //TODO: Timeout
+
                 if (false == socket.Poll(500 * 1000, SelectMode.SelectRead))
                 {
-                    var timeSpan = DateTime.Now - polling;
+                    var polling = DateTime.Now - pollingTime;
                     var up = DateTime.Now - upTime;
                     Console.Write($"Polling..." +
-                                  $" {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}" +
+                                  $" {polling.Days:00}D {polling.Hours:00}H {polling.Minutes:00}M {polling.Seconds:00}S {polling.Milliseconds:000}" +
                                   $" / UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}");
                 }
                 else
                 {
-                    polling = DateTime.Now;
                     Console.WriteLine();
 
-                    var bytes = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint);
-                    Console.Write($"Receive {bytes} byte From {remoteEndPoint}");
-
-                    try
+                    do
                     {
-                        var packet = new TftpPacket(buffer, bytes);
-                        switch (packet.OpCode)
-                        {
-                            default:
+                        var bytes = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint);
+                        Console.Write($"Receive {bytes} byte From {remoteEndPoint}");
 
-                                break;
+                        try
+                        {
+                            var packet = new TftpPacket(buffer, bytes);
+                            Console.Write($" OpCode:{packet.OpCode}");
+
+                            TftpTransferSession session;
+                            switch (packet.OpCode)
+                            {
+                                default: throw new NotImplementedException();
+
+                                case TftpOpCode.Error:
+                                    Console.Write($" {packet.ErrorNumber} 0x{(short)packet.ErrorNumber:X4} {packet.ErrorMessage}");
+                                    if (dicSessions.Remove(remoteEndPoint))
+                                    {
+                                        Console.Write(" Session closed");
+                                    }
+                                    break;
+
+                                case TftpOpCode.Read:
+                                    Console.Write($" FileName:{packet.FileName} [{packet.Mode}] BlockSize:{packet.BlockSize}");
+                                    var path = Path.Combine(Root, packet.FileName.TrimStart('/'));
+                                    if (false == File.Exists(path))
+                                    {
+                                        Console.Write(" ** File Not Found");
+                                        var response = new TftpPacket("File Not Found", TftpErrorNumber.FileNoFound);
+                                        bytes = response.WriteToBuffer(buffer);
+
+                                        Console.WriteLine();
+                                        Console.Write($"Send {bytes} bytes to {remoteEndPoint} OpCode:{response.OpCode} Message:{response.ErrorMessage}");
+                                        socket.SendTo(buffer, 0, bytes, SocketFlags.None, remoteEndPoint);
+                                    }
+                                    else if (0 == packet.TransferSize)
+                                    {
+                                        var response = new TftpPacket(TftpOpCode.OptionAck) { TransferSize = (int?)new FileInfo(path).Length };
+                                        bytes = response.WriteToBuffer(buffer);
+
+                                        Console.WriteLine();
+                                        Console.Write($"Send {bytes} bytes to {remoteEndPoint} OpCode:{response.OpCode} TransferSize:{response.TransferSize}");
+                                        socket.SendTo(buffer, 0, bytes, SocketFlags.None, remoteEndPoint);
+                                    }
+                                    else
+                                    {
+                                        session = new TftpTransferSession(path, packet.BlockSize);
+                                        var response = new TftpPacket(1, session.BlockBuffer, session.Read(1, GetBlockReader(path)));
+                                        bytes = response.WriteToBuffer(buffer);
+                                        Console.WriteLine();
+                                        Console.Write($"Transfer session <{remoteEndPoint}> start. Path: <{session.FilePath}>");
+
+                                        Console.WriteLine();
+                                        Console.Write($"Send {bytes} bytes to {remoteEndPoint} OpCode:{response.OpCode} BlockNumber:{response.BlockNumber}/{session.BlockCount}");
+                                        socket.SendTo(buffer, 0, bytes, SocketFlags.None, remoteEndPoint);
+                                        dicSessions[remoteEndPoint] = session;
+                                    }
+                                    break;
+
+                                case TftpOpCode.Ack:
+                                    Console.Write($" BlockNumber:{packet.BlockNumber}");
+                                    if (dicSessions.TryGetValue(remoteEndPoint, out session))
+                                    {
+                                        if (packet.BlockNumber == session.BlockCount)
+                                        {
+                                            Console.WriteLine();
+                                            Console.Write($"Transfer complete session <{remoteEndPoint}> closed");
+                                            dicSessions.Remove(remoteEndPoint);
+                                        }
+                                        else
+                                        {
+                                            var nextBlockNumber = packet.BlockNumber;
+                                            ++nextBlockNumber;
+
+                                            var response = new TftpPacket(nextBlockNumber, session.BlockBuffer, session.Read(nextBlockNumber, GetBlockReader(session.FilePath)));
+                                            bytes = response.WriteToBuffer(buffer);
+
+                                            Console.WriteLine();
+                                            Console.Write($"Send {bytes} bytes to {remoteEndPoint} OpCode:{response.OpCode} BlockNumber:{response.BlockNumber}/{session.BlockCount}");
+                                            socket.SendTo(buffer, 0, bytes, SocketFlags.None, remoteEndPoint);
+                                        }
+                                    }
+                                    else
+                                    {
+                                        Console.Write(" ** Transfer session Not Found");
+                                        var response = new TftpPacket("Session not found", TftpErrorNumber.UnknownTransferId);
+                                        bytes = response.WriteToBuffer(buffer);
+
+                                        Console.WriteLine();
+                                        Console.Write($"Send {bytes} bytes to {remoteEndPoint} OpCode:{response.OpCode} Message:{response.ErrorMessage}");
+                                        socket.SendTo(buffer, 0, bytes, SocketFlags.None, remoteEndPoint);
+                                    }
+                                    break;
+                            }
+                        }
+                        catch (Exception e)
+                        {
+                            Console.Write(e);
                         }
+                        Console.WriteLine();
+                    } while (socket.Poll(50 * 1000, SelectMode.SelectRead));
 
-                    }
-                    catch (Exception e)
-                    {
-                        Console.WriteLine(e);
-                    }
-                    Console.WriteLine();
+                    pollingTime = DateTime.Now;
                 }
             }
         }

+ 11 - 1
TftpServer/TftpServer.csproj

@@ -45,6 +45,9 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="App\BlockReader.cs" />
+    <Compile Include="App\TftpTransferSession.cs" />
+    <Compile Include="Protocol\TftpErrorNumber.cs" />
     <Compile Include="TftpProgram.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\Settings.Designer.cs">
@@ -57,6 +60,10 @@
     <Compile Include="Utils\ExtensionMethods.cs" />
   </ItemGroup>
   <ItemGroup>
+    <None Include="..\..\..\PxeWorkingSpace\sample1\ipxe-bin\undionly.kpxe">
+      <Link>Root\undionly.kpxe</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
     <None Include="App.config" />
     <None Include="Properties\Settings.settings">
       <Generator>SettingsSingleFileGenerator</Generator>
@@ -64,7 +71,10 @@
     </None>
   </ItemGroup>
   <ItemGroup>
-    <Folder Include="App\" />
+    <ProjectReference Include="..\Utils\Utils.csproj">
+      <Project>{369BBF56-A30E-478F-AE99-8427BC7415E8}</Project>
+      <Name>Utils</Name>
+    </ProjectReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 20 - 1
TftpServer/Utils/ExtensionMethods.cs

@@ -1,7 +1,26 @@
-namespace TftpServer.Utils
+using System.IO;
+using System.Text;
+
+namespace TftpServer.Utils
 {
     internal static class ExtensionMethods
     {
+        public static void WriteNullTerminatedString(this MemoryStream stream, string str)
+        {
+            var buffer = Encoding.ASCII.GetBytes(str);
+            stream.Write(buffer, 0, buffer.Length);
+            stream.WriteByte(0);
+        }
+
+        public static string ReadNullTerminatedString(this MemoryStream stream)
+        {
+            var buffer = stream.GetBuffer();
+            var len = ScanNullTerminated(buffer, stream.Position);
+            var ret = Encoding.ASCII.GetString(buffer, (int)stream.Position, len);
+            stream.Position += len + 1;
+            return ret;
+        }
+
         public static int ScanNullTerminated(this byte[] bytes, long offset)
         {
             return ScanTo(bytes, offset, 0);

+ 2 - 2
DhcpServer/Utils/ExtensionMethods.cs

@@ -1,9 +1,9 @@
 using System;
 using System.Collections.Generic;
 
-namespace DhcpServer.Utils
+namespace Utils
 {
-    internal static class ExtensionMethods
+    public static class ExtensionMethods
     {
         public static TR MapValue<TK, TV, TR>(this Dictionary<TK, TV> dic, TK key, Func<TV, TR> found)
         {

+ 36 - 0
Utils/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("Utils")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Utils")]
+[assembly: AssemblyCopyright("Copyright ©  2020")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("369bbf56-a30e-478f-ae99-8427bc7415e8")]
+
+// 程序集的版本信息由下列四个值组成: 
+//
+//      主版本
+//      次版本
+//      生成号
+//      修订号
+//
+//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 48 - 0
Utils/Utils.csproj

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{369BBF56-A30E-478F-AE99-8427BC7415E8}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Utils</RootNamespace>
+    <AssemblyName>Utils</AssemblyName>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <Deterministic>true</Deterministic>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ExtensionMethods.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>