lcmlog-server 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #!/usr/bin/env python3
  2. DIR = "/var/local/log/lcmlog-data"
  3. FROM_DOMAIN = "lcm.mi.infn.it"
  4. import os
  5. import os.path
  6. #from os import stat
  7. #from pwd import getpwuid
  8. import sys
  9. import shlex
  10. import pwd
  11. import logging
  12. import logging.handlers
  13. import hashlib
  14. import contextlib
  15. import toml
  16. import subprocess
  17. # We log what happens every time someone connects
  18. # Preparing the logger
  19. logger = logging.getLogger(__name__)
  20. file_formatter = logging.Formatter("%(asctime)s | %(levelname)8s | %(message)s")
  21. logger.setLevel(logging.INFO)
  22. # Logger handle to log all info
  23. # We use a TimedRotatingFileHandler to rotate logs once a week
  24. file_handler = logging.handlers.TimedRotatingFileHandler(filename = DIR + "/logs/logfile", when = "W6", backupCount = 10)
  25. file_handler.setFormatter(file_formatter)
  26. logger.addHandler(file_handler)
  27. # Update logfile acl
  28. #if pwd.getpwuid(os.stat(DIR + "/logs/logfile").st_uid).pw_name == pwd.getpwuid(os.geteuid()).pw_name:
  29. # subprocess.call(["touch", DIR + "/logs/logfile"])
  30. # #subprocess.call(["chmod", "444", DIR + "/logs/*"])
  31. # subprocess.call(["chmod", "666", DIR + "/logs/logfile"])
  32. #------------------------------------------------------------------------------
  33. def main():
  34. # The user is going to call us through ssh, so to know who he is we can simply get his effective uid
  35. user_id = os.geteuid()
  36. user_name = pwd.getpwuid(user_id).pw_name
  37. logger.info("Started by user " + user_name + " (id " + str(user_id) + ")")
  38. try:
  39. method = input() # Can be GET, POST or UPDATE
  40. logger.info("Method: " + method)
  41. if method == "UPDATE": # We don't need more input lines for the UPDATE method
  42. auth(user_id, "UPDATE", "") # Check if the user can update the database
  43. method_update()
  44. else:
  45. kind = input() # This is the kind of the log, and it can be 150 or Admin
  46. logger.info("Kind: " + kind)
  47. # Now date check is implemented in the client side script
  48. # Maybe, for the future, date control can be implemented aslo here
  49. date = input() # The date of the log
  50. logger.info("Date: " + date)
  51. tags = input() # Tags are comma separated
  52. logger.info("Tags: " + tags)
  53. if kind != "150" and kind != "Admin": # We only have this two log types
  54. raise KindError
  55. if method == "POST":
  56. optional_line = input()
  57. auth(user_id, "POST", kind) # Check if the user can post for the requested kind
  58. log = sys.stdin.read() # Read the log content
  59. if optional_line == "MAIL":
  60. send_mail = True
  61. if kind == "Admin":
  62. TO_ADDRESS = "admins@lcm.mi.infn.it"
  63. REPLY_TO = True
  64. else:
  65. TO_ADDRESS = "working@lcm.mi.infn.it"
  66. REPLY_TO = True
  67. else:
  68. send_mail = False
  69. # optional_line consumes the input but it is not
  70. # a recognized header, so it's
  71. # the first line of the log
  72. log = optional_line + log
  73. method_post(kind, user_name, date, tags, log, send_mail)
  74. elif method == "GET":
  75. auth(user_id, "GET", kind) # Check if the user can get logs for the requested kind
  76. user_to_find = input() # Read the user name of the log writer to search
  77. logger.info("User to find: " + user_to_find)
  78. method_get(kind, user_to_find, date, tags)
  79. else:
  80. raise MethodError
  81. except EOFError as error:
  82. logger.critical("1 Not enough input lines")
  83. sys.exit(1)
  84. except FileNotFoundError as error:
  85. logger.critical("2 File not found")
  86. sys.exit(2)
  87. except FileExistsError as error:
  88. logger.critical("3 File already exists")
  89. sys.exit(3)
  90. except OSError as error:
  91. logger.critical("4 File error")
  92. sys.exit(4)
  93. except MethodError as error:
  94. logger.critical("5 Undefined method")
  95. sys.exit(5)
  96. except KindError as error:
  97. logger.critical("6 Undefined log kind")
  98. sys.exit(6)
  99. except AuthError as error:
  100. logger.critical("7 Authentication error")
  101. sys.exit(7)
  102. except Exception as error:
  103. logger.critical("8 Generic error: " + str(error))
  104. sys.exit(8)
  105. finally:
  106. logger.info("End\n")
  107. #------------------------------------------------------------------------------
  108. # Create new log file and adds it to the database
  109. # kind, user_name, date and tags is the log metadata
  110. # log is the log content
  111. # The log metadata and content is hashed, and the hash is saved in the database and used as the filename for the log
  112. # The return value of the function is the hash
  113. def log_create(kind, user_name, date, tags, log):
  114. name = hashlib.sha512((kind + user_name + date + tags + log).encode("utf-8")).hexdigest()
  115. with open(DIR + "/data/" + name, "x") as f:
  116. f.write(name + "\n" + kind + "\n" + user_name + "\n" + date + "\n" + tags + "\n" + log) # Write the file
  117. with open(DIR + "/data/.data", "a") as f:
  118. f.write(name + ":" + kind + ":" + user_name + ":" + date + ":" + tags + "\n") # And add the entry to the .data file
  119. return name
  120. # Search for the requested entry
  121. # The functions returns a list containing the hash (saved in the database) of all the files that meet the specified criteria
  122. # The kind parameter is mandatory (because different users have different privileges based on it).
  123. # All the other arguments can be empty. Only the arguments that are not empty are taken into consideration for the search
  124. def log_find(kind, user_name, date, tags):
  125. file_list = list()
  126. with open(DIR + "/data/.data", "r") as f:
  127. for line in f:
  128. found = True
  129. l = line.split(":")
  130. # The kind is different
  131. if l[1].find(kind) == -1:
  132. continue
  133. # The username is different, or we aren't searching by username
  134. if user_name and l[2].find(user_name) == -1:
  135. continue
  136. # The date is different, or we aren't searching by date
  137. if date and l[3].find(date) == -1:
  138. continue
  139. # Searh tags
  140. for t in tags.split(","):
  141. if t and l[4].find(t) == -1:
  142. found = False
  143. break
  144. # Save
  145. if found:
  146. file_list.append(l[0])
  147. return file_list
  148. # 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,
  149. # 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,
  150. # it needs to be changed also in the other two locations.
  151. # Add log file to .data
  152. # This function reads an existing log file and adds it to the database
  153. # 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
  154. def log_add(name):
  155. with open(DIR + "/data/" + name, "r") as f, open(DIR + "/data/.data", "a") as data:
  156. data.write(f.readline().rstrip("\n") + ":" + # Hash
  157. f.readline().rstrip("\n") + ":" + # Kind
  158. f.readline().rstrip("\n") + ":" + # User name
  159. f.readline().rstrip("\n") + ":" + # Date
  160. f.readline().rstrip("\n") + "\n") # Tags
  161. # Check if the saved hash is correct, and if it is not, ask the user what to do
  162. # 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
  163. # 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,
  164. # 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.
  165. def log_check(name):
  166. with open(DIR + "/data/" + name, "r") as f:
  167. saved_hash = f.readline().rstrip("\n")
  168. kind = f.readline().rstrip("\n")
  169. user = f.readline().rstrip("\n")
  170. date = f.readline().rstrip("\n")
  171. tags = f.readline().rstrip("\n")
  172. log = f.read()
  173. calc_hash = log_hash(name)
  174. result = saved_hash == calc_hash
  175. if not result:
  176. logger.warning(calc_hash + " hash does not correspond to saved one")
  177. while True:
  178. print("Warning: " + calc_hash + " sh does not correspond to saved one.\n" +
  179. "Do you want to: print the log (p), change the saved hash (c), or leave it as it is (l)?")
  180. c = input()
  181. if c == "p":
  182. sys.stdout.write("Hash: " + saved_hash + "\n")
  183. sys.stdout.write("Kind: " + kind + "\n")
  184. sys.stdout.write("User: " + user + "\n")
  185. sys.stdout.write("Date: " + date + "\n")
  186. sys.stdout.write("Tags: " + tags + "\n")
  187. sys.stdout.write("\n" + log + "\n")
  188. elif c == "l":
  189. logger.info("Hash unchanged")
  190. break
  191. elif c == "c":
  192. with open(DIR + "/data/" + name, "w") as f:
  193. f.write(calc_hash + "\n" + kind + "\n" + user + "\n" + date + "\n" + tags + "\n" + log)
  194. logger.info("Hash changed")
  195. break
  196. return result
  197. # Calculates hash of file
  198. # The first line of the file is the saved hash, therefore it is not considered in the calculation
  199. def log_hash(name):
  200. with open(DIR + "/data/" + name, "r") as f:
  201. f.readline() # The saved hash doesn't enter in the calculation
  202. kind = f.readline().rstrip("\n")
  203. user_name = f.readline().rstrip("\n")
  204. date = f.readline().rstrip("\n")
  205. tags = f.readline().rstrip("\n")
  206. log = f.read()
  207. return hashlib.sha512((kind + user_name + date + tags + log).encode("utf-8")).hexdigest()
  208. # Email the log contents from USER_NAME@FROM_DOMAIN to TO_ADDRESS
  209. # Emails the log contents using the 'mail(1)' program. If REPLY_TO is
  210. # True add the 'Reply-to: ' header to the email
  211. def log_mail(kind, user_name, date, tags, log):
  212. if REPLY_TO:
  213. subprocess.run('printf "%s\n" ' + f'{shlex.quote(log)} | mail -s "Log{kind} {date} {tags}" ' +
  214. f'-r {user_name}@{FROM_DOMAIN} -S replyto="{TO_ADDRESS}" {TO_ADDRESS}', shell=True)
  215. else:
  216. subprocess.run('printf "%s\n" ' + f'{shlex.quote(log)} | mail -s "Log{kind} {date} {tags}" ' +
  217. f'-r {user_name}@{FROM_DOMAIN} {TO_ADDRESS}', shell=True)
  218. return
  219. #------------------------------------------------------------------------------
  220. # Print specified log on stdout
  221. def method_get(kind, user_to_find, date, tags):
  222. file_list = log_find(kind, user_to_find, date, tags)
  223. for name in file_list:
  224. with open(DIR + "/data/" + name, "r") as f:
  225. sys.stdout.write("Hash: " + f.readline())
  226. sys.stdout.write("Kind: " + f.readline())
  227. sys.stdout.write("User: " + f.readline())
  228. sys.stdout.write("Date: " + f.readline())
  229. sys.stdout.write("Tags: " + f.readline())
  230. sys.stdout.write("\n" + f.read() + "------------------\n")
  231. logger.info("GET successful: got " + str(len(file_list)) + " files")
  232. # Write log
  233. def method_post(kind, user_name, date, tags, log, send_mail):
  234. name = log_create(kind, user_name, date, tags, log)
  235. logger.info("POST successful: hash " + name)
  236. if send_mail:
  237. log_mail(kind, user_name, date, tags, log)
  238. logger.info("MAIL sent.")
  239. # Generate .data file
  240. def method_update():
  241. with contextlib.suppress(FileNotFoundError):
  242. os.remove(DIR + "/data/.data")
  243. file_list = os.listdir(DIR + "/data/")
  244. open(DIR + "/data/.data", "x").close()
  245. for name in file_list:
  246. newname = log_hash(name)
  247. if not log_check(name):
  248. os.rename(DIR + "/data/" + name, DIR + "/data/" + newname)
  249. log_add(newname)
  250. logger.info("UPDATE successful: added " + str(len(file_list)) + " files")
  251. #------------------------------------------------------------------------------
  252. # Checks if the user has the permissions to use the requested method
  253. def auth(user_id, method, kind):
  254. # Check if user id is in auth files
  255. # We suppose that every authorized user is ONLY IN A FILE!
  256. user_type = ""
  257. for file_name in ["150","Admin","Valhalla","Nirvana"]:
  258. with open(DIR + "/auth/" + file_name) as f:
  259. for line in f:
  260. line = line.split()[0]
  261. if int(line) == user_id:
  262. # If present, we consider only the user type
  263. user_type = file_name
  264. break
  265. if not user_type == "":
  266. break
  267. else:
  268. # If not in auth files, the user cannot do anything
  269. raise AuthError()
  270. # Now we check the user type permissions
  271. auth_list = toml.load(DIR + "/auth/auth.toml")[user_type]["auth"]
  272. if not method + " " + kind in auth_list:
  273. raise AuthError()
  274. return
  275. #------------------------------------------------------------------------------
  276. # Error definitions
  277. class AuthError(Exception):
  278. pass
  279. class KindError(Exception):
  280. pass
  281. class MethodError(Exception):
  282. pass
  283. #------------------------------------------------------------------------------
  284. # Starting point
  285. if __name__ == "__main__":
  286. main()
  287. # Change permissions to logfile just before leaving. Dirty fix to a not well understood problem
  288. subprocess.call(["chmod", "666", DIR + "/logs/logfile"])