diff --git a/src/ftp.c b/src/ftp.c index 29c970e..a8a1671 100644 --- a/src/ftp.c +++ b/src/ftp.c @@ -38,7 +38,7 @@ int ftplogin(struct clientparam *param, char *nbuf, int *innbuf) { if(i < 3) return 706; buf[i] = 0; if(atoi(buf)/100 != 2) { - *innbuf = i; + if(innbuf)*innbuf = i; return 702; } sprintf(buf, "USER %.128s\r\n", *param->extusername?param->extusername:(unsigned char *)"anonymous"); @@ -74,6 +74,9 @@ int ftplogin(struct clientparam *param, char *nbuf, int *innbuf) { return 701; } } + /* USER answered at once, logged in (230) or refused: that answer is + the client's, waiting for one to its PASS */ + else if(innbuf)*innbuf = i; if(res != 2) { return 700; } diff --git a/tests/cases/ftppr.py b/tests/cases/ftppr.py new file mode 100644 index 0000000..0fd9837 --- /dev/null +++ b/tests/cases/ftppr.py @@ -0,0 +1,106 @@ +"""The FTP proxy logging in to its server: whatever the server answers the +login with reaches the client, which waits for the answer to its PASS.""" + +import socket +import threading + +# what the server answers USER with, and PASS if it asks for one +LOGINS = { + "230 to USER": ([b"230 Anonymous user logged in"], None), + "multiline 230 to USER": ([b"230-Welcome", b"230-to the server", b"230 Login successful."], None), + "331 to USER, 230 to PASS": ([b"331 Please specify the password."], [b"230 Login successful."]), + "530 to USER": ([b"530 Not allowed here"], None), + "331 to USER, 530 to PASS": ([b"331 Please specify the password."], [b"530 Login incorrect."]), +} + + +def ftp_server(listener, user, password): + """Greets, answers the login as told, then NOOP and QUIT.""" + while True: + try: + c, _ = listener.accept() + except OSError: + return + try: + f = c.makefile("rb") + c.sendall(b"220 Fake FTP\r\n") + if not f.readline().upper().startswith(b"USER "): + continue + c.sendall(b"".join(line + b"\r\n" for line in user)) + if password is not None: + if not f.readline().upper().startswith(b"PASS "): + continue + c.sendall(b"".join(line + b"\r\n" for line in password)) + for line in f: + if line.upper().startswith(b"NOOP"): + c.sendall(b"200 NOOP ok\r\n") + elif line.upper().startswith(b"QUIT"): + c.sendall(b"221 Bye\r\n") + break + else: + c.sendall(b"502 Not implemented\r\n") + except OSError: + pass + finally: + c.close() + + +def reply(f): + """The last line of a reply, multiline or not; empty at the end or on a + time out.""" + while True: + try: + line = f.readline() + except OSError: + return b"" + if not line: + return b"" + if len(line) < 4 or line[3:4] != b"-": + return line.rstrip() + + +def run(t): + servers = {} + for name, (user, password) in LOGINS.items(): + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen(5) + threading.Thread(target=ftp_server, args=(listener, user, password), daemon=True).start() + servers[name] = listener + + port = t.free_port() + t.start("ftppr", f""" + log + auth none + ftppr -p{port} + """, ports=[port]) + + # without WITH_FTP the service closes the connection at once + s = socket.create_connection(("127.0.0.1", port), timeout=5) + built = reply(s.makefile("rb")) + s.close() + if not built: + t.skip("the FTP proxy (not built in this configuration)") + for listener in servers.values(): + listener.close() + t.stop_all() + return + + for name, listener in servers.items(): + s = socket.create_connection(("127.0.0.1", port), timeout=5) + f = s.makefile("rb") + reply(f) + s.sendall(b"USER anonymous@127.0.0.1:%d\r\n" % listener.getsockname()[1]) + reply(f) + s.sendall(b"PASS test@example.com\r\n") + got = reply(f) + ok = LOGINS[name][1] if LOGINS[name][1] is not None else LOGINS[name][0] + t.eq(ok[-1][:3], got[:3], f"{name}: the client gets the server's answer") + if got.startswith(b"230"): + s.sendall(b"NOOP\r\n") + t.eq(b"200", reply(f)[:3], f"{name}: and the session goes on") + s.close() + + for listener in servers.values(): + listener.close() + t.stop_all()