LordMirai

ftp server

Aug 6th, 2025 (edited)
7
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.31 KB | None | 0 0
  1. -- Logs FTP Server (Protocol: "LOGS")
  2. -- Enhanced server with robust client input handling
  3. local modemSide = "back" -- Change this to your modem's side
  4.  
  5. KVList = {} -- volatile list for file numbers to names
  6.  
  7. -- Terminal colors for prettier output
  8. local colors = {
  9. primary = colors.cyan,
  10. secondary = colors.lightBlue,
  11. accent = colors.yellow,
  12. success = colors.lime,
  13. error = colors.red,
  14. info = colors.white
  15. }
  16.  
  17. -- Utility: Colored print
  18. local function printColored(text, color)
  19. color = color or colors.info
  20. term.setTextColor(color)
  21. print(text)
  22. term.setTextColor(colors.info)
  23. end
  24.  
  25. -- Setup
  26. rednet.open(modemSide)
  27. term.setTextColor(colors.success)
  28. print("=" .. string.rep("=", 40) .. "=")
  29. print(" LOGS FTP SERVER ONLINE")
  30. print("=" .. string.rep("=", 40) .. "=")
  31. term.setTextColor(colors.primary)
  32. print("Protocol: LOGS")
  33. print("Modem: " .. modemSide)
  34. print("Listening for connections...")
  35. term.setTextColor(colors.info)
  36.  
  37. -- Ensure logs folder exists
  38. if not fs.exists("/logs") then
  39. fs.makeDir("/logs")
  40. end
  41.  
  42. -- Ensure access log exists
  43. if not fs.exists("/accesses.txt") then
  44. local file = fs.open("/accesses.txt", "w")
  45. file.writeLine("Access log initialized at " .. textutils.formatTime(os.time(), true))
  46. file.close()
  47. end
  48.  
  49. -- Utility: Log access
  50. local function logAccess(id, action)
  51. local timestamp = textutils.formatTime(os.time(), true)
  52. local file = fs.open("/accesses.txt", "a")
  53. file.writeLine("[" .. timestamp .. "] ID " .. id .. ": " .. action)
  54. file.close()
  55.  
  56. printColored(string.format("[%s] Client %d: %s", timestamp, id, action), colors.accent)
  57. end
  58.  
  59. -- Utility: Safe string handling
  60. local function safeString(str)
  61. if not str then return "" end
  62. return tostring(str):gsub("[%c%z]", "") -- Remove control characters
  63. end
  64.  
  65. -- Utility: Validate filename
  66. local function isValidFilename(filename)
  67. if not filename or filename == "" then return false end
  68. -- Check for basic invalid characters (keep it simple for CC)
  69. if filename:match("[/\\<>:|?*\"]") then return false end
  70. return true
  71. end
  72.  
  73.  
  74. local function handleList(id)
  75. -- Filter to only show .txt files
  76. local allFiles = fs.list("/logs")
  77. local files = {}
  78.  
  79. for _, file in ipairs(allFiles) do
  80. if file:match("%.txt$") and not fs.isDir("/logs/" .. file) then
  81. table.insert(files, file)
  82. end
  83. end
  84.  
  85. local count = #files
  86. KVList = {} -- Reset the list
  87.  
  88. if count == 0 then
  89. local response = {
  90. method = "list",
  91. title = "Log Directory Listing",
  92. content = "No .txt log files found.\nThe logs directory is empty or contains no .txt files.",
  93. files = {},
  94. count = 0
  95. }
  96. rednet.send(id, textutils.serialize(response), "LOGS")
  97. logAccess(id, "Listed logs (empty)")
  98. return
  99. end
  100.  
  101. local response = "There " .. (count == 1 and "is" or "are") .. " " .. count .. " .txt log" .. (count == 1 and "" or "s") .. " available:\n"
  102. response = response .. string.rep("-", 35) .. "\n"
  103.  
  104. for i, filename in ipairs(files) do
  105. response = response .. string.format("%2d. %s\n", i, filename)
  106. KVList[i] = filename
  107. end
  108.  
  109. -- Create structured response for list
  110. local listResponse = {
  111. method = "list",
  112. title = "Log Directory Listing",
  113. content = response,
  114. files = files,
  115. count = count
  116. }
  117.  
  118. rednet.send(id, textutils.serialize(listResponse), "LOGS")
  119. logAccess(id, "Listed " .. count .. " .txt log files")
  120. end
  121.  
  122.  
  123. -- Helper function to handle file retrieval for both get and read
  124. local function handleFileRetrieval(id, filename, method)
  125. if not filename then
  126. rednet.send(id, "ERROR: No filename provided", "LOGS")
  127. logAccess(id, "FAILED " .. string.upper(method) .. " - No filename")
  128. return
  129. end
  130.  
  131. -- Sanitize input
  132. filename = safeString(filename):trim()
  133. if filename == "" then
  134. rednet.send(id, "ERROR: Empty filename provided", "LOGS")
  135. logAccess(id, "FAILED " .. string.upper(method) .. " - Empty filename")
  136. return
  137. end
  138.  
  139. -- Check if filename is a number and use KVList lookup
  140. local actualFilename = filename
  141. local fileNumber = tonumber(filename)
  142. if fileNumber then
  143. if KVList[fileNumber] then
  144. actualFilename = KVList[fileNumber]
  145. else
  146. rednet.send(id, "ERROR: Invalid file number '" .. filename .. "'", "LOGS")
  147. logAccess(id, "FAILED " .. string.upper(method) .. " - Invalid file number: " .. filename)
  148. return
  149. end
  150. end
  151.  
  152. local path = "/logs/" .. actualFilename
  153. if fs.exists(path) and not fs.isDir(path) then
  154. local file = fs.open(path, "r")
  155. if not file then
  156. rednet.send(id, "ERROR: Could not open file '" .. actualFilename .. "'", "LOGS")
  157. logAccess(id, "FAILED " .. string.upper(method) .. " - Cannot open: " .. actualFilename)
  158. return
  159. end
  160.  
  161. local content = file.readAll()
  162. file.close()
  163.  
  164. -- Create structured response
  165. local response = {
  166. method = method,
  167. title = actualFilename,
  168. content = content or "",
  169. filesize = #(content or "")
  170. }
  171.  
  172. rednet.send(id, textutils.serialize(response), "LOGS")
  173. logAccess(id, string.upper(method) .. " file '" .. actualFilename .. "' (" .. #(content or "") .. " bytes)")
  174. else
  175. rednet.send(id, "ERROR: File '" .. actualFilename .. "' not found", "LOGS")
  176. logAccess(id, "FAILED " .. string.upper(method) .. " - File not found: " .. actualFilename)
  177. end
  178. end
  179.  
  180. local function handleGet(id, filename)
  181. handleFileRetrieval(id, filename, "get")
  182. end
  183.  
  184. local function handleRead(id, filename)
  185. handleFileRetrieval(id, filename, "read")
  186. end
  187.  
  188.  
  189. local function handleAccesses(id, count)
  190. local maxLines = tonumber(count) or 20 -- default to 20 lines
  191.  
  192. if not fs.exists("/accesses.txt") then
  193. rednet.send(id, "ERROR: Access log not found", "LOGS")
  194. return
  195. end
  196.  
  197. local file = fs.open("/accesses.txt", "r")
  198. local lines = {}
  199. local line = file.readLine()
  200.  
  201. -- Read all lines into table
  202. while line do
  203. table.insert(lines, line)
  204. line = file.readLine()
  205. end
  206. file.close()
  207.  
  208. -- Get the last maxLines entries
  209. local startIndex = math.max(1, #lines - maxLines + 1)
  210. local recentLines = {}
  211.  
  212. for i = startIndex, #lines do
  213. table.insert(recentLines, lines[i])
  214. end
  215.  
  216. local content = table.concat(recentLines, "\n")
  217.  
  218. -- Create structured response
  219. local response = {
  220. method = "accesses",
  221. title = "Last " .. math.min(maxLines, #lines) .. " Access Log Entries",
  222. content = content,
  223. totalEntries = #lines,
  224. requestedCount = maxLines
  225. }
  226.  
  227. rednet.send(id, textutils.serialize(response), "LOGS")
  228. logAccess(id, "ACCESSES viewed last " .. maxLines .. " entries")
  229. end
  230.  
  231.  
  232. local function handleSend(id, filename, content)
  233. if not filename or not content then
  234. rednet.send(id, "ERROR: Missing filename or content", "LOGS")
  235. logAccess(id, "FAILED SEND - Missing filename or content")
  236. return
  237. end
  238.  
  239. -- Sanitize inputs
  240. filename = safeString(filename):trim()
  241. content = safeString(content)
  242.  
  243. if filename == "" then
  244. rednet.send(id, "ERROR: Empty filename provided", "LOGS")
  245. logAccess(id, "FAILED SEND - Empty filename")
  246. return
  247. end
  248.  
  249. if not isValidFilename(filename) then
  250. rednet.send(id, "ERROR: Invalid filename '" .. filename .. "'", "LOGS")
  251. logAccess(id, "FAILED SEND - Invalid filename: " .. filename)
  252. return
  253. end
  254.  
  255. -- Ensure .txt extension for consistency
  256. if not filename:match("%.txt$") then
  257. filename = filename .. ".txt"
  258. end
  259.  
  260. local path = "/logs/" .. filename
  261.  
  262. -- Check if file already exists and warn
  263. local fileExists = fs.exists(path)
  264.  
  265. local file = fs.open(path, "w")
  266. if not file then
  267. rednet.send(id, "ERROR: Could not create file '" .. filename .. "'", "LOGS")
  268. logAccess(id, "FAILED SEND - Cannot create file: " .. filename)
  269. return
  270. end
  271.  
  272. file.write(content)
  273. file.close()
  274.  
  275. -- Create structured response for send
  276. local sendResponse = {
  277. method = "send",
  278. title = filename,
  279. content = fileExists and "File overwritten successfully" or "File saved successfully",
  280. filesize = #content,
  281. overwritten = fileExists
  282. }
  283.  
  284. rednet.send(id, textutils.serialize(sendResponse), "LOGS")
  285. local action = fileExists and "OVERWRITE" or "UPLOAD"
  286. logAccess(id, action .. " file '" .. filename .. "' (" .. #content .. " bytes)")
  287. end
  288.  
  289. local commandHandlers = {
  290. list = function(id) handleList(id) end,
  291. get = function(id, a1, a2) handleGet(id, a1) end,
  292. read = function(id, a1, a2) handleRead(id, a1) end, -- read-only access to logs
  293. send = function(id, a1, a2) handleSend(id, a1, a2) end,
  294. accesses = function(id, a1, a2) handleAccesses(id, a1) end -- view access history
  295. }
  296.  
  297.  
  298.  
  299. -- Main server loop with enhanced error handling
  300. local TIMEOUT = 5
  301.  
  302. -- Add string trim function
  303. string.trim = string.trim or function(s)
  304. return s:match("^%s*(.-)%s*$")
  305. end
  306.  
  307. while true do
  308. ::continue::
  309.  
  310. local id, message = rednet.receive("LOGS", TIMEOUT)
  311.  
  312. if not id then
  313. os.sleep(0.1) -- No message received, wait a bit
  314. goto continue
  315. end
  316.  
  317. -- Handle ping requests for server discovery
  318. if message == "ping" then
  319. rednet.send(id, "pong", "LOGS")
  320. printColored("Client " .. id .. " discovered server", colors.secondary)
  321. goto continue
  322. end
  323.  
  324. -- Sanitize and parse the message
  325. message = safeString(message):trim()
  326. if message == "" then
  327. rednet.send(id, "ERROR: Empty command", "LOGS")
  328. logAccess(id, "FAILED - Empty command")
  329. goto continue
  330. end
  331.  
  332. local parts = {}
  333. for word in string.gmatch(message, "%S+") do
  334. table.insert(parts, word)
  335. end
  336.  
  337. if #parts == 0 then
  338. rednet.send(id, "ERROR: No command provided", "LOGS")
  339. logAccess(id, "FAILED - No command")
  340. goto continue
  341. end
  342.  
  343. local command = parts[1]:lower() -- Make commands case-insensitive
  344. local arg1 = parts[2]
  345. local arg2 = table.concat(parts, " ", 3) -- for send, allows spaces in content
  346.  
  347. local handler = commandHandlers[command]
  348. if handler then
  349. -- Wrap handler in pcall for error protection
  350. local success, error = pcall(handler, id, arg1, arg2)
  351. if not success then
  352. rednet.send(id, "ERROR: Server error processing command", "LOGS")
  353. printColored("ERROR handling command '" .. command .. "' from client " .. id .. ": " .. tostring(error), colors.error)
  354. logAccess(id, "ERROR - " .. command .. " failed: " .. tostring(error))
  355. end
  356. else
  357. rednet.send(id, "ERROR: Unknown command '" .. tostring(command) .. "'", "LOGS")
  358. logAccess(id, "FAILED - Unknown command: " .. tostring(command))
  359. end
  360. end
  361.  
Advertisement
Add Comment
Please, Sign In to add comment