-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathftp_server.py
More file actions
701 lines (543 loc) · 23.7 KB
/
Copy pathftp_server.py
File metadata and controls
701 lines (543 loc) · 23.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
## IN THE NAME OF ALLAH ##
## SERVER ##
import socket
import threading
import os
import re
import random
import shutil
import sqlite3
import datetime
# Define a range of ports for data transfer
DATA_PORTS = {}
PORT_RANGE = (50000, 60000)
# Iterate over the port range and create a dictionary of open ports
for port in range(*PORT_RANGE):
DATA_PORTS[port] = True
# Get the absolute path of the directory containing the current file
BASE_DIR = os.path.dirname(os.path.realpath(__file__)) + "/data"
# Check if the data directory exists, and create it if it doesn't
if not os.path.exists(BASE_DIR):
os.makedirs(BASE_DIR)
print(f"Directory '{BASE_DIR}' created successfully.")
IP = 'localhost'
PORT = 2100
ADDR = (IP, PORT)
FORMAT = "utf-8"
SIZE = 1024
def command_is(usercommand, command):
return usercommand.upper().startswith(command)
def is_valid_string(text, pattern):
"""
Checks if a string is valid based on a regular expression pattern.
Args:
text: The string to validate.
pattern: The regular expression pattern.
Returns:
True if the string is valid, False otherwise.
"""
match = re.fullmatch(pattern, text)
return bool(match)
def validate_command2(command):
"""
Validates a FTP command based on its format.
Args:
command: The FTP command string.
Returns:
True if the command is valid, False otherwise.
"""
if command.upper().startswith("USER"):
pattern = r"^USER\s+(\w+)$"
elif command.upper().startswith("PASS"):
if len(command.split(' ')) < 3:
return True
else:
return False
#pattern = r"^PASS\s+(\w+)$"
elif command.upper().startswith("LIST"):
if len(command.split(' ')) < 3:
return True
else:
return False
#pattern = r"^LIST\s+/(.+)$"
elif command.upper().startswith("RETR"):
pattern = r"^RETR\s+(.+)$" # Capture the filename
elif command.upper().startswith("STOR"):
pattern = r"^STOR\s+(.+)\s+(.+)$" # Capture two filenames
elif command.upper().startswith("DELE"):
pattern = r"^DELE\s+(.+)$" # Capture the filename
elif command.upper().startswith("MKD"):
pattern = r"^MKD\s+(.+)$" # Capture the directory name
elif command.upper().startswith("RMD"):
pattern = r"^RMD\s+(.+)$" # Capture the directory name
elif command.upper().startswith("PWD"):
pattern = r"^PWD$" # No arguments expected
elif command.upper().startswith("CWD"):
pattern = r"^CWD\s+(.+)$" # Capture the directory name
elif command.upper().startswith("CDUP"):
pattern = r"^CDUP$" # No arguments expected
elif command.upper().startswith("QUIT"):
pattern = r"^QUIT$" # No arguments expected
elif command.upper().startswith("REPORT"):
pattern = r"^REPORT$" # No arguments expected
else:
return False
return is_valid_string(command.upper(), pattern)
def validate_command(command):
# Check if the command has the correct format
#if command_is(usercommand=command, command="LIST") or command_is(usercommand=command, command="PWD") or command_is(usercommand=command, command="CDUP") or command_is(usercommand=command, command="QUIT"):
try:
# 0 arg
if command.upper().split(' ')[0] in ["PWD", "CDUP", "QUIT", "REPORT"]:
if len(command.split(' ')) != 1:
return False
# 1 arg
elif command.upper().split(' ')[0] in ["USER", "PASS", "DELE", "RETR", "MKD", "CWD", "RMD"]:
if len(command.split(' ')) != 2:
return False
# 2 arg
elif command.upper().split(' ')[0] in ["STOR"]:
if len(command.split(' ')) != 3:
return False
elif command.upper().split(' ')[0] == "LIST":
if len(command.split(' ')) > 2:
return False
else:
return False
except ValueError:
print("Invalid format for FTP command:", command)
return False
return True
def manage_dir(dir, current_dir):
if dir.startswith('/'):
# If the directory path starts with a slash '/', it means it's an absolute path
# Add the base directory to the absolute path
dir = BASE_DIR + dir
else:
# If the directory path does not start with a slash '/', it means it's a relative path
# Add the current directory to the relative path
dir = current_dir + '/' + dir
# This checks whether the specified directory is within the base directory
real_dir = os.path.realpath(dir)
index = real_dir.find(BASE_DIR)
# If the index is not 0, it means the specified directory is not within the base directory
if index != 0:
return BASE_DIR
return real_dir
def access(command, user_al):
if command.upper() in ["STOR", "MKD"]:
if user_al > 2:
return False
elif command.upper() in ["DELE", "RMD"]:
if user_al > 1:
return False
return True
def get_data_port():
# Find a random port number for the data channel
with threading.Lock():
data_port = random.randint(*PORT_RANGE)
while DATA_PORTS[data_port] == False:
data_port = random.randint(*PORT_RANGE)
DATA_PORTS[data_port] = False # Close the port
return data_port
def pad_string(text, target_length, fill_char='n'):
# Calculate the padding length
padding_length = target_length - len(text)
# Create a padding string with the specified fill character
padding_string = fill_char * padding_length
# Concatenate the original string and the padding string
result = text + padding_string
return result
def handle_list(command, current_dir, control_channel):
print(f"Start of LIST command: {command}")
print(f'current_dir: {current_dir}')
if len(command.split(' ')) > 1:
directory = manage_dir(command.split(' ')[1], current_dir)
else:
directory = current_dir
print(f'LIST directory: {directory}')
try:
# listing = os.listdir(directory)
listing = ""
for file in os.scandir(directory):
if file.is_file():
creation_date = file.stat().st_ctime
creation_date_datetime = datetime.datetime.fromtimestamp(creation_date)
date = creation_date_datetime.strftime("%Y-%m-%d")
print(date)
listing += file.name + ' ' + str(file.stat().st_size) + ' ' + date + '\n'
else:
listing += file.name + '\n'
file_size = str(len(listing))
print(f'file_size: {file_size}')
file_size = pad_string(file_size, 1013)
print(len(file_size))
control_channel.sendall(f'FILE_SIZE: {file_size}'.encode())
if listing:
return listing
else:
return 'Directory is empty.'
except OSError as e:
print(f"Error retrieving directory listing: {e}")
return f"Error retrieving directory listing"
def handle_retr(command, current_dir, control_channel):
print("Start of RETR command.")
directory = manage_dir(command.split(' ')[1], current_dir)
print(f"directory: {directory}")
try:
# Get a port number for the data channel
data_port = get_data_port()
print(f'data_port: {data_port}')
# Send the port number to the client over the control channel
if os.path.exists(directory):
file_size = os.path.getsize(directory)
else:
return '550 File not found'
print(f'file_size: {file_size}')
control_channel.send(f"200 PORT {data_port} ,FILE_SIZE: {file_size}".encode())
# Create the data socket and listen for the client's connection
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind(('localhost', data_port))
data_socket.listen(1)
data_channel, _ = data_socket.accept()
# Read the entire file into a buffer
with threading.Lock():
with open(directory, 'rb') as file:
while True:
data = file.read(1024)
if not data:
break
data_channel.sendall(data)
# Close the data channel
data_socket.close()
data_channel.close()
with threading.Lock():
DATA_PORTS[data_port] = True # Open the port
response = "226 Transfer complete"
except FileNotFoundError:
response = "450 Requested file action not taken. File unavailable"
except Exception as e:
print(f"An error occurred: {e}")
response = "451 Requested action aborted. Local error in processing"
return response
def handle_stor(command, control_channel, current_dir, access_level):
print(f"start of STOR command: {command}")
filename = manage_dir(dir=command.split(' ')[1], current_dir=current_dir)
file_size = int(command.split(' ')[2])
response = ""
access = True
print(f'a_l: {access_level}')
if os.path.exists(filename) and access_level > 1:
access = False
data_port = get_data_port()
directory = os.path.dirname(filename)
print(f'directory: {directory}')
print(f'data_port:{data_port}')
if data_port and os.path.exists(directory) and access:
print("mamad")
control_channel.send(f"200 PORT {data_port}".encode(FORMAT))
elif not access:
return "501 Permission denied."
else:
return "401 not found."
# Create the data socket and listen for the client's connection
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind((IP, data_port))
data_socket.listen(1)
data_channel, _ = data_socket.accept()
print("STOR data_channel connected.")
with open(filename, 'wb') as file: # Open the file or create it
print("STOR file opened.")
rcv_size = 0
print(f'file_size:{file_size}')
while True:
data = data_channel.recv(SIZE)
file.write(data)
rcv_size += len(data)
print(f'rcv_size:{rcv_size}')
if rcv_size >= file_size:
response = '226 Transfer complete'
break
print("STOR file recived. closing data_channel.")
DATA_PORTS[data_port] = True # Open the port
data_socket.close()
data_channel.close()
return response
def handle_dele(command, current_dir):
print(f"Start of DELE command: {command}")
filename = manage_dir(command.split(' ')[1], current_dir)
print(f'filename: {filename}')
try:
os.remove(filename)
response = '250 File deleted successfully'
except FileNotFoundError:
response = '550 File not found'
except:
response = '550 Invalid file name'
return response
def handle_mkd(command, current_dir):
print(f"Start of MKD command: {command}")
directory = manage_dir(command.split(' ')[1], current_dir)
print(f'directory: {directory}')
if not os.path.exists(directory):
os.makedirs(directory)
response = f"Directory '{command.split(' ')[1]}' created successfully."
else:
response = f"Directory '{command.split(' ')[1]}' already exists"
return response
def handle_rmd(command, current_dir):
print(f"Start of RMD command: {command}")
directory = manage_dir(command.split(' ')[1], current_dir)
print(f'directory: {directory}')
if not os.path.isdir(directory):
response = "550 Directory does not exist"
try:
shutil.rmtree(directory)
response = '250 Directory successfully removed'
except OSError:
response = '550 Directory does not exist'
return response
def handle_pwd(current_dir):
dir = current_dir.replace(BASE_DIR, "")
if dir:
return dir
else:
return "/"
def handle_report(username, curs, control_channel):
print("Start of REPORT command")
curs.execute('SELECT command FROM report WHERE username = ?', (username,))
commands = curs.fetchall()
report = '\n'.join(command[0] for command in commands)
file_size = len(report)
print(f'file_size: {file_size}')
data_port = get_data_port()
print(f'data_port: {data_port} , tp: {type(data_port)}')
control_channel.sendall(f'PORT: {data_port} ,FILE_SIZE: {file_size}'.encode(FORMAT))
# Create the data socket and listen for the client's connection
data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data_socket.bind((IP, data_port))
data_socket.listen(1)
data_channel, _ = data_socket.accept()
data_channel.sendall(report.encode(FORMAT))
data_socket.close()
data_channel.close()
with threading.Lock():
DATA_PORTS[data_port] = True # Open the port
return "200 Sent data successfully"
def handle_client(conn, addr):
current_dir = BASE_DIR
username = ""
password = ""
inp_password = ""
access_level = 4
authenticated = False
db = sqlite3.connect('ftp_users.db')
curs = db.cursor()
try:
while True:
# Receive data from the client
command = conn.recv(SIZE).decode()
print(f'command: {command}')
if not validate_command(command):
response = f"Command '{command}' not supported"
conn.sendall(response.encode())
continue
if command_is(usercommand=command, command="USER"):
cursor = db.cursor()
username = command.split(' ')[1]
cursor.execute('SELECT username, password, access_level FROM users WHERE username = ?', (username,))
existing_user = cursor.fetchone()
if existing_user:
username, password, access_level = existing_user
response = "200 User login successful"
else:
response = "401 Invalid username"
conn.sendall(response.encode())
continue
elif command_is(usercommand=command, command="PASS"):
inp_password = command.split(' ')[1]
if username and password == inp_password:
response = "200 Password accepted"
authenticated = True
else:
response = "401 Invalid password"
conn.sendall(response.encode())
continue
elif command_is(usercommand=command, command="QUIT"):
print(f'Client {addr} disconnected')
conn.sendall("You may disconnect.".encode())
break
if authenticated and access(command.split(' ')[0], access_level):
print("authenticated user gonna handle his command")
curs.execute('INSERT INTO report (username, command) VALUES (?, ?)', (username, command))
db.commit()
if command.upper().startswith("LIST"):
response = handle_list(command=command, current_dir=current_dir, control_channel=conn)
elif command.upper().startswith("RETR"):
response = handle_retr(command=command, current_dir=current_dir, control_channel=conn)
elif command.upper().startswith("STOR"):
response = handle_stor(command=command, control_channel=conn, current_dir=current_dir, access_level=access_level)
elif command.upper().startswith("DELE"):
response = handle_dele(command=command, current_dir=current_dir)
elif command.upper().startswith("MKD"):
response = handle_mkd(command=command, current_dir=current_dir)
elif command.upper().startswith("RMD"):
response = handle_rmd(command=command, current_dir=current_dir)
elif command.upper().startswith("PWD"):
response = handle_pwd(current_dir=current_dir)
elif command.upper().startswith("CWD"):
curs.execute('INSERT INTO report (username, command) VALUES (?, ?)',
(username, command))
print(f"Start of CWD command: {command}")
directory = manage_dir(command.split(' ')[1], current_dir)
print(f'directory: {directory}')
# check the directory
if not os.path.isdir(directory):
response = "550 Directory does not exist"
else:
current_dir = directory
print(f'current_dir: {current_dir}')
response = f"Current directory changed to '{command.split(' ')[1]}'"
elif command.upper().startswith("CDUP"):
curs.execute('INSERT INTO report (username, command) VALUES (?, ?)',
(username, command))
print(f"Start of CDUP command: {command}")
if current_dir == BASE_DIR:
response = '550 Cannot change to parent directory of root directory'
else:
parent_dir = os.path.dirname(current_dir)
current_dir = parent_dir
response = '250 Directory successfully changed'
elif command.upper().startswith("REPORT"):
response = handle_report(username=username, curs=curs, control_channel=conn)
else:
response = "550 Permission Denied"
conn.sendall(response.encode(FORMAT))
except Exception as e:
print(f"Error handling client {addr}: {e}")
finally:
print(f"User {addr} disconnected!")
db.close()
conn.close()
def main():
db = sqlite3.connect('ftp_users.db')
cursor = db.cursor()
# Create the 'users' table if it doesn't already exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
access_level INTEGER NOT NULL
)
''')
db.commit()
# Create the 'report' table if it doesn't already exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS report (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
command TEXT NOT NULL
)
''')
db.commit()
db.close()
while True:
print("--- --- --- --- ---")
print("-1- Start the server.")
print("-2- Manage Users.")
print("-3- Report.")
print("-4- EXIT.")
choice = input("--- Enter Your Choice: ")
print("--- --- --- --- ---")
if choice == '1': # Start the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(ADDR)
s.listen(10)
print("Server listening on port", PORT)
while True:
conn, addr = s.accept()
conn.sendall(f"Welcome to the server, {addr}! You are now connected.".encode())
print("Connected by", addr)
# Create a new thread for each client connection
client_thread = threading.Thread(target=handle_client, args=(conn, addr))
client_thread.start()
elif choice == '2': # Manage users
print("-1- Add a new user")
print("-2- Delete a user")
print("-3- User's profile")
choice = input("--- Enter Your Choice: ")
print("--- --- --- --- ---")
db = sqlite3.connect('ftp_users.db')
cursor = db.cursor()
if choice == '1': # Add a new user
# Get input for username, password, and access level of new user
username = input("Enter user's username: ")
password = input("Enter user's password: ")
access_level = int(input("Enter user's access_level [1, 2, 3, 4]: "))
# Check if the user already exists in the database
cursor.execute('SELECT * FROM users WHERE username = ?', (username,))
existing_user = cursor.fetchone()
if existing_user:
print(f"User '{username}' already exists.")
else:
# Insert the new user into the database
cursor.execute('INSERT INTO users (username, password, access_level) VALUES (?, ?, ?)',(username, password, access_level))
print(f"User '{username}' registered with access level {access_level}.")
# End of adding a new user
elif choice == '2': # Delete a user
# Get the username to delete from the user
username = input("Enter user's username: ")
# Check if the user exists in the database
cursor.execute('SELECT * FROM users WHERE username = ?', (username,))
existing_user = cursor.fetchone()
if existing_user:
# Delete the user from the database
cursor.execute('DELETE FROM users WHERE username = ?', (username,))
print(f"User '{username}' deleted from the database.")
else:
print(f"User '{username}' not found in the database.")
# End of deleting a user
elif choice == '3': # User's profile
# Get the username
username = input("Enter user's username: ")
# Check if the user exists in the database
cursor.execute('SELECT username, access_level FROM users WHERE username = ?', (username,))
existing_user = cursor.fetchone()
if existing_user:
# Display the user's current information
username, access_level = existing_user
print(f'username: {username}')
print(f'access_level: {access_level}')
# Fetch the user's past commands from the 'report' table
cursor.execute('SELECT command FROM report WHERE username = ?', (username,))
commands = cursor.fetchall()
# Display the list of past commands
print('\n'.join(command[0] for command in commands))
print("\n-1- Edit")
print("-2- To Continue")
choice = input("-Enter Your Choice: ")
if choice == '1': # Edit
# Prompt the user for new password and access level
new_password = input("Enter user's password: ")
new_access_level = int(input("Enter user's access_level [1, 2, 3, 4]: "))
# Update the user's information in the 'users' table
cursor.execute('UPDATE users SET password = ?, access_level = ? WHERE username = ?',(new_password, new_access_level, username))
print(f"User '{username}' updated in the database.")
# Else continue
else:
print(f"User '{username}' not found in the database.")
db.commit()
db.close()
elif choice == '3': # Report
db = sqlite3.connect('ftp_users.db')
cursor = db.cursor()
cursor.execute('SELECT command, username FROM report')
commands = cursor.fetchall()
db.close()
print('\n'.join(command[0]+','+command[1] for command in commands))
main()
else: # EXIT
break
if __name__ == "__main__":
main()