Repository navigation
Expand file tree
/
Copy pathserver.py
More file actions
341 lines (266 loc) · 10.8 KB
/
Copy pathserver.py
File metadata and controls
341 lines (266 loc) · 10.8 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
import os
import socket
import threading
import time
# Status codes
OK = b'200 OK'
NOT_FOUND = b'404 Not Found'
BAD_REQUEST = b'400 Bad Request'
SERVER_ERROR = b'500 Internal Server Error'
NOT_MODIFIED = b'304 Not Modified'
def handle_connection(client, _):
"""Handle incoming client connection.
Args:
client (object): Client socket
"""
while True:
# Get the headers from the client request
headers = get_headers(client)
print('[REQUEST]', headers)
# If the headers don't contain the Host: header, respond with 400: Bad Request
if not b'Host: ' in headers:
client.send(create_error_message(BAD_REQUEST))
else:
if headers.startswith(b'GET'):
do_get(client, headers)
elif headers.startswith(b'HEAD'):
do_head(client, headers)
elif headers.startswith(b'PUT') or headers.startswith(b'POST'):
do_post_put(client, headers)
else:
# Send server error if command not found
client.send(create_error_message(BAD_REQUEST))
if b'Connection: close' in headers:
client.close()
break
def get_headers(client):
"""Extract the headers from the HTTP client request
Args:
client (object): client socket
Returns:
bytes: headers from request
"""
headers = b''
# The headers always end with b'\r\n\r\n'
while not headers.endswith(b'\r\n\r\n'):
response = client.recv(1)
headers += response
return headers
def create_head_message(path, close):
"""Create a HTTP message for HEAD request
Args:
path (bytes): path of the requested file
close (boolean): close connection or not
Returns:
response (bytes): HTTP message
data (bytes): content of requested file
"""
if path.endswith(b'png') or path.endswith(b'jpg'):
file = open(path, 'rb')
else:
file = open(path, 'r')
data = file.read()
file.close()
# Create response bytes
response = b'HTTP/1.1 ' + OK + b'\r\n'
response += b'Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S GMT").encode() + b'\r\n' # Returns date as needed in RFC 2616
response += b'Content-Type: '
# Currently only supports png, jpeg and html files
if path.endswith(b'png'):
response += b'image/png\r\n'
elif path.endswith(b'jpg'):
response += b'image/jpg\r\n'
elif path.endswith(b'html'):
response += b'text/html\r\n'
if path.endswith(b'png') or path.endswith(b'jpg'):
response += b'Content-Length: ' + str(len(data)).encode() + b'\r\n'
else:
response += b'Content-Length: ' + str(len(data) + len(b'\r\n\r\n')).encode() + b'\r\n'
if close:
response += b'Connection: close\r\n'
response += b'\r\n'
return response, data
def head_response(headers):
"""Generate the HTTP response for a HEAD request.
Args:
headers (bytes): Headers of the client request.
Returns:
response (bytes): Response to a HEAD request
path (string): path of the requested file
data (object): the content of the requested file if it exists or something_went_wrong.html if the requested file is not found
"""
data = None
# Get path of requested file
path = headers.split(b' ')[1]
# If path is /, server.html is served
if path == b'/':
path = b'server.html'
if path.startswith(b'/'):
path = path[1:] # Path without first /
# Only png, jpg and html are supported
if not (path.endswith(b'png') or path.endswith(b'jpg') or path.endswith(b'html')):
response = create_error_message(SERVER_ERROR)
# Check if file exists
elif not os.path.isfile(path):
response = create_error_message(NOT_FOUND)
# Check if file is modified since given date
elif b'If-Modified-Since' in headers and not is_modified_since(headers): # If-Modified-Since header only possible in GET and HEAD
response = b'HTTP/1.1 ' + NOT_MODIFIED + b'\r\n'
response += b'Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S GMT").encode() + b'\r\n' # Returns date as needed in RFC 2616
response += b'\r\n'
else:
response, data = create_head_message(path, b'Connection: close' in headers)
return response, path, data
def do_head(client, headers):
"""Send response to a HEAD request.
Args:
client (object): Client socket
headers (bytes): Headers of the client request
"""
response, _, _ = head_response(headers)
client.send(response)
def do_get(client, headers):
"""Send a response to a GET request.
Args:
client (object): Client socket
headers (bytes): Headers of the client request
"""
# The response to a GET request is the same as for a HEAD request, except now the requested file is included (if found)
response, path, data = head_response(headers)
# If there was an error, data will be None (so an error message will be sent)
if data is not None:
if path.endswith(b'png') or path.endswith(b'jpg'):
response += data
else:
response += data.encode()
response += b'\r\n\r\n'
client.send(response)
def get_content_length(headers):
"""Return Content-Length of response
Args:
headers (bytes): headers of HTTP response
Returns:
int: length of the body of the HTTP response
"""
keyword = b'Content-Length:'
_, keyword, after_keyword = headers.partition(keyword)
# after_keyword now contains something like 1562\r\n..., so we only need the part before \r\n
ind = after_keyword.index(b'\r')
return int(after_keyword[:ind])
def get_client_input(client, headers):
"""Get the client input from the HTTP request
Args:
client (object): Client socket
headers (bytes): Headers of the client request
Returns:
bytes: Body of the client request
"""
body = b''
content_length = get_content_length(headers)
# NOTE: we can't just do client.recv(content_length), because the buffersize in recv(bufsize) is a maximum
# so client.recv() can return less bytes than the given buffer size
buff_size = 1024
# Loop as long as the received body is not the same length as given by Content-Length
while len(body) != content_length:
body += client.recv(buff_size)
return body
def do_post_put(client, headers):
"""Handle POST and PUT request. Both are similar in functionality.
For a POST, the data is appended to the existing file or a new file is created.
For a PUT, a new file is created.
NOTE: For this project I just store the incoming files in the same folder (server_text_files),
so I don't look at the specified given path (but I do look at the name of the file).
We can create different directories to match the given path, but then this directory would look ugly.
Args:
client (object): Client socket
headers (bytes): Headers of the client request
"""
# Get name of the given file (without the given directory)
path = headers.split(b' ')[1].decode('utf-8')
total_file_name = path.split('/')[-1] # E.g. index.html
file_name = total_file_name.split('.')[0] # E.g. index
# Get client input
data = get_client_input(client, headers)
# If POST: append to existing file or create new one if file doesn't exist
if headers.startswith(b'POST'):
file = open('server_text_files/' + file_name + '.txt', 'ab')
# If PUT: create new file
elif headers.startswith(b'PUT'):
file = open('server_text_files/' + file_name + '.txt', 'wb')
file.write(data)
file.close()
# Send response to server
response = b'HTTP/1.1 ' + OK + b'\r\n'
response += b'Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S GMT").encode() + b'\r\n' # Returns date as needed in RFC 2616
file = open('server.html', 'r')
data = file.read()
file.close()
response += b'Content-Type: text/html\r\n'
response += b'Content-Length: ' + str(len(data) + len(b'\r\n\r\n')).encode() + b'\r\n'
if b'Connection: close' in headers:
response += b'Connection: close\r\n'
response += b'\r\n'
response += data.encode()
response += b'\r\n\r\n'
client.send(response)
def is_modified_since(headers):
"""Check if the requested file is modified since the date given in the If-Modified-Since header
Args:
headers (bytes): Headers of the client HTTP request
Returns:
boolean: Is file modified since given date?
"""
# Get path of requested file
path = headers.split(b' ')[1].decode('utf-8')
# If path is /, server.html is served
if path == '/':
path = 'server.html'
if path.startswith('/'):
path = path[1:] # Path without first /
# Date will be given as: If-Modified-Since: Sun, 14 Mar 2021 17:10:27 GMT\r\n
index_start = headers.index(b'If-Modified-Since: ') + len(b'If-Modified-Since: ')
temp = headers[index_start:]
index_end = temp.index(b'\r\n')
date = temp[:index_end].decode('utf-8') # E.g. Sun, 14 Mar 2021 17:10:27 GMT
# Parse RFC 2616 date to seconds since epoch
date_to_check = time.mktime(time.strptime(date, '%a, %d %b %Y %H:%M:%S %Z'))
# Get date/time of last modification (returns seconds since epoch)
moddate = os.stat(path)[8]
return moddate > date_to_check
def create_error_message(error):
"""Create an HTTP response for an error message
Args:
error (string): The type of error
Returns:
bytes: HTTP response
"""
response = b'HTTP/1.1 ' + error + b'\r\n'
response += b'Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S GMT").encode() + b'\r\n' # Returns date as needed in RFC 2616
file = open('something_went_wrong.html', 'r')
data = file.read()
file.close()
response += b'Content-Type: text/html\r\n'
response += b'Content-Length: ' + str(len(data) + len(b'\r\n\r\n')).encode() + b'\r\n\r\n'
response += data.encode()
response += b'\r\n\r\n'
return response
def main():
"""Run the code
"""
# Get ipv4 adress (socket.gethostbyname(socket.gethostname()) returns 127..., not 192...)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
server = s.getsockname()[0]
s.close()
port = 5050 # Just some port that's not used by the machine
addr = (server, port)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(addr)
server.listen()
while True:
client, address = server.accept() # blocking: waits until client connected
print('[NEW CONNECTION]', address[0], 'connected.')
threading.Thread(target=handle_connection, args=(client, address)).start()
if __name__ == '__main__':
print('[START] server is running...')
main()