WebFTPServer.Mod 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. (* Aos, Copyright 2001, Pieter Muller, ETH Zurich *)
  2. MODULE WebFTPServer; (** AUTHOR "be"; PURPOSE "FTP Server" *)
  3. (* based on a first version by prk *)
  4. IMPORT
  5. Kernel, Modules, IP, TCP, TCPServices, Objects, Commands,
  6. Streams, Files,KernelLog, Dates, Strings;
  7. CONST
  8. moduleName = "WebFTPServer: ";
  9. LogFile = "FTP.Log";
  10. PathDelimiter = Files.PathDelimiter;
  11. CmdLen = 32;
  12. LineLen = 1024;
  13. UserFile = "WebFTPUsers.dat"; (* user file *)
  14. BufSize = 16*1024; (* internal buffer size, used for file transfer *)
  15. dirLen = 1024; (* Maximum length of a filename (including FS-prefix, root and working directory) *)
  16. nameLen = 32; (* Maximum length of username *)
  17. pwdLen = 32; (* Maximum length of password *)
  18. CR = 0DX; LF = 0AX; Tab = 09X;
  19. (* Timeout & max subsequent error count*)
  20. Timeout = 900*1000; (* [ms] timeout on control connection *)
  21. PasvTimeout = 60*1000; (* [ms] timeout on passive connections *)
  22. MaxErrors = 10; (* control connection is closed after 'MaxErrors' consecutive requests that did produce a 2** result code *)
  23. (* Ports *)
  24. FTPControlPort = 21;
  25. FTPDataPort = 20;
  26. (* Session types *)
  27. ASCII = 0; (* type A *)
  28. IMAGE = 1; (* type I *)
  29. (* Connection Modes *)
  30. Active = 0;
  31. Passive = 1;
  32. (* States *)
  33. Ok = 0; RNFR = 1; REST = 2;
  34. (* Default Messages *)
  35. (* message classes:
  36. 1**: positive preliminary reply
  37. 2**: positive completion reply
  38. 3**: postitive intermediate reply
  39. 4**: transient negative completion reply
  40. 5**: permanent negative completion reply
  41. *)
  42. Msg215 = "UNIX";
  43. Msg220 = "Aos FTP Server ready.";
  44. Msg221 = "Goodbye.";
  45. Msg226 = "Closing data connection.";
  46. Msg230 = "User logged in, proceed.";
  47. Msg350 = "Requested file action pending further information.";
  48. Msg425 = "Can't open data connection.";
  49. Msg500 = ": not understood."; (* add to offending command *)
  50. Msg504 = "Command not implemented for that parameter.";
  51. Msg530 = "Please login with USER and PASS.";
  52. Msg553 = "File name not allowed.";
  53. NoPermissionMsg = "No permission.";
  54. (* permissions *)
  55. read = 0; (* can read the files - bit 0 *)
  56. write = 1; (* can write the files - bit 1 *)
  57. passwrq = 2; (* password required - bit 2 *)
  58. mailpwd = 3; (* password is e-mail address - bit 3 *)
  59. TYPE
  60. User = POINTER TO RECORD
  61. name: ARRAY nameLen OF CHAR;
  62. password, currentlogins, maxlogins: LONGINT;
  63. permissions: SET;
  64. root: ARRAY dirLen OF CHAR;
  65. next: User;
  66. END;
  67. LogEntry = RECORD
  68. user: ARRAY nameLen OF CHAR;
  69. ip: IP.Adr;
  70. method: ARRAY 16 OF CHAR;
  71. uri: ARRAY 1024 OF CHAR;
  72. status: LONGINT;
  73. result: WORD;
  74. pending: BOOLEAN;
  75. END;
  76. FTPAgent = OBJECT (TCPServices.Agent)
  77. VAR
  78. running: BOOLEAN;
  79. in: Streams.Reader;
  80. out: Streams.Writer;
  81. dataAdr: IP.Adr; dataPort: LONGINT;
  82. timeout, pasvTimeout: Objects.Timer;
  83. line: ARRAY LineLen OF CHAR;
  84. cmd: ARRAY CmdLen OF CHAR;
  85. logged, quit: BOOLEAN;
  86. user: User;
  87. type: SHORTINT;
  88. workDir: ARRAY dirLen OF CHAR;
  89. rnfrName: ARRAY dirLen OF CHAR; (* RNFR parameter *)
  90. state: LONGINT; (* one of: Ok, RNFR, REST *)
  91. mode: LONGINT; (* one of: Active, Passive *)
  92. consecutiveErrors: LONGINT; (* count of consecutive invalid commands *)
  93. restMarker: LONGINT; (* position in file where next file transfer should start. *)
  94. pasvListener: TCP.Connection;
  95. logEntry: LogEntry;
  96. PROCEDURE TimeoutHandler;
  97. BEGIN
  98. logEntry.pending := TRUE;
  99. COPY("TIMEOUT", logEntry.method); logEntry.uri := "";
  100. SendMessage(421, "Timeout, closing control connection.");
  101. IF (pasvListener # NIL) & (pasvListener.state = TCP.Established) THEN pasvListener.Close END;
  102. client.Close
  103. END TimeoutHandler;
  104. PROCEDURE PasvTimeoutHandler;
  105. BEGIN
  106. pasvListener.Close
  107. END PasvTimeoutHandler;
  108. PROCEDURE ReadCommand(VAR cmd, param: ARRAY OF CHAR);
  109. VAR i,l: LONGINT; c: CHAR;
  110. BEGIN
  111. Objects.SetTimeout(timeout, TimeoutHandler, Timeout);
  112. in.SkipSpaces;
  113. i := 0; l := LEN(cmd)-1; c := in.Peek();
  114. WHILE (i < l) & (c # " ") & (c # CR) & (c # LF) & (in.res = Streams.Ok) DO
  115. cmd[i] := CAP(in.Get()); INC(i);
  116. c := in.Peek()
  117. END;
  118. cmd[i] := 0X;
  119. WHILE (c = " ") & (in.res = Streams.Ok) DO c := in.Get(); c := in.Peek() END;
  120. i := 0; l := LEN(param)-1;
  121. WHILE (i < l) & (c # CR) & (c # LF) & (in.res = Streams.Ok) DO
  122. param[i] := in.Get(); INC(i);
  123. c := in.Peek()
  124. END;
  125. param[i] := 0X;
  126. in.SkipLn();
  127. Objects.CancelTimeout(timeout)
  128. END ReadCommand;
  129. PROCEDURE SendString(str: ARRAY OF CHAR);
  130. BEGIN
  131. out.String(str); out.Ln(); out.Update
  132. END SendString;
  133. PROCEDURE SendMessage(code: LONGINT; msg: ARRAY OF CHAR);
  134. BEGIN
  135. IF logEntry.pending THEN
  136. logEntry.status := code;W3CLog(logEntry);
  137. logEntry.result := 0; logEntry.pending := FALSE
  138. END;
  139. out.Int(code, 0);out.String(" "); out.String(msg); out.Ln;
  140. out.Update
  141. END SendMessage;
  142. PROCEDURE GetWorkingDirMsg(VAR msg: ARRAY OF CHAR);
  143. BEGIN
  144. IF (user.root # "") & (workDir = "") THEN COPY('"/" is current directory.', msg)
  145. ELSE
  146. IF (user.root # "") THEN Strings.Concat('"/', workDir, msg)
  147. ELSE Strings.Concat('"', workDir, msg)
  148. END;
  149. Strings.Append(msg, '" is current directory.')
  150. END
  151. END GetWorkingDirMsg;
  152. PROCEDURE GetDirectories(name: ARRAY OF CHAR; VAR usr, system: ARRAY OF CHAR);
  153. BEGIN
  154. ComposeDirectory(workDir, name, usr);
  155. Strings.Concat(user.root, usr, system)
  156. END GetDirectories;
  157. PROCEDURE CheckDirectory(name: ARRAY OF CHAR): BOOLEAN;
  158. VAR prefix: Files.Prefix; path: ARRAY dirLen OF CHAR;
  159. BEGIN
  160. Strings.Concat(user.root, name, name); Strings.TrimRight(name, PathDelimiter);
  161. Files.SplitName(path, prefix, path);
  162. IF (prefix = "") OR (Files.This(prefix) # NIL) THEN
  163. RETURN (path = "") OR (Files.Old(name) # NIL)
  164. ELSE
  165. RETURN FALSE (* file system not found *)
  166. END
  167. END CheckDirectory;
  168. PROCEDURE Directory(name: ARRAY OF CHAR; full: BOOLEAN);
  169. VAR data: TCP.Connection; w: Streams.Writer; e: Files.Enumerator; t: Kernel.MilliTimer;
  170. prefix: Files.Prefix; str: ARRAY 20 OF CHAR; date, size, time: LONGINT; res: WORD; flags: SET; c: CHAR;
  171. split: BOOLEAN;
  172. BEGIN
  173. SendMessage(150, "Opening ASCII mode data connection for file list.");
  174. IF (mode = Active) THEN
  175. NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
  176. dataAdr := client.fip; dataPort := FTPDataPort (*default*)
  177. ELSE
  178. ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
  179. Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
  180. pasvListener.Accept(data, res);
  181. pasvListener.Close;
  182. Objects.CancelTimeout(pasvTimeout);
  183. mode := Active
  184. END;
  185. logEntry.result := res;
  186. IF res # TCP.Ok THEN
  187. SendMessage(425, Msg425)
  188. ELSE
  189. ComposeDirectory(workDir, name, name); Strings.Concat(user.root, name, name);
  190. IF (name = "") THEN split := FALSE ELSE split := TRUE END; (* remove prefix & path only if we are not on root level *)
  191. IF full THEN flags := {Files.EnumSize, Files.EnumTime} ELSE flags := {} END;
  192. NEW(e); e.Open(name, flags);
  193. Streams.OpenWriter(w, data.Send);
  194. WHILE e.GetEntry(name, flags, time, date, size) DO
  195. IF split THEN Files.SplitPath(name, prefix, name) END; (* remove prefix & path *)
  196. IF full THEN
  197. (* format: <flags:10>" "<type:3>" "<user:8>" "<group:8>" "<size:8>" "<month:3>" "<day:2>" "<time:5>" "<filename> *)
  198. IF (Files.Directory IN flags) THEN c := "d" ELSE c := "-" END;
  199. w.Char(c);
  200. w.String("rw-rw-rw-");
  201. w.String(" 1 Aos Aos ");
  202. w.Int(size, 8);
  203. Strings.FormatDateTime(" mmm dd hh:nn ", Dates.OberonToDateTime(date, time), str);
  204. w.String(str)
  205. END;
  206. w.String(name); w.Ln;
  207. END;
  208. w.Update;
  209. SendMessage(226, Msg226);
  210. e.Close;
  211. IF (data.state # TCP.Established) THEN (* clients may hang if the data connection is closed before it is established *)
  212. Kernel.SetTimer(t, 1000);
  213. WHILE (data.state # TCP.Established) & ~Kernel.Expired(t) DO
  214. Objects.Yield
  215. END
  216. END;
  217. data.Close
  218. END
  219. END Directory;
  220. PROCEDURE Size(name: ARRAY OF CHAR);
  221. VAR filename: ARRAY dirLen OF CHAR; f: Files.File;
  222. BEGIN
  223. ComposeDirectory(workDir, name, name);
  224. Strings.Concat(user.root, name, filename);
  225. f := Files.Old(filename);
  226. IF (f = NIL) THEN
  227. Strings.Append(name, ": file not found."); SendMessage(550, name)
  228. ELSE
  229. Strings.IntToStr(f.Length(), name); SendMessage(213, name)
  230. END
  231. END Size;
  232. PROCEDURE WaitEstablished(c: TCP.Connection);
  233. VAR t: Kernel.MilliTimer;
  234. BEGIN
  235. IF (c.state # TCP.Established) THEN
  236. Kernel.SetTimer(t, 500);
  237. WHILE (c.state # TCP.Established) & ~Kernel.Expired(t) DO
  238. Objects.Yield
  239. END
  240. END
  241. END WaitEstablished;
  242. PROCEDURE Retrieve(name: ARRAY OF CHAR; marker: LONGINT);
  243. VAR data: TCP.Connection; w: Streams.Writer; f: Files.File; r: Files.Reader;
  244. filename, msg: ARRAY dirLen OF CHAR; res: WORD;
  245. BEGIN
  246. IF (type = ASCII) THEN COPY("ASCII", msg) ELSE COPY("Binary", msg) END;
  247. Strings.Append(msg, " data connection for "); Strings.Append(msg, name);
  248. logEntry.pending := FALSE;
  249. SendMessage(150, msg);
  250. logEntry.pending := TRUE;
  251. IF (mode = Active) THEN
  252. NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
  253. dataAdr := client.fip; dataPort := FTPDataPort; (*default*)
  254. ELSE
  255. ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
  256. Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
  257. pasvListener.Accept(data, res);
  258. pasvListener.Close;
  259. Objects.CancelTimeout(pasvTimeout);
  260. mode := Active
  261. END;
  262. logEntry.result := res;
  263. IF res # TCP.Ok THEN
  264. SendMessage(425, Msg425)
  265. ELSE
  266. ComposeDirectory(workDir, name, name);
  267. Strings.Concat(user.root, name, filename);
  268. Streams.OpenWriter(w, data.Send);
  269. f := Files.Old(filename);
  270. WaitEstablished(data); (* clients may hang if the data connection is closed before it is established *)
  271. IF f = NIL THEN
  272. Strings.Append(name, ": file not found."); SendMessage(550, name);
  273. ELSIF (Files.Directory IN f.flags) THEN
  274. Strings.Append(name, ": is a directory."); SendMessage(550, name)
  275. ELSE
  276. Files.OpenReader(r, f, marker);
  277. IF (type = ASCII) THEN ASCIITransfer(r, w)
  278. ELSE BinaryTransfer(r, w)
  279. END;
  280. IncreaseSent(f.Length());
  281. SendMessage(226, "Transfer complete.")
  282. END;
  283. data.Close
  284. END
  285. END Retrieve;
  286. PROCEDURE Store(name: ARRAY OF CHAR; marker: LONGINT);
  287. VAR data: TCP.Connection; r: Streams.Reader; f: Files.File; w: Files.Writer;
  288. filename, msg: ARRAY dirLen OF CHAR; res: WORD;
  289. BEGIN
  290. IF (type = ASCII) THEN COPY("ASCII", msg) ELSE COPY("Binary", msg) END;
  291. Strings.Append(msg, " data connection for "); Strings.Append(msg, name);
  292. logEntry.pending := FALSE;
  293. SendMessage(150, msg);
  294. logEntry.pending := TRUE;
  295. IF (mode = Active) THEN
  296. NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
  297. dataAdr := client.fip; dataPort := FTPDataPort (*default*)
  298. ELSE
  299. ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
  300. Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
  301. pasvListener.Accept(data, res);
  302. pasvListener.Close;
  303. Objects.CancelTimeout(pasvTimeout);
  304. mode := Active
  305. END;
  306. logEntry.result := res;
  307. IF res # TCP.Ok THEN
  308. SendMessage(425, Msg425)
  309. ELSE
  310. ComposeDirectory(workDir, name, name);
  311. Strings.Concat(user.root, name, filename);
  312. Streams.OpenReader(r, data.Receive);
  313. IF (marker = -1) THEN (* append *)
  314. f := Files.Old(filename);
  315. marker := f.Length()
  316. ELSIF (marker > 0) THEN (* restart *)
  317. f := Files.Old(filename)
  318. ELSE
  319. f := Files.New(filename);
  320. IF (f # NIL) THEN Files.Register(f) END
  321. END;
  322. WaitEstablished(data); (* clients may hang if the data connection is closed before it is established *)
  323. IF f = NIL THEN
  324. SendMessage(553, Msg553)
  325. ELSE
  326. Files.OpenWriter(w, f, marker);
  327. IF (type = ASCII) THEN ASCIITransfer(r, w)
  328. ELSE BinaryTransfer(r, w)
  329. END;
  330. f.Update();
  331. IncreaseReceived(f.Length());
  332. SendMessage(226, Msg226)
  333. END;
  334. data.Close
  335. END
  336. END Store;
  337. PROCEDURE Execute(VAR cmd, param: ARRAY OF CHAR);
  338. VAR
  339. tmp, filename, str: ARRAY dirLen OF CHAR;
  340. i, code, lastState: LONGINT; res: WORD;
  341. BEGIN
  342. lastState := state; state := Ok;
  343. COPY(cmd, logEntry.method); COPY(param, logEntry.uri); logEntry.pending := TRUE;
  344. code := 550; COPY("Requested action not taken.", str);
  345. IF shutdown THEN
  346. code := 421; COPY("Server shutting down, closing control connection.", str);
  347. quit := TRUE
  348. ELSIF cmd = "USER" THEN
  349. COPY(param, logEntry.user);
  350. user := FindUser(param);
  351. IF (user # NIL) THEN
  352. IF UserLogin(user) THEN
  353. Strings.Concat("Password required for ", param, str); Strings.Append(str, ".");
  354. code := 331;
  355. workDir[0] := 0X;
  356. IF (passwrq IN user.permissions) THEN
  357. IF (mailpwd IN user.permissions) THEN
  358. str := "Anonymous access allowed, send identity (e-mail name) as password."
  359. END
  360. ELSE code := 230; COPY(Msg230, str); logged := TRUE
  361. END
  362. ELSE
  363. user := NIL; code := 421; COPY("Too many users.", str)
  364. END
  365. ELSE
  366. code := 530; Strings.Concat("Unknown user ", param, str); Strings.Append(str, ".")
  367. END
  368. ELSIF cmd = "PASS" THEN
  369. code := 530; COPY(Msg530, str);
  370. IF (user # NIL) & (user.name # "") THEN
  371. IF (mailpwd IN user.permissions) THEN (* password = e-mail address *)
  372. IF Strings.Match("?*@?*.?*", param) THEN code := 230; COPY(Msg230, str); logged := TRUE END
  373. ELSE
  374. logEntry.uri := ""; (* do not log password *)
  375. IF (Code(param) = user.password) THEN
  376. code := 230; COPY(Msg230, str); logged := TRUE
  377. END
  378. END
  379. END
  380. ELSIF cmd = "QUIT" THEN
  381. code := 221; COPY(Msg221, str); quit := TRUE
  382. ELSIF cmd = "NOOP" THEN
  383. code := 220; COPY(Msg220, str)
  384. ELSIF logged THEN (* these commands are only available if the user is logged in *)
  385. IF cmd = "CWD" THEN (* change working directory *)
  386. ComposeDirectory(workDir, param, tmp);
  387. IF CheckDirectory(tmp) THEN
  388. COPY(tmp, workDir);
  389. IF (workDir # "") THEN Files.ForceTrailingDelimiter(workDir) END;
  390. code := 250; GetWorkingDirMsg(str)
  391. ELSE
  392. code := 550; Strings.Concat(param, ": no such file or directory.", str)
  393. END
  394. ELSIF (cmd = "CDUP") OR (cmd = "XCUP") THEN (* change to parent directory *)
  395. ComposeDirectory(workDir, "..", workDir);
  396. IF (workDir # "") THEN Files.ForceTrailingDelimiter(workDir) END;
  397. code := 212; GetWorkingDirMsg(str)
  398. ELSIF (cmd = "PWD") OR (cmd = "XPWD") THEN (* print working directory *)
  399. code := 257; GetWorkingDirMsg(str)
  400. ELSIF (cmd = "MKD") OR (cmd = "XMKD") THEN
  401. IF (write IN user.permissions) THEN
  402. GetDirectories(param, tmp, filename);
  403. Files.CreateDirectory(filename, res);
  404. logEntry.result := res;
  405. IF (res = 0) THEN
  406. code := 257; Strings.Concat('"', tmp, str); Strings.Append(str, '": directory successfully created.')
  407. ELSE
  408. code := 550; Strings.Concat(tmp, ": failed to create directory", str)
  409. END
  410. ELSE
  411. code := 550; COPY(NoPermissionMsg, str)
  412. END
  413. ELSIF (cmd = "RMD") OR (cmd = "XRMD") THEN
  414. IF (write IN user.permissions) THEN
  415. GetDirectories(param, tmp, filename);
  416. Files.RemoveDirectory(filename, FALSE, res);
  417. logEntry.result := res;
  418. IF (res = 0) THEN
  419. code := 257; Strings.Concat('"', tmp, str); Strings.Append(str, '": directory successfully deleted.')
  420. ELSE code := 550; Strings.Concat(tmp, ": failed to delete directory", str)
  421. END
  422. ELSE
  423. code := 550; COPY(NoPermissionMsg, str)
  424. END
  425. ELSIF cmd = "DELE" THEN
  426. IF (write IN user.permissions) THEN
  427. GetDirectories(param, tmp, filename);
  428. Files.Delete(filename, res);
  429. logEntry.result := res;
  430. IF (res = 0) THEN code := 200; Strings.Concat('"', tmp, str); Strings.Append(str, '" deleted.')
  431. ELSE code := 450; Strings.Concat(tmp, ": cannot delete file.", str)
  432. END
  433. ELSE
  434. code := 550; COPY(NoPermissionMsg, str)
  435. END
  436. ELSIF cmd = "PASV" THEN
  437. mode := Passive;
  438. NEW(pasvListener);
  439. pasvListener.Open(TCP.NilPort, IP.NilAdr, TCP.NilPort, res);
  440. logEntry.result := res;
  441. IF (res = IP.Ok) THEN
  442. IP.AdrToStr(client.int.localAdr, str);
  443. i := 0; WHILE (str[i] # 0X) DO IF (str[i] = ".") THEN str[i] := "," END; INC(i) END;
  444. str[i] := ","; str[i+1] := 0X;
  445. Strings.IntToStr(pasvListener.lport DIV 100H, tmp);
  446. Strings.Append(str, tmp); Strings.Append(str, ",");
  447. Strings.IntToStr(pasvListener.lport MOD 100H, tmp);
  448. Strings.Append(str, tmp);
  449. Strings.Concat("Entering Passive Mode (", str, str);
  450. Strings.Append(str, ")");
  451. code := 227
  452. ELSE (* 425 is not an official reply, but the only one that makes sense *)
  453. code := 425; COPY("Can't open data connection.", str)
  454. END
  455. ELSIF cmd = "EPSV" THEN
  456. mode := Passive;
  457. NEW(pasvListener);
  458. pasvListener.Open(TCP.NilPort, IP.NilAdr, TCP.NilPort, res);
  459. logEntry.result := res;
  460. IF (res = IP.Ok) THEN
  461. str := "";
  462. Strings.IntToStr(pasvListener.lport, tmp);
  463. Strings.Append(str, "Entering Extended Passive Mode (|||");
  464. Strings.Append(str, tmp);
  465. Strings.Append(str, "|)");
  466. code := 229;
  467. ELSE
  468. code := 500; COPY("Can't open data connection.", str)
  469. END
  470. ELSIF cmd = "SYST" THEN
  471. code := 215; COPY(Msg215, str)
  472. ELSIF cmd = "TYPE" THEN
  473. IF (param = "A") OR (param = "I") THEN
  474. IF (param = "A") THEN type := ASCII
  475. ELSE type := IMAGE
  476. END;
  477. code := 200; Strings.Concat("Type set to ", param, str)
  478. ELSE
  479. code := 504; COPY(Msg504, str)
  480. END
  481. ELSIF (cmd = "NLST") OR (cmd = "LIST") THEN
  482. IF (read IN user.permissions) THEN
  483. Directory(param, (cmd="LIST")); code := -1
  484. ELSE
  485. code := 550; COPY(NoPermissionMsg, str)
  486. END
  487. ELSIF cmd = "PORT" THEN
  488. SplitPort(param, dataAdr, dataPort);
  489. code := 200; COPY("PORT command successful.", str)
  490. ELSIF cmd = "EPRT" THEN
  491. SplitEPRT(param, dataAdr, dataPort);
  492. code := 200; COPY("EPRT command successful.", str);
  493. ELSIF cmd = "SIZE" THEN
  494. IF (read IN user.permissions) THEN
  495. Size(param); code := -1
  496. ELSE
  497. code := 550; COPY(NoPermissionMsg, str)
  498. END
  499. ELSIF cmd = "REST" THEN
  500. Strings.StrToInt(param, restMarker); (* traps on invalid strings *)
  501. IF (restMarker < 0) THEN restMarker := 0 END;
  502. state := REST;
  503. code := 350; COPY(Msg350, str)
  504. ELSIF cmd = "RETR" THEN
  505. IF (read IN user.permissions) THEN
  506. IF (lastState # REST) THEN restMarker := 0 END;
  507. Retrieve(param, restMarker); code := -1
  508. ELSE
  509. code := 550; COPY(NoPermissionMsg, str)
  510. END
  511. ELSIF cmd = "STOR" THEN
  512. IF (write IN user.permissions) THEN
  513. IF (lastState # REST) THEN restMarker := 0 END;
  514. Store(param, restMarker); code := -1
  515. ELSE
  516. code := 550; COPY(NoPermissionMsg, str)
  517. END
  518. ELSIF cmd = "APPE" THEN
  519. IF (write IN user.permissions) THEN
  520. Store(param, -1); code := -1
  521. ELSE
  522. code := 550; COPY(NoPermissionMsg, str)
  523. END
  524. ELSIF cmd = "RNFR" THEN
  525. IF (write IN user.permissions) THEN
  526. IF (Strings.Pos(PathDelimiter, param) = -1) THEN (* path in filename not allowed *)
  527. GetDirectories(param, tmp, rnfrName);
  528. IF (Files.Old(rnfrName) # NIL) THEN
  529. state := RNFR;
  530. code := 350; COPY("File found, send new name.", str);
  531. ELSE
  532. code := 550; Strings.Concat(param, ": file not found.", str)
  533. END
  534. ELSE
  535. code := 550; Strings.Concat(param, ": invalid filename.", str)
  536. END
  537. ELSE
  538. code := 550; COPY(NoPermissionMsg, str)
  539. END
  540. ELSIF cmd = "RNTO" THEN
  541. IF (lastState = RNFR) THEN
  542. IF (Strings.Pos(PathDelimiter, param) = -1) THEN (* path in filename not allowed *)
  543. Files.SplitPath(rnfrName, filename, tmp);
  544. IF (Strings.Pos(":", filename) = -1) THEN
  545. Strings.Append(filename, ":");
  546. ELSE
  547. Strings.Append(filename, "/");
  548. END;
  549. Strings.Append(filename, param);
  550. Files.Rename(rnfrName, filename, res);
  551. logEntry.result := res;
  552. IF (res = 0) THEN
  553. code := 250; Strings.Concat(param, ": successfully renamed.", str)
  554. ELSE
  555. code := 550; Strings.Concat(param, ": renaming failed.", str)
  556. END
  557. ELSE
  558. code := 550; Strings.Concat(param, ": invalid filename.", str)
  559. END
  560. ELSE
  561. code := 530; COPY("Bad sequence of commands.", str)
  562. END
  563. ELSIF (cmd = "SITE") THEN
  564. Strings.UpperCase(param);
  565. IF (param = "HELP") THEN
  566. SendString("214-The following SITE commands are recognized (* =>'s unimplemented).");
  567. SendString(" HELP");
  568. code := 214; COPY("HELP command successful.", str)
  569. ELSE
  570. code := 500; Strings.Concat("SITE ", param, str); Strings.Concat(str, Msg500, str)
  571. END
  572. ELSE
  573. code := 500; Strings.Concat(param, Msg500, str)
  574. END
  575. END;
  576. IF (code > 0) THEN SendMessage(code, str) END;
  577. IF (code < 200) OR (code >= 300) THEN (* error or positive preliminary/intermediate reply *)
  578. INC(consecutiveErrors);
  579. IF (consecutiveErrors = MaxErrors) THEN quit := TRUE END
  580. ELSE
  581. consecutiveErrors := 0
  582. END
  583. END Execute;
  584. BEGIN {ACTIVE, SAFE}
  585. IF ~running THEN
  586. running := TRUE;
  587. NEW(timeout); Objects.SetTimeout(timeout, TimeoutHandler, Timeout);
  588. NEW(pasvTimeout);
  589. logged := FALSE; quit := FALSE; consecutiveErrors := 0; type := IMAGE;
  590. dataAdr := client.fip; dataPort := FTPDataPort; (*default*)
  591. logEntry.user := ""; logEntry.ip := client.fip; logEntry.pending := FALSE;
  592. Streams.OpenReader(in, client.Receive); Streams.OpenWriter(out, client.Send);
  593. SendMessage(220, Msg220);
  594. LOOP
  595. ReadCommand(cmd, line);
  596. IF (in.res # Streams.Ok) THEN EXIT END;
  597. Execute(cmd, line);
  598. IF (in.res # Streams.Ok) OR quit THEN EXIT END
  599. END
  600. ELSE
  601. (* trapped & restarted *)
  602. IF (client.state = TCP.Established) & (out.res = Streams.Ok) THEN
  603. logEntry.pending := TRUE;
  604. SendMessage(550, "Server Error")
  605. END
  606. END;
  607. IF (pasvListener # NIL) & (pasvListener.state = TCP.Listen) THEN pasvListener.Close END;
  608. IncreaseReceived(client.rcvnxt-client.irs); IncreaseSent(client.sndnxt-client.iss);
  609. IncreaseActive(-1);
  610. UserLogout(user);
  611. FlushLog;
  612. Terminate
  613. END FTPAgent;
  614. VAR
  615. Hex: ARRAY 16 OF CHAR;
  616. ftp : TCPServices.Service;
  617. users: User;
  618. shutdown: BOOLEAN; (* shutdown flag *)
  619. w3cf: Files.File;
  620. w3cw: Streams.Writer;
  621. (** statistical counters. #bytes sent/received := NMebiBX * 2**20 + NbytesX *)
  622. NclientsTotal*, NclientsActive*, NMebiBReceived*, NMebiBSent*, NbytesReceived*, NbytesSent*: LONGINT;
  623. (* --------- statisitcal counter handling --------------*)
  624. PROCEDURE IncreaseSent(delta: LONGINT);
  625. BEGIN {EXCLUSIVE}
  626. (* wp: delta >= 0 *)
  627. ASSERT(delta >= 0);
  628. NbytesSent := NbytesSent + delta;
  629. NMebiBSent := NMebiBSent + NbytesSent DIV 100000H;
  630. NbytesSent := NbytesSent MOD 100000H
  631. END IncreaseSent;
  632. PROCEDURE IncreaseReceived(delta: LONGINT);
  633. BEGIN {EXCLUSIVE}
  634. (* wp: delta >= 0 *)
  635. ASSERT(delta >= 0);
  636. NbytesReceived := NbytesReceived + delta;
  637. NMebiBReceived := NMebiBReceived + NbytesReceived DIV 100000H;
  638. NbytesReceived := NbytesReceived MOD 100000H
  639. END IncreaseReceived;
  640. PROCEDURE IncreaseActive(delta: LONGINT);
  641. BEGIN {EXCLUSIVE}
  642. NclientsActive := NclientsActive + delta
  643. END IncreaseActive;
  644. (* ------------- Helper Phunctions ----------------- *)
  645. PROCEDURE IsDigit(ch: CHAR): BOOLEAN;
  646. BEGIN
  647. RETURN (ch >= "0") & (ch <= "9")
  648. END IsDigit;
  649. PROCEDURE StrToInt(str: ARRAY OF CHAR; VAR val: LONGINT);
  650. VAR i, d: LONGINT; neg: BOOLEAN;
  651. BEGIN
  652. i := 0;
  653. WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END;
  654. IF str[i] = "-" THEN
  655. neg := TRUE; INC(i);
  656. WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END
  657. ELSE neg := FALSE END;
  658. val := 0;
  659. WHILE (str[i] # 0X) & (str[i] >= "0") & (str[i] <= "9") DO
  660. d := ORD(str[i])-ORD("0");
  661. IF val <= ((MAX(LONGINT)-d) DIV 10) THEN val := 10*val+d ELSE HALT(99) END;
  662. INC(i)
  663. END;
  664. IF neg THEN val := -val END
  665. END StrToInt;
  666. PROCEDURE StrToIntPos(VAR str: ARRAY OF CHAR; VAR i: INTEGER): LONGINT;
  667. VAR noStr: ARRAY 16 OF CHAR;
  668. j: LONGINT;
  669. BEGIN
  670. WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END;
  671. j := 0;
  672. IF str[i] = "-" THEN
  673. noStr[j] := str[i];
  674. INC(j); INC(i);
  675. WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END
  676. END;
  677. WHILE IsDigit(str[i]) DO noStr[j] := str[i]; INC(j); INC(i) END;
  678. noStr[j] := 0X;
  679. StrToInt(noStr, j);
  680. RETURN j
  681. END StrToIntPos;
  682. PROCEDURE SplitPort(parm: ARRAY OF CHAR; VAR adr: IP.Adr; VAR port: LONGINT);
  683. VAR pos: INTEGER; i, n: LONGINT;
  684. BEGIN
  685. pos := 0;
  686. FOR i := 0 TO 3 DO n := StrToIntPos(parm, pos); parm[pos] := '.'; INC(pos) END;
  687. parm[pos-1] := 0X;
  688. adr := IP.StrToAdr(parm);
  689. port := StrToIntPos(parm, pos)*256; INC(pos);
  690. port := port+StrToIntPos(parm, pos)
  691. END SplitPort;
  692. (* Parses the EPRT command *)
  693. PROCEDURE SplitEPRT(param: ARRAY OF CHAR; VAR adr: IP.Adr; VAR port: LONGINT);
  694. VAR
  695. i: LONGINT;
  696. protocol: LONGINT;
  697. tempString: ARRAY 128 OF CHAR;
  698. j: LONGINT;
  699. BEGIN
  700. (* read protocol *)
  701. i := 0;
  702. WHILE (i < LEN(param)) & (param[i] # "|") DO
  703. INC(i);
  704. END;
  705. IF i < LEN(param) THEN
  706. protocol := ORD(param[i+1]) - ORD("0");
  707. END;
  708. (* parse IP address *)
  709. i := i+3;
  710. j := i;
  711. WHILE (i < LEN(param)) & ((param[i] # "|") & (param[i] # "%")) DO
  712. INC(i);
  713. END;
  714. IF i < LEN(param) THEN
  715. Strings.Copy(param, j, i-j, tempString);
  716. END;
  717. adr := IP.StrToAdr(tempString);
  718. (* port *)
  719. IF param[i] = "%" THEN
  720. WHILE (i < LEN(param)) & (param[i] # "|") DO
  721. INC(i);
  722. END;
  723. END;
  724. IF i < LEN(param) THEN
  725. INC(i);
  726. j := i;
  727. WHILE (i < LEN(param)) & (param[i] # "|") DO
  728. INC(i);
  729. END;
  730. IF i < LEN(param) THEN
  731. Strings.Copy(param, j, i-j, tempString);
  732. StrToInt(tempString, port);
  733. END;
  734. END;
  735. END SplitEPRT;
  736. PROCEDURE BinaryTransfer(r: Streams.Reader; w: Streams.Writer);
  737. VAR buf: ARRAY BufSize OF CHAR; len: LONGINT;
  738. BEGIN
  739. REPEAT
  740. r.Bytes(buf, 0, BufSize, len); w.Bytes(buf, 0, len);
  741. UNTIL r.res # 0;
  742. w.Update
  743. END BinaryTransfer;
  744. PROCEDURE ASCIITransfer(r: Streams.Reader; w: Streams.Writer);
  745. VAR buf: ARRAY BufSize OF CHAR; i, len: LONGINT; c: CHAR;
  746. BEGIN
  747. REPEAT
  748. r.Bytes(buf, 0, BufSize, len);
  749. i := 0;
  750. WHILE (i < len) DO
  751. c := buf[i];
  752. IF (c = CR) THEN (* ignore CR *)
  753. ELSIF (c = LF) THEN w.Ln
  754. ELSE w.Char(c)
  755. END;
  756. INC(i)
  757. END
  758. UNTIL (r.res # 0);
  759. w.Update
  760. END ASCIITransfer;
  761. PROCEDURE Code(VAR s: ARRAY OF CHAR): LONGINT;
  762. VAR i: INTEGER; a, b, c: LONGINT;
  763. BEGIN
  764. a := 0; b := 0; i := 0;
  765. WHILE s[i] # 0X DO
  766. c := b; b := a; a := (c MOD 509 + 1) * 127 + ORD(s[i]);
  767. INC(i)
  768. END;
  769. IF b >= 32768 THEN b := b - 65536 END;
  770. RETURN b * 65536 + a
  771. END Code;
  772. PROCEDURE ComposeDirectory(path, name: ARRAY OF CHAR; VAR res: ARRAY OF CHAR);
  773. VAR
  774. prefix: Files.Prefix; tmp: ARRAY dirLen OF CHAR; p: SIZE;
  775. absolute : BOOLEAN;
  776. BEGIN
  777. COPY(path, res); absolute := PathDelimiter = name[0];
  778. Strings.TrimRight(res, PathDelimiter);
  779. Strings.TrimRight(name, PathDelimiter);
  780. Files.SplitName(name, prefix, tmp);
  781. IF (prefix # "") OR absolute THEN (* absolute path *)
  782. COPY(name, res); Strings.TrimLeft(res, PathDelimiter)
  783. ELSE (* relative path *)
  784. WHILE (name # "") DO
  785. p := Strings.Pos(PathDelimiter, name);
  786. IF (p >= 0) THEN
  787. Strings.Copy(name, 0, p, tmp);
  788. Strings.Delete(name, 0, p+1)
  789. ELSE
  790. COPY(name, tmp); name[0] := 0X
  791. END;
  792. IF (tmp = ".") THEN
  793. ELSIF (tmp = "..") THEN
  794. COPY(res, tmp); Strings.TrimRight(tmp, PathDelimiter); Strings.TrimRight(tmp, ":");
  795. IF (Files.This(tmp) # NIL) THEN (* it's a prefix *)
  796. COPY("", res)
  797. ELSE
  798. Files.SplitPath(res, res, tmp); Strings.TrimRight(res, PathDelimiter)
  799. END
  800. ELSE
  801. IF (res # "") THEN Strings.Append(res, PathDelimiter) END;
  802. Strings.Append(res, tmp)
  803. END
  804. END
  805. END
  806. END ComposeDirectory;
  807. (** ------------- TCP Service Handling ----------------- *)
  808. PROCEDURE NewFTPAgent(c: TCP.Connection; s: TCPServices.Service): TCPServices.Agent;
  809. VAR a: FTPAgent;
  810. BEGIN
  811. INC(NclientsTotal); INC(NclientsActive);
  812. NEW(a, c, s); RETURN a
  813. END NewFTPAgent;
  814. PROCEDURE Start*(context : Commands.Context); (** ["\l:" log file] *)
  815. VAR c, opt: CHAR; str, log: ARRAY 1024 OF CHAR; res : WORD;
  816. BEGIN
  817. IF ftp = NIL THEN
  818. COPY(LogFile, log);
  819. context.arg.SkipWhitespace;
  820. LOOP
  821. c := context.arg.Get();
  822. IF (c # "\") THEN EXIT END;
  823. opt := CAP(context.arg.Get());
  824. c := context.arg.Get();
  825. IF (c # ":") THEN EXIT END;
  826. context.arg.SkipWhitespace;
  827. context.arg.String(str);
  828. context.arg.SkipWhitespace;
  829. CASE opt OF
  830. | "L": COPY(str, log)
  831. ELSE EXIT
  832. END
  833. END;
  834. LoadUsers(users);
  835. shutdown := FALSE;
  836. NclientsTotal := 0; NclientsActive := 0; NbytesReceived := 0; NbytesSent := 0;
  837. OpenW3CLog(log);
  838. NEW(ftp, FTPControlPort, NewFTPAgent, res);
  839. IF (res = TCPServices.Ok) THEN
  840. KernelLog.Enter; KernelLog.String("WebFTPServer started"); KernelLog.Exit;
  841. context.out.String("WebFTPServer started"); context.out.Ln;
  842. ELSE
  843. context.error.String("WebFTPServer not started, res: "); context.error.Int(res, 0); context.error.Ln;
  844. END;
  845. ELSE
  846. context.out.String("WebFTPServer is already running."); context.out.Ln;
  847. END;
  848. END Start;
  849. PROCEDURE Stop*(context : Commands.Context);
  850. BEGIN
  851. IF ftp # NIL THEN
  852. shutdown := TRUE;
  853. ftp.Stop; ftp := NIL;
  854. KernelLog.Enter; KernelLog.String("WebFTPServer closed"); KernelLog.Exit;
  855. IF (context # NIL) THEN context.out.String("WebFTPServer closed."); context.out.Ln; END;
  856. ELSE
  857. IF (context # NIL) THEN context.out.String("WebFTPServer is not running."); context.out.Ln; END;
  858. END;
  859. END Stop;
  860. (** ------------- User Handling ----------------- *)
  861. (** Adds a user to the user file. Syntax:
  862. Aos.Call FTPServer.AddUser <name> <password> <max concurrent logins> <permissions> [<root>] ~
  863. name = string, may not contain spaces
  864. password = string, must be enquoted if it contains spaces
  865. max concurrent logins = integer. if = -1 then any number of concurrent logins are allowed
  866. permissions = ["R"]["W"]["P"["M"]];
  867. R = user has read permissions
  868. W = user has write permissions
  869. P = user must supply a password
  870. M = password is an e-mail address
  871. root = valid file system prefix, may include a path (if it does, do not forget the trailing backslash!)
  872. *)
  873. PROCEDURE AddUser*(context : Commands.Context);
  874. VAR
  875. username, permissions: ARRAY nameLen OF CHAR; root: ARRAY dirLen OF CHAR;
  876. password: ARRAY pwdLen+1 OF CHAR;
  877. user: User; i, maxlogins: LONGINT;
  878. BEGIN {EXCLUSIVE}
  879. context.arg.SkipWhitespace;
  880. context.arg.Token(username); context.arg.SkipWhitespace;
  881. context.arg.String(password); context.arg.SkipWhitespace;
  882. context.arg.Int(maxlogins, FALSE); context.arg.SkipWhitespace;
  883. context.arg.Token(permissions); context.arg.SkipWhitespace;
  884. context.arg.String(root);
  885. IF (username # "") & (password # "") & (maxlogins # 0) & (permissions # "") THEN
  886. IF (FindUser(username) = NIL) THEN
  887. NEW(user);
  888. COPY(username, user.name); user.password := Code(password); user.maxlogins := maxlogins;
  889. user.permissions := {}; COPY(root, user.root);
  890. i := 0;
  891. WHILE (permissions[i] # 0X) DO
  892. IF (CAP(permissions[i]) = "R") THEN INCL(user.permissions, read)
  893. ELSIF (CAP(permissions[i]) = "W") THEN INCL(user.permissions, write)
  894. ELSIF (CAP(permissions[i]) = "P") THEN INCL(user.permissions, passwrq)
  895. ELSIF (CAP(permissions[i]) = "M") THEN INCL(user.permissions, mailpwd)
  896. ELSE
  897. context.error.String("AddUser: Invalid permissions"); context.error.Ln;
  898. RETURN;
  899. END;
  900. INC(i)
  901. END;
  902. user.next := users; users := user;
  903. StoreUsers(users, context);
  904. context.out.String(moduleName); context.out.String("User '"); context.out.String(username);
  905. context.out.String("' added. Max concurrent logins = ");
  906. IF (user.maxlogins < 0) THEN context.out.String("unlimited") ELSE context.out.Int(user.maxlogins, 0) END;
  907. context.out.String("; permissions = "); context.out.String(permissions); context.out.String(", root = '"); context.out.String(root);
  908. context.out.Char("'"); context.out.Ln;
  909. ELSE
  910. context.error.String(moduleName); context.error.String("User '"); context.error.String(username); context.error.String("' already exists.");
  911. context.error.Ln;
  912. END
  913. ELSE
  914. context.error.String(moduleName);
  915. context.error.String("Expected parameters: <username> <password> <maxlogins> <permissions>"); context.error.Ln;
  916. END;
  917. END AddUser;
  918. (** Removes a user from the user file. Syntax:
  919. Aos.Call FTPServer.RemoveUser <name> ~
  920. name = string, may not contain spaces
  921. *)
  922. PROCEDURE RemoveUser*(context : Commands.Context);
  923. VAR prev, u: User; name: ARRAY nameLen OF CHAR; nofRemovals : LONGINT;
  924. BEGIN {EXCLUSIVE}
  925. context.arg.SkipWhitespace; context.arg.Token(name);
  926. IF (name # "") THEN
  927. context.out.String(moduleName); context.out.String("Removing user '"); context.out.String(name); context.out.String("'... ");
  928. context.out.Update;
  929. nofRemovals := 0;
  930. u := users; prev := NIL;
  931. WHILE (u # NIL) DO
  932. IF (u.name = name) THEN
  933. INC(nofRemovals);
  934. IF (prev = NIL) THEN users := u.next
  935. ELSE prev.next := u.next
  936. END
  937. END;
  938. prev := u; u := u.next
  939. END;
  940. IF (nofRemovals = 1) THEN
  941. context.out.String("done.");
  942. ELSIF (nofRemovals > 1) THEN
  943. context.out.String(" removed "); context.out.Int(nofRemovals, 0); context.out.String(" times, done.");
  944. ELSE
  945. context.out.String(" user not found.");
  946. END;
  947. context.out.Ln;
  948. StoreUsers(users, context)
  949. ELSE
  950. context.error.String("RemoveUser: invalid parameters"); context.error.Ln;
  951. END;
  952. END RemoveUser;
  953. PROCEDURE ListUsers*(context : Commands.Context);
  954. VAR user: User;
  955. BEGIN {EXCLUSIVE}
  956. context.out.String(moduleName); context.out.String("Registered users:"); context.out.Ln;
  957. IF (users # NIL) THEN
  958. user := users;
  959. WHILE (user # NIL) DO
  960. context.out.String(" "); context.out.String(user.name);
  961. IF (passwrq IN user.permissions) THEN context.out.String("; password-protected login") END;
  962. IF (mailpwd IN user.permissions) THEN context.out.String("; password = e-mail address") END;
  963. context.out.Ln;
  964. context.out.String(" currently active: "); context.out.Int(user.currentlogins, 0);
  965. context.out.String("; max logins: ");
  966. IF (user.maxlogins < 0) THEN context.out.String("unlimited") ELSE context.out.Int(user.maxlogins, 0) END;
  967. context.out.Ln;
  968. context.out.String(" root = '"); context.out.String(user.root); context.out.String("'; permissions: ");
  969. IF (read IN user.permissions) THEN context.out.Char("R") END;
  970. IF (write IN user.permissions) THEN context.out.Char("W") END;
  971. context.out.Ln;
  972. user := user.next
  973. END
  974. ELSE
  975. context.out.String("no users"); context.out.Ln;
  976. END;
  977. END ListUsers;
  978. PROCEDURE LoadUsers(VAR users: User);
  979. VAR u: User; f: Files.File; r: Files.Reader;
  980. BEGIN
  981. users := NIL;
  982. f := Files.Old(UserFile);
  983. IF (f # NIL) THEN
  984. Files.OpenReader(r, f, 0);
  985. WHILE (r.res = Streams.Ok) DO
  986. NEW(u);
  987. r.RawString(u.name); r.RawLInt(u.password); r.RawLInt(u.maxlogins);
  988. r.RawSet(u.permissions); r.RawString(u.root);
  989. IF (r.res = Streams.Ok) THEN
  990. u.next := users;
  991. users := u
  992. END
  993. END
  994. END
  995. END LoadUsers;
  996. PROCEDURE StoreUsers(users: User; context : Commands.Context);
  997. VAR f: Files.File; w: Files.Writer;
  998. BEGIN
  999. f := Files.New(UserFile);
  1000. IF (f # NIL) THEN
  1001. Files.OpenWriter(w, f, 0);
  1002. WHILE (w.res = Streams.Ok) & (users # NIL) DO
  1003. w.RawString(users.name); w.RawLInt(users.password); w.RawLInt(users.maxlogins);
  1004. w.RawSet(users.permissions); w.RawString(users.root);
  1005. users := users.next
  1006. END;
  1007. IF (w.res = Streams.Ok) THEN
  1008. w.Update;
  1009. Files.Register(f)
  1010. END
  1011. ELSE
  1012. context.error.String(moduleName); context.error.String("can't write user file"); context.error.Ln;
  1013. END
  1014. END StoreUsers;
  1015. PROCEDURE FindUser(name: ARRAY OF CHAR): User;
  1016. VAR u: User;
  1017. BEGIN
  1018. u := users;
  1019. WHILE (u # NIL) & (u.name # name) DO u := u.next END;
  1020. RETURN u
  1021. END FindUser;
  1022. PROCEDURE UserLogin(user: User): BOOLEAN;
  1023. BEGIN {EXCLUSIVE}
  1024. IF (user # NIL) & ((user.currentlogins < user.maxlogins) OR (user.maxlogins = -1)) THEN
  1025. INC(user.currentlogins);
  1026. RETURN TRUE
  1027. ELSE
  1028. RETURN FALSE
  1029. END
  1030. END UserLogin;
  1031. PROCEDURE UserLogout(user: User);
  1032. BEGIN {EXCLUSIVE}
  1033. IF (user # NIL) THEN
  1034. IF (user.currentlogins > 0) THEN
  1035. DEC(user.currentlogins)
  1036. ELSE
  1037. KernelLog.Enter; KernelLog.String(moduleName); KernelLog.String("warning: user count <= 0. user: "); KernelLog.String(user.name);
  1038. KernelLog.String("; #active: "); KernelLog.Int(user.currentlogins, 0); KernelLog.Exit
  1039. END
  1040. END
  1041. END UserLogout;
  1042. (* ------------- Logging ----------------- *)
  1043. PROCEDURE OpenW3CLog(fn: ARRAY OF CHAR);
  1044. VAR w : Files.Writer;
  1045. BEGIN
  1046. w3cf := Files.Old(fn);
  1047. IF w3cf = NIL THEN
  1048. w3cf := Files.New(fn);
  1049. IF (w3cf # NIL) THEN
  1050. Files.OpenWriter(w, w3cf, 0);
  1051. w.String("#Version: 1.0"); w.Ln;
  1052. w.String("#Fields: date"); w.Char(Tab);
  1053. w.String("time"); w.Char(Tab);
  1054. w.String("x-user"); w.Char(Tab);
  1055. w.String("c-ip"); w.Char(Tab);
  1056. w.String("cs-method"); w.Char(Tab);
  1057. w.String("cs-uri"); w.Char(Tab);
  1058. w.String("sc-status"); w.Char(Tab);
  1059. w.String("x-result");
  1060. w.Ln;
  1061. w.Update;
  1062. Files.Register(w3cf)
  1063. ELSE
  1064. KernelLog.Enter; KernelLog.String(moduleName); KernelLog.String("cannot open log file '"); KernelLog.String(fn); KernelLog.Char("'"); KernelLog.Exit
  1065. END
  1066. ELSE
  1067. Files.OpenWriter(w, w3cf, w3cf.Length())
  1068. END;
  1069. w3cw := w;
  1070. END OpenW3CLog;
  1071. PROCEDURE W3CLog(e: LogEntry);
  1072. VAR s: ARRAY 36 OF CHAR;
  1073. PROCEDURE ToURI(ascii: ARRAY OF CHAR; VAR uri: ARRAY OF CHAR); (* cf. RFC 1738 *)
  1074. VAR i,k,l: LONGINT; c: CHAR;
  1075. BEGIN
  1076. i := 0; k := 0; l := LEN(uri)-1;
  1077. WHILE (k < l) & (ascii[i] # 0X) DO
  1078. c := ascii[i];
  1079. IF (("A" <= CAP(c)) & (CAP(c) <= "Z")) OR (("0" <= c) & (c <= "9")) OR
  1080. (c = "$") OR (c = "-") OR (c = "_") OR (c = ".") OR (c = "+") OR
  1081. (c = "!") OR (c = "*") OR (c = "'") OR (c = "(") OR (c = ")") OR (c = ",")
  1082. THEN
  1083. uri[k] := c; INC(k)
  1084. ELSIF (k < l-2) THEN
  1085. uri[k] := "%"; INC(k);
  1086. uri[k] := Hex[ORD(c) DIV 10H]; INC(k);
  1087. uri[k] := Hex[ORD(c) MOD 10H]; INC(k)
  1088. ELSE
  1089. ascii[i+1] := 0X (* abort, uri too short *)
  1090. END;
  1091. INC(i)
  1092. END;
  1093. uri[k] := 0X
  1094. END ToURI;
  1095. BEGIN {EXCLUSIVE}
  1096. IF (w3cf = NIL) THEN RETURN END;
  1097. Strings.FormatDateTime("yyyy-mm-dd", Dates.Now(), s);
  1098. w3cw.String(s); w3cw.Char(Tab);
  1099. Strings.FormatDateTime("hh:nn:ss", Dates.Now(), s);
  1100. w3cw.String(s); w3cw.Char(Tab);
  1101. w3cw.String(e.user); w3cw.Char(Tab);
  1102. IP.AdrToStr(e.ip, s);
  1103. w3cw.String(s); w3cw.Char(Tab);
  1104. w3cw.String(e.method); w3cw.Char(Tab);
  1105. ToURI(e.uri, e.uri);
  1106. w3cw.String(e.uri); w3cw.Char(Tab);
  1107. Strings.IntToStr(e.status, s);
  1108. w3cw.String(s); w3cw.Char(Tab);
  1109. Strings.IntToStr(e.result, s);
  1110. w3cw.String(s);
  1111. w3cw.Ln
  1112. END W3CLog;
  1113. PROCEDURE FlushLog*;
  1114. BEGIN {EXCLUSIVE}
  1115. IF (w3cf # NIL) THEN
  1116. w3cw.Update; w3cf.Update
  1117. END
  1118. END FlushLog;
  1119. PROCEDURE Cleanup;
  1120. BEGIN
  1121. Stop(NIL);
  1122. END Cleanup;
  1123. BEGIN
  1124. Hex[0] := "0"; Hex[1] := "1"; Hex[2] := "2"; Hex[3] := "3";
  1125. Hex[4] := "4"; Hex[5] := "5"; Hex[6] := "6"; Hex[7] := "7";
  1126. Hex[8] := "8"; Hex[9] := "9"; Hex[10] := "A"; Hex[11] := "B";
  1127. Hex[12] := "C"; Hex[13] := "D"; Hex[14] := "2"; Hex[15] := "E";
  1128. Modules.InstallTermHandler(Cleanup)
  1129. END WebFTPServer.
  1130. Aos.Call WebFTPServer.Start ~\l:FAT:/logs/FTP.Log~
  1131. Aos.Call WebFTPServer.Stop
  1132. NetTracker.CloseAll
  1133. System.Free WebFTPServer ~
  1134. ET.OpenAscii FTP.Log ~
  1135. Aos.Call WebFTPServer.AddUser user password -1 rwp FAT:~
  1136. Aos.Call WebFTPServer.AddUser anonymous none 3 rwpm FAT:/ftproot/ ~
  1137. Aos.Call WebFTPServer.RemoveUser begger ~
  1138. Aos.Call WebFTPServer.ListUsers
  1139. System.DeleteFiles WebFTPUsers.dat ~ deletes all users