├── 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 |
10 | {% if custom_html %} 11 | {{ custom_html }} 12 | {% elif login_service %} 13 | 14 |
15 | 16 | Sign in with {{login_service}} 17 | 18 |
19 | {% else %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 75 | 76 | 77 | 78 |
79 |

Jupyter @ Bryn Mawr College

80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 | Sign in 88 |
89 |
90 | 91 | 95 | 96 | {% if login_error %} 97 | 100 | {% endif %} 101 | 102 | 113 | 114 | 121 | 122 | 129 |
130 |
131 |
132 | 133 |
134 |

Welcome to Jupyter!

135 | 136 |

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 |

Useful Links

146 | 147 | 150 | 151 |

News

152 | 153 |
    154 |
  • New Jupyter computer installed and operational! September 1, 2017.
  • 155 |
156 | 157 |

Courses using Jupyter

158 | 159 | 187 | 188 |

Administration

189 | 190 | 193 | 194 |

Funding, for equipment and development, provided by:

195 | 196 | 197 | 198 | 201 | 204 | 205 |
199 | 200 | 202 | 203 |
206 | 207 |
208 | 209 |
210 | {% endif %} 211 |
212 | {% endblock login %} 213 | 214 | {% endblock %} 215 | 216 | {% block script %} 217 | {{super()}} 218 | 219 | 226 | 227 | {% endblock %} 228 | -------------------------------------------------------------------------------- /accounts.py: -------------------------------------------------------------------------------- 1 | """An example service authenticating with the Hub. 2 | 3 | This serves `/services/accounts/`, authenticated with the Hub, showing the user their own info. 4 | """ 5 | 6 | import os 7 | import pwd 8 | import subprocess 9 | from xkcdpass import xkcd_password 10 | from getpass import getuser 11 | from urllib.parse import urlparse 12 | from tornado.ioloop import IOLoop 13 | from tornado.httpserver import HTTPServer 14 | from tornado.web import RequestHandler, Application 15 | from jupyterhub.services.auth import HubAuthenticated 16 | 17 | DEFAULT_EMAIL = "brynmawr.edu" 18 | 19 | class AccountsHandler(HubAuthenticated, RequestHandler): 20 | hub_users = None # the users allowed to access me (everyone can try) 21 | 22 | def get(self): 23 | user_model = self.get_current_user() 24 | self.set_header('content-type', 'text/html') 25 | if user_model is not None and user_model["admin"]: 26 | self.write(""" 27 | 28 | 29 | 30 |

Create Jupyter Accounts

31 | 32 |

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 |
43 | 44 |

45 | Professor name and email:
46 | Send Email:
47 | Display Passwords:

48 | 49 | 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!

") 105 | continue 106 | ### Now, let's see if there is an account: 107 | if not display: 108 | self.write("processing: %s %s %s...

" % (username, realname, email)) 109 | account_info = get_user_info(username) 110 | if account_info: 111 | username = account_info[0] 112 | realname = account_info[4] 113 | self.write("Account exists! username: %s realname: %s

" %(username, realname)) 114 | #continue ## Do it anyway 115 | # otherwise, make account 116 | gecos = "%s <%s>" % (realname, email) 117 | password = make_password("-w safe6 -n 4 --min 1 --max=6") 118 | env = { 119 | "username": username, 120 | "gecos": gecos, 121 | "password": password, 122 | "prof_email": prof_email, 123 | "email": email, 124 | } 125 | #print("env:", env) 126 | system('useradd -m -d /home/{username} -c "{gecos}" {username}'.format(**env)) 127 | system('echo {username}:{password} | chpasswd'.format(**env)) 128 | if display: 129 | self.write(""" 130 |
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=""" 69 | 70 | 73 | 76 | 81 | 86 | 87 | 88 | 91 | 92 |
71 | 72 | 74 |

Jupyter at Bryn Mawr College

75 |
77 | 78 | 79 | 80 | 82 | 83 | 84 | 85 |
89 | Public notebooks: {breadcrumbs} 90 |
""".format(**env)) 93 | nb_json["cells"].insert(0, cell) 94 | (body, resources) = exporter.from_notebook_node(nb_json) 95 | body = body.replace('Notebook', '%s' % filename.split("/")[-1]) 96 | self.write(body) 97 | elif command == "download": # download notebook json 98 | self.download(user, filename, "text/plain") 99 | else: # raw, just get file contents 100 | self.set_header('Content-Type', "application/json") 101 | with open("/home/%s/public_html/%s" % (user, filename), "rb") as fp: 102 | self.write(fp.read()) 103 | else: # some other kind of file 104 | # FIXME: how to get all of custom stuff? 105 | if True: # whatever, just get or download it 106 | base_filename = os.path.basename(filename) 107 | base, ext = os.path.splitext(base_filename) 108 | app_log.info("extension is: %s" % ext) 109 | if base_filename == "custom.css": 110 | file_path = "/home/%s/.ipython/profile_default/static/custom/custom.css" % user 111 | self.set_header('Content-Type', "text/css") 112 | with open(file_path, "rb") as fp: 113 | self.write(fp.read()) 114 | elif ext in [".txt", ".html", ".js", ".css", ".pdf", ".gif", ".jpeg", ".jpg", ".png"]: # show in browser 115 | app_log.info("mime: %s" % str(mimetypes.guess_type(filename)[0])) 116 | self.set_header('Content-Type', mimetypes.guess_type(filename)[0]) 117 | with open("/home/%s/public_html/%s" % (user, filename), "rb") as fp: 118 | self.write(fp.read()) 119 | else: 120 | self.download(user, filename) 121 | else: # not a file; directory listing 122 | # filename can have a full path, and might be empty 123 | url_path = "%s/%s" % (prefix, user) 124 | ## 125 | path = "%s/%s" % (prefix, user) 126 | parts = [(path, path)] 127 | for part in filename.split("/")[:]: 128 | path += "/" + part 129 | parts.append((path, part)) 130 | breadcrumbs = " / ".join(map(lambda pair: '%s' % pair, parts)) 131 | ## 132 | # could be: images, images/, images/subdir, images/subdir/ 133 | if not filename.endswith("/") and filename.strip() != "": 134 | filename += "/" 135 | files = glob.glob("/home/%s/public_html/%s*" % (user, filename)) 136 | self.write("

Jupyter Project at Bryn Mawr College

\n") 137 | self.write("[Home] ") 138 | if self.get_current_user_name(): 139 | self.write("[%(current_user)s] " % {"current_user": self.get_current_user_name()}) 140 | self.write("

\n") 141 | self.write("

Public notebooks: %s

\n" % breadcrumbs) 142 | self.write("
    \n") 143 | for absolute_filename in sorted(files): 144 | if os.path.isdir(absolute_filename): 145 | dir_path = absolute_filename.split("/") 146 | dir_name = dir_path[-1] 147 | public_path = "/".join(dir_path[dir_path.index("public_html") + 1:]) 148 | self.write("
  1. %(dir_name)s
  2. \n" % {"url_path": url_path, 149 | "dir_name": dir_name, 150 | "public_path": public_path}) 151 | else: 152 | file_path, filename = absolute_filename.rsplit("/", 1) 153 | dir_path = absolute_filename.split("/") 154 | public_path = "/".join(dir_path[dir_path.index("public_html") + 1:]) 155 | variables = {"user": user, "filename": filename, "url_path": url_path, "next": next, 156 | "public_path": public_path} 157 | if filename.endswith(".ipynb"): 158 | if self.get_current_user_name(): 159 | self.write(("
  3. %(filename)s "+ 160 | "(download" + 161 | ")
  4. \n") % variables) 162 | else: 163 | self.write(("
  5. %(filename)s "+ 164 | "(download)" + 165 | "
  6. \n") % variables) 166 | else: 167 | # some other kind of file (eg, .zip, .css): 168 | self.write("
  7. %(filename)s
  8. \n" % variables) 169 | self.write("
\n") 170 | self.write("
\n") 171 | 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 | --------------------------------------------------------------------------------