-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
343 lines (308 loc) · 11.4 KB
/
main.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/python3
# FLASK_APP=main.py FLASK_DEBUG=1 flask run
import os
import fcntl
from flask import Flask, request, make_response, render_template_string, redirect, current_app
from flask_wtf.csrf import CSRFProtect, CSRFError
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
import hmac
import json
import collections
import urllib
import secrets
import string
app = Flask(__name__)
csrf = CSRFProtect(app)
login_manager = LoginManager(app)
app.debug = True
import subprocess
def random_string(n, characters=string.ascii_letters + string.digits):
return ''.join([secrets.choice(characters) for _ in range(n)])
def install_secret_key(filename='secret_key'):
#filename = os.path.join(app.instance_path, filename)
try:
secret_key = open(filename, 'rb').read()
except IOError:
secret_key = None
if secret_key:
return secret_key
print('create secret file')
secret_key_string = random_string(24)
with open(filename, 'wb') as secret_file:
secret_file.write(secret_key_string.encode())
secret_key = open(filename, 'rb').read()
if not secret_key:
print('still cannot read %s'%filename)
sys.exit(1)
return secret_key
app.config['SECRET_KEY'] = install_secret_key("%s.secret_key"%__name__)
class User(UserMixin):
salt = install_secret_key("%s.salt"%__name__)
users = {}
def __init__(self, t):
self.id = t['id']
self.data = t
def valid(self, password):
message = password.encode() + self.salt
h = hmac.new(message, digestmod='sha1')
h.update(message)
digest = h.hexdigest()
if not hmac.compare_digest(self.data['password'], digest):
return False
return True;
# h = hmac.new(app.config['SECRET_KEY'], digestmod='sha256')
# h.update(salt)
# @classmethod
# def valid_token(cls, message, token):
# h = cls.h.clone()
# h.update(message.decode())
# return hmac.compare_digest(token, h.hexdigest())
def load_users(filename='users'):
users = {}
with open(filename, 'r') as f:
for line in f:
user = User(json.loads(line))
users[user.id] = user
return users
User.users = load_users("%s.users"%__name__)
@login_manager.user_loader
def load_user(id):
return User.users.get(id)
about_links = []
header_template = """
<div>
{% if not current_user.is_authenticated %}
<span><a href='/login?next={{current_url}}'>/login</a></span>
{% else %}
<span>{{current_user.data.id}} <a href='/logout'>/logout</a></span>
{% endif %}
{% for link in links %}
<span><a href='{{link}}'>{{link}}</a></span>
{% endfor %}
</div>
{{body|safe}}
"""
about_message = """
GET /about show usage
GET /list show all list
GET /add show add form
POST /add {path} new jupyter instance at path
GET /kill show kill form
POST /kill {pid} kill the instance of pid
GET /notebooks/<port> redirect to /p/<id>/init/<port>
GET /p/<id>/signup Set-Cookie and redirect
PROXY /p/<id>/* forward using jupyter_port cookie
"""
def render_template_string_with_header(*args, **kwargs):
return render_template_string(header_template, links=about_links, body=render_template_string(*args, **kwargs))
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template_string_with_header('<div>CSRF Error: {{reason}}</div>', reason=e.description), 400
@app.errorhandler(401)
def unauthorized_error(e):
return render_template_string_with_header('<div>Unauthorized: {{reason}}</div>', reason=e.description), 401
login_template = """
{% if message %}
<div>{{ message }}</div>
{% endif %}
{% if not current_user.is_authenticated %}
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type='text' name='username'>
<input type='password' name='password'>
<input type='submit' value='login'>
</form>
{% else %}
<div>
Hello, {{ current_user.data.id }}
<a href='/logout'>logout</a>
</div>
{% endif %}
"""
@app.route("/login", methods=("get", "post"))
def login():
message = None
if request.method.upper() == 'POST':
username = request.form['username']
password = request.form['password']
user = User.users.get(username)
if not user:
message = "username not exist"
elif user.valid(password):
login_user(user)
next = request.args.get("next")
if not next:
next = '/list'
return redirect(next)
else:
message = "wrong password"
return render_template_string_with_header(login_template, message=message)
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect('/login')
@app.route('/about')
@login_required
def about_hello():
if request.values.get('t') == 'json':
return json.dumps({'text': about_message, 'api': about_links, 'options': request.query_string.decode()})
return render_template_string_with_header("<div>{% for m in message.strip().splitlines() %}{{m}}<br>{% endfor %}</div>", message=about_message)
def run_command(*args, verbose=True, **kwargs):
if verbose:
print("RUN", args, kwargs)
result = subprocess.run(*args, stdout=subprocess.PIPE, **kwargs)
return result.stdout.decode('utf-8')
notebook_keys = ['base_url', 'hostname', 'notebook_dir', 'password', 'pid', 'port', 'secure', 'token', 'url']
def list_notebooks():
return [json.loads(x) for x in run_command(['jupyter', 'notebook', 'list', '--json'], verbose=False).splitlines()]
list_template = """
{% if message %}
<div>{{ message }}</div>
{% endif %}
<table>
<thead>
<th>open</th>
{% for key in keys %}<th>{{key}}</th>{% endfor %}
<th>kill</th>
</thead>
<tbody>
{% for notebook in notebooks %}
<tr>
<td><a href='/notebooks/{{notebook.port}}'>@</a></td>
{% for value in notebook._values %}<td>{{value}}</td>{% endfor %}
<td>
<form action='/kill' method='post' style='margin-bottom: 0'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input name='pid' type='text' value='{{notebook.pid}}' hidden>
<input type='submit' value='X'>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div><a href='/add'>new instance</a></div>
"""
@app.route('/list', methods=('get', 'post'))
@login_required
def api_list():
notebooks = list_notebooks()
if request.values.get('t') == 'json':
return json.dumps(list_notebooks())
message = request.args.get('m')
if message:
try:
message = message.format(**request.form.to_dict())
except:
message = 'build message "%s" failed' % message
return render_template_string_with_header(list_template,
message = message,
keys = notebook_keys,
notebooks = [{'_values':[x[k] for k in notebook_keys], **x} for x in notebooks])
def redirect_to_list(message):
return redirect('/list?' + urllib.parse.urlencode({'m': message}), code=307)
kill_template = """
<form method='post'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input name='pid' type='text'>
<input type='submit' value='kill'>
</form>
"""
@app.route('/kill', methods=('get', 'post'))
@login_required
def api_kill():
pid = request.form.get('pid')
if not pid:
return render_template_string_with_header(kill_template)
notebooks = list_notebooks()
pid = int(pid)
result = "failed, notebook for the pid not found"
for notebook in notebooks:
if pid == notebook['pid']:
result = run_command(['kill', '%d'%pid])
if result == "":
result = "successfully"
break
if request.values.get('t') == 'json':
return json.dumps({'result': result})
return redirect_to_list('kill {pid} %s'%result)
add_template = """
{% if message %}
<div>{{ message }}</div>
{% endif %}
<form method='post'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input name='path' type='text' placeholder='path' autofocus
{% if path %}value="{{path}}"{% endif %}
>
<input type='checkbox' id='check_force' name='force'><label for='check_force'>f</label>
<input type='submit' value='add'>
</form>
"""
def non_block_read(output):
# Note: only works in linux
fd = output.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
try:
return output.read().decode('utf-8')
except:
return ""
@app.route('/add', methods=('get', 'post'))
@login_required
def api_add():
path = request.form.get('path')
force = request.form.get('force')
orig_path = path
if path and not os.path.isabs(path):
path = os.path.join(os.path.expanduser('~'), path)
if force and not os.path.exists(path):
run_command(['mkdir', path])
if not path or not os.path.exists(path):
message = orig_path and "%s is not a valid path" % orig_path
return render_template_string_with_header(add_template, message = message, path = orig_path)
result = "failed"
prefix = random_string(6, string.ascii_lowercase + string.digits)
add_args = ['jupyter', 'lab', '--no-browser', '--LabApp.base_url=/p/%s'%prefix, '--LabApp.static_url=/static/']
print("RUN", add_args)
proc = subprocess.Popen(add_args, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
proc.wait(0.5)
result = "failed"
except subprocess.TimeoutExpired:
result = "successfully"
if request.values.get('t') == 'json':
out, err = non_block_read(proc.stdout), non_block_read(proc.stderr)
return json.dumps({'result': result, 'path': path, 'pid': proc.pid, 'returncode': proc.returncode, 'args': proc.args, 'stdout': out, 'stderr': err})
return redirect_to_list('add {path} %s'%result)
@app.route('/notebooks/<int:port>')
@login_required
def api_notebook(port):
notebooks = list_notebooks()
for notebook in notebooks:
if port == notebook['port']:
base_url = notebook['base_url']
if not base_url.startswith('/'):
base_url = '/' + base_url
return redirect('%s'%base_url+"signup")
return redirect_to_list('open %d failed, notebook for the pid not found'%port)
@app.route('/p/<notebook_id>/signup')
@login_required
def api_signup(notebook_id):
notebooks = list_notebooks()
base_url = '/p/%s/' % notebook_id
for notebook in notebooks:
if base_url == notebook['base_url']:
response = make_response(redirect('%s?'%base_url+urllib.parse.urlencode({'token': notebook['token']})))
response.set_cookie('jupyter_port', "%d"%notebook['port'], path=base_url)
return response
return redirect_to_list('signup %d failed, notebook for the pid not found'%port)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
with app.app_context():
adapter = current_app.url_map.bind('')
about_links = [adapter.build(rule.endpoint, **(rule.defaults or {})) for rule in current_app.url_map.iter_rules() if "GET" in rule.methods and has_no_empty_params(rule)]
about_links = [link for link in about_links if link != '/login' and link != '/logout']