1 // 2 // Copyright (c) 2010-2024 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 8 using System; 9 using System.IO; 10 using System.Net; 11 using System.Linq; 12 using System.Collections.Generic; 13 using System.Text; 14 using System.Threading.Tasks; 15 16 using libtftp; 17 using PacketDotNet; 18 19 using Antmicro.Renode.Core; 20 using Antmicro.Renode.Logging; 21 using Antmicro.Renode.Exceptions; 22 using Antmicro.Renode.Utilities; 23 24 namespace Antmicro.Renode.Network 25 { 26 public static class TftpServerExtensions 27 { StartTFTP(this NetworkServer server, int port, string name = R)28 public static void StartTFTP(this NetworkServer server, int port, string name = "tftp") 29 { 30 var module = new TftpServerModule(port); 31 if(!server.RegisterModule(module, port, name)) 32 { 33 throw new RecoverableException($"Couldn't start TFTP server on port {port}. See log for details"); 34 } 35 } 36 } 37 38 public class TftpServerModule : IServerModule, IEmulationElement 39 { TftpServerModule(int port)40 public TftpServerModule(int port) 41 { 42 Port = port; 43 44 callbacks = new Dictionary<IPEndPoint, Action<IPEndPoint, UdpPacket>>(); 45 files = new Dictionary<string, string>(); 46 directories = new List<string>(); 47 48 server = TftpServer.Instance; 49 50 server.Log = HandleLog; 51 server.DataReady = HandleResponse; 52 53 server.GetStream += HandleStream; 54 server.FileReceived += HandleFileReceived; 55 56 this.Log(LogLevel.Info, "TFTP server started at port {0}", Port); 57 } 58 ServeFile(string path, string name = null)59 public void ServeFile(string path, string name = null) 60 { 61 name = name ?? Path.GetFileName(path); 62 if(files.ContainsKey(name)) 63 { 64 throw new RecoverableException($"File named \"{name}\" is already being served."); 65 } 66 files.Add(name, path); 67 } 68 ServeDirectory(string directory)69 public void ServeDirectory(string directory) 70 { 71 if(directories.Contains(directory)) 72 { 73 throw new RecoverableException($"Directory \"{directory}\" is already being served."); 74 } 75 directories.Add(directory); 76 } 77 HandleUdp(IPEndPoint source, UdpPacket packet, Action<IPEndPoint, UdpPacket> callback)78 public void HandleUdp(IPEndPoint source, UdpPacket packet, Action<IPEndPoint, UdpPacket> callback) 79 { 80 callbacks[source] = callback; 81 server.OnUdpData(source, packet.PayloadData); 82 } 83 84 public int Port { get; } 85 86 public bool LogReceivedFiles { get; set; } 87 HandleLog(object src, TftpLogEventArgs args)88 private void HandleLog(object src, TftpLogEventArgs args) 89 { 90 LogLevel logLevel; 91 92 switch(args.Severity) 93 { 94 case ETftpLogSeverity.Debug: 95 logLevel = LogLevel.Debug; 96 break; 97 98 case ETftpLogSeverity.Informational: 99 case ETftpLogSeverity.Notice: 100 logLevel = LogLevel.Info; 101 break; 102 103 case ETftpLogSeverity.Warning: 104 logLevel = LogLevel.Warning; 105 break; 106 107 case ETftpLogSeverity.Error: 108 case ETftpLogSeverity.Critical: 109 case ETftpLogSeverity.Alert: 110 case ETftpLogSeverity.Emergency: 111 logLevel = LogLevel.Error; 112 break; 113 114 default: 115 throw new ArgumentException($"Unhandled log severity: {args.Severity}"); 116 } 117 118 this.Log(logLevel, args.Message); 119 } 120 HandleResponse(IPEndPoint source, byte[] buffer, int count)121 private void HandleResponse(IPEndPoint source, byte[] buffer, int count) 122 { 123 var response = new UdpPacket((ushort)Port, (ushort)source.Port); 124 125 if(count == buffer.Length) 126 { 127 response.PayloadData = buffer; 128 } 129 else 130 { 131 var newBuffer = new byte[count]; 132 Array.Copy(buffer, 0, newBuffer, 0, count); 133 response.PayloadData = newBuffer; 134 } 135 136 callbacks[source](source, response); 137 } 138 HandleStream(object caller, TftpGetStreamEventArgs args)139 private async Task HandleStream(object caller, TftpGetStreamEventArgs args) 140 { 141 this.Log(LogLevel.Noisy, "Searching for file {0}", args.Filename); 142 143 var path = await Task.Run(() => FindFile(args.Filename)); 144 if(path == null) 145 { 146 this.Log(LogLevel.Warning, "Asked for {0} file, but it does not exist", args.Filename); 147 return; 148 } 149 150 try 151 { 152 using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 153 { 154 var buffer = new byte[stream.Length]; 155 await stream.ReadAsync(buffer, 0, (int)stream.Length); 156 args.Result = new MemoryStream(buffer); 157 } 158 } 159 catch (Exception e) 160 { 161 this.Log(LogLevel.Warning, "There was an error when reading {0} file: {1}", path, e.Message); 162 } 163 } 164 HandleFileReceived(object caller, TftpTransferCompleteEventArgs args)165 private void HandleFileReceived(object caller, TftpTransferCompleteEventArgs args) 166 { 167 if(!LogReceivedFiles) 168 { 169 return; 170 } 171 172 var data = args.Stream.ToArray(); 173 if(data.Length == 0) 174 { 175 this.Log(LogLevel.Info, "Received file '{0}' without any content", args.Filename); 176 return; 177 } 178 179 string toPrint; 180 181 try 182 { 183 // Remove null terminator before trying to convert data to a string 184 var length = data.Last() == '\0' ? data.Length - 1 : data.Length; 185 toPrint = Encoding.ASCII.GetString(data, 0, length); 186 } 187 catch(ArgumentException) 188 { 189 // If the data is not a valid string then present it as an array of hex values 190 toPrint = Misc.PrettyPrintCollectionHex(data); 191 } 192 193 this.Log(LogLevel.Info, "Received file '{0}': {1}", args.Filename, toPrint); 194 } 195 FindFile(string filename)196 private string FindFile(string filename) 197 { 198 // first check list of files 199 if(!files.TryGetValue(filename, out var result)) 200 { 201 // if not found, scan all the directories 202 foreach(var dir in directories) 203 { 204 foreach(var file in Directory.GetFiles(dir)) 205 { 206 if(file.Substring(dir.Length + 1) == filename) 207 { 208 result = file; 209 break; 210 } 211 } 212 } 213 } 214 return result; 215 } 216 217 private readonly Dictionary<string, string> files; 218 private readonly List<string> directories; 219 220 private readonly Dictionary<IPEndPoint, Action<IPEndPoint, UdpPacket>> callbacks; 221 private readonly TftpServer server; 222 } 223 } 224