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