123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254 |
- (* Aos, Copyright 2001, Pieter Muller, ETH Zurich *)
- MODULE WebFTPServer; (** AUTHOR "be"; PURPOSE "FTP Server" *)
- (* based on a first version by prk *)
- IMPORT
- Kernel, Modules, IP, TCP, TCPServices, Objects, Commands,
- Streams, Files,KernelLog, Dates, Strings;
- CONST
- moduleName = "WebFTPServer: ";
- LogFile = "FTP.Log";
- PathDelimiter = Files.PathDelimiter;
- CmdLen = 32;
- LineLen = 1024;
- UserFile = "WebFTPUsers.dat"; (* user file *)
- BufSize = 16*1024; (* internal buffer size, used for file transfer *)
- dirLen = 1024; (* Maximum length of a filename (including FS-prefix, root and working directory) *)
- nameLen = 32; (* Maximum length of username *)
- pwdLen = 32; (* Maximum length of password *)
- CR = 0DX; LF = 0AX; Tab = 09X;
- (* Timeout & max subsequent error count*)
- Timeout = 900*1000; (* [ms] timeout on control connection *)
- PasvTimeout = 60*1000; (* [ms] timeout on passive connections *)
- MaxErrors = 10; (* control connection is closed after 'MaxErrors' consecutive requests that did produce a 2** result code *)
- (* Ports *)
- FTPControlPort = 21;
- FTPDataPort = 20;
- (* Session types *)
- ASCII = 0; (* type A *)
- IMAGE = 1; (* type I *)
- (* Connection Modes *)
- Active = 0;
- Passive = 1;
- (* States *)
- Ok = 0; RNFR = 1; REST = 2;
- (* Default Messages *)
- (* message classes:
- 1**: positive preliminary reply
- 2**: positive completion reply
- 3**: postitive intermediate reply
- 4**: transient negative completion reply
- 5**: permanent negative completion reply
- *)
- Msg215 = "UNIX";
- Msg220 = "Aos FTP Server ready.";
- Msg221 = "Goodbye.";
- Msg226 = "Closing data connection.";
- Msg230 = "User logged in, proceed.";
- Msg350 = "Requested file action pending further information.";
- Msg425 = "Can't open data connection.";
- Msg500 = ": not understood."; (* add to offending command *)
- Msg504 = "Command not implemented for that parameter.";
- Msg530 = "Please login with USER and PASS.";
- Msg553 = "File name not allowed.";
- NoPermissionMsg = "No permission.";
- (* permissions *)
- read = 0; (* can read the files - bit 0 *)
- write = 1; (* can write the files - bit 1 *)
- passwrq = 2; (* password required - bit 2 *)
- mailpwd = 3; (* password is e-mail address - bit 3 *)
- TYPE
- User = POINTER TO RECORD
- name: ARRAY nameLen OF CHAR;
- password, currentlogins, maxlogins: LONGINT;
- permissions: SET;
- root: ARRAY dirLen OF CHAR;
- next: User;
- END;
- LogEntry = RECORD
- user: ARRAY nameLen OF CHAR;
- ip: IP.Adr;
- method: ARRAY 16 OF CHAR;
- uri: ARRAY 1024 OF CHAR;
- status: LONGINT;
- result: WORD;
- pending: BOOLEAN;
- END;
- FTPAgent = OBJECT (TCPServices.Agent)
- VAR
- running: BOOLEAN;
- in: Streams.Reader;
- out: Streams.Writer;
- dataAdr: IP.Adr; dataPort: LONGINT;
- timeout, pasvTimeout: Objects.Timer;
- line: ARRAY LineLen OF CHAR;
- cmd: ARRAY CmdLen OF CHAR;
- logged, quit: BOOLEAN;
- user: User;
- type: SHORTINT;
- workDir: ARRAY dirLen OF CHAR;
- rnfrName: ARRAY dirLen OF CHAR; (* RNFR parameter *)
- state: LONGINT; (* one of: Ok, RNFR, REST *)
- mode: LONGINT; (* one of: Active, Passive *)
- consecutiveErrors: LONGINT; (* count of consecutive invalid commands *)
- restMarker: LONGINT; (* position in file where next file transfer should start. *)
- pasvListener: TCP.Connection;
- logEntry: LogEntry;
- PROCEDURE TimeoutHandler;
- BEGIN
- logEntry.pending := TRUE;
- COPY("TIMEOUT", logEntry.method); logEntry.uri := "";
- SendMessage(421, "Timeout, closing control connection.");
- IF (pasvListener # NIL) & (pasvListener.state = TCP.Established) THEN pasvListener.Close END;
- client.Close
- END TimeoutHandler;
- PROCEDURE PasvTimeoutHandler;
- BEGIN
- pasvListener.Close
- END PasvTimeoutHandler;
- PROCEDURE ReadCommand(VAR cmd, param: ARRAY OF CHAR);
- VAR i,l: LONGINT; c: CHAR;
- BEGIN
- Objects.SetTimeout(timeout, TimeoutHandler, Timeout);
- in.SkipSpaces;
- i := 0; l := LEN(cmd)-1; c := in.Peek();
- WHILE (i < l) & (c # " ") & (c # CR) & (c # LF) & (in.res = Streams.Ok) DO
- cmd[i] := CAP(in.Get()); INC(i);
- c := in.Peek()
- END;
- cmd[i] := 0X;
- WHILE (c = " ") & (in.res = Streams.Ok) DO c := in.Get(); c := in.Peek() END;
- i := 0; l := LEN(param)-1;
- WHILE (i < l) & (c # CR) & (c # LF) & (in.res = Streams.Ok) DO
- param[i] := in.Get(); INC(i);
- c := in.Peek()
- END;
- param[i] := 0X;
- in.SkipLn();
- Objects.CancelTimeout(timeout)
- END ReadCommand;
- PROCEDURE SendString(str: ARRAY OF CHAR);
- BEGIN
- out.String(str); out.Ln(); out.Update
- END SendString;
- PROCEDURE SendMessage(code: LONGINT; msg: ARRAY OF CHAR);
- BEGIN
- IF logEntry.pending THEN
- logEntry.status := code;W3CLog(logEntry);
- logEntry.result := 0; logEntry.pending := FALSE
- END;
- out.Int(code, 0);out.String(" "); out.String(msg); out.Ln;
- out.Update
- END SendMessage;
- PROCEDURE GetWorkingDirMsg(VAR msg: ARRAY OF CHAR);
- BEGIN
- IF (user.root # "") & (workDir = "") THEN COPY('"/" is current directory.', msg)
- ELSE
- IF (user.root # "") THEN Strings.Concat('"/', workDir, msg)
- ELSE Strings.Concat('"', workDir, msg)
- END;
- Strings.Append(msg, '" is current directory.')
- END
- END GetWorkingDirMsg;
- PROCEDURE GetDirectories(name: ARRAY OF CHAR; VAR usr, system: ARRAY OF CHAR);
- BEGIN
- ComposeDirectory(workDir, name, usr);
- Strings.Concat(user.root, usr, system)
- END GetDirectories;
- PROCEDURE CheckDirectory(name: ARRAY OF CHAR): BOOLEAN;
- VAR prefix: Files.Prefix; path: ARRAY dirLen OF CHAR;
- BEGIN
- Strings.Concat(user.root, name, name); Strings.TrimRight(name, PathDelimiter);
- Files.SplitName(path, prefix, path);
- IF (prefix = "") OR (Files.This(prefix) # NIL) THEN
- RETURN (path = "") OR (Files.Old(name) # NIL)
- ELSE
- RETURN FALSE (* file system not found *)
- END
- END CheckDirectory;
- PROCEDURE Directory(name: ARRAY OF CHAR; full: BOOLEAN);
- VAR data: TCP.Connection; w: Streams.Writer; e: Files.Enumerator; t: Kernel.MilliTimer;
- prefix: Files.Prefix; str: ARRAY 20 OF CHAR; date, size, time: LONGINT; res: WORD; flags: SET; c: CHAR;
- split: BOOLEAN;
- BEGIN
- SendMessage(150, "Opening ASCII mode data connection for file list.");
- IF (mode = Active) THEN
- NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
- dataAdr := client.fip; dataPort := FTPDataPort (*default*)
- ELSE
- ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
- Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
- pasvListener.Accept(data, res);
- pasvListener.Close;
- Objects.CancelTimeout(pasvTimeout);
- mode := Active
- END;
- logEntry.result := res;
- IF res # TCP.Ok THEN
- SendMessage(425, Msg425)
- ELSE
- ComposeDirectory(workDir, name, name); Strings.Concat(user.root, name, name);
- IF (name = "") THEN split := FALSE ELSE split := TRUE END; (* remove prefix & path only if we are not on root level *)
- IF full THEN flags := {Files.EnumSize, Files.EnumTime} ELSE flags := {} END;
- NEW(e); e.Open(name, flags);
- Streams.OpenWriter(w, data.Send);
- WHILE e.GetEntry(name, flags, time, date, size) DO
- IF split THEN Files.SplitPath(name, prefix, name) END; (* remove prefix & path *)
- IF full THEN
- (* format: <flags:10>" "<type:3>" "<user:8>" "<group:8>" "<size:8>" "<month:3>" "<day:2>" "<time:5>" "<filename> *)
- IF (Files.Directory IN flags) THEN c := "d" ELSE c := "-" END;
- w.Char(c);
- w.String("rw-rw-rw-");
- w.String(" 1 Aos Aos ");
- w.Int(size, 8);
- Strings.FormatDateTime(" mmm dd hh:nn ", Dates.OberonToDateTime(date, time), str);
- w.String(str)
- END;
- w.String(name); w.Ln;
- END;
- w.Update;
- SendMessage(226, Msg226);
- e.Close;
- IF (data.state # TCP.Established) THEN (* clients may hang if the data connection is closed before it is established *)
- Kernel.SetTimer(t, 1000);
- WHILE (data.state # TCP.Established) & ~Kernel.Expired(t) DO
- Objects.Yield
- END
- END;
- data.Close
- END
- END Directory;
- PROCEDURE Size(name: ARRAY OF CHAR);
- VAR filename: ARRAY dirLen OF CHAR; f: Files.File;
- BEGIN
- ComposeDirectory(workDir, name, name);
- Strings.Concat(user.root, name, filename);
- f := Files.Old(filename);
- IF (f = NIL) THEN
- Strings.Append(name, ": file not found."); SendMessage(550, name)
- ELSE
- Strings.IntToStr(f.Length(), name); SendMessage(213, name)
- END
- END Size;
- PROCEDURE WaitEstablished(c: TCP.Connection);
- VAR t: Kernel.MilliTimer;
- BEGIN
- IF (c.state # TCP.Established) THEN
- Kernel.SetTimer(t, 500);
- WHILE (c.state # TCP.Established) & ~Kernel.Expired(t) DO
- Objects.Yield
- END
- END
- END WaitEstablished;
- PROCEDURE Retrieve(name: ARRAY OF CHAR; marker: LONGINT);
- VAR data: TCP.Connection; w: Streams.Writer; f: Files.File; r: Files.Reader;
- filename, msg: ARRAY dirLen OF CHAR; res: WORD;
- BEGIN
- IF (type = ASCII) THEN COPY("ASCII", msg) ELSE COPY("Binary", msg) END;
- Strings.Append(msg, " data connection for "); Strings.Append(msg, name);
- logEntry.pending := FALSE;
- SendMessage(150, msg);
- logEntry.pending := TRUE;
- IF (mode = Active) THEN
- NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
- dataAdr := client.fip; dataPort := FTPDataPort; (*default*)
- ELSE
- ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
- Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
- pasvListener.Accept(data, res);
- pasvListener.Close;
- Objects.CancelTimeout(pasvTimeout);
- mode := Active
- END;
- logEntry.result := res;
- IF res # TCP.Ok THEN
- SendMessage(425, Msg425)
- ELSE
- ComposeDirectory(workDir, name, name);
- Strings.Concat(user.root, name, filename);
- Streams.OpenWriter(w, data.Send);
- f := Files.Old(filename);
- WaitEstablished(data); (* clients may hang if the data connection is closed before it is established *)
- IF f = NIL THEN
- Strings.Append(name, ": file not found."); SendMessage(550, name);
- ELSIF (Files.Directory IN f.flags) THEN
- Strings.Append(name, ": is a directory."); SendMessage(550, name)
- ELSE
- Files.OpenReader(r, f, marker);
- IF (type = ASCII) THEN ASCIITransfer(r, w)
- ELSE BinaryTransfer(r, w)
- END;
- IncreaseSent(f.Length());
- SendMessage(226, "Transfer complete.")
- END;
- data.Close
- END
- END Retrieve;
- PROCEDURE Store(name: ARRAY OF CHAR; marker: LONGINT);
- VAR data: TCP.Connection; r: Streams.Reader; f: Files.File; w: Files.Writer;
- filename, msg: ARRAY dirLen OF CHAR; res: WORD;
- BEGIN
- IF (type = ASCII) THEN COPY("ASCII", msg) ELSE COPY("Binary", msg) END;
- Strings.Append(msg, " data connection for "); Strings.Append(msg, name);
- logEntry.pending := FALSE;
- SendMessage(150, msg);
- logEntry.pending := TRUE;
- IF (mode = Active) THEN
- NEW(data); data.Open(FTPDataPort, dataAdr, dataPort, res);
- dataAdr := client.fip; dataPort := FTPDataPort (*default*)
- ELSE
- ASSERT((pasvListener # NIL) & (pasvListener.state = TCP.Listen));
- Objects.SetTimeout(pasvTimeout, PasvTimeoutHandler, PasvTimeout);
- pasvListener.Accept(data, res);
- pasvListener.Close;
- Objects.CancelTimeout(pasvTimeout);
- mode := Active
- END;
- logEntry.result := res;
- IF res # TCP.Ok THEN
- SendMessage(425, Msg425)
- ELSE
- ComposeDirectory(workDir, name, name);
- Strings.Concat(user.root, name, filename);
- Streams.OpenReader(r, data.Receive);
- IF (marker = -1) THEN (* append *)
- f := Files.Old(filename);
- marker := f.Length()
- ELSIF (marker > 0) THEN (* restart *)
- f := Files.Old(filename)
- ELSE
- f := Files.New(filename);
- IF (f # NIL) THEN Files.Register(f) END
- END;
- WaitEstablished(data); (* clients may hang if the data connection is closed before it is established *)
- IF f = NIL THEN
- SendMessage(553, Msg553)
- ELSE
- Files.OpenWriter(w, f, marker);
- IF (type = ASCII) THEN ASCIITransfer(r, w)
- ELSE BinaryTransfer(r, w)
- END;
- f.Update();
- IncreaseReceived(f.Length());
- SendMessage(226, Msg226)
- END;
- data.Close
- END
- END Store;
- PROCEDURE Execute(VAR cmd, param: ARRAY OF CHAR);
- VAR
- tmp, filename, str: ARRAY dirLen OF CHAR;
- i, code, lastState: LONGINT; res: WORD;
- BEGIN
- lastState := state; state := Ok;
- COPY(cmd, logEntry.method); COPY(param, logEntry.uri); logEntry.pending := TRUE;
- code := 550; COPY("Requested action not taken.", str);
- IF shutdown THEN
- code := 421; COPY("Server shutting down, closing control connection.", str);
- quit := TRUE
- ELSIF cmd = "USER" THEN
- COPY(param, logEntry.user);
- user := FindUser(param);
- IF (user # NIL) THEN
- IF UserLogin(user) THEN
- Strings.Concat("Password required for ", param, str); Strings.Append(str, ".");
- code := 331;
- workDir[0] := 0X;
- IF (passwrq IN user.permissions) THEN
- IF (mailpwd IN user.permissions) THEN
- str := "Anonymous access allowed, send identity (e-mail name) as password."
- END
- ELSE code := 230; COPY(Msg230, str); logged := TRUE
- END
- ELSE
- user := NIL; code := 421; COPY("Too many users.", str)
- END
- ELSE
- code := 530; Strings.Concat("Unknown user ", param, str); Strings.Append(str, ".")
- END
- ELSIF cmd = "PASS" THEN
- code := 530; COPY(Msg530, str);
- IF (user # NIL) & (user.name # "") THEN
- IF (mailpwd IN user.permissions) THEN (* password = e-mail address *)
- IF Strings.Match("?*@?*.?*", param) THEN code := 230; COPY(Msg230, str); logged := TRUE END
- ELSE
- logEntry.uri := ""; (* do not log password *)
- IF (Code(param) = user.password) THEN
- code := 230; COPY(Msg230, str); logged := TRUE
- END
- END
- END
- ELSIF cmd = "QUIT" THEN
- code := 221; COPY(Msg221, str); quit := TRUE
- ELSIF cmd = "NOOP" THEN
- code := 220; COPY(Msg220, str)
- ELSIF logged THEN (* these commands are only available if the user is logged in *)
- IF cmd = "CWD" THEN (* change working directory *)
- ComposeDirectory(workDir, param, tmp);
- IF CheckDirectory(tmp) THEN
- COPY(tmp, workDir);
- IF (workDir # "") THEN Files.ForceTrailingDelimiter(workDir) END;
- code := 250; GetWorkingDirMsg(str)
- ELSE
- code := 550; Strings.Concat(param, ": no such file or directory.", str)
- END
- ELSIF (cmd = "CDUP") OR (cmd = "XCUP") THEN (* change to parent directory *)
- ComposeDirectory(workDir, "..", workDir);
- IF (workDir # "") THEN Files.ForceTrailingDelimiter(workDir) END;
- code := 212; GetWorkingDirMsg(str)
- ELSIF (cmd = "PWD") OR (cmd = "XPWD") THEN (* print working directory *)
- code := 257; GetWorkingDirMsg(str)
- ELSIF (cmd = "MKD") OR (cmd = "XMKD") THEN
- IF (write IN user.permissions) THEN
- GetDirectories(param, tmp, filename);
- Files.CreateDirectory(filename, res);
- logEntry.result := res;
- IF (res = 0) THEN
- code := 257; Strings.Concat('"', tmp, str); Strings.Append(str, '": directory successfully created.')
- ELSE
- code := 550; Strings.Concat(tmp, ": failed to create directory", str)
- END
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF (cmd = "RMD") OR (cmd = "XRMD") THEN
- IF (write IN user.permissions) THEN
- GetDirectories(param, tmp, filename);
- Files.RemoveDirectory(filename, FALSE, res);
- logEntry.result := res;
- IF (res = 0) THEN
- code := 257; Strings.Concat('"', tmp, str); Strings.Append(str, '": directory successfully deleted.')
- ELSE code := 550; Strings.Concat(tmp, ": failed to delete directory", str)
- END
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "DELE" THEN
- IF (write IN user.permissions) THEN
- GetDirectories(param, tmp, filename);
- Files.Delete(filename, res);
- logEntry.result := res;
- IF (res = 0) THEN code := 200; Strings.Concat('"', tmp, str); Strings.Append(str, '" deleted.')
- ELSE code := 450; Strings.Concat(tmp, ": cannot delete file.", str)
- END
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "PASV" THEN
- mode := Passive;
- NEW(pasvListener);
- pasvListener.Open(TCP.NilPort, IP.NilAdr, TCP.NilPort, res);
- logEntry.result := res;
- IF (res = IP.Ok) THEN
- IP.AdrToStr(client.int.localAdr, str);
- i := 0; WHILE (str[i] # 0X) DO IF (str[i] = ".") THEN str[i] := "," END; INC(i) END;
- str[i] := ","; str[i+1] := 0X;
- Strings.IntToStr(pasvListener.lport DIV 100H, tmp);
- Strings.Append(str, tmp); Strings.Append(str, ",");
- Strings.IntToStr(pasvListener.lport MOD 100H, tmp);
- Strings.Append(str, tmp);
- Strings.Concat("Entering Passive Mode (", str, str);
- Strings.Append(str, ")");
- code := 227
- ELSE (* 425 is not an official reply, but the only one that makes sense *)
- code := 425; COPY("Can't open data connection.", str)
- END
- ELSIF cmd = "EPSV" THEN
- mode := Passive;
- NEW(pasvListener);
- pasvListener.Open(TCP.NilPort, IP.NilAdr, TCP.NilPort, res);
- logEntry.result := res;
- IF (res = IP.Ok) THEN
- str := "";
- Strings.IntToStr(pasvListener.lport, tmp);
- Strings.Append(str, "Entering Extended Passive Mode (|||");
- Strings.Append(str, tmp);
- Strings.Append(str, "|)");
- code := 229;
- ELSE
- code := 500; COPY("Can't open data connection.", str)
- END
- ELSIF cmd = "SYST" THEN
- code := 215; COPY(Msg215, str)
- ELSIF cmd = "TYPE" THEN
- IF (param = "A") OR (param = "I") THEN
- IF (param = "A") THEN type := ASCII
- ELSE type := IMAGE
- END;
- code := 200; Strings.Concat("Type set to ", param, str)
- ELSE
- code := 504; COPY(Msg504, str)
- END
- ELSIF (cmd = "NLST") OR (cmd = "LIST") THEN
- IF (read IN user.permissions) THEN
- Directory(param, (cmd="LIST")); code := -1
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "PORT" THEN
- SplitPort(param, dataAdr, dataPort);
- code := 200; COPY("PORT command successful.", str)
- ELSIF cmd = "EPRT" THEN
- SplitEPRT(param, dataAdr, dataPort);
- code := 200; COPY("EPRT command successful.", str);
- ELSIF cmd = "SIZE" THEN
- IF (read IN user.permissions) THEN
- Size(param); code := -1
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "REST" THEN
- Strings.StrToInt(param, restMarker); (* traps on invalid strings *)
- IF (restMarker < 0) THEN restMarker := 0 END;
- state := REST;
- code := 350; COPY(Msg350, str)
- ELSIF cmd = "RETR" THEN
- IF (read IN user.permissions) THEN
- IF (lastState # REST) THEN restMarker := 0 END;
- Retrieve(param, restMarker); code := -1
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "STOR" THEN
- IF (write IN user.permissions) THEN
- IF (lastState # REST) THEN restMarker := 0 END;
- Store(param, restMarker); code := -1
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "APPE" THEN
- IF (write IN user.permissions) THEN
- Store(param, -1); code := -1
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "RNFR" THEN
- IF (write IN user.permissions) THEN
- IF (Strings.Pos(PathDelimiter, param) = -1) THEN (* path in filename not allowed *)
- GetDirectories(param, tmp, rnfrName);
- IF (Files.Old(rnfrName) # NIL) THEN
- state := RNFR;
- code := 350; COPY("File found, send new name.", str);
- ELSE
- code := 550; Strings.Concat(param, ": file not found.", str)
- END
- ELSE
- code := 550; Strings.Concat(param, ": invalid filename.", str)
- END
- ELSE
- code := 550; COPY(NoPermissionMsg, str)
- END
- ELSIF cmd = "RNTO" THEN
- IF (lastState = RNFR) THEN
- IF (Strings.Pos(PathDelimiter, param) = -1) THEN (* path in filename not allowed *)
- Files.SplitPath(rnfrName, filename, tmp);
- IF (Strings.Pos(":", filename) = -1) THEN
- Strings.Append(filename, ":");
- ELSE
- Strings.Append(filename, "/");
- END;
- Strings.Append(filename, param);
- Files.Rename(rnfrName, filename, res);
- logEntry.result := res;
- IF (res = 0) THEN
- code := 250; Strings.Concat(param, ": successfully renamed.", str)
- ELSE
- code := 550; Strings.Concat(param, ": renaming failed.", str)
- END
- ELSE
- code := 550; Strings.Concat(param, ": invalid filename.", str)
- END
- ELSE
- code := 530; COPY("Bad sequence of commands.", str)
- END
- ELSIF (cmd = "SITE") THEN
- Strings.UpperCase(param);
- IF (param = "HELP") THEN
- SendString("214-The following SITE commands are recognized (* =>'s unimplemented).");
- SendString(" HELP");
- code := 214; COPY("HELP command successful.", str)
- ELSE
- code := 500; Strings.Concat("SITE ", param, str); Strings.Concat(str, Msg500, str)
- END
- ELSE
- code := 500; Strings.Concat(param, Msg500, str)
- END
- END;
- IF (code > 0) THEN SendMessage(code, str) END;
- IF (code < 200) OR (code >= 300) THEN (* error or positive preliminary/intermediate reply *)
- INC(consecutiveErrors);
- IF (consecutiveErrors = MaxErrors) THEN quit := TRUE END
- ELSE
- consecutiveErrors := 0
- END
- END Execute;
- BEGIN {ACTIVE, SAFE}
- IF ~running THEN
- running := TRUE;
- NEW(timeout); Objects.SetTimeout(timeout, TimeoutHandler, Timeout);
- NEW(pasvTimeout);
- logged := FALSE; quit := FALSE; consecutiveErrors := 0; type := IMAGE;
- dataAdr := client.fip; dataPort := FTPDataPort; (*default*)
- logEntry.user := ""; logEntry.ip := client.fip; logEntry.pending := FALSE;
- Streams.OpenReader(in, client.Receive); Streams.OpenWriter(out, client.Send);
- SendMessage(220, Msg220);
- LOOP
- ReadCommand(cmd, line);
- IF (in.res # Streams.Ok) THEN EXIT END;
- Execute(cmd, line);
- IF (in.res # Streams.Ok) OR quit THEN EXIT END
- END
- ELSE
- (* trapped & restarted *)
- IF (client.state = TCP.Established) & (out.res = Streams.Ok) THEN
- logEntry.pending := TRUE;
- SendMessage(550, "Server Error")
- END
- END;
- IF (pasvListener # NIL) & (pasvListener.state = TCP.Listen) THEN pasvListener.Close END;
- IncreaseReceived(client.rcvnxt-client.irs); IncreaseSent(client.sndnxt-client.iss);
- IncreaseActive(-1);
- UserLogout(user);
- FlushLog;
- Terminate
- END FTPAgent;
- VAR
- Hex: ARRAY 16 OF CHAR;
- ftp : TCPServices.Service;
- users: User;
- shutdown: BOOLEAN; (* shutdown flag *)
- w3cf: Files.File;
- w3cw: Streams.Writer;
- (** statistical counters. #bytes sent/received := NMebiBX * 2**20 + NbytesX *)
- NclientsTotal*, NclientsActive*, NMebiBReceived*, NMebiBSent*, NbytesReceived*, NbytesSent*: LONGINT;
- (* --------- statisitcal counter handling --------------*)
- PROCEDURE IncreaseSent(delta: LONGINT);
- BEGIN {EXCLUSIVE}
- (* wp: delta >= 0 *)
- ASSERT(delta >= 0);
- NbytesSent := NbytesSent + delta;
- NMebiBSent := NMebiBSent + NbytesSent DIV 100000H;
- NbytesSent := NbytesSent MOD 100000H
- END IncreaseSent;
- PROCEDURE IncreaseReceived(delta: LONGINT);
- BEGIN {EXCLUSIVE}
- (* wp: delta >= 0 *)
- ASSERT(delta >= 0);
- NbytesReceived := NbytesReceived + delta;
- NMebiBReceived := NMebiBReceived + NbytesReceived DIV 100000H;
- NbytesReceived := NbytesReceived MOD 100000H
- END IncreaseReceived;
- PROCEDURE IncreaseActive(delta: LONGINT);
- BEGIN {EXCLUSIVE}
- NclientsActive := NclientsActive + delta
- END IncreaseActive;
- (* ------------- Helper Phunctions ----------------- *)
- PROCEDURE IsDigit(ch: CHAR): BOOLEAN;
- BEGIN
- RETURN (ch >= "0") & (ch <= "9")
- END IsDigit;
- PROCEDURE StrToInt(str: ARRAY OF CHAR; VAR val: LONGINT);
- VAR i, d: LONGINT; neg: BOOLEAN;
- BEGIN
- i := 0;
- WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END;
- IF str[i] = "-" THEN
- neg := TRUE; INC(i);
- WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END
- ELSE neg := FALSE END;
- val := 0;
- WHILE (str[i] # 0X) & (str[i] >= "0") & (str[i] <= "9") DO
- d := ORD(str[i])-ORD("0");
- IF val <= ((MAX(LONGINT)-d) DIV 10) THEN val := 10*val+d ELSE HALT(99) END;
- INC(i)
- END;
- IF neg THEN val := -val END
- END StrToInt;
- PROCEDURE StrToIntPos(VAR str: ARRAY OF CHAR; VAR i: INTEGER): LONGINT;
- VAR noStr: ARRAY 16 OF CHAR;
- j: LONGINT;
- BEGIN
- WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END;
- j := 0;
- IF str[i] = "-" THEN
- noStr[j] := str[i];
- INC(j); INC(i);
- WHILE (str[i] # 0X) & (str[i] <= " ") DO INC(i) END
- END;
- WHILE IsDigit(str[i]) DO noStr[j] := str[i]; INC(j); INC(i) END;
- noStr[j] := 0X;
- StrToInt(noStr, j);
- RETURN j
- END StrToIntPos;
- PROCEDURE SplitPort(parm: ARRAY OF CHAR; VAR adr: IP.Adr; VAR port: LONGINT);
- VAR pos: INTEGER; i, n: LONGINT;
- BEGIN
- pos := 0;
- FOR i := 0 TO 3 DO n := StrToIntPos(parm, pos); parm[pos] := '.'; INC(pos) END;
- parm[pos-1] := 0X;
- adr := IP.StrToAdr(parm);
- port := StrToIntPos(parm, pos)*256; INC(pos);
- port := port+StrToIntPos(parm, pos)
- END SplitPort;
- (* Parses the EPRT command *)
- PROCEDURE SplitEPRT(param: ARRAY OF CHAR; VAR adr: IP.Adr; VAR port: LONGINT);
- VAR
- i: LONGINT;
- protocol: LONGINT;
- tempString: ARRAY 128 OF CHAR;
- j: LONGINT;
- BEGIN
- (* read protocol *)
- i := 0;
- WHILE (i < LEN(param)) & (param[i] # "|") DO
- INC(i);
- END;
- IF i < LEN(param) THEN
- protocol := ORD(param[i+1]) - ORD("0");
- END;
- (* parse IP address *)
- i := i+3;
- j := i;
- WHILE (i < LEN(param)) & ((param[i] # "|") & (param[i] # "%")) DO
- INC(i);
- END;
- IF i < LEN(param) THEN
- Strings.Copy(param, j, i-j, tempString);
- END;
- adr := IP.StrToAdr(tempString);
- (* port *)
- IF param[i] = "%" THEN
- WHILE (i < LEN(param)) & (param[i] # "|") DO
- INC(i);
- END;
- END;
- IF i < LEN(param) THEN
- INC(i);
- j := i;
- WHILE (i < LEN(param)) & (param[i] # "|") DO
- INC(i);
- END;
- IF i < LEN(param) THEN
- Strings.Copy(param, j, i-j, tempString);
- StrToInt(tempString, port);
- END;
- END;
- END SplitEPRT;
- PROCEDURE BinaryTransfer(r: Streams.Reader; w: Streams.Writer);
- VAR buf: ARRAY BufSize OF CHAR; len: LONGINT;
- BEGIN
- REPEAT
- r.Bytes(buf, 0, BufSize, len); w.Bytes(buf, 0, len);
- UNTIL r.res # 0;
- w.Update
- END BinaryTransfer;
- PROCEDURE ASCIITransfer(r: Streams.Reader; w: Streams.Writer);
- VAR buf: ARRAY BufSize OF CHAR; i, len: LONGINT; c: CHAR;
- BEGIN
- REPEAT
- r.Bytes(buf, 0, BufSize, len);
- i := 0;
- WHILE (i < len) DO
- c := buf[i];
- IF (c = CR) THEN (* ignore CR *)
- ELSIF (c = LF) THEN w.Ln
- ELSE w.Char(c)
- END;
- INC(i)
- END
- UNTIL (r.res # 0);
- w.Update
- END ASCIITransfer;
- PROCEDURE Code(VAR s: ARRAY OF CHAR): LONGINT;
- VAR i: INTEGER; a, b, c: LONGINT;
- BEGIN
- a := 0; b := 0; i := 0;
- WHILE s[i] # 0X DO
- c := b; b := a; a := (c MOD 509 + 1) * 127 + ORD(s[i]);
- INC(i)
- END;
- IF b >= 32768 THEN b := b - 65536 END;
- RETURN b * 65536 + a
- END Code;
- PROCEDURE ComposeDirectory(path, name: ARRAY OF CHAR; VAR res: ARRAY OF CHAR);
- VAR
- prefix: Files.Prefix; tmp: ARRAY dirLen OF CHAR; p: SIZE;
- absolute : BOOLEAN;
- BEGIN
- COPY(path, res); absolute := PathDelimiter = name[0];
- Strings.TrimRight(res, PathDelimiter);
- Strings.TrimRight(name, PathDelimiter);
- Files.SplitName(name, prefix, tmp);
- IF (prefix # "") OR absolute THEN (* absolute path *)
- COPY(name, res); Strings.TrimLeft(res, PathDelimiter)
- ELSE (* relative path *)
- WHILE (name # "") DO
- p := Strings.Pos(PathDelimiter, name);
- IF (p >= 0) THEN
- Strings.Copy(name, 0, p, tmp);
- Strings.Delete(name, 0, p+1)
- ELSE
- COPY(name, tmp); name[0] := 0X
- END;
- IF (tmp = ".") THEN
- ELSIF (tmp = "..") THEN
- COPY(res, tmp); Strings.TrimRight(tmp, PathDelimiter); Strings.TrimRight(tmp, ":");
- IF (Files.This(tmp) # NIL) THEN (* it's a prefix *)
- COPY("", res)
- ELSE
- Files.SplitPath(res, res, tmp); Strings.TrimRight(res, PathDelimiter)
- END
- ELSE
- IF (res # "") THEN Strings.Append(res, PathDelimiter) END;
- Strings.Append(res, tmp)
- END
- END
- END
- END ComposeDirectory;
- (** ------------- TCP Service Handling ----------------- *)
- PROCEDURE NewFTPAgent(c: TCP.Connection; s: TCPServices.Service): TCPServices.Agent;
- VAR a: FTPAgent;
- BEGIN
- INC(NclientsTotal); INC(NclientsActive);
- NEW(a, c, s); RETURN a
- END NewFTPAgent;
- PROCEDURE Start*(context : Commands.Context); (** ["\l:" log file] *)
- VAR c, opt: CHAR; str, log: ARRAY 1024 OF CHAR; res : WORD;
- BEGIN
- IF ftp = NIL THEN
- COPY(LogFile, log);
- context.arg.SkipWhitespace;
- LOOP
- c := context.arg.Get();
- IF (c # "\") THEN EXIT END;
- opt := CAP(context.arg.Get());
- c := context.arg.Get();
- IF (c # ":") THEN EXIT END;
- context.arg.SkipWhitespace;
- context.arg.String(str);
- context.arg.SkipWhitespace;
- CASE opt OF
- | "L": COPY(str, log)
- ELSE EXIT
- END
- END;
- LoadUsers(users);
- shutdown := FALSE;
- NclientsTotal := 0; NclientsActive := 0; NbytesReceived := 0; NbytesSent := 0;
- OpenW3CLog(log);
- NEW(ftp, FTPControlPort, NewFTPAgent, res);
- IF (res = TCPServices.Ok) THEN
- KernelLog.Enter; KernelLog.String("WebFTPServer started"); KernelLog.Exit;
- context.out.String("WebFTPServer started"); context.out.Ln;
- ELSE
- context.error.String("WebFTPServer not started, res: "); context.error.Int(res, 0); context.error.Ln;
- END;
- ELSE
- context.out.String("WebFTPServer is already running."); context.out.Ln;
- END;
- END Start;
- PROCEDURE Stop*(context : Commands.Context);
- BEGIN
- IF ftp # NIL THEN
- shutdown := TRUE;
- ftp.Stop; ftp := NIL;
- KernelLog.Enter; KernelLog.String("WebFTPServer closed"); KernelLog.Exit;
- IF (context # NIL) THEN context.out.String("WebFTPServer closed."); context.out.Ln; END;
- ELSE
- IF (context # NIL) THEN context.out.String("WebFTPServer is not running."); context.out.Ln; END;
- END;
- END Stop;
- (** ------------- User Handling ----------------- *)
- (** Adds a user to the user file. Syntax:
- Aos.Call FTPServer.AddUser <name> <password> <max concurrent logins> <permissions> [<root>] ~
- name = string, may not contain spaces
- password = string, must be enquoted if it contains spaces
- max concurrent logins = integer. if = -1 then any number of concurrent logins are allowed
- permissions = ["R"]["W"]["P"["M"]];
- R = user has read permissions
- W = user has write permissions
- P = user must supply a password
- M = password is an e-mail address
- root = valid file system prefix, may include a path (if it does, do not forget the trailing backslash!)
- *)
- PROCEDURE AddUser*(context : Commands.Context);
- VAR
- username, permissions: ARRAY nameLen OF CHAR; root: ARRAY dirLen OF CHAR;
- password: ARRAY pwdLen+1 OF CHAR;
- user: User; i, maxlogins: LONGINT;
- BEGIN {EXCLUSIVE}
- context.arg.SkipWhitespace;
- context.arg.Token(username); context.arg.SkipWhitespace;
- context.arg.String(password); context.arg.SkipWhitespace;
- context.arg.Int(maxlogins, FALSE); context.arg.SkipWhitespace;
- context.arg.Token(permissions); context.arg.SkipWhitespace;
- context.arg.String(root);
- IF (username # "") & (password # "") & (maxlogins # 0) & (permissions # "") THEN
- IF (FindUser(username) = NIL) THEN
- NEW(user);
- COPY(username, user.name); user.password := Code(password); user.maxlogins := maxlogins;
- user.permissions := {}; COPY(root, user.root);
- i := 0;
- WHILE (permissions[i] # 0X) DO
- IF (CAP(permissions[i]) = "R") THEN INCL(user.permissions, read)
- ELSIF (CAP(permissions[i]) = "W") THEN INCL(user.permissions, write)
- ELSIF (CAP(permissions[i]) = "P") THEN INCL(user.permissions, passwrq)
- ELSIF (CAP(permissions[i]) = "M") THEN INCL(user.permissions, mailpwd)
- ELSE
- context.error.String("AddUser: Invalid permissions"); context.error.Ln;
- RETURN;
- END;
- INC(i)
- END;
- user.next := users; users := user;
- StoreUsers(users, context);
- context.out.String(moduleName); context.out.String("User '"); context.out.String(username);
- context.out.String("' added. Max concurrent logins = ");
- IF (user.maxlogins < 0) THEN context.out.String("unlimited") ELSE context.out.Int(user.maxlogins, 0) END;
- context.out.String("; permissions = "); context.out.String(permissions); context.out.String(", root = '"); context.out.String(root);
- context.out.Char("'"); context.out.Ln;
- ELSE
- context.error.String(moduleName); context.error.String("User '"); context.error.String(username); context.error.String("' already exists.");
- context.error.Ln;
- END
- ELSE
- context.error.String(moduleName);
- context.error.String("Expected parameters: <username> <password> <maxlogins> <permissions>"); context.error.Ln;
- END;
- END AddUser;
- (** Removes a user from the user file. Syntax:
- Aos.Call FTPServer.RemoveUser <name> ~
- name = string, may not contain spaces
- *)
- PROCEDURE RemoveUser*(context : Commands.Context);
- VAR prev, u: User; name: ARRAY nameLen OF CHAR; nofRemovals : LONGINT;
- BEGIN {EXCLUSIVE}
- context.arg.SkipWhitespace; context.arg.Token(name);
- IF (name # "") THEN
- context.out.String(moduleName); context.out.String("Removing user '"); context.out.String(name); context.out.String("'... ");
- context.out.Update;
- nofRemovals := 0;
- u := users; prev := NIL;
- WHILE (u # NIL) DO
- IF (u.name = name) THEN
- INC(nofRemovals);
- IF (prev = NIL) THEN users := u.next
- ELSE prev.next := u.next
- END
- END;
- prev := u; u := u.next
- END;
- IF (nofRemovals = 1) THEN
- context.out.String("done.");
- ELSIF (nofRemovals > 1) THEN
- context.out.String(" removed "); context.out.Int(nofRemovals, 0); context.out.String(" times, done.");
- ELSE
- context.out.String(" user not found.");
- END;
- context.out.Ln;
- StoreUsers(users, context)
- ELSE
- context.error.String("RemoveUser: invalid parameters"); context.error.Ln;
- END;
- END RemoveUser;
- PROCEDURE ListUsers*(context : Commands.Context);
- VAR user: User;
- BEGIN {EXCLUSIVE}
- context.out.String(moduleName); context.out.String("Registered users:"); context.out.Ln;
- IF (users # NIL) THEN
- user := users;
- WHILE (user # NIL) DO
- context.out.String(" "); context.out.String(user.name);
- IF (passwrq IN user.permissions) THEN context.out.String("; password-protected login") END;
- IF (mailpwd IN user.permissions) THEN context.out.String("; password = e-mail address") END;
- context.out.Ln;
- context.out.String(" currently active: "); context.out.Int(user.currentlogins, 0);
- context.out.String("; max logins: ");
- IF (user.maxlogins < 0) THEN context.out.String("unlimited") ELSE context.out.Int(user.maxlogins, 0) END;
- context.out.Ln;
- context.out.String(" root = '"); context.out.String(user.root); context.out.String("'; permissions: ");
- IF (read IN user.permissions) THEN context.out.Char("R") END;
- IF (write IN user.permissions) THEN context.out.Char("W") END;
- context.out.Ln;
- user := user.next
- END
- ELSE
- context.out.String("no users"); context.out.Ln;
- END;
- END ListUsers;
- PROCEDURE LoadUsers(VAR users: User);
- VAR u: User; f: Files.File; r: Files.Reader;
- BEGIN
- users := NIL;
- f := Files.Old(UserFile);
- IF (f # NIL) THEN
- Files.OpenReader(r, f, 0);
- WHILE (r.res = Streams.Ok) DO
- NEW(u);
- r.RawString(u.name); r.RawLInt(u.password); r.RawLInt(u.maxlogins);
- r.RawSet(u.permissions); r.RawString(u.root);
- IF (r.res = Streams.Ok) THEN
- u.next := users;
- users := u
- END
- END
- END
- END LoadUsers;
- PROCEDURE StoreUsers(users: User; context : Commands.Context);
- VAR f: Files.File; w: Files.Writer;
- BEGIN
- f := Files.New(UserFile);
- IF (f # NIL) THEN
- Files.OpenWriter(w, f, 0);
- WHILE (w.res = Streams.Ok) & (users # NIL) DO
- w.RawString(users.name); w.RawLInt(users.password); w.RawLInt(users.maxlogins);
- w.RawSet(users.permissions); w.RawString(users.root);
- users := users.next
- END;
- IF (w.res = Streams.Ok) THEN
- w.Update;
- Files.Register(f)
- END
- ELSE
- context.error.String(moduleName); context.error.String("can't write user file"); context.error.Ln;
- END
- END StoreUsers;
- PROCEDURE FindUser(name: ARRAY OF CHAR): User;
- VAR u: User;
- BEGIN
- u := users;
- WHILE (u # NIL) & (u.name # name) DO u := u.next END;
- RETURN u
- END FindUser;
- PROCEDURE UserLogin(user: User): BOOLEAN;
- BEGIN {EXCLUSIVE}
- IF (user # NIL) & ((user.currentlogins < user.maxlogins) OR (user.maxlogins = -1)) THEN
- INC(user.currentlogins);
- RETURN TRUE
- ELSE
- RETURN FALSE
- END
- END UserLogin;
- PROCEDURE UserLogout(user: User);
- BEGIN {EXCLUSIVE}
- IF (user # NIL) THEN
- IF (user.currentlogins > 0) THEN
- DEC(user.currentlogins)
- ELSE
- KernelLog.Enter; KernelLog.String(moduleName); KernelLog.String("warning: user count <= 0. user: "); KernelLog.String(user.name);
- KernelLog.String("; #active: "); KernelLog.Int(user.currentlogins, 0); KernelLog.Exit
- END
- END
- END UserLogout;
- (* ------------- Logging ----------------- *)
- PROCEDURE OpenW3CLog(fn: ARRAY OF CHAR);
- VAR w : Files.Writer;
- BEGIN
- w3cf := Files.Old(fn);
- IF w3cf = NIL THEN
- w3cf := Files.New(fn);
- IF (w3cf # NIL) THEN
- Files.OpenWriter(w, w3cf, 0);
- w.String("#Version: 1.0"); w.Ln;
- w.String("#Fields: date"); w.Char(Tab);
- w.String("time"); w.Char(Tab);
- w.String("x-user"); w.Char(Tab);
- w.String("c-ip"); w.Char(Tab);
- w.String("cs-method"); w.Char(Tab);
- w.String("cs-uri"); w.Char(Tab);
- w.String("sc-status"); w.Char(Tab);
- w.String("x-result");
- w.Ln;
- w.Update;
- Files.Register(w3cf)
- ELSE
- KernelLog.Enter; KernelLog.String(moduleName); KernelLog.String("cannot open log file '"); KernelLog.String(fn); KernelLog.Char("'"); KernelLog.Exit
- END
- ELSE
- Files.OpenWriter(w, w3cf, w3cf.Length())
- END;
- w3cw := w;
- END OpenW3CLog;
- PROCEDURE W3CLog(e: LogEntry);
- VAR s: ARRAY 36 OF CHAR;
- PROCEDURE ToURI(ascii: ARRAY OF CHAR; VAR uri: ARRAY OF CHAR); (* cf. RFC 1738 *)
- VAR i,k,l: LONGINT; c: CHAR;
- BEGIN
- i := 0; k := 0; l := LEN(uri)-1;
- WHILE (k < l) & (ascii[i] # 0X) DO
- c := ascii[i];
- IF (("A" <= CAP(c)) & (CAP(c) <= "Z")) OR (("0" <= c) & (c <= "9")) OR
- (c = "$") OR (c = "-") OR (c = "_") OR (c = ".") OR (c = "+") OR
- (c = "!") OR (c = "*") OR (c = "'") OR (c = "(") OR (c = ")") OR (c = ",")
- THEN
- uri[k] := c; INC(k)
- ELSIF (k < l-2) THEN
- uri[k] := "%"; INC(k);
- uri[k] := Hex[ORD(c) DIV 10H]; INC(k);
- uri[k] := Hex[ORD(c) MOD 10H]; INC(k)
- ELSE
- ascii[i+1] := 0X (* abort, uri too short *)
- END;
- INC(i)
- END;
- uri[k] := 0X
- END ToURI;
- BEGIN {EXCLUSIVE}
- IF (w3cf = NIL) THEN RETURN END;
- Strings.FormatDateTime("yyyy-mm-dd", Dates.Now(), s);
- w3cw.String(s); w3cw.Char(Tab);
- Strings.FormatDateTime("hh:nn:ss", Dates.Now(), s);
- w3cw.String(s); w3cw.Char(Tab);
- w3cw.String(e.user); w3cw.Char(Tab);
- IP.AdrToStr(e.ip, s);
- w3cw.String(s); w3cw.Char(Tab);
- w3cw.String(e.method); w3cw.Char(Tab);
- ToURI(e.uri, e.uri);
- w3cw.String(e.uri); w3cw.Char(Tab);
- Strings.IntToStr(e.status, s);
- w3cw.String(s); w3cw.Char(Tab);
- Strings.IntToStr(e.result, s);
- w3cw.String(s);
- w3cw.Ln
- END W3CLog;
- PROCEDURE FlushLog*;
- BEGIN {EXCLUSIVE}
- IF (w3cf # NIL) THEN
- w3cw.Update; w3cf.Update
- END
- END FlushLog;
- PROCEDURE Cleanup;
- BEGIN
- Stop(NIL);
- END Cleanup;
- BEGIN
- Hex[0] := "0"; Hex[1] := "1"; Hex[2] := "2"; Hex[3] := "3";
- Hex[4] := "4"; Hex[5] := "5"; Hex[6] := "6"; Hex[7] := "7";
- Hex[8] := "8"; Hex[9] := "9"; Hex[10] := "A"; Hex[11] := "B";
- Hex[12] := "C"; Hex[13] := "D"; Hex[14] := "2"; Hex[15] := "E";
- Modules.InstallTermHandler(Cleanup)
- END WebFTPServer.
- Aos.Call WebFTPServer.Start ~\l:FAT:/logs/FTP.Log~
- Aos.Call WebFTPServer.Stop
- NetTracker.CloseAll
- System.Free WebFTPServer ~
- ET.OpenAscii FTP.Log ~
- Aos.Call WebFTPServer.AddUser user password -1 rwp FAT:~
- Aos.Call WebFTPServer.AddUser anonymous none 3 rwpm FAT:/ftproot/ ~
- Aos.Call WebFTPServer.RemoveUser begger ~
- Aos.Call WebFTPServer.ListUsers
- System.DeleteFiles WebFTPUsers.dat ~ deletes all users
|