123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- #!/usr/bin/env python3
- __authors__ = [ 'zPlus <zplus@peers.community>' ]
- __version__ = '0.0.0'
- __license__ = 'AGPL-3.0-or-later'
- ###############################################################################
- import bottle, json, requests
- import os
- import time
- import subprocess
- import gitosis_configen
- from bottle import get, HTTPResponse, post, request, route
- # Load settings
- import settings
- # Fire up database
- import database
- PROTOCOL = "http://"
- def debug_log(message):
- with open(os.path.join("..",settings.log_path), mode = "a") as file:
- file.write(str(time.thread_time()) + " " + message + "\n")
- file.close()
-
- # This is used to export the bottle object for the WSGI server
- application = bottle.app()
- if not "gitosis-admin" in os.listdir():
- subprocess.run(["git","clone", "ssh://gitosis@127.0.0.1/gitosis-admin"])
- if not "gitosis-admin" in os.listdir():
- debug_log("couldn't clone gitosis admin")
- exit(1)
- os.chdir("gitosis-admin")
- def commit_all(message):
- subprocess.run(["git","add","-A"])
- subprocess.run(["git","commit","-a","-m", '"' + message + '"'])
- subprocess.run(["git","push"])
-
- @get('/inbox/<forge>/<actor>')
- def read_inbox(forge, actor):
- ''' Read INBOX. '''
-
- if forge not in settings.forge:
- return HTTPResponse(status=400, body=None)
-
- if 'Authorization' in request.headers:
- ''' Forge own INBOX. '''
-
- if request.get_header('Authorization') != 'Bearer {}'.format(settings.forge[forge]['authorization_token']):
- return HTTPResponse(status=401, body=None)
-
- messages = database.get_inbox_messages(forge, actor)
-
- return {
- "@context": "https://www.w3.org/ns/activitystreams",
- "summary": actor + " messages",
- "type": "OrderedCollection",
- "totalItems": len(messages),
- "orderedItems": messages
- }
-
- else:
- ''' Public access to INBOX. '''
-
- pass # Not implemented
- @post('/inbox/<forge>/<actor>')
- def write_inbox(forge, actor):
- ''' Write to INBOX. '''
-
- if forge not in settings.forge:
- return HTTPResponse(status=400, body=None)
-
- if 'Authorization' in request.headers:
- if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
- return HTTPResponse(status=401, body=None)
-
- pass # Not implemented
-
- else:
- ''' We have received a messaged from a remote actor to our INBOX. '''
-
- message = request.json
-
- if not message:
- return HTTPResponse(status=400, body=None)
-
- # If the action is "Follow", remember remote followers
- if message['type'] == 'Follow':
- database.add_follower(forge, message['actor'], actor)
-
- # Store message
- database.store_inbox(forge, actor, message)
-
- # Forward message to the forge
- if settings.forge[forge]['callback'] is not None:
- requests.post(
- settings.forge[forge]['callback'] + actor,
- headers = {
- 'Authorization': 'Bearer {}'.format(settings.forge[forge]['authorization_token'])
- },
- json = message)
- @get('/outbox/<forge>/<actor>')
- def read_outbox(forge, actor):
- ''' Read OUTBOX. '''
-
- if forge not in settings.forge:
- return HTTPResponse(status=400, body=None)
-
- if 'Authorization' in request.headers:
- ''' Forge own OUTBOX. '''
-
- pass # Not implemented
- else:
- pass # Not implemented
- @post('/outbox/<forge>/<actor>')
- def write_outbox(forge, actor):
- ''' Write to OUTBOX. '''
-
- if forge not in settings.forge:
- return HTTPResponse(status=400, body=None)
-
- # Must be authenticated to write the OUTBOX
- if 'Authorization' not in request.headers:
- return HTTPResponse(status=401, body=None)
-
- # Validate authentication token
- if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
- return HTTPResponse(status=401, body=None)
-
- message = request.json
-
- # Store message to database
- database.store_outbox(forge, actor, message)
-
- if message['type'] == 'Follow':
- # Retrieve remote actor
- response = requests.get(message['object'], headers={ 'Content-Type': 'application/json' })
-
- if response.status_code == 200:
- remote_actor = response.json()
-
- database.add_following(forge, actor, message['object'])
-
- # Now send a notification to the actor being followed
- requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)
- elif message['type'] == 'Create':
- # If there is no "to:" field, assume "all followers"
- if "to" not in message:
- message['to'] = database.get_followers(forge, actor)
-
- if not isinstance(message['to'], list):
- message['to'] = [ message['to'] ]
-
- for follower in message['to']:
- # Retrieve remote actor
- response = requests.get(follower, headers={ 'Content-Type': 'application/json' })
-
- if response.status_code == 200:
- remote_actor = response.json()
-
- # Now send a notification to the actor being followed
- requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)
- def key_path(name):
-
- key_name = "{}.pub".format(name)
- key_path = os.path.join("keydir", key_name)
- return key_path
- def add_key(name,key):
-
- with open(key_path(name), mode="w") as file:
- file.write(key)
- file.close()
- def key_exists(name):
- if os.path.isfile(key_path(name)):
- return True
- return False
- def import_foreign_key(address):
-
- user, host = address.split("@")
- foreigner = PROTOCOL+host+"/users/"+user #IMPORTANT: don't forget to change if routing changes
- response = requests.get(foreigner, headers={ 'Content-Type': 'application/json' })
- if response.status_code != 200:
- return False
- res_body = response.json()
- if "publicKey" not in res_body.keys():
- return False
-
- add_key(address, res_body["publicKey"]) # we add a key without adding a user to a db
-
- return True
- @post('/users/register')
- def user_register():
- ''' Register a user '''
- #TODO: it shouldn't contain @ or be "admin-daemon" or "register" obviously
-
- debug_log("incoming post at /users/register")
-
- required_keys = [
- "username",
- "password",
- "email",
- "key",
- ]
-
- req = request.json
- if any([(x not in req.keys()) for x in required_keys]):
- return HTTPResponse(status=400, body=None)
- if key_exists(req["username"]):
- return HTTPResponse(status=400, body=None)
- #TODO also it should respond with 400 if smb tries to add the same key under a different username
- res = database.add_user(
- req["username"],
- req["email"],
- req["password"],
- req["key"],
- )
-
- if not res:
- return HTTPResponse(status=400, body=None)
-
- add_key(req["username"], req["key"])
-
- commit_all("daemon: user created")
-
-
- return HTTPResponse(status=204, body=None)
- @post('/users/<user>/new')
- def add_repo(user):
- ''' make a new repo '''
-
- debug_log("incoming post at /users/" + user + "/new")
-
- required_keys = [
- "repository_name",
- ]
-
- req = request.json
- if any([(x not in req.keys()) for x in required_keys]):
- return HTTPResponse(status=400, body=None)
- if not key_exists(user): #check user exists
- return HTTPResponse(status=400, body=None)
-
- if req["repository_name"] in [ x["repository"] for x in database.get_repositories() ]: #check repo nonexistent
- return HTTPResponse(status=400, body=None)
-
- database.add_repository( #TODO maybe better to move existence check to add function
- req["repository_name"],
- user,
- )
-
- database.regenerate_gitosis_config()
-
- commit_all("daemon: repo created")
- return HTTPResponse(status=204, body=None)
- @post('/users/<user>/<repository_name>/collaborators/new')
- def add_collaboration(user, repository_name):
- ''' make a new collaboration'''
-
- debug_log("incoming post at /users/" + user + "/" + repository_name + "/collaborators/new")
-
- required_keys = [ # complex collaborators names including instances are bad for inlining in address
- "collaborator",
- ]
-
- req = request.json
- if any([(x not in req.keys()) for x in required_keys]):
- return HTTPResponse(status=400, body="some fields are missing")
- if not key_exists(user): #check user exists
- return HTTPResponse(status=404, body=None)
-
- if not repository_name in [ x["repository"] for x in database.get_repositories() ]:#check repo
- return HTTPResponse(status=404, body=None)
-
- if req["collaborator"] in database.get_collaborators(repository_name):
- return HTTPResponse(status=400, body="collaborator exists")
-
- if "@" in req["collaborator"]:
- if not import_foreign_key(req["collaborator"]):
- return HTTPResponse(status=400, body="foreign collaborator does not exist")
-
- else:
- #TODO:check existence of local collaborator's user
- pass
-
- database.add_collaboration(
- repository_name, #TODO: disambugating reponames so different users can have repos with same names
- req["collaborator"],
- )
-
- database.regenerate_gitosis_config()
-
- commit_all("daemon: collaborator added")
- return HTTPResponse(status=204, body=None)
- #@get('/<forge>/<actor>')
- #def describe_actor(forge, actor):
- # ''' Return profile of an actor. '''
- #
- # if forge not in settings.forge:
- # return HTTPResponse(status=400, body=None)
- #
- # return {
- # '@context': 'https://www.w3.org/ns/activitystreams',
- # 'type': '',
- # 'id': '{}/{}/{}'.format(settings.mcfi_url, forge, actor),
- # 'name': '{}'.format(actor),
- # 'summary': '',
- # 'inbox': '{}/inbox/{}/{}'.format(settings.mcfi_url, forge, actor),
- # 'outbox': '{}/outbox/{}/{}'.format(settings.mcfi_url, forge, actor),
- # 'followers': '{}/followers/{}/{}'.format(settings.mcfi_url, forge, actor),
- # 'following': '{}/following/{}/{}'.format(settings.mcfi_url, forge, actor)
- # }
- @get('/users/<username>')
- def describe_user(username):
- ''' Return profile of a user '''
- id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
- user = database.get_user_local(username)
- if not user:
- return HTTPResponse(status=400, body=None)
-
- return {
- '@context': [
- "https://www.w3.org/ns/activitystreams",
- "https://forgefed.peers.community/ns"
- ],
- 'type': 'Person',
- 'id': id_,
- 'name': '{}'.format(username),
- 'preferredUsername': '{}'.format(username),
- 'summary': '',
- 'inbox': '{}/inbox'.format(id_),
- 'outbox': '{}/outbox'.format(id_),
- 'followers': '{}/followers'.format(id_),
- 'following': '{}/following'.format(id_),
- 'publicKey': user["public_key"]
- }
- @get('/users/<username>/<repository>')
- def describe_repo(username,repository):
- ''' describe a repo'''
- #id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
- #user = database.get_user_local(username)
- # if not user:
- # return HTTPResponse(status=400, body=None)
-
- return {
- '@context': [
- "https://www.w3.org/ns/activitystreams",
- "https://forgefed.peers.community/ns"
- ],
- 'collaborators': json.dumps(database.get_collaborators(repository))#TODO:don't forget if reponame mech changes.
- # 'id': id_,
- # 'name': '{}'.format(username),
- # 'preferredUsername': '{}'.format(username),
- # 'summary': '',
- # 'inbox': '{}/inbox'.format(id_),
- # 'outbox': '{}/outbox'.format(id_),
- # 'followers': '{}/followers'.format(id_),
- # 'following': '{}/following'.format(id_),
- # 'publicKey': user["public_key"]
- }
- @get('/administration/reset-database')
- def reset_db():
- ''' drop all users and reset things'''
- if settings.DEBUG:
- for key in os.listdir("keydir"):
- if key != 'admin-daemon.pub':
- path = os.path.join("keydir", key)
- os.remove(path)
-
- config = gitosis_configen.Config()
- config.flush()
- commit_all("daemon: drop all users")
- db_path=os.path.join("..",settings.database_path)
- if os.path.isfile(db_path):
- os.remove(db_path)
- exit(0) #forcing a worker restart so that restarting worker regenerates database.
- @get('/oauth-clients/local')
- def generate_clientid():
- ''' Generate or "generate" client id and secret''' #maybe real generation someday, but i can't see a point yet
-
- return {
- 'client_id': settings.client_id,
- 'client_secret': settings.client_secret,
- }
- @route('/')
- def index():
- return 'MCFI version ' + __version__
|