-
Notifications
You must be signed in to change notification settings - Fork 832
/
core.py
298 lines (251 loc) · 10.3 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
from __future__ import unicode_literals
import locale
import logging
import os
import psutil
import signal
import socket
import sys
import syslog
import subprocess
from jadi import Context
from importlib import reload
import aj
import aj.plugins
# from aj.auth import AuthenticationService # Test for callback with certificate
from aj.config import AjentiUsers, SmtpConfig, TFAConfig
from aj.http import HttpRoot, HttpMiddlewareAggregator
from aj.plugins import PluginManager
from aj.wsgi import RequestHandler
from aj.api.http import HttpMasterMiddleware
# Dummy import to register middleware as component from HttpMasterMiddleware
from aj.security.pwreset import PasswordResetMiddleware
import gevent
import ssl
import gevent.ssl
from gevent import monkey
# Gevent monkeypatch ---------------------
monkey.patch_all(select=True, thread=True, aggressive=False, subprocess=True)
from gevent.event import Event
import threading
threading.Event = Event
def restart():
logging.warning('Will restart the process now')
if '-d' in sys.argv:
sys.argv.remove('-d')
os.execv(sys.argv[0], sys.argv)
try:
# If Namespace is not provided, then the wrong socketio library is installed
from socketio import Namespace
except ImportError:
logging.warning('Replacing gevent-socketio with python-socketio')
subprocess.check_call([sys.executable, '-m', 'pip', 'uninstall', '-y', 'gevent-socketio-hartwork', 'python-socketio'])
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-socketio'])
restart()
try:
# If mixins is provided, then gevent-socketio-hartwork was overwritten by
# python-socketio and we need to clean this
from socketio import mixins
if 'BroadcastMixin' in dir(mixins):
logging.warning('Removing gevent-socketio')
subprocess.check_call([sys.executable, '-m', 'pip', 'uninstall', '-y', 'gevent-socketio-hartwork', 'python-socketio'])
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-socketio'])
restart()
except ImportError:
# It's alright to have an error since we don't want to use gevent-socketio
# anymore.
pass
from socketio import Server, WSGIApp
# ----------------------------------------
import aj.compat
from aj.gate.middleware import GateMiddleware, SocketIONamespace
from gevent import pywsgi
def run(config=None, plugin_providers=None, product_name='ajenti', dev_mode=False,
debug_mode=False, autologin=False):
"""
A global entry point for Ajenti.
:param config: config file implementation instance to use
:type config: :class:`aj.config.BaseConfig`
:param plugin_providers: list of plugin providers to load plugins from
:type plugin_providers: list(:class:`aj.plugins.PluginProvider`)
:param str product_name: a product name to use
:param bool dev_mode: enables dev mode (automatic resource recompilation)
:param bool debug_mode: enables debug mode (verbose and extra logging)
:param bool autologin: disables authentication and logs everyone in as the user running the panel. This is EXTREMELY INSECURE.
"""
if config is None:
raise TypeError('`config` can\'t be None')
reload(sys)
if hasattr(sys, 'setdefaultencoding'):
sys.setdefaultencoding('utf8')
aj.product = product_name
aj.debug = debug_mode
aj.dev = dev_mode
aj.dev_autologin = autologin
aj.init()
aj.log.set_log_params(tag='master', master_pid=os.getpid())
aj.context = Context()
aj.config = config
aj.plugin_providers = plugin_providers or []
logging.info(f'Loading config from {aj.config}')
aj.config.load()
aj.config.ensure_structure()
logging.info('Loading users from /etc/ajenti/users.yml')
aj.users = AjentiUsers(aj.config.data['auth']['users_file'])
aj.users.load()
logging.info('Loading smtp config from /etc/ajenti/smtp.yml')
aj.smtp_config = SmtpConfig()
aj.smtp_config.load()
aj.smtp_config.ensure_structure()
logging.info('Loading tfa config from /etc/ajenti/tfa.yml')
aj.tfa_config = TFAConfig()
aj.tfa_config.load()
aj.tfa_config.ensure_structure()
if aj.debug:
logging.warning('Debug mode')
if aj.dev:
logging.warning('Dev mode')
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
logging.warning('Couldn\'t set default locale')
# install a passthrough gettext replacement since all localization is handled in frontend
# and _() is here only for string extraction
__builtins__['_'] = lambda x: x
logging.info(f'Ajenti Core {aj.version}')
logging.info(f'Master PID - {os.getpid()}')
logging.info(f'Detected platform: {aj.platform} / {aj.platform_string}')
logging.info(f'Python version: {aj.python_version}')
# Load plugins
PluginManager.get(aj.context).load_all_from(aj.plugin_providers)
if len(PluginManager.get(aj.context)) == 0:
logging.warning('No plugins were loaded!')
if aj.config.data['bind']['mode'] == 'unix':
path = aj.config.data['bind']['socket']
if os.path.exists(path):
os.unlink(path)
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
listener.bind(path)
except OSError:
logging.error(f'Could not bind to {path}')
sys.exit(1)
if aj.config.data['bind']['mode'] == 'tcp':
host = aj.config.data['bind']['host']
port = aj.config.data['bind']['port']
listener = socket.socket(
socket.AF_INET6 if ':' in host else socket.AF_INET, socket.SOCK_STREAM
)
if aj.platform not in ['freebsd', 'osx']:
try:
listener.setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
except socket.error:
logging.warning('Could not set TCP_CORK')
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
logging.info(f'Binding to [{host}]:{port}')
try:
listener.bind((host, port))
except socket.error as e:
logging.error(f'Could not bind: {str(e)}')
sys.exit(1)
listener.listen(10)
gateway = GateMiddleware.get(aj.context)
middleware_stack = HttpMasterMiddleware.all(aj.context) + [gateway]
if aj.config.data['trusted_domains']:
sio = Server(async_mode='gevent', cors_allowed_origins=aj.config.data['trusted_domains'])
else:
# Not trusted domain set, only allow same origin
sio = Server(async_mode='gevent')
application = WSGIApp(sio, HttpRoot(HttpMiddlewareAggregator(middleware_stack)).dispatch)
sio.register_namespace(SocketIONamespace(context=aj.context))
if aj.config.data['ssl']['enable'] and aj.config.data['bind']['mode'] == 'tcp':
aj.server = pywsgi.WSGIServer(
listener,
log=open(os.devnull, 'w'),
application=application,
handler_class=RequestHandler,
policy_server=False,
)
aj.server.ssl_args = {'server_side': True}
cert_path = aj.config.data['ssl']['certificate']
if aj.config.data['ssl']['fqdn_certificate']:
fqdn_cert_path = aj.config.data['ssl']['fqdn_certificate']
else:
fqdn_cert_path = cert_path
context = gevent.ssl.SSLContext(ssl.PROTOCOL_TLS)
context.load_cert_chain(certfile=fqdn_cert_path, keyfile=fqdn_cert_path)
context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
context.set_ciphers('ALL:!ADH:!EXP:!LOW:!RC2:!3DES:!SEED:!RC4:+HIGH:+MEDIUM')
if aj.config.data['ssl']['client_auth']['enable']:
logging.info('Enabling SSL client authentication')
context.load_verify_locations(cafile=cert_path)
if aj.config.data['ssl']['client_auth']['force']:
context.verify_mode = ssl.CERT_REQUIRED
else:
context.verify_mode = ssl.CERT_OPTIONAL
## Test callback : client_certificate_callback must return None to get forward
# context.set_servername_callback(AuthenticationService.get(aj.context).client_certificate_callback)
aj.server.wrap_socket = lambda socket, **args:context.wrap_socket(sock=socket, server_side=True)
logging.info('SSL enabled')
if aj.config.data['ssl']['force']:
from aj.https_redirect import http_to_https
target_url = f'https://{aj.config.data["name"]}:{port}'
gevent.spawn(http_to_https, target_url)
logging.info(f'HTTP to HTTPS redirection activated to {target_url}')
else:
# No policy_server argument for http
aj.server = pywsgi.WSGIServer(
listener,
log=open(os.devnull, 'w'),
application=application,
handler_class=RequestHandler,
)
# auth.log
try:
syslog.openlog(
ident=str(aj.product),
facility=syslog.LOG_AUTH,
)
except Exception as e:
syslog.openlog(aj.product)
def cleanup():
if hasattr(cleanup, 'started'):
return
cleanup.started = True
logging.info(f'Process {os.getpid()} exiting normally')
gevent.signal_handler(signal.SIGINT, lambda: None)
gevent.signal_handler(signal.SIGTERM, lambda: None)
if aj.master:
gateway.destroy()
p = psutil.Process(os.getpid())
for c in p.children(recursive=True):
try:
os.killpg(c.pid, signal.SIGTERM)
os.killpg(c.pid, signal.SIGKILL)
except OSError:
pass
def signal_handler():
cleanup()
sys.exit(0)
gevent.signal_handler(signal.SIGINT, signal_handler)
gevent.signal_handler(signal.SIGTERM, signal_handler)
aj.server.serve_forever()
if not aj.master:
# child process, server is stopped, wait until killed
gevent.wait()
if hasattr(aj.server, 'restart_marker'):
logging.warning('Restarting by request')
cleanup()
fd = 20 # Close all descriptors. Creepy thing
while fd > 2:
try:
os.close(fd)
logging.debug(f'Closed descriptor #{fd:d}')
except OSError:
pass
fd -= 1
restart()
else:
if aj.master:
logging.debug('Server stopped')
cleanup()