mcfi.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. #!/usr/bin/env python3
  2. __authors__ = [ 'zPlus <zplus@peers.community>' ]
  3. __version__ = '0.0.0'
  4. __license__ = 'AGPL-3.0-or-later'
  5. ###############################################################################
  6. import bottle, json, requests
  7. import os
  8. import time
  9. import subprocess
  10. import gitosis_configen
  11. from bottle import get, HTTPResponse, post, request, route
  12. # Load settings
  13. import settings
  14. # Fire up database
  15. import database
  16. PROTOCOL = "http://"
  17. def debug_log(message):
  18. with open(os.path.join("..",settings.log_path), mode = "a") as file:
  19. file.write(str(time.thread_time()) + " " + message + "\n")
  20. file.close()
  21. # This is used to export the bottle object for the WSGI server
  22. application = bottle.app()
  23. if not "gitosis-admin" in os.listdir():
  24. subprocess.run(["git","clone", "ssh://gitosis@127.0.0.1/gitosis-admin"])
  25. if not "gitosis-admin" in os.listdir():
  26. debug_log("couldn't clone gitosis admin")
  27. exit(1)
  28. os.chdir("gitosis-admin")
  29. def commit_all(message):
  30. subprocess.run(["git","add","-A"])
  31. subprocess.run(["git","commit","-a","-m", '"' + message + '"'])
  32. subprocess.run(["git","push"])
  33. @get('/inbox/<forge>/<actor>')
  34. def read_inbox(forge, actor):
  35. ''' Read INBOX. '''
  36. if forge not in settings.forge:
  37. return HTTPResponse(status=400, body=None)
  38. if 'Authorization' in request.headers:
  39. ''' Forge own INBOX. '''
  40. if request.get_header('Authorization') != 'Bearer {}'.format(settings.forge[forge]['authorization_token']):
  41. return HTTPResponse(status=401, body=None)
  42. messages = database.get_inbox_messages(forge, actor)
  43. return {
  44. "@context": "https://www.w3.org/ns/activitystreams",
  45. "summary": actor + " messages",
  46. "type": "OrderedCollection",
  47. "totalItems": len(messages),
  48. "orderedItems": messages
  49. }
  50. else:
  51. ''' Public access to INBOX. '''
  52. pass # Not implemented
  53. @post('/inbox/<forge>/<actor>')
  54. def write_inbox(forge, actor):
  55. ''' Write to INBOX. '''
  56. if forge not in settings.forge:
  57. return HTTPResponse(status=400, body=None)
  58. if 'Authorization' in request.headers:
  59. if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
  60. return HTTPResponse(status=401, body=None)
  61. pass # Not implemented
  62. else:
  63. ''' We have received a messaged from a remote actor to our INBOX. '''
  64. message = request.json
  65. if not message:
  66. return HTTPResponse(status=400, body=None)
  67. # If the action is "Follow", remember remote followers
  68. if message['type'] == 'Follow':
  69. database.add_follower(forge, message['actor'], actor)
  70. # Store message
  71. database.store_inbox(forge, actor, message)
  72. # Forward message to the forge
  73. if settings.forge[forge]['callback'] is not None:
  74. requests.post(
  75. settings.forge[forge]['callback'] + actor,
  76. headers = {
  77. 'Authorization': 'Bearer {}'.format(settings.forge[forge]['authorization_token'])
  78. },
  79. json = message)
  80. @get('/outbox/<forge>/<actor>')
  81. def read_outbox(forge, actor):
  82. ''' Read OUTBOX. '''
  83. if forge not in settings.forge:
  84. return HTTPResponse(status=400, body=None)
  85. if 'Authorization' in request.headers:
  86. ''' Forge own OUTBOX. '''
  87. pass # Not implemented
  88. else:
  89. pass # Not implemented
  90. @post('/outbox/<forge>/<actor>')
  91. def write_outbox(forge, actor):
  92. ''' Write to OUTBOX. '''
  93. if forge not in settings.forge:
  94. return HTTPResponse(status=400, body=None)
  95. # Must be authenticated to write the OUTBOX
  96. if 'Authorization' not in request.headers:
  97. return HTTPResponse(status=401, body=None)
  98. # Validate authentication token
  99. if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
  100. return HTTPResponse(status=401, body=None)
  101. message = request.json
  102. # Store message to database
  103. database.store_outbox(forge, actor, message)
  104. if message['type'] == 'Follow':
  105. # Retrieve remote actor
  106. response = requests.get(message['object'], headers={ 'Content-Type': 'application/json' })
  107. if response.status_code == 200:
  108. remote_actor = response.json()
  109. database.add_following(forge, actor, message['object'])
  110. # Now send a notification to the actor being followed
  111. requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)
  112. elif message['type'] == 'Create':
  113. # If there is no "to:" field, assume "all followers"
  114. if "to" not in message:
  115. message['to'] = database.get_followers(forge, actor)
  116. if not isinstance(message['to'], list):
  117. message['to'] = [ message['to'] ]
  118. for follower in message['to']:
  119. # Retrieve remote actor
  120. response = requests.get(follower, headers={ 'Content-Type': 'application/json' })
  121. if response.status_code == 200:
  122. remote_actor = response.json()
  123. # Now send a notification to the actor being followed
  124. requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)
  125. def key_path(name):
  126. key_name = "{}.pub".format(name)
  127. key_path = os.path.join("keydir", key_name)
  128. return key_path
  129. def add_key(name,key):
  130. with open(key_path(name), mode="w") as file:
  131. file.write(key)
  132. file.close()
  133. def key_exists(name):
  134. if os.path.isfile(key_path(name)):
  135. return True
  136. return False
  137. def import_foreign_key(address):
  138. user, host = address.split("@")
  139. foreigner = PROTOCOL+host+"/users/"+user #IMPORTANT: don't forget to change if routing changes
  140. response = requests.get(foreigner, headers={ 'Content-Type': 'application/json' })
  141. if response.status_code != 200:
  142. return False
  143. res_body = response.json()
  144. if "publicKey" not in res_body.keys():
  145. return False
  146. add_key(address, res_body["publicKey"]) # we add a key without adding a user to a db
  147. return True
  148. @post('/users/register')
  149. def user_register():
  150. ''' Register a user '''
  151. #TODO: it shouldn't contain @ or be "admin-daemon" or "register" obviously
  152. debug_log("incoming post at /users/register")
  153. required_keys = [
  154. "username",
  155. "password",
  156. "email",
  157. "key",
  158. ]
  159. req = request.json
  160. if any([(x not in req.keys()) for x in required_keys]):
  161. return HTTPResponse(status=400, body=None)
  162. if key_exists(req["username"]):
  163. return HTTPResponse(status=400, body=None)
  164. #TODO also it should respond with 400 if smb tries to add the same key under a different username
  165. res = database.add_user(
  166. req["username"],
  167. req["email"],
  168. req["password"],
  169. req["key"],
  170. )
  171. if not res:
  172. return HTTPResponse(status=400, body=None)
  173. add_key(req["username"], req["key"])
  174. commit_all("daemon: user created")
  175. return HTTPResponse(status=204, body=None)
  176. @post('/users/<user>/new')
  177. def add_repo(user):
  178. ''' make a new repo '''
  179. debug_log("incoming post at /users/" + user + "/new")
  180. required_keys = [
  181. "repository_name",
  182. ]
  183. req = request.json
  184. if any([(x not in req.keys()) for x in required_keys]):
  185. return HTTPResponse(status=400, body=None)
  186. if not key_exists(user): #check user exists
  187. return HTTPResponse(status=400, body=None)
  188. if req["repository_name"] in [ x["repository"] for x in database.get_repositories() ]: #check repo nonexistent
  189. return HTTPResponse(status=400, body=None)
  190. database.add_repository( #TODO maybe better to move existence check to add function
  191. req["repository_name"],
  192. user,
  193. )
  194. database.regenerate_gitosis_config()
  195. commit_all("daemon: repo created")
  196. return HTTPResponse(status=204, body=None)
  197. @post('/users/<user>/<repository_name>/collaborators/new')
  198. def add_collaboration(user, repository_name):
  199. ''' make a new collaboration'''
  200. debug_log("incoming post at /users/" + user + "/" + repository_name + "/collaborators/new")
  201. required_keys = [ # complex collaborators names including instances are bad for inlining in address
  202. "collaborator",
  203. ]
  204. req = request.json
  205. if any([(x not in req.keys()) for x in required_keys]):
  206. return HTTPResponse(status=400, body="some fields are missing")
  207. if not key_exists(user): #check user exists
  208. return HTTPResponse(status=404, body=None)
  209. if not repository_name in [ x["repository"] for x in database.get_repositories() ]:#check repo
  210. return HTTPResponse(status=404, body=None)
  211. if req["collaborator"] in database.get_collaborators(repository_name):
  212. return HTTPResponse(status=400, body="collaborator exists")
  213. if "@" in req["collaborator"]:
  214. if not import_foreign_key(req["collaborator"]):
  215. return HTTPResponse(status=400, body="foreign collaborator does not exist")
  216. else:
  217. #TODO:check existence of local collaborator's user
  218. pass
  219. database.add_collaboration(
  220. repository_name, #TODO: disambugating reponames so different users can have repos with same names
  221. req["collaborator"],
  222. )
  223. database.regenerate_gitosis_config()
  224. commit_all("daemon: collaborator added")
  225. return HTTPResponse(status=204, body=None)
  226. #@get('/<forge>/<actor>')
  227. #def describe_actor(forge, actor):
  228. # ''' Return profile of an actor. '''
  229. #
  230. # if forge not in settings.forge:
  231. # return HTTPResponse(status=400, body=None)
  232. #
  233. # return {
  234. # '@context': 'https://www.w3.org/ns/activitystreams',
  235. # 'type': '',
  236. # 'id': '{}/{}/{}'.format(settings.mcfi_url, forge, actor),
  237. # 'name': '{}'.format(actor),
  238. # 'summary': '',
  239. # 'inbox': '{}/inbox/{}/{}'.format(settings.mcfi_url, forge, actor),
  240. # 'outbox': '{}/outbox/{}/{}'.format(settings.mcfi_url, forge, actor),
  241. # 'followers': '{}/followers/{}/{}'.format(settings.mcfi_url, forge, actor),
  242. # 'following': '{}/following/{}/{}'.format(settings.mcfi_url, forge, actor)
  243. # }
  244. @get('/users/<username>')
  245. def describe_user(username):
  246. ''' Return profile of a user '''
  247. id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
  248. user = database.get_user_local(username)
  249. if not user:
  250. return HTTPResponse(status=400, body=None)
  251. return {
  252. '@context': [
  253. "https://www.w3.org/ns/activitystreams",
  254. "https://forgefed.peers.community/ns"
  255. ],
  256. 'type': 'Person',
  257. 'id': id_,
  258. 'name': '{}'.format(username),
  259. 'preferredUsername': '{}'.format(username),
  260. 'summary': '',
  261. 'inbox': '{}/inbox'.format(id_),
  262. 'outbox': '{}/outbox'.format(id_),
  263. 'followers': '{}/followers'.format(id_),
  264. 'following': '{}/following'.format(id_),
  265. 'publicKey': user["public_key"]
  266. }
  267. @get('/users/<username>/<repository>')
  268. def describe_repo(username,repository):
  269. ''' describe a repo'''
  270. #id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
  271. #user = database.get_user_local(username)
  272. # if not user:
  273. # return HTTPResponse(status=400, body=None)
  274. return {
  275. '@context': [
  276. "https://www.w3.org/ns/activitystreams",
  277. "https://forgefed.peers.community/ns"
  278. ],
  279. 'collaborators': json.dumps(database.get_collaborators(repository))#TODO:don't forget if reponame mech changes.
  280. # 'id': id_,
  281. # 'name': '{}'.format(username),
  282. # 'preferredUsername': '{}'.format(username),
  283. # 'summary': '',
  284. # 'inbox': '{}/inbox'.format(id_),
  285. # 'outbox': '{}/outbox'.format(id_),
  286. # 'followers': '{}/followers'.format(id_),
  287. # 'following': '{}/following'.format(id_),
  288. # 'publicKey': user["public_key"]
  289. }
  290. @get('/administration/reset-database')
  291. def reset_db():
  292. ''' drop all users and reset things'''
  293. if settings.DEBUG:
  294. for key in os.listdir("keydir"):
  295. if key != 'admin-daemon.pub':
  296. path = os.path.join("keydir", key)
  297. os.remove(path)
  298. config = gitosis_configen.Config()
  299. config.flush()
  300. commit_all("daemon: drop all users")
  301. db_path=os.path.join("..",settings.database_path)
  302. if os.path.isfile(db_path):
  303. os.remove(db_path)
  304. exit(0) #forcing a worker restart so that restarting worker regenerates database.
  305. @get('/oauth-clients/local')
  306. def generate_clientid():
  307. ''' Generate or "generate" client id and secret''' #maybe real generation someday, but i can't see a point yet
  308. return {
  309. 'client_id': settings.client_id,
  310. 'client_secret': settings.client_secret,
  311. }
  312. @route('/')
  313. def index():
  314. return 'MCFI version ' + __version__