├── requirements.txt ├── full-tabs.tpl ├── jupyterhub_config.py ├── README.md ├── login.html ├── accounts.py └── public_handler.py /requirements.txt: -------------------------------------------------------------------------------- 1 | nbformat>=4.4.0 2 | nbconvert>=5.3.1 3 | xkcdpass>=1.16.2 4 | -------------------------------------------------------------------------------- /full-tabs.tpl: -------------------------------------------------------------------------------- 1 | {%- extends 'full.tpl' -%} 2 | 3 | {%- block header -%} 4 | {{ super() }} 5 | 6 | {%- endblock header -%} 7 | 8 | {%- block input_group -%} 9 | {%- if cell['metadata'].get('format','') == 'tab' -%} 10 | 11 | {%- else -%} 12 | {{ super() }} 13 | {%- endif -%} 14 | {%- endblock input_group %} 15 | -------------------------------------------------------------------------------- /jupyterhub_config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | c.Authenticator.admin_users = set(["dblank"]) 4 | 5 | c.JupyterHub.services = [ 6 | { 7 | 'name': 'public', 8 | 'url': 'http://127.0.0.1:10101', 9 | 'command': [sys.executable, './public_handler.py'], 10 | }, 11 | { 12 | 'name': 'accounts', 13 | 'url': 'http://127.0.0.1:10102', 14 | 'command': [sys.executable, './accounts.py'], 15 | }, 16 | ] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Services for Jupyterhub 2 | 3 | ## Public, a nbviewer service for jupyterhub 4 | 5 | This code adds nbviewer-like functionality, via a JupyterHub Service, to a jupyterhub installation. 6 | That is, it allows users to "publish" their notebooks so that they can be seen as HTML. 7 | 8 | Served at /services/public/username/... 9 | 10 | See also the Javascript nbextension for copying notebooks to a user's public_html folder: 11 | 12 | https://github.com/Calysto/notebook-extensions 13 | 14 | ## Accounts, a more flexible way of adding accounts 15 | 16 | Creates accounts and passwords following the xkcd password style. 17 | 18 | Served at /services/accounts 19 | 20 | Opens a form and textarea where each line should be in one of the following forms: 21 | 22 | ``` 23 | username OR 24 | username@address OR 25 | Real Name, username@school.edu OR 26 | Real Name, username-email OR 27 | Real Name, email@school.edu, username 28 | ``` 29 | 30 | Can send email, and/or display the passwords. Optional field for professor/adminstrator's name. 31 | -------------------------------------------------------------------------------- /login.html: -------------------------------------------------------------------------------- 1 | {% extends "page.html" %} 2 | 3 | {% block login_widget %} 4 | {% endblock %} 5 | 6 | {% block main %} 7 | 8 | {% block login %} 9 |
To login into Jupyter, enter the username and password provided to 137 | you by your instructor in the fields on the left. This is not the 138 | same as your school ID/password. If you would like an account or 139 | need assistance, please 140 | email Matt Rice.
141 | 142 |You may also be interested services on our 143 | supercomputer, Athena.
144 | 145 |Funding, for equipment and development, provided by:
195 | 196 |
199 |
200 | |
201 |
202 |
203 | |
204 |
Each line should be in one of the following forms:
33 | 34 |35 | username (assumes default email) OR 36 | username@school.edu OR 37 | Real Name, username@school.edu OR 38 | Real Name, username (assumes default email) OR 39 | Real Name, email@school.edu, username 40 |41 | 42 | 50 | 51 | 52 | """) 53 | else: 54 | self.write("Not an admin!") 55 | 56 | def post(self): 57 | user_model = self.get_current_user() 58 | usernames = self.get_argument('usernames').split("\n") 59 | prof_email = self.get_argument('prof_email') 60 | display = self.get_argument('Display Passwords', "") == "display" 61 | send_email = self.get_argument('Send Email', "") == "send" 62 | self.set_header('content-type', 'text/html') 63 | if user_model is not None and user_model["admin"]: 64 | self.process_lines(usernames, prof_email, send_email, display) 65 | else: 66 | self.write("Not an admin!") 67 | 68 | def process_lines(self, lines, prof_email, send_email, display): 69 | """ 70 | filename is a file: 71 | username OR 72 | username@address OR 73 | Real Name, email+username OR 74 | Real Name, email+username@address OR 75 | Real Name, email@address, username 76 | """ 77 | for line in lines: 78 | line = line.strip() 79 | if line.startswith("#") or line == "": 80 | continue 81 | data = [item.strip() for item in line.split(",")] 82 | if len(data) == 1: # USERNAME/EMAIL 83 | if "@" in data[0]: 84 | email = data[0] 85 | username = email.split("@")[0] 86 | else: 87 | username = data[0] 88 | email = data[0] + "@" + DEFAULT_EMAIL 89 | realname = "Jupyter User" 90 | elif len(data) == 2: # REALNAME, USERNAME/EMAIL 91 | if "@" in data[1]: 92 | realname = data[0] 93 | email = data[1] 94 | username = email.split("@")[0] 95 | else: 96 | realname = data[0] 97 | username = data[1] 98 | email = data[1] + "@" + DEFAULT_EMAIL 99 | elif len(data) == 3: # REALNAME, EMAIL, USERNAME 100 | realname = data[0] 101 | email = data[1] 102 | username = data[2] 103 | else: 104 | self.write("invalid line: " + line + "; skipping!
131 | ===============================================
132 | Bryn Mawr College
133 | Jupyter Computer Resource
134 |
135 | {prof_email}
136 | -----------------------------------------------
137 |
138 | Computer: http://jupyter.brynmawr.edu
139 |
140 | User: {gecos}
141 |
142 | Username : {username}
143 | Pass phrase : {password}
144 | Email address: {email}
145 |
146 | Note: you must type any spaces in pass phrase when you login.
147 |
148 |
149 | %< ---------------------------------------------
150 |
151 | """.format(**env))
152 | if send_email and env["email"]:
153 | message = """
154 | Welcome to Computing at Bryn Mawr College!
155 |
156 | You are currently enrolled in a course using this resource.
157 |
158 | You will be given details in class on how to login and use the system
159 | using the following information.
160 |
161 | Computer: https://jupyter.brynmawr.edu/
162 | Username: {username}
163 | Password: {password}
164 |
165 | If you have any questions, please check with your instructor
166 | {prof_email}
167 |
168 | Thank you!
169 | """
170 | env["message"] = message.format(**env)
171 | system('echo -e "{message}" | mail -s "Bryn Mawr College - Jupyter Computer Resource" {email}'.format(**env))
172 |
173 | def make_password(arg_string=None):
174 | if arg_string is None:
175 | arg_string = "-w safe6 -n 4 --min 1 --max=6"
176 | argv = arg_string.split()
177 | parser = xkcd_password.XkcdPassArgumentParser(prog="xkcdpass")
178 |
179 | options = parser.parse_args(argv)
180 | xkcd_password.validate_options(parser, options)
181 |
182 | my_wordlist = xkcd_password.generate_wordlist(
183 | wordfile=options.wordfile,
184 | min_length=options.min_length,
185 | max_length=options.max_length,
186 | valid_chars=options.valid_chars)
187 |
188 | if options.verbose:
189 | xkcd_password.verbose_reports(my_wordlist, options)
190 |
191 | return xkcd_password.generate_xkcdpassword(
192 | my_wordlist,
193 | interactive=options.interactive,
194 | numwords=options.numwords,
195 | acrostic=options.acrostic,
196 | delimiter=options.delimiter)
197 |
198 | def get_user_info(username):
199 | """
200 | Returns pwd.struct_passwd(pw_name='baldwin01',
201 | pw_passwd='x',
202 | pw_uid=1544,
203 | pw_gid=1544,
204 | pw_gecos='Baldwin Student 01',
205 | pw_dir='/home/baldwin01',
206 | pw_shell='/bin/bash')
207 | """
208 | retval = None
209 | try:
210 | retval = pwd.getpwnam(username)
211 | except KeyError:
212 | retval = None
213 | return retval
214 |
215 | def system(command):
216 | #print("COMMAND: ", command)
217 | proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
218 | (out, err) = proc.communicate()
219 | return out.decode().strip()
220 |
221 | def main():
222 | app = Application([
223 | (os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', AccountsHandler),
224 | (r'.*', AccountsHandler),
225 | ])
226 | http_server = HTTPServer(app)
227 | url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
228 | http_server.listen(url.port, url.hostname)
229 | IOLoop.current().start()
230 |
231 | if __name__ == '__main__':
232 | main()
233 |
--------------------------------------------------------------------------------
/public_handler.py:
--------------------------------------------------------------------------------
1 | """
2 | This serves `/services/public/user/directory/Notebook.ipynb` and other files.
3 | """
4 |
5 | import os
6 | import re
7 | import glob
8 | import mimetypes
9 | import shutil
10 | from urllib.parse import urlparse
11 | from tornado.ioloop import IOLoop
12 | from tornado.httpserver import HTTPServer
13 | from tornado.web import RequestHandler, Application, HTTPError
14 | from tornado.log import app_log
15 |
16 | import nbformat
17 | from nbconvert.exporters import HTMLExporter, PDFExporter
18 | from nbformat.v4 import new_markdown_cell
19 |
20 | class PublicHandler(RequestHandler):
21 | def get(self, user, filename):
22 | ## filename can have a path on it
23 | prefix = os.environ['JUPYTERHUB_SERVICE_PREFIX']
24 | next = "%s/%s/%s" % (prefix, user, filename)
25 | filesystem_path = "/home/%s/public_html/%s" % (user, filename)
26 | if filename and filename.endswith(".raw.ipynb"):
27 | filename = filename[:-10] + ".ipynb"
28 | self.set_header('Content-Type', "application/json")
29 | with open("/home/%s/public_html/%s" % (user, filename), "rb") as fp:
30 | self.write(fp.read())
31 | return
32 | elif os.path.isfile(filesystem_path): # download, raw, or view notebook
33 | command = "view"
34 | if len(self.get_arguments("view")) > 0:
35 | command = "view"
36 | elif len(self.get_arguments("download")) > 0:
37 | command = "download"
38 | elif len(self.get_arguments("pdf")) > 0:
39 | command = "pdf"
40 | elif len(self.get_arguments("raw")) > 0:
41 | command = "raw"
42 | # else: view
43 | if filename.endswith(".ipynb"):
44 | if command in ["view", "pdf"]:
45 | if command == "view":
46 | exporter = HTMLExporter(template_file='full-tabs')
47 | else:
48 | exporter = PDFExporter(latex_count=1)
49 |
50 | nb_json = nbformat.read("/home/%s/public_html/%s" % (user, filename), as_version=4)
51 | if command == "pdf":
52 | self.set_header('Content-Type', "application/pdf")
53 | base_filename = os.path.basename(filename)
54 | self.set_header('Content-Disposition', 'attachment; filename="%s"' % base_filename)
55 | else: # render as HTML
56 | # add header/footer:
57 | path = "%s/%s" % (prefix, user)
58 | parts = [(path, path)]
59 | for part in filename.split("/")[:-1]:
60 | path += "/" + part
61 | parts.append((path, part))
62 | breadcrumbs = " / ".join(map(lambda pair: '%s' % pair, parts))
63 | env = {
64 | "breadcrumbs": breadcrumbs,
65 | "url": next + "?download",
66 | "prefix": prefix,
67 | }
68 | cell = new_markdown_cell(source="""
71 |
72 | |
73 |
74 | Jupyter at Bryn Mawr College75 | |
76 |
77 |
78 | |
81 |
82 |
83 | |
86 |
| 89 | Public notebooks: {breadcrumbs} 90 | | 91 ||||
Public notebooks: %s
\n" % breadcrumbs) 142 | self.write("Please see Jupyter Help for more information about this server.
\n".format(prefix=prefix)) 172 | 173 | def download(self, user, filename, mime_type=None): 174 | self.download_file(filename, "/home/%s/public_html/%s" % (user, filename), mime_type) 175 | 176 | def download_file(self, filename, file_path, mime_type=None): 177 | # just download it 178 | # filename can be a full path + filename 179 | if os.path.exists(file_path): 180 | if mime_type is None: 181 | mime_type, encoding = mimetypes.guess_type(filename) 182 | if mime_type is None: 183 | mime_type = "text/plain" 184 | base_filename = os.path.basename(filename) 185 | self.set_header('Content-Type', mime_type) 186 | self.set_header('Content-Disposition', 'attachment; filename="%s"' % base_filename) 187 | fp = open(file_path, "rb") 188 | try: 189 | self.write(fp.read()) 190 | except: 191 | # file read/write issue 192 | print("File IO issue") 193 | finally: 194 | fp.close() 195 | else: 196 | raise HTTPError(404) 197 | 198 | def get_current_user_name(self): 199 | user = self.get_current_user() 200 | if user: 201 | return user.name 202 | else: 203 | return None 204 | 205 | def main(): 206 | app = Application([ 207 | (os.environ['JUPYTERHUB_SERVICE_PREFIX'] + r"/([^/]+)/?(.*)", PublicHandler), 208 | ]) 209 | http_server = HTTPServer(app) 210 | url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) 211 | http_server.listen(url.port, url.hostname) 212 | IOLoop.current().start() 213 | 214 | if __name__ == '__main__': 215 | main() 216 | --------------------------------------------------------------------------------