Compare commits
No commits in common. "865529c402afe77e5d42d70392e68e21a4d9b498" and "73b4e06d02642ff3fa7ad87c7cd277f1bbcee035" have entirely different histories.
865529c402
...
73b4e06d02
@ -9,10 +9,9 @@ WORKDIR /server
|
|||||||
|
|
||||||
# Create files with random content in the /server directory
|
# Create files with random content in the /server directory
|
||||||
RUN mkdir /server_directory
|
RUN mkdir /server_directory
|
||||||
|
# these files is only 750 bytes
|
||||||
# These files is only 750 bytes, can fit inside a UDP/TCP packet
|
|
||||||
RUN dd if=/dev/urandom of=/server_directory/file_server.txt bs=1 count=750
|
RUN dd if=/dev/urandom of=/server_directory/file_server.txt bs=1 count=750
|
||||||
RUN dd if=/dev/urandom of=/server_directory/image_server.png bs=1 count=750
|
RUN dd if=/dev/urandom of=/server_directory/image_server.png bs=1 count=750
|
||||||
|
|
||||||
# generate a file that has random numbers
|
# Start your Python application
|
||||||
RUN for _ in $(seq 1 150); do shuf -i 0-1000 -n 1 >> /server_directory/numbers.txt; done
|
#CMD python server.py --port_number 12000 --debug 1
|
||||||
|
5
Makefile
5
Makefile
@ -15,8 +15,3 @@ client-no-debug:
|
|||||||
clean:
|
clean:
|
||||||
docker-compose down --volumes
|
docker-compose down --volumes
|
||||||
|
|
||||||
docker-client:
|
|
||||||
docker exec -it project-ftp_client-1 bash
|
|
||||||
|
|
||||||
docker-server:
|
|
||||||
docker exec -it project-ftp_server-1 bash
|
|
||||||
|
23
README.md
23
README.md
@ -1,35 +1,20 @@
|
|||||||
# MyFTP
|
# MyFTP
|
||||||
|
|
||||||
A Python implementation of a FTP server. Supports both TCP and UDP protocols. Tested on Python 3.11 and Python 3.10.
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Zero. Only python standard libs were used.
|
Zero. Only python standard libs were used. Tested on Python 3.11
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
||||||
You can run `python3 src/myftp/client.py --directory <insert valid directory that you have read/write permissions>` to start the client.
|
You can run `python3 src/myftp/client.py --directory <insert valid directory that you have read/write permissions>` to start the client or `python3 src/myftp/client.py --debug 1 --directory <insert valid directory that you have read/write permissions>` for debugging purposes.
|
||||||
|
|
||||||
To run with debug info: `python3 src/myftp/client.py --debug 1 --directory <insert valid directory that you have read/write permissions>`.
|
|
||||||
|
|
||||||
Some example test commands:
|
|
||||||
|
|
||||||
- `get file_server.txt`
|
|
||||||
- `summary numbers.txt`
|
|
||||||
- `put file_local.txt`
|
|
||||||
- `put image_local.png`
|
|
||||||
- `change file_server.txt file_server1.txt`
|
|
||||||
- `help`
|
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
By default, the server IP address or hostname or server name will be `0.0.0.0` or `localhost` (meaning it will bind to all interfaces). The `--port_number` flag, if not specified will be by default `12000`.
|
By default, the server IP address or hostname or server name will be `0.0.0.0` or `localhost` (meaning it will bind to all interfaces). The `--port_number` flag, if not specified will be by default `12000`.
|
||||||
|
|
||||||
You can run `python3 src/myftp/server.py --directory <insert valid directory that you have read/write permissions>` to start the server.
|
You can run `python3 src/myftp/server.py --directory <insert valid directory that you have read/write permissions>` to start the server or `python3 src/myftp/server.py --ip_addr <insert ip addr of the server> --port_number <insert port number here> --debug 1 --directory <insert valid directory that you have read/write permissions>` for debugging purposes and to specify the port number.
|
||||||
|
|
||||||
Or run `python3 src/myftp/server.py --ip_addr <insert ip addr of the server> --port_number <insert port number here> --debug 1 --directory <insert valid directory that you have read/write permissions>` for debugging purposes.
|
|
||||||
|
|
||||||
## Localhost testing
|
## Localhost testing
|
||||||
|
|
||||||
@ -61,8 +46,6 @@ Run `python3 src/myftp/server.py --debug 1 --directory server_directory`
|
|||||||
- Run the server with `make server` in a terminal.
|
- Run the server with `make server` in a terminal.
|
||||||
- Run the client with `make client` in a terminal.
|
- Run the client with `make client` in a terminal.
|
||||||
- For the client, when asked to put in the ip address and port number of the server, you can put in `ftp_server 12000` or adjust to your chosen port number. The IP address is resolved by Docker so ftp_server can not be changed.
|
- For the client, when asked to put in the ip address and port number of the server, you can put in `ftp_server 12000` or adjust to your chosen port number. The IP address is resolved by Docker so ftp_server can not be changed.
|
||||||
- Go into the `client` docker container with `make docker-client`. The folder in which FTP is using to host client files is located at `/client_directory/`
|
|
||||||
- Or go into the `server` docker container with `make docker-server`. The folder in which FTP is using to host server files is located at `/server_directory/`
|
|
||||||
- Tear down everything with `make clean`.
|
- Tear down everything with `make clean`.
|
||||||
|
|
||||||
#### Fast setup
|
#### Fast setup
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
970
|
|
||||||
408
|
|
||||||
627
|
|
||||||
774
|
|
||||||
374
|
|
||||||
425
|
|
||||||
292
|
|
||||||
570
|
|
||||||
681
|
|
||||||
971
|
|
||||||
141
|
|
||||||
468
|
|
||||||
271
|
|
||||||
120
|
|
||||||
450
|
|
||||||
298
|
|
||||||
802
|
|
||||||
34
|
|
||||||
448
|
|
||||||
979
|
|
||||||
181
|
|
||||||
117
|
|
||||||
322
|
|
||||||
885
|
|
||||||
733
|
|
||||||
31
|
|
||||||
818
|
|
||||||
582
|
|
||||||
83
|
|
||||||
524
|
|
||||||
104
|
|
||||||
285
|
|
||||||
932
|
|
||||||
964
|
|
||||||
292
|
|
||||||
306
|
|
||||||
621
|
|
||||||
584
|
|
||||||
109
|
|
||||||
302
|
|
||||||
555
|
|
||||||
250
|
|
||||||
2
|
|
||||||
59
|
|
||||||
602
|
|
||||||
452
|
|
||||||
357
|
|
||||||
404
|
|
||||||
486
|
|
||||||
37
|
|
||||||
616
|
|
||||||
667
|
|
||||||
154
|
|
||||||
938
|
|
||||||
552
|
|
||||||
887
|
|
||||||
969
|
|
||||||
602
|
|
||||||
701
|
|
||||||
284
|
|
||||||
358
|
|
||||||
37
|
|
||||||
802
|
|
||||||
290
|
|
||||||
1
|
|
||||||
94
|
|
||||||
829
|
|
||||||
854
|
|
||||||
678
|
|
||||||
938
|
|
||||||
156
|
|
||||||
466
|
|
||||||
420
|
|
||||||
391
|
|
||||||
757
|
|
||||||
22
|
|
||||||
843
|
|
||||||
346
|
|
||||||
658
|
|
||||||
330
|
|
||||||
383
|
|
||||||
274
|
|
||||||
229
|
|
||||||
537
|
|
||||||
444
|
|
||||||
14
|
|
||||||
656
|
|
||||||
646
|
|
||||||
616
|
|
||||||
357
|
|
||||||
930
|
|
||||||
975
|
|
||||||
626
|
|
||||||
964
|
|
||||||
497
|
|
||||||
860
|
|
||||||
290
|
|
||||||
558
|
|
||||||
714
|
|
||||||
969
|
|
||||||
496
|
|
||||||
871
|
|
||||||
667
|
|
||||||
148
|
|
||||||
262
|
|
||||||
424
|
|
||||||
402
|
|
||||||
337
|
|
||||||
770
|
|
||||||
61
|
|
||||||
899
|
|
||||||
153
|
|
||||||
567
|
|
||||||
129
|
|
||||||
922
|
|
||||||
12
|
|
||||||
375
|
|
||||||
810
|
|
||||||
658
|
|
||||||
991
|
|
||||||
399
|
|
||||||
820
|
|
||||||
198
|
|
||||||
25
|
|
||||||
17
|
|
||||||
696
|
|
||||||
885
|
|
||||||
307
|
|
||||||
254
|
|
||||||
600
|
|
||||||
508
|
|
||||||
983
|
|
||||||
703
|
|
||||||
175
|
|
||||||
363
|
|
||||||
197
|
|
||||||
831
|
|
||||||
998
|
|
||||||
534
|
|
||||||
833
|
|
||||||
291
|
|
||||||
434
|
|
||||||
218
|
|
||||||
858
|
|
||||||
795
|
|
||||||
140
|
|
||||||
102
|
|
||||||
170
|
|
||||||
182
|
|
||||||
992
|
|
@ -3,7 +3,7 @@
|
|||||||
# Description: FTP client (both UDP and TCP implemented)
|
# Description: FTP client (both UDP and TCP implemented)
|
||||||
|
|
||||||
|
|
||||||
from socket import socket, AF_INET, SOCK_DGRAM, SOCK_STREAM
|
from socket import socket, AF_INET, SOCK_DGRAM
|
||||||
from typing import Pattern, Tuple, Optional
|
from typing import Pattern, Tuple, Optional
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
import traceback
|
import traceback
|
||||||
@ -11,15 +11,12 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
# Patterns for command matchings
|
# patterns for command matchings
|
||||||
# - compiled for extra performance
|
# compiled for extra performance
|
||||||
# - Ignore case, get or GET both works
|
get_command_pattern: Pattern = re.compile(r"^get\s+[^\s]+$")
|
||||||
get_command_pattern: Pattern = re.compile(r"^get\s+[^\s]+$", re.IGNORECASE)
|
put_command_pattern: Pattern = re.compile(r"^put\s+[^\s]+$")
|
||||||
put_command_pattern: Pattern = re.compile(r"^put\s+[^\s]+$", re.IGNORECASE)
|
summary_command_pattern: Pattern = re.compile(r"^summary\s+[^\s]+$")
|
||||||
summary_command_pattern: Pattern = re.compile(r"^summary\s+[^\s]+$", re.IGNORECASE)
|
change_command_pattern: Pattern = re.compile(r"^change\s+[^\s]+\s+[^\s]+$")
|
||||||
change_command_pattern: Pattern = re.compile(
|
|
||||||
r"^change\s+[^\s]+\s+[^\s]+$", re.IGNORECASE
|
|
||||||
)
|
|
||||||
|
|
||||||
# opcodes
|
# opcodes
|
||||||
put_request_opcode: int = 0b000
|
put_request_opcode: int = 0b000
|
||||||
@ -61,35 +58,22 @@ class Client:
|
|||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.client_socket = socket(
|
client_socket = socket(AF_INET, SOCK_DGRAM)
|
||||||
AF_INET, (SOCK_DGRAM if self.protocol == "UDP" else SOCK_STREAM)
|
client_socket.settimeout(10)
|
||||||
)
|
|
||||||
self.client_socket.settimeout(10)
|
|
||||||
|
|
||||||
# only if using TCP
|
|
||||||
try:
|
|
||||||
self.client_socket.connect(
|
|
||||||
(self.server_name, self.server_port)
|
|
||||||
) if self.protocol == "TCP" else None
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - ConnectionRefusedError happened. Please restart the client program, make sure the server is running and/or put a different server name and server port."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# get command from user
|
# get command from user
|
||||||
command = input(f"myftp> - {self.protocol} - : ").strip()
|
command = input(f"myftp> - {self.protocol} - : ").strip().lower()
|
||||||
|
|
||||||
# handling the "bye" command
|
# handling the "bye" command
|
||||||
if command == "bye" or command == "BYE":
|
if command == "bye":
|
||||||
self.client_socket.close()
|
client_socket.close()
|
||||||
print(f"myftp> - {self.protocol} - Session is terminated")
|
print(f"myftp> - {self.protocol} - Session is terminated")
|
||||||
break
|
break
|
||||||
|
|
||||||
# help
|
# help
|
||||||
elif command == "help" or command == "HELP":
|
elif command == "help":
|
||||||
first_byte: int = help_request_opcode << 5
|
first_byte: int = help_request_opcode << 5
|
||||||
command_name = "help"
|
command_name = "help"
|
||||||
|
|
||||||
@ -99,8 +83,7 @@ class Client:
|
|||||||
|
|
||||||
# get command handling
|
# get command handling
|
||||||
elif get_command_pattern.match(command):
|
elif get_command_pattern.match(command):
|
||||||
_, filename = command.split(" ", 1)
|
command_name, filename = command.split(" ", 1)
|
||||||
command_name = "get"
|
|
||||||
|
|
||||||
first_byte = (get_request_opcode << 5) + len(filename)
|
first_byte = (get_request_opcode << 5) + len(filename)
|
||||||
|
|
||||||
@ -112,8 +95,7 @@ class Client:
|
|||||||
|
|
||||||
# put command handling
|
# put command handling
|
||||||
elif put_command_pattern.match(command):
|
elif put_command_pattern.match(command):
|
||||||
_, filename = command.split(" ", 1)
|
command_name, filename = command.split(" ", 1)
|
||||||
command_name = "put"
|
|
||||||
|
|
||||||
first_byte, second_byte_to_n_byte, data = self.put_payload_handling(
|
first_byte, second_byte_to_n_byte, data = self.put_payload_handling(
|
||||||
filename
|
filename
|
||||||
@ -125,45 +107,25 @@ class Client:
|
|||||||
|
|
||||||
# summary command handling
|
# summary command handling
|
||||||
elif summary_command_pattern.match(command):
|
elif summary_command_pattern.match(command):
|
||||||
_, filename = command.split(" ", 1)
|
command_name, filename = command.split(" ", 1)
|
||||||
command_name = "summary"
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Summary file {filename} from the server"
|
f"myftp> - {self.protocol} - Summary file {filename} from the server"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
first_byte = (summary_request_opcode << 5) + len(filename)
|
|
||||||
|
|
||||||
second_byte_to_n_byte = filename.encode("ascii")
|
|
||||||
|
|
||||||
# change command handling
|
# change command handling
|
||||||
elif change_command_pattern.match(command):
|
elif change_command_pattern.match(command):
|
||||||
_, old_filename, new_filename = command.split()
|
command_name, old_filename, new_filename = command.split()
|
||||||
command_name = "change"
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Changing file named {old_filename} into {new_filename} on the server"
|
f"myftp> - {self.protocol} - Changing file named {old_filename} into {new_filename} on the server"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
first_byte = (change_request_opcode << 5) + len(old_filename)
|
|
||||||
|
|
||||||
second_byte_to_n_byte = (
|
|
||||||
old_filename.encode("ascii")
|
|
||||||
+ len(new_filename).to_bytes(1, "big")
|
|
||||||
+ new_filename.encode("ascii")
|
|
||||||
)
|
|
||||||
|
|
||||||
# unknown request, assigned opcode is 0b101
|
# unknown request, assigned opcode is 0b101
|
||||||
else:
|
else:
|
||||||
command_name = None
|
command_name = None
|
||||||
first_byte: int = unknown_request_opcode << 5
|
first_byte: int = unknown_request_opcode << 5
|
||||||
|
|
||||||
# get change put cases
|
# get or put case
|
||||||
if (
|
if command_name == "get":
|
||||||
command_name == "get"
|
|
||||||
or command_name == "summary"
|
|
||||||
or command_name == "change"
|
|
||||||
):
|
|
||||||
payload = first_byte.to_bytes(1, "big") + second_byte_to_n_byte # type: ignore
|
payload = first_byte.to_bytes(1, "big") + second_byte_to_n_byte # type: ignore
|
||||||
|
|
||||||
elif command_name == "put":
|
elif command_name == "put":
|
||||||
@ -173,22 +135,23 @@ class Client:
|
|||||||
else first_byte.to_bytes(1, "big") # type: ignore
|
else first_byte.to_bytes(1, "big") # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif command_name == "summary":
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif command == "change":
|
||||||
|
pass
|
||||||
|
|
||||||
# help case and unknown request
|
# help case and unknown request
|
||||||
else:
|
else:
|
||||||
payload: bytes = first_byte.to_bytes(1, "big") # type: ignore
|
payload: bytes = first_byte.to_bytes(1, "big") # type: ignore
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - sent payload {payload} to the server. Payload length is {len(payload)}" # type: ignore
|
f"myftp> - {self.protocol} - sent payload {bin(int.from_bytes(payload, byteorder='big'))[2:]} to the server" # type: ignore
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
if self.protocol == "UDP":
|
client_socket.sendto(payload, (self.server_name, self.server_port)) # type: ignore
|
||||||
self.client_socket.sendto(
|
|
||||||
payload, (self.server_name, self.server_port)
|
|
||||||
) # type: ignore
|
|
||||||
else:
|
|
||||||
self.client_socket.sendall(payload) # type: ignore
|
|
||||||
|
|
||||||
response_payload = self.client_socket.recv(2048)
|
response_payload = client_socket.recv(2048)
|
||||||
|
|
||||||
self.parse_response_payload(response_payload)
|
self.parse_response_payload(response_payload)
|
||||||
|
|
||||||
@ -203,9 +166,6 @@ class Client:
|
|||||||
f"myftp> - {self.protocol} - Server at {self.server_name} did not respond within 5 seconds. Check the address or server status."
|
f"myftp> - {self.protocol} - Server at {self.server_name} did not respond within 5 seconds. Check the address or server status."
|
||||||
)
|
)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print(f"\nmyftp> - {self.protocol} - Client shutting down")
|
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
traceback_info = traceback.format_exc()
|
traceback_info = traceback.format_exc()
|
||||||
|
|
||||||
@ -214,14 +174,9 @@ class Client:
|
|||||||
print(traceback_info)
|
print(traceback_info)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.client_socket.close()
|
client_socket.close()
|
||||||
|
|
||||||
def parse_response_payload(self, response_payload: bytes):
|
def parse_response_payload(self, response_payload: bytes):
|
||||||
"""
|
|
||||||
Parse response payload for further processing
|
|
||||||
|
|
||||||
response_payload is the the entire packet that was sent from the server
|
|
||||||
"""
|
|
||||||
first_byte = bytes([response_payload[0]])
|
first_byte = bytes([response_payload[0]])
|
||||||
first_byte_binary = int.from_bytes(first_byte, "big")
|
first_byte_binary = int.from_bytes(first_byte, "big")
|
||||||
rescode = first_byte_binary >> 5
|
rescode = first_byte_binary >> 5
|
||||||
@ -242,7 +197,6 @@ class Client:
|
|||||||
|
|
||||||
# error rescodes
|
# error rescodes
|
||||||
if rescode in [0b011, 0b100, 0b101]:
|
if rescode in [0b011, 0b100, 0b101]:
|
||||||
# print to client
|
|
||||||
print(f"myftp> - {self.protocol} - {rescode_dict[rescode]}")
|
print(f"myftp> - {self.protocol} - {rescode_dict[rescode]}")
|
||||||
|
|
||||||
# successful rescodes
|
# successful rescodes
|
||||||
@ -255,15 +209,12 @@ class Client:
|
|||||||
# get rescode
|
# get rescode
|
||||||
elif rescode == 0b001:
|
elif rescode == 0b001:
|
||||||
self.handle_get_response_from_server(filename_length, response_data)
|
self.handle_get_response_from_server(filename_length, response_data)
|
||||||
# summary rescode
|
|
||||||
elif rescode == 0b010:
|
|
||||||
self.handle_summary_response_from_server(filename_length, response_data)
|
|
||||||
|
|
||||||
def put_payload_handling(
|
def put_payload_handling(
|
||||||
self, filename: str
|
self, filename: str
|
||||||
) -> Tuple[int, Optional[bytes], Optional[bytes]]:
|
) -> Tuple[int, Optional[bytes], Optional[bytes]]:
|
||||||
"""
|
"""
|
||||||
Assemble the payload to put the file onto server
|
Assemble the pay load to put the file onto server
|
||||||
|
|
||||||
Return first_byte, second_byte_to_n_byte and data if successful
|
Return first_byte, second_byte_to_n_byte and data if successful
|
||||||
Or (None, None, None) if file not found
|
Or (None, None, None) if file not found
|
||||||
@ -290,8 +241,6 @@ class Client:
|
|||||||
self, filename_length: int, response_data: bytes
|
self, filename_length: int, response_data: bytes
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Handle the get response from the server
|
|
||||||
|
|
||||||
Response_data is
|
Response_data is
|
||||||
File name (filename_length bytes) +
|
File name (filename_length bytes) +
|
||||||
File size (4 bytes) +
|
File size (4 bytes) +
|
||||||
@ -308,42 +257,8 @@ class Client:
|
|||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Filename: {filename}, File_size: {file_size} bytes"
|
f"myftp> - {self.protocol} - Filename: {filename}, File_size: {file_size} bytes"
|
||||||
) if self.debug else None
|
|
||||||
|
|
||||||
with open(os.path.join(self.directory_path, filename), "wb") as file:
|
|
||||||
file.write(file_content)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - File {filename} has been downloaded successfully"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def handle_summary_response_from_server(
|
|
||||||
self, filename_length: int, response_data: bytes
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Handle summary response from server
|
|
||||||
|
|
||||||
Response_data is
|
|
||||||
File name (filename_length bytes) +
|
|
||||||
File size (4 bytes) +
|
|
||||||
File content (rest of the bytes)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
filename = response_data[:filename_length].decode("ascii")
|
|
||||||
file_size = int.from_bytes(
|
|
||||||
response_data[filename_length : filename_length + 4], "big"
|
|
||||||
)
|
|
||||||
file_content = response_data[
|
|
||||||
filename_length + 4 : filename_length + 4 + file_size
|
|
||||||
]
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - Filename: {filename}, File_size: {file_size} bytes"
|
|
||||||
) if self.debug else None
|
|
||||||
|
|
||||||
with open(os.path.join(self.directory_path, filename), "wb") as file:
|
with open(os.path.join(self.directory_path, filename), "wb") as file:
|
||||||
file.write(file_content)
|
file.write(file_content)
|
||||||
|
|
||||||
@ -426,15 +341,27 @@ def init():
|
|||||||
|
|
||||||
user_supplied_address = get_address_input()
|
user_supplied_address = get_address_input()
|
||||||
|
|
||||||
client = Client(
|
# UDP client selected here
|
||||||
|
if protocol_selection == "2":
|
||||||
|
udp_client = Client(
|
||||||
user_supplied_address[0],
|
user_supplied_address[0],
|
||||||
user_supplied_address[1],
|
user_supplied_address[1],
|
||||||
args.directory,
|
args.directory,
|
||||||
args.debug,
|
args.debug,
|
||||||
("UDP" if protocol_selection == "2" else "TCP"),
|
"UDP",
|
||||||
)
|
)
|
||||||
|
|
||||||
client.run()
|
udp_client.run()
|
||||||
|
else:
|
||||||
|
tcp_client = Client(
|
||||||
|
user_supplied_address[0],
|
||||||
|
user_supplied_address[1],
|
||||||
|
args.directory,
|
||||||
|
args.debug,
|
||||||
|
"TCP",
|
||||||
|
)
|
||||||
|
|
||||||
|
tcp_client.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# Description: FTP server (both UDP and TCP implemented)
|
# Description: FTP server (both UDP and TCP implemented)
|
||||||
|
|
||||||
|
|
||||||
from socket import socket, AF_INET, SOCK_DGRAM, SOCK_STREAM
|
from socket import socket, AF_INET, SOCK_DGRAM
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
import traceback
|
import traceback
|
||||||
@ -50,14 +50,8 @@ class Server:
|
|||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
server_socket = socket(
|
self.server_socket = socket(AF_INET, SOCK_DGRAM)
|
||||||
AF_INET, (SOCK_DGRAM if self.protocol == "UDP" else SOCK_STREAM)
|
self.server_socket.bind((self.server_name, self.server_port))
|
||||||
)
|
|
||||||
|
|
||||||
server_socket.bind((self.server_name, self.server_port))
|
|
||||||
|
|
||||||
# only needed for TCP
|
|
||||||
server_socket.listen(5) if self.protocol == "TCP" else None
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Server is ready to receive at {self.server_name}:{self.server_port}"
|
f"myftp> - {self.protocol} - Server is ready to receive at {self.server_name}:{self.server_port}"
|
||||||
@ -66,27 +60,12 @@ class Server:
|
|||||||
shut_down = False
|
shut_down = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.protocol == "TCP":
|
|
||||||
client_socket, clientAddress = server_socket.accept()
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - Connected to TCP client at {clientAddress}"
|
|
||||||
) if self.debug else None
|
|
||||||
|
|
||||||
while not shut_down:
|
while not shut_down:
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} ------------------------------------------------------------------"
|
f"myftp> - {self.protocol} ------------------------------------------------------------------"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
if self.protocol == "UDP":
|
req_payload, clientAddress = self.server_socket.recvfrom(2048)
|
||||||
req_payload, clientAddress = server_socket.recvfrom(2048)
|
|
||||||
else:
|
|
||||||
req_payload = client_socket.recv(2048) # type: ignore
|
|
||||||
|
|
||||||
# TCP client disconnected
|
|
||||||
if not req_payload:
|
|
||||||
client_socket.close() # type: ignore
|
|
||||||
return
|
|
||||||
|
|
||||||
first_byte = bytes([req_payload[0]])
|
first_byte = bytes([req_payload[0]])
|
||||||
|
|
||||||
@ -95,7 +74,7 @@ class Server:
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Received message from client at {clientAddress}: {req_payload}. Payload length is {len(req_payload)}" # type: ignore
|
f"myftp> - {self.protocol} - Received message from client at {clientAddress}: {req_payload}"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
# help request handling
|
# help request handling
|
||||||
@ -145,28 +124,6 @@ class Server:
|
|||||||
filename = None
|
filename = None
|
||||||
response_data = None
|
response_data = None
|
||||||
|
|
||||||
elif request_type == "summary":
|
|
||||||
# empty filename error
|
|
||||||
if filename_length_in_bytes <= 0:
|
|
||||||
rescode = rescode_fail_dict["file_not_error_rescode"]
|
|
||||||
else:
|
|
||||||
(
|
|
||||||
rescode,
|
|
||||||
filename, # "summary.txt"
|
|
||||||
filename_length_in_bytes, # of the summary file
|
|
||||||
response_data, # summary.txt file content
|
|
||||||
) = self.process_summary_req(
|
|
||||||
filename_length_in_bytes, req_payload[1:]
|
|
||||||
)
|
|
||||||
|
|
||||||
elif request_type == "change":
|
|
||||||
rescode = self.process_change_req(
|
|
||||||
filename_length_in_bytes, req_payload[1:]
|
|
||||||
)
|
|
||||||
filename_length_in_bytes = None
|
|
||||||
filename = None
|
|
||||||
response_data = None
|
|
||||||
|
|
||||||
elif request_type == "unknown":
|
elif request_type == "unknown":
|
||||||
rescode = rescode_fail_dict["unknown_request_rescode"]
|
rescode = rescode_fail_dict["unknown_request_rescode"]
|
||||||
filename_length_in_bytes = None
|
filename_length_in_bytes = None
|
||||||
@ -180,22 +137,15 @@ class Server:
|
|||||||
response_data=response_data, # type:ignore
|
response_data=response_data, # type:ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.protocol == "UDP":
|
self.server_socket.sendto(res_payload, clientAddress)
|
||||||
server_socket.sendto(res_payload, clientAddress) # type: ignore
|
|
||||||
else:
|
|
||||||
client_socket.sendall(res_payload) # type: ignore
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"myftp> - {self.protocol} - Sent message to client at {clientAddress}: {res_payload}. Payload length is {len(res_payload)}" # type: ignore
|
f"myftp> - {self.protocol} - Sent message to client at {clientAddress}: {res_payload}"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
shut_down = True
|
shut_down = True
|
||||||
if self.protocol == "UDP":
|
self.server_socket.close()
|
||||||
server_socket.close()
|
|
||||||
else:
|
|
||||||
client_socket.close() # type: ignore
|
|
||||||
|
|
||||||
print(f"myftp> - {self.protocol} - Server shutting down")
|
print(f"myftp> - {self.protocol} - Server shutting down")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@ -224,112 +174,6 @@ class Server:
|
|||||||
|
|
||||||
return request_type, filename_length_in_bytes
|
return request_type, filename_length_in_bytes
|
||||||
|
|
||||||
def process_change_req(
|
|
||||||
self, old_filename_length_in_bytes: int, req_payload: bytes
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
Process change request from client
|
|
||||||
"""
|
|
||||||
old_filename = req_payload[:old_filename_length_in_bytes].decode("ascii")
|
|
||||||
new_filename_length = int.from_bytes(
|
|
||||||
req_payload[
|
|
||||||
old_filename_length_in_bytes : old_filename_length_in_bytes + 1
|
|
||||||
],
|
|
||||||
"big",
|
|
||||||
)
|
|
||||||
new_filename = req_payload[old_filename_length_in_bytes + 1 :].decode("ascii")
|
|
||||||
|
|
||||||
actual_new_filename_length = len(new_filename)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if new_filename_length <= 31 or actual_new_filename_length <= 31:
|
|
||||||
old_filename_full_path = os.path.normpath(
|
|
||||||
os.path.join(self.directory_path, old_filename)
|
|
||||||
)
|
|
||||||
new_filename_full_path = os.path.normpath(
|
|
||||||
os.path.join(self.directory_path, new_filename)
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - Changing file named {old_filename_full_path} to new file {new_filename_full_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
os.rename(old_filename_full_path, new_filename_full_path)
|
|
||||||
|
|
||||||
return rescode_success_dict["correct_put_and_change_request_rescode"]
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - New file name longer than 31 characters error"
|
|
||||||
)
|
|
||||||
return rescode_fail_dict["unsuccessful_change_rescode"]
|
|
||||||
|
|
||||||
except Exception as error:
|
|
||||||
traceback_info = traceback.format_exc()
|
|
||||||
|
|
||||||
print(f"myftp> - {self.protocol} - {error} happened.")
|
|
||||||
|
|
||||||
print(traceback_info)
|
|
||||||
return rescode_fail_dict["unsuccessful_change_rescode"]
|
|
||||||
|
|
||||||
def process_summary_req(
|
|
||||||
self, filename_length: int, req_payload: bytes
|
|
||||||
) -> Tuple[int, Optional[str], Optional[int], Optional[bytes]]:
|
|
||||||
"""
|
|
||||||
Find the filename mentioned
|
|
||||||
Calculate the min,max,avg
|
|
||||||
Put those numbers into a file called summary.txt
|
|
||||||
"""
|
|
||||||
filename = req_payload[:filename_length].decode("ascii")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - Summarizing the file named {filename} on the server"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(os.path.join(self.directory_path, filename), "r") as file:
|
|
||||||
numbers = [int(line.strip()) for line in file if line.strip().isdigit()]
|
|
||||||
|
|
||||||
# Find the largest, smallest, and calculate the average
|
|
||||||
largest_number = max(numbers)
|
|
||||||
smallest_number = min(numbers)
|
|
||||||
average_value = sum(numbers) / len(numbers) if numbers else 0
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - File {filename} summarized successfully. The max is {largest_number}, the min is {smallest_number}, the average is {average_value}"
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(
|
|
||||||
os.path.join(self.directory_path, "summary.txt"), "w"
|
|
||||||
) as summary_file:
|
|
||||||
summary_file.write(f"min: {smallest_number}\n")
|
|
||||||
summary_file.write(f"max: {largest_number}\n")
|
|
||||||
summary_file.write(f"avg: {average_value}\n")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"myftp> - {self.protocol} - Created file summary.txt summarized successfully. Sending it back to the client"
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(
|
|
||||||
os.path.join(self.directory_path, "summary.txt"), "rb"
|
|
||||||
) as summary_file:
|
|
||||||
binary_content = summary_file.read()
|
|
||||||
|
|
||||||
return (
|
|
||||||
rescode_success_dict["correct_summary_request_rescode"],
|
|
||||||
"summary.txt",
|
|
||||||
11,
|
|
||||||
binary_content,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as error:
|
|
||||||
traceback_info = traceback.format_exc()
|
|
||||||
|
|
||||||
print(f"myftp> - {self.protocol} - {error} happened.")
|
|
||||||
|
|
||||||
print(traceback_info)
|
|
||||||
return rescode_fail_dict["file_not_error_rescode"], None, None, None
|
|
||||||
|
|
||||||
def process_put_req(self, filename_length: int, req_payload: bytes) -> int:
|
def process_put_req(self, filename_length: int, req_payload: bytes) -> int:
|
||||||
"""
|
"""
|
||||||
Reconstruct file put by client
|
Reconstruct file put by client
|
||||||
@ -373,7 +217,6 @@ class Server:
|
|||||||
If not, return None, None, None tuple
|
If not, return None, None, None tuple
|
||||||
"""
|
"""
|
||||||
filename = second_byte_to_byte_n.decode("ascii")
|
filename = second_byte_to_byte_n.decode("ascii")
|
||||||
print(f"myftp> - {self.protocol} - trying to find file {filename}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(self.directory_path, filename), "rb") as file:
|
with open(os.path.join(self.directory_path, filename), "rb") as file:
|
||||||
@ -386,10 +229,6 @@ class Server:
|
|||||||
print(f"myftp> - {self.protocol} - file {filename} not found")
|
print(f"myftp> - {self.protocol} - file {filename} not found")
|
||||||
return (None, None, None)
|
return (None, None, None)
|
||||||
|
|
||||||
except IsADirectoryError:
|
|
||||||
print(f"myftp> - {self.protocol} - filename is blank")
|
|
||||||
return (None, None, None)
|
|
||||||
|
|
||||||
# assembling the payload to send back to the client
|
# assembling the payload to send back to the client
|
||||||
def build_res_payload(
|
def build_res_payload(
|
||||||
self,
|
self,
|
||||||
@ -424,14 +263,13 @@ class Server:
|
|||||||
# help case
|
# help case
|
||||||
elif filename is None and response_data is not None:
|
elif filename is None and response_data is not None:
|
||||||
first_byte = ((rescode << 5) + len(response_data)).to_bytes(1, "big")
|
first_byte = ((rescode << 5) + len(response_data)).to_bytes(1, "big")
|
||||||
# other cases
|
# unsuccessful cases
|
||||||
else:
|
else:
|
||||||
first_byte = (rescode << 5).to_bytes(1, "big")
|
first_byte = (rescode << 5).to_bytes(1, "big")
|
||||||
|
|
||||||
# we only need the firstbyte
|
# we only need the firstbyte
|
||||||
if filename is None:
|
if filename is None:
|
||||||
second_byte_to_FL_plus_five = None
|
second_byte_to_FL_plus_five = None
|
||||||
# second byte and more are needed
|
|
||||||
else:
|
else:
|
||||||
# get case
|
# get case
|
||||||
second_byte_to_FL_plus_five = (
|
second_byte_to_FL_plus_five = (
|
||||||
@ -444,13 +282,11 @@ class Server:
|
|||||||
f"myftp> - {self.protocol} - First byte assembled for rescode {format(rescode, '03b')}: {bin(int.from_bytes(first_byte, byteorder='big'))[2:]}"
|
f"myftp> - {self.protocol} - First byte assembled for rescode {format(rescode, '03b')}: {bin(int.from_bytes(first_byte, byteorder='big'))[2:]}"
|
||||||
) if self.debug else None
|
) if self.debug else None
|
||||||
|
|
||||||
# get/summary case
|
|
||||||
if second_byte_to_FL_plus_five is not None and response_data is not None:
|
if second_byte_to_FL_plus_five is not None and response_data is not None:
|
||||||
res_payload = first_byte + second_byte_to_FL_plus_five + response_data
|
res_payload = first_byte + second_byte_to_FL_plus_five + response_data
|
||||||
# help case
|
# help case
|
||||||
elif second_byte_to_FL_plus_five is None and response_data is not None:
|
elif second_byte_to_FL_plus_five is None and response_data is not None:
|
||||||
res_payload = first_byte + response_data
|
res_payload = first_byte + response_data
|
||||||
# change/put case
|
|
||||||
else:
|
else:
|
||||||
res_payload = first_byte
|
res_payload = first_byte
|
||||||
|
|
||||||
@ -518,16 +354,20 @@ def init():
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# start the server
|
# UDP client selected here
|
||||||
server = Server(
|
if protocol_selection == "2":
|
||||||
args.ip_addr,
|
udp_server = Server(
|
||||||
args.port_number,
|
args.ip_addr, args.port_number, args.directory, args.debug, "UDP"
|
||||||
args.directory,
|
|
||||||
args.debug,
|
|
||||||
("UDP" if protocol_selection == "2" else "TCP"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
server.run()
|
udp_server.run()
|
||||||
|
|
||||||
|
else:
|
||||||
|
tcp_server = Server(
|
||||||
|
args.ip_addr, args.port_number, args.directory, args.debug, "TCP"
|
||||||
|
)
|
||||||
|
|
||||||
|
tcp_server.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user