2013-06-28 19:58:13 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import gettext
|
|
|
|
import ldap
|
|
|
|
import yaml
|
|
|
|
import json
|
2013-07-07 20:19:26 +02:00
|
|
|
|
|
|
|
sys.path.append('/usr/share/pyshared')
|
|
|
|
|
2013-09-23 12:29:49 +02:00
|
|
|
from twisted.python.log import ILogObserver, FileLogObserver, startLogging, msg
|
2013-06-30 13:25:17 +02:00
|
|
|
from twisted.python.logfile import DailyLogFile
|
2013-09-23 12:29:49 +02:00
|
|
|
from twisted.web.server import Site, http
|
2013-06-30 13:25:17 +02:00
|
|
|
from twisted.internet import reactor
|
|
|
|
from twisted.application import internet,service
|
2013-06-28 19:58:13 +02:00
|
|
|
from txrestapi.resource import APIResource
|
2013-09-23 18:35:17 +02:00
|
|
|
from yunohost import YunoHostError, YunoHostLDAP, str_to_func, colorize, pretty_print_dict, display_error, validate, win, parse_dict, reset_win_messages
|
2013-06-28 19:58:13 +02:00
|
|
|
|
|
|
|
if not __debug__:
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
gettext.install('YunoHost')
|
|
|
|
|
2013-07-03 21:50:42 +02:00
|
|
|
dev = False
|
2013-07-04 10:06:44 +02:00
|
|
|
installed = True
|
2013-06-28 19:58:13 +02:00
|
|
|
action_dict = {}
|
2013-06-30 13:25:17 +02:00
|
|
|
api = APIResource()
|
2013-06-28 19:58:13 +02:00
|
|
|
|
2013-06-30 18:13:26 +02:00
|
|
|
def http_exec(request, **kwargs):
|
2013-06-30 13:25:17 +02:00
|
|
|
|
|
|
|
request.setHeader('Access-Control-Allow-Origin', '*') # Allow cross-domain requests
|
|
|
|
request.setHeader('Content-Type', 'application/json') # Return JSON anyway
|
|
|
|
|
|
|
|
# Return OK to 'OPTIONS' xhr requests
|
|
|
|
if request.method == 'OPTIONS':
|
|
|
|
request.setResponseCode(200, 'OK')
|
2013-07-03 14:23:07 +02:00
|
|
|
request.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
|
|
|
request.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
2013-06-30 13:25:17 +02:00
|
|
|
return ''
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-06-30 13:25:17 +02:00
|
|
|
# Simple HTTP auth
|
2013-07-04 10:06:44 +02:00
|
|
|
elif installed:
|
2013-06-30 13:25:17 +02:00
|
|
|
authorized = request.getUser() == 'admin'
|
2013-07-03 14:23:07 +02:00
|
|
|
pwd = request.getPassword()
|
2013-07-03 21:50:42 +02:00
|
|
|
if dev and 'api_key' in request.args:
|
2013-07-03 14:23:07 +02:00
|
|
|
pwd = request.args['api_key'][0]
|
|
|
|
authorized = True
|
2013-06-30 13:57:23 +02:00
|
|
|
if authorized:
|
2013-07-03 14:23:07 +02:00
|
|
|
try: YunoHostLDAP(password=pwd)
|
2013-06-30 13:57:23 +02:00
|
|
|
except YunoHostError: authorized = False
|
2013-06-30 13:25:17 +02:00
|
|
|
if not authorized:
|
|
|
|
request.setResponseCode(401, 'Unauthorized')
|
|
|
|
request.setHeader('Access-Control-Allow-Origin', '*')
|
2013-06-30 13:57:23 +02:00
|
|
|
request.setHeader('www-authenticate', 'Basic realm="Restricted Area"')
|
2013-06-30 13:25:17 +02:00
|
|
|
return 'Unauthorized'
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-06-30 18:13:26 +02:00
|
|
|
path = request.path
|
2013-09-23 12:29:49 +02:00
|
|
|
if request.method == 'PUT':
|
|
|
|
given_args = http.parse_qs(request.content.read(), 1)
|
|
|
|
else:
|
|
|
|
given_args = request.args
|
2013-06-30 18:13:26 +02:00
|
|
|
if kwargs:
|
|
|
|
for k, v in kwargs.iteritems():
|
|
|
|
dynamic_key = path.split('/')[-1]
|
2013-07-03 21:50:42 +02:00
|
|
|
path = path.replace(dynamic_key, '{'+ k +'}')
|
2013-06-30 18:13:26 +02:00
|
|
|
given_args[k] = [v]
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-09-23 12:29:49 +02:00
|
|
|
msg(given_args)
|
2013-07-02 22:02:26 +02:00
|
|
|
# Sanitize arguments
|
2013-06-30 18:13:26 +02:00
|
|
|
dict = action_dict[request.method +' '+ path]
|
|
|
|
if 'arguments' in dict: possible_args = dict['arguments']
|
|
|
|
else: possible_args = {}
|
|
|
|
for arg, params in possible_args.items():
|
2013-06-28 19:58:13 +02:00
|
|
|
sanitized_key = arg.replace('-', '_')
|
|
|
|
if sanitized_key is not arg:
|
2013-06-30 18:13:26 +02:00
|
|
|
possible_args[sanitized_key] = possible_args[arg]
|
|
|
|
del possible_args[arg]
|
2013-06-28 19:58:13 +02:00
|
|
|
arg = sanitized_key
|
|
|
|
if arg[0] == '_':
|
|
|
|
if 'nargs' not in params:
|
2013-06-30 18:13:26 +02:00
|
|
|
possible_args[arg]['nargs'] = '*'
|
2013-06-28 19:58:13 +02:00
|
|
|
if 'full' in params:
|
2013-07-03 14:23:07 +02:00
|
|
|
new_key = params['full'][2:]
|
2013-06-28 19:58:13 +02:00
|
|
|
else:
|
2013-07-03 14:23:07 +02:00
|
|
|
new_key = arg[2:]
|
|
|
|
new_key = new_key.replace('-', '_')
|
|
|
|
possible_args[new_key] = possible_args[arg]
|
2013-06-30 18:13:26 +02:00
|
|
|
del possible_args[arg]
|
2013-06-28 19:58:13 +02:00
|
|
|
|
|
|
|
try:
|
2013-06-30 13:25:17 +02:00
|
|
|
|
|
|
|
# Validate arguments
|
2013-06-28 19:58:13 +02:00
|
|
|
validated_args = {}
|
2013-06-30 18:13:26 +02:00
|
|
|
for key, value in given_args.items():
|
|
|
|
if key in possible_args:
|
2013-06-28 19:58:13 +02:00
|
|
|
# Validate args
|
2013-06-30 18:13:26 +02:00
|
|
|
if 'pattern' in possible_args[key]:
|
|
|
|
validate(possible_args[key]['pattern'], value)
|
|
|
|
if 'nargs' not in possible_args[key] or ('nargs' != '*' and 'nargs' != '+'):
|
2013-06-30 14:11:30 +02:00
|
|
|
value = value[0]
|
2013-06-30 18:13:26 +02:00
|
|
|
if 'choices' in possible_args[key] and value not in possible_args[key]['choices']:
|
2013-06-30 14:11:30 +02:00
|
|
|
raise YunoHostError(22, _('Invalid argument') + ' ' + value)
|
2013-06-30 18:13:26 +02:00
|
|
|
if 'action' in possible_args[key] and possible_args[key]['action'] == 'store_true':
|
2013-06-28 19:58:13 +02:00
|
|
|
yes = ['true', 'True', 'yes', 'Yes']
|
2013-07-02 22:02:26 +02:00
|
|
|
value = value in yes
|
2013-06-28 19:58:13 +02:00
|
|
|
validated_args[key] = value
|
|
|
|
|
|
|
|
func = str_to_func(dict['function'])
|
2013-06-30 18:13:26 +02:00
|
|
|
if func is None:
|
|
|
|
raise YunoHostError(168, _('Function not yet implemented : ') + dict['function'].split('.')[1])
|
2013-06-30 13:25:17 +02:00
|
|
|
|
|
|
|
# Execute requested function
|
2013-06-28 19:58:13 +02:00
|
|
|
with YunoHostLDAP(password=request.getPassword()):
|
|
|
|
result = func(**validated_args)
|
|
|
|
if result is None:
|
|
|
|
result = {}
|
2013-09-23 18:35:17 +02:00
|
|
|
if len(win) > 0:
|
2013-06-28 19:58:13 +02:00
|
|
|
result['win'] = win
|
2013-09-23 18:35:17 +02:00
|
|
|
reset_win_messages()
|
2013-06-30 13:25:17 +02:00
|
|
|
|
|
|
|
# Build response
|
2013-06-29 01:05:28 +02:00
|
|
|
if request.method == 'POST':
|
|
|
|
request.setResponseCode(201, 'Created')
|
|
|
|
elif request.method == 'DELETE':
|
|
|
|
request.setResponseCode(204, 'No Content')
|
|
|
|
else:
|
|
|
|
request.setResponseCode(200, 'OK')
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-06-28 19:58:13 +02:00
|
|
|
except YunoHostError, error:
|
2013-06-30 13:25:17 +02:00
|
|
|
|
|
|
|
# Set response code with function's raised code
|
2013-06-30 18:13:26 +02:00
|
|
|
server_errors = [1, 111, 168, 169]
|
|
|
|
client_errors = [13, 17, 22, 87, 122, 125, 167]
|
2013-06-29 01:05:28 +02:00
|
|
|
if error.code in client_errors:
|
|
|
|
request.setResponseCode(400, 'Bad Request')
|
|
|
|
else:
|
|
|
|
request.setResponseCode(500, 'Internal Server Error')
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-06-30 18:13:26 +02:00
|
|
|
result = { 'error' : error.message }
|
2013-06-29 01:05:28 +02:00
|
|
|
|
2013-06-28 19:58:13 +02:00
|
|
|
return json.dumps(result)
|
|
|
|
|
2013-07-03 14:23:07 +02:00
|
|
|
def api_doc(request):
|
|
|
|
request.setHeader('Access-Control-Allow-Origin', '*') # Allow cross-domain requests
|
|
|
|
request.setHeader('Content-Type', 'application/json') # Return JSON anyway
|
|
|
|
|
|
|
|
# Return OK to 'OPTIONS' xhr requests
|
|
|
|
if request.method == 'OPTIONS':
|
|
|
|
request.setResponseCode(200, 'OK')
|
|
|
|
request.setHeader('Access-Control-Allow-Headers', 'Authorization')
|
|
|
|
return ''
|
|
|
|
|
|
|
|
if request.path == '/api':
|
|
|
|
with open('doc/resources.json') as f:
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
category = request.path.split('/')[2]
|
|
|
|
try:
|
|
|
|
with open('doc/'+ category +'.json') as f:
|
|
|
|
return f.read()
|
|
|
|
except IOError:
|
|
|
|
return ''
|
|
|
|
|
|
|
|
def favicon(request):
|
|
|
|
request.setHeader('Access-Control-Allow-Origin', '*') # Allow cross-domain requests
|
|
|
|
request.setResponseCode(404, 'Not Found')
|
|
|
|
return ''
|
2013-06-28 19:58:13 +02:00
|
|
|
|
2013-07-03 21:51:33 +02:00
|
|
|
def main():
|
2013-06-28 19:58:13 +02:00
|
|
|
global action_dict
|
2013-06-30 13:25:17 +02:00
|
|
|
global api
|
|
|
|
|
2013-07-03 14:28:18 +02:00
|
|
|
# Generate API doc
|
|
|
|
os.system('python ./generate_api_doc.py')
|
|
|
|
|
2013-07-03 14:23:07 +02:00
|
|
|
# Register API doc service
|
|
|
|
api.register('ALL', '/api', api_doc)
|
|
|
|
|
|
|
|
# favicon.ico error
|
|
|
|
api.register('ALL', '/favicon.ico', favicon)
|
|
|
|
|
2013-06-30 13:25:17 +02:00
|
|
|
# Load & parse yaml file
|
2013-06-28 19:58:13 +02:00
|
|
|
with open('action_map.yml') as f:
|
|
|
|
action_map = yaml.load(f)
|
|
|
|
|
|
|
|
del action_map['general_arguments']
|
|
|
|
for category, category_params in action_map.items():
|
2013-07-03 14:23:07 +02:00
|
|
|
api.register('ALL', '/api/'+ category, api_doc)
|
2013-06-28 19:58:13 +02:00
|
|
|
for action, action_params in category_params['actions'].items():
|
2013-07-02 22:02:26 +02:00
|
|
|
if 'action_help' not in action_params:
|
|
|
|
action_params['action_help'] = ''
|
2013-06-28 19:58:13 +02:00
|
|
|
if 'api' not in action_params:
|
|
|
|
action_params['api'] = 'GET /'+ category +'/'+ action
|
|
|
|
method, path = action_params['api'].split(' ')
|
2013-06-30 13:25:17 +02:00
|
|
|
# Register route
|
2013-07-03 21:23:11 +02:00
|
|
|
if '{' in path:
|
|
|
|
path = path.replace('{', '(?P<').replace('}', '>[^/]+)')
|
2013-06-28 19:58:13 +02:00
|
|
|
api.register(method, path, http_exec)
|
2013-06-30 13:25:17 +02:00
|
|
|
api.register('OPTIONS', path, http_exec)
|
2013-06-28 19:58:13 +02:00
|
|
|
action_dict[action_params['api']] = {
|
|
|
|
'function': 'yunohost_'+ category +'.'+ category +'_'+ action,
|
2013-07-02 22:02:26 +02:00
|
|
|
'help' : action_params['action_help']
|
2013-06-28 19:58:13 +02:00
|
|
|
}
|
2013-07-02 22:02:26 +02:00
|
|
|
if 'arguments' in action_params:
|
2013-06-28 19:58:13 +02:00
|
|
|
action_dict[action_params['api']]['arguments'] = action_params['arguments']
|
2013-06-30 13:25:17 +02:00
|
|
|
|
2013-07-02 22:02:26 +02:00
|
|
|
|
2013-06-30 13:25:17 +02:00
|
|
|
# Register only postinstall action if YunoHost isn't completely set up
|
2013-06-28 19:58:13 +02:00
|
|
|
try:
|
|
|
|
with open('/etc/yunohost/installed') as f: pass
|
|
|
|
except IOError:
|
2013-07-04 10:06:44 +02:00
|
|
|
installed = False
|
2013-06-30 13:25:17 +02:00
|
|
|
api = APIResource()
|
|
|
|
api.register('POST', '/postinstall', http_exec)
|
|
|
|
api.register('OPTIONS', '/postinstall', http_exec)
|
|
|
|
action_dict['POST /postinstall'] = {
|
|
|
|
'function' : 'yunohost_tools.tools_postinstall',
|
|
|
|
'help' : 'Execute post-install',
|
|
|
|
'arguments' : action_map['tools']['postinstall']['arguments']
|
|
|
|
}
|
2013-06-28 19:58:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2013-07-03 21:50:42 +02:00
|
|
|
if '--dev' in sys.argv:
|
|
|
|
dev = True
|
|
|
|
startLogging(sys.stdout)
|
|
|
|
else:
|
|
|
|
startLogging(open('/var/log/yunohost.log', 'a+')) # Log actions to file
|
2013-06-28 19:58:13 +02:00
|
|
|
main()
|
2013-06-30 13:30:23 +02:00
|
|
|
reactor.listenTCP(6767, Site(api, timeout=None))
|
2013-06-30 13:25:17 +02:00
|
|
|
reactor.run()
|
|
|
|
else:
|
|
|
|
application = service.Application("YunoHost API")
|
|
|
|
logfile = DailyLogFile("yunohost.log", "/var/log")
|
|
|
|
application.setComponent(ILogObserver, FileLogObserver(logfile).emit)
|
2013-06-30 13:30:23 +02:00
|
|
|
main()
|
2013-06-30 13:25:17 +02:00
|
|
|
internet.TCPServer(6767, Site(api, timeout=None)).setServiceParent(application)
|