이 문서에서는 Python의 모듈을 활용하여 기본적인 FTP 클라이언트와 서버를 구현하는 방법을 살펴봅니다. 주요 내용은 원격 명령 실행, 파일 전송, 인증 처리 등을 포함합니다. 예제 코드는 교육 목적으로 간소화되었으며, 실제 환경에서는 보안 및 오류 처리를 강화해야 합니다.
서버 측 구현: socketserver 기반
서버는 socketserver.TCPServer를 사용하여 다중 클라이언트 요청을 처리합니다. 각 클라이언트 연결에 대해 BaseRequestHandler를 상속받은 핸들러가 동작합니다.
import socketserver
import json
import os
import hashlib
import configparser
from conf import settings
STATUS_CODE = {
250: "Invalid cmd format",
251: "Invalid cmd",
252: "Invalid auth data",
253: "Wrong username or password",
254: "Passed authentication",
255: "Filename not provided",
256: "File not found",
257: "Ready to send file",
258: "MD5 verification",
200: "OK"
}
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
raw_data = self.request.recv(1024).strip()
if not raw_data:
break
data = json.loads(raw_data.decode())
action = data.get("action", "")
handler = getattr(self, f"_{action}", None)
if handler:
handler(data)
else:
self.send_response(250)
def send_response(self, code, extra=None):
resp = {"status_code": code, "message": STATUS_CODE[code]}
if extra:
resp.update(extra)
self.request.send(json.dumps(resp).encode())
def _auth(self, data):
user = self.authenticate(data.get("username"), data.get("password"))
if user:
self.user = user
self.send_response(254)
else:
self.send_response(253)
def authenticate(self, username, password):
cfg = configparser.ConfigParser()
cfg.read(settings.ACCOUNT_FILE)
if username in cfg.sections() and cfg[username]["Password"] == password:
cfg[username]["User"] = username
return cfg[username]
return None
def _get(self, data):
filename = data.get("filename")
if not filename:
self.send_response(255)
return
abs_path = os.path.join(settings.USER_HOME, self.user["User"], filename)
if os.path.isfile(abs_path):
fsize = os.path.getsize(abs_path)
self.send_response(257, {"file_size": fsize})
self.request.recv(1) # 클라이언트 확인 대기
with open(abs_path, "rb") as f:
md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
self.request.send(chunk)
md5.update(chunk)
self.request.send(md5.hexdigest().encode())
else:
self.send_response(256)
if __name__ == "__main__":
server = socketserver.TCPServer(("0.0.0.0", 2121), MyHandler)
server.serve_forever()
클라이언트 측 구현
클라이언트는 명령줄 인자를 받아 서버에 연결하고, 사용자 인증 후 파일 다운로드 등의 작업을 수행합니다.
import socket
import json
import hashlib
import optparse
class FTPClient:
def __init__(self):
parser = optparse.OptionParser()
parser.add_option("-s", "--server", dest="server", default="localhost")
parser.add_option("-P", "--port", dest="port", type="int", default=2121)
parser.add_option("-u", "--username", dest="username")
parser.add_option("-p", "--password", dest="password")
self.options, _ = parser.parse_args()
self.sock = socket.socket()
self.sock.connect((self.options.server, self.options.port))
def auth(self):
user = self.options.username or input("Username: ")
pwd = self.options.password or input("Password: ")
payload = {"action": "auth", "username": user, "password": pwd}
self.sock.send(json.dumps(payload).encode())
resp = json.loads(self.sock.recv(1024).decode())
if resp.get("status_code") == 254:
self.user = user
print("Authentication success")
return True
print(resp.get("message"))
return False
def download(self, filename):
payload = {"action": "get", "filename": filename}
self.sock.send(json.dumps(payload).encode())
resp = json.loads(self.sock.recv(1024).decode())
if resp.get("status_code") != 257:
print(resp.get("message"))
return
fsize = resp["file_size"]
self.sock.send(b"1")
received = 0
md5 = hashlib.md5()
with open(filename, "wb") as f:
while received < fsize:
chunk = self.sock.recv(4096)
f.write(chunk)
md5.update(chunk)
received += len(chunk)
remote_md5 = self.sock.recv(32).decode()
if md5.hexdigest() == remote_md5:
print("File integrity verified")
else:
print("MD5 mismatch")
def interactive(self):
if not self.auth():
return
while True:
cmd = input(f"[{self.user}]> ").strip()
if not cmd:
continue
parts = cmd.split()
if parts[0] == "get" and len(parts) == 2:
self.download(parts[1])
elif parts[0] == "quit":
break
else:
print("Available commands: get <file>, quit")
if __name__ == "__main__":
client = FTPClient()
client.interactive()
핵심 포인트
- 프로토콜 설계: JSON 기반의 요청/응답 구조를 사용하여 명령과 상태 코드를 교환합니다. 각 명령은
"action"키로 구분됩니다. - 파일 전송:
get명령은 파일 크기를 먼저 전송하고, 이후 바이너리 데이터를 스트리밍합니다. MD5 해시를 통해 무결성을 검증합니다. - 사용자 인증: 설정 파일(ini 형식)에 저장된 사용자 정보를 확인합니다. 실제 배포 시에는 비밀번호 해싱이 필요합니다.
확장 가능성
- 업로드 기능:
put명령을 추가하여 클라이언트의 파일을 서버로 전송할 수 있습니다. - 디렉터리 목록:
ls명령으로 서버의 파일 목록을 조회하는 기능을 구현할 수 있습니다. - 멀티스레드 처리:
socketserver.ThreadingTCPServer를 사용하면 동시 접속을 처리할 수 있습니다.