lcmlog-server 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. #!/usr/bin/env python3
  2. DIR="/usr/local/src/lcm-unimi/lcmlog-server"
  3. import os
  4. import os.path
  5. import sys
  6. import pwd
  7. import logging
  8. import logging.handlers
  9. import hashlib
  10. import contextlib
  11. # We log what happens every time someone connects
  12. # Preparing the logger
  13. logger = logging.getLogger(__name__)
  14. file_formatter = logging.Formatter("%(asctime)s | %(levelname)8s | %(message)s")
  15. logger.setLevel(logging.INFO)
  16. # Logger handle to log all info
  17. # We use a TimedRotatingFileHandler to rotate logs once a week
  18. file_handler = logging.handlers.TimedRotatingFileHandler(filename = DIR + "/logs/logfile", when = "W6", backupCount = 10)
  19. file_handler.setFormatter(file_formatter)
  20. logger.addHandler(file_handler)
  21. #------------------------------------------------------------------------------
  22. def main():
  23. # The user is going to call us through ssh, so to know who he is we can simply get his effective uid
  24. user_id = os.geteuid()
  25. user_name = pwd.getpwuid(user_id).pw_name
  26. logger.info("Started by user " + user_name + " (id " + str(user_id) + ")")
  27. try:
  28. method = input() # Can be GET, POST or UPDATE
  29. logger.info("Method: " + method)
  30. if method == "UPDATE": # We don't need more input lines for the UPDATE method
  31. auth(user_id, "UPDATE", "") # Check if the user can update the database
  32. method_update()
  33. else:
  34. kind = input() # This is the kind of the log, and it can be 150 or Admin
  35. logger.info("Kind: " + kind)
  36. # TODO: check if the date format is valid (after deciding which format is to be considered valid)
  37. date = input() # The date of the log
  38. logger.info("Date: " + date)
  39. tags = input() # Tags are comma separated
  40. logger.info("Tags: " + tags)
  41. if kind != "150" and kind != "Admin": # We only have this two log types
  42. raise KindError
  43. if method == "POST":
  44. auth(user_id, "POST", kind) # Check if the user can post for the requested kind
  45. log = sys.stdin.read() # Read the log content
  46. method_post(kind, user_name, date, tags, log)
  47. elif method == "GET":
  48. auth(user_id, "GET", kind) # Check if the user can get logs for the requested kind
  49. user_to_find = input() # Read the user name of the log writer to search
  50. logger.info("User to find: " + user_to_find)
  51. method_get(kind, user_to_find, date, tags)
  52. else:
  53. raise MethodError
  54. except EOFError as error:
  55. logger.critical("1 Not enough input lines")
  56. sys.exit(1)
  57. except FileNotFoundError as error:
  58. logger.critical("2 File not found")
  59. sys.exit(2)
  60. except FileExistsError as error:
  61. logger.critical("3 File already exists")
  62. sys.exit(3)
  63. except OSError as error:
  64. logger.critical("4 File error")
  65. sys.exit(4)
  66. except MethodError as error:
  67. logger.critical("5 Undefined method")
  68. sys.exit(5)
  69. except KindError as error:
  70. logger.critical("6 Undefined log kind")
  71. sys.exit(6)
  72. except AuthError as error:
  73. logger.critical("7 Authentication error")
  74. sys.exit(7)
  75. except Exception as error:
  76. logger.critical("8 Generic error: " + str(error))
  77. sys.exit(8)
  78. finally:
  79. logger.info("End\n")
  80. #------------------------------------------------------------------------------
  81. # Create new log file and adds it to the database
  82. # kind, user_name, date and tags is the log metadata
  83. # log is the log content
  84. # The log metadata and content is hashed, and the hash is saved in the database and used as the filename for the log
  85. # The return value of the function is the hash
  86. def log_create(kind, user_name, date, tags, log):
  87. # TODO: utf-8 encoding doesn't allow accented characters. Find a more suitable encoding
  88. name = hashlib.sha512((kind + user_name + date + tags + log).encode("utf-8")).hexdigest()
  89. with open(DIR + "/data/" + name, "x") as f:
  90. f.write(name + "\n" + kind + "\n" + user_name + "\n" + date + "\n" + tags + "\n" + log) # Write the file
  91. with open(DIR + "/data/.data", "a") as f:
  92. f.write(name + ":" + kind + ":" + user_name + ":" + date + ":" + tags + "\n") # And add the entry to the .data file
  93. return name
  94. # Search for the requested entry
  95. # The functions returns a list containing the hash (saved in the database) of all the files that meet the specified criteria
  96. # The kind parameter is mandatory (because different users have different privileges based on it).
  97. # All the other arguments can be empty. Only the arguments that are not empty are taken into consideration for the search
  98. def log_find(kind, user_name, date, tags):
  99. file_list = list()
  100. with open(DIR + "/data/.data", "r") as f:
  101. for line in f:
  102. found = True
  103. l = line.split(":")
  104. if l[1].find(kind) == -1: # The kind is different
  105. continue
  106. if user_name and l[2].find(user_name) == -1: # The username is different, or we aren't searching by username
  107. continue
  108. if date and l[3].find(date) == -1: # The date is different, or we aren't searching by date
  109. continue
  110. for t in tags.split(","):
  111. if t and l[4].find(t) == -1:
  112. found = False
  113. break
  114. if found:
  115. file_list.append(l[0])
  116. return file_list
  117. # TODO: the following functions work with the hash of the log files. The problem is that there are three different places where the hash is: the first line of the file, the database entry for the log and the filename of the log. I have to decide which function operates on which hash, because for example if the hash is changed in the file, it needs to be changed also in the other two locations.
  118. # Add log file to .data
  119. # This function reads an existing log file and adds it to the database
  120. # Tha hash that is saved in the database is not calculated: the first line in the file is considered to be the hash. Use log_check to check if they are the same
  121. def log_add(name):
  122. with open(DIR + "/data/" + name, "r") as f, open(DIR + "/data/.data", "a") as data:
  123. data.write(f.readline().rstrip("\n") + ":" + # Hash
  124. f.readline().rstrip("\n") + ":" + # Kind
  125. f.readline().rstrip("\n") + ":" + # User name
  126. f.readline().rstrip("\n") + ":" + # Date
  127. f.readline().rstrip("\n") + "\n") # Tags
  128. # Check if the saved hash is correct, and if it is not, ask the user what to do
  129. # This function calculates the hash of the file with filename name, and returns True if it is the same as the first line of the file, False otherwise
  130. # If it doesn't correspond, it asks the user if he wants to keep it like it is or change it. Currently, it is pretty messed up: only the hash saved in the file is changed, not the one saved in the database or the file name. Also, the dialog to ask if the hash is to be changed probably should not be in this function.
  131. def log_check(name):
  132. with open(DIR + "/data/" + name, "r") as f:
  133. saved_hash = f.readline().rstrip("\n")
  134. kind = f.readline().rstrip("\n")
  135. user = f.readline().rstrip("\n")
  136. date = f.readline().rstrip("\n")
  137. tags = f.readline().rstrip("\n")
  138. log = f.read()
  139. calc_hash = log_hash(name)
  140. result = saved_hash == calc_hash
  141. if not result:
  142. logger.warning(calc_hash + " hash does not correspond to saved one")
  143. while True:
  144. print("Warning: " + calc_hash + " sh does not correspond to saved one.\n" +
  145. "Do you want to: print the log (p), change the saved hash (c), or leave it as it is (l)?")
  146. c = input()
  147. if c == "p":
  148. sys.stdout.write("Hash: " + saved_hash + "\n")
  149. sys.stdout.write("Kind: " + kind + "\n")
  150. sys.stdout.write("User: " + user + "\n")
  151. sys.stdout.write("Date: " + date + "\n")
  152. sys.stdout.write("Tags: " + tags + "\n")
  153. sys.stdout.write("\n" + log + "\n")
  154. elif c == "l":
  155. logger.info("Hash unchanged")
  156. break
  157. elif c == "c":
  158. with open(DIR + "/data/" + name, "w") as f:
  159. f.write(calc_hash + "\n" + kind + "\n" + user + "\n" + date + "\n" + tags + "\n" + log)
  160. logger.info("Hash changed")
  161. break
  162. return result
  163. # Calculates hash of file
  164. # The first line of the file is the saved hash, therefore it is not considered in the calculation
  165. def log_hash(name):
  166. with open(DIR + "/data/" + name, "r") as f:
  167. f.readline() # The saved hash doesn'e enter in the calculation
  168. kind = f.readline().rstrip("\n")
  169. user_name = f.readline().rstrip("\n")
  170. date = f.readline().rstrip("\n")
  171. tags = f.readline().rstrip("\n")
  172. log = f.read()
  173. return hashlib.sha512((kind + user_name + date + tags + log).encode("utf-8")).hexdigest()
  174. #------------------------------------------------------------------------------
  175. # Print specified log on stdout
  176. def method_get(kind, user_to_find, date, tags):
  177. file_list = log_find(kind, user_to_find, date, tags)
  178. for name in file_list:
  179. with open(DIR + "/data/" + name, "r") as f:
  180. sys.stdout.write("Hash: " + f.readline())
  181. sys.stdout.write("Kind: " + f.readline())
  182. sys.stdout.write("User: " + f.readline())
  183. sys.stdout.write("Date: " + f.readline())
  184. sys.stdout.write("Tags: " + f.readline())
  185. sys.stdout.write("\n" + f.read() + "------------------\n")
  186. logger.info("GET succesful: got " + str(len(file_list)) + " files")
  187. # Write log
  188. def method_post(kind, user_name, date, tags, log):
  189. name = log_create(kind, user_name, date, tags, log)
  190. logger.info("POST succesful: hash " + name)
  191. # Generate .data file
  192. def method_update():
  193. with contextlib.suppress(FileNotFoundError):
  194. os.remove(DIR + "/data/.data")
  195. file_list = os.listdir(DIR + "/data/")
  196. open(DIR + "/data/.data", "x").close()
  197. for name in file_list:
  198. newname = log_hash(name)
  199. if not log_check(name):
  200. os.rename(DIR + "/data/" + name, DIR + "/data/" + newname)
  201. log_add(newname)
  202. logger.info("UPDATE succesful: added " + str(len(file_list)) + " files")
  203. #------------------------------------------------------------------------------
  204. # Checks if the user has the permissions to use the requested method
  205. def auth(user_id, method, kind):
  206. with open(DIR + "/auth/" + kind + "/" + method) as f:
  207. for line in f:
  208. if int(line) == user_id:
  209. return
  210. raise AuthError()
  211. #------------------------------------------------------------------------------
  212. # Error definitions
  213. class AuthError(Exception):
  214. pass
  215. class KindError(Exception):
  216. pass
  217. class MethodError(Exception):
  218. pass
  219. #------------------------------------------------------------------------------
  220. # Starting point
  221. if __name__ == "__main__":
  222. main()