server.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/python3
  2. import socket
  3. import codecs
  4. ## define Request and Responce classes to simplify the actual code
  5. class Request:
  6. def __init__(self, request_data):
  7. try:
  8. decoded_request = request_data.decode("UTF-8")
  9. self.successful = True
  10. except Exception as e:
  11. print("Failed to decode request ({})".format(e))
  12. self.successful = False
  13. if self.successful:
  14. self.method = decoded_request.split(" ")[0]
  15. self.path = decoded_request.split(" ")[1]
  16. class Responce:
  17. def __init__(self):
  18. self.code = ""
  19. self.content_type = ""
  20. self.body = ""
  21. def generate(self):
  22. responce_data = "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}".format(self.code, self.content_type, len(self.body.encode("UTF-8")), self.body)
  23. return responce_data.encode("UTF-8")
  24. ## defining intify() function to make code reading easier
  25. ## and to create a failsafe wrapper around the int() built-in function
  26. def intify(x):
  27. try:
  28. return int(x)
  29. except Exception as e:
  30. print("Failed to parse integer from {} ({})".format(x, e))
  31. return False
  32. ## defining readfile() function to make code easier to read
  33. def readfile(path):
  34. try:
  35. return codecs.open(path, "r", encoding = "UTF-8").read()
  36. except FileNotFoundError:
  37. print("No such file: {}".format(path))
  38. return False
  39. except Exception as e:
  40. print("Failed to read file {} ({})".format(path, e))
  41. return False
  42. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  43. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  44. s.bind( ("", 8080) )
  45. s.listen()
  46. print("[INFO] Server started")
  47. while True:
  48. try:
  49. conn, addr = s.accept()
  50. ## allowing very high time intervals as they
  51. ## are possible on slow networks during
  52. ## high loads (or during war)
  53. conn.settimeout(1800)
  54. connection_alive = True
  55. request_buffer = bytes()
  56. ## receiving request
  57. while True:
  58. new_data = conn.recv(1024)
  59. if len(new_data) == 0:
  60. ## connection closed, cleaning up
  61. conn.close()
  62. connection_alive = False
  63. break
  64. request_buffer += new_data
  65. if request_buffer[-4:] == "\r\n\r\n".encode("UTF-8"):
  66. ## request finished, stopping the loop
  67. break
  68. ## check if client is still there
  69. if not connection_alive:
  70. print("[LOG] Lost connection from {}".format(addr[0] + ":" + str(addr[1])))
  71. conn.close()
  72. continue
  73. ## processing the request
  74. req = Request(request_buffer)
  75. ## if it is not a proper request, ignore it
  76. if not req.successful:
  77. conn.close()
  78. continue
  79. ## logging requests to terminal
  80. print("[LOG] {} - {} {}".format( addr[0] + ":" + str(addr[1]), req.method, req.path ))
  81. if req.method == "GET":
  82. ## giving index page at /
  83. if req.path == "/":
  84. responce_body = readfile("www/index.html")
  85. if responce_body == False:
  86. conn.close()
  87. continue
  88. res = Responce()
  89. res.code = "200"
  90. res.content_type = "text/html"
  91. res.body = responce_body
  92. responce_data = res.generate()
  93. conn.send(responce_data)
  94. conn.close()
  95. ## giving requested amount of lines in the end of the file at /last/*
  96. elif req.path[:6] == "/last/":
  97. raw_request_amount = req.path.split("/")[2]
  98. x = intify(raw_request_amount)
  99. if x == False:
  100. conn.close()
  101. continue
  102. file_content = readfile("files/links.txt")
  103. if file_content == False:
  104. conn.close()
  105. continue
  106. file_lines = file_content.split("\n")
  107. res = Responce()
  108. res.code = "200"
  109. res.content_type = "text/plain"
  110. res.body = "\r\n".join(file_lines[-x-1:]) # selecting requested amount of lines
  111. responce_data = res.generate()
  112. conn.send(responce_data)
  113. conn.close()
  114. ## giving the whole file at /all
  115. elif req.path == "/all":
  116. responce_body = readfile("files/links.txt")
  117. if responce_body == False:
  118. conn.close()
  119. continue
  120. res = Responce()
  121. res.code = "200"
  122. res.content_type = "text/plain"
  123. res.body = responce_body
  124. responce_data = res.generate()
  125. conn.send(responce_data)
  126. conn.close()
  127. ## at everything else return 404 Not Found error
  128. else:
  129. res = Responce()
  130. res.code = "404"
  131. res.content_type = "text/plain"
  132. res.body = "{} is not a supported request path".format(req.path)
  133. responce_data = res.generate()
  134. conn.send(responce_data)
  135. conn.close()
  136. ## HEAD is not supported yet to reduce debug time and possible error surface
  137. elif req.method == "HEAD":
  138. res = Responce()
  139. res.code = "501"
  140. res.content_type = "text/plain"
  141. res.body = "{} is not a supported request method - yes, it IS intentional (and yes, it WILL be added in upcoming releases; it's not there yet to make a working release faster by reducing the amount of code to debug and polish)".format(req.method)
  142. responce_data = res.generate()
  143. conn.send(responce_data)
  144. conn.close()
  145. ## other methods are not supported yet
  146. else:
  147. res = Responce()
  148. res.code = "501"
  149. res.content_type = "text/plain"
  150. res.body = "{} is not a supported request method".format(req.method)
  151. responce_data = res.generate()
  152. conn.send(responce_data)
  153. conn.close()
  154. ## handling Ctrl+C signal
  155. except KeyboardInterrupt:
  156. print("\r[INFO] Exiting properly")
  157. break
  158. ## show thread exception as non-critical error
  159. except Exception as e:
  160. print("[ERROR] Got exception {}".format(e))
  161. conn.close()
  162. continue