app.py
Python script, ASCII text executable
1import os 2import random 3import subprocess 4from functools import wraps 5 6import cairosvg 7import flask 8from flask_sqlalchemy import SQLAlchemy 9import git 10import mimetypes 11import magic 12from flask_bcrypt import Bcrypt 13from markupsafe import escape, Markup 14from flask_migrate import Migrate 15from datetime import datetime 16from enum import Enum 17import shutil 18from PIL import Image 19from cairosvg import svg2png 20import platform 21 22import config 23 24app = flask.Flask(__name__) 25 26from flask_httpauth import HTTPBasicAuth 27 28auth = HTTPBasicAuth() 29 30app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 31app.config["SECRET_KEY"] = config.DB_PASSWORD 32app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 33db = SQLAlchemy(app) 34bcrypt = Bcrypt(app) 35migrate = Migrate(app, db) 36 37 38def gitCommand(repo, data, *args): 39if not os.path.isdir(repo): 40raise FileNotFoundError("Repo not found") 41env = os.environ.copy() 42 43command = ["git", *args] 44 45proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, 46stdin=subprocess.PIPE) 47print(command) 48 49if data: 50proc.stdin.write(data) 51 52out, err = proc.communicate() 53return out 54 55 56def onlyChars(string, chars): 57for i in string: 58if i not in chars: 59return False 60return True 61 62 63with app.app_context(): 64class RepoAccess(db.Model): 65id = db.Column(db.Integer, primary_key=True) 66userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 67repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 68accessLevel = db.Column(db.SmallInteger(), nullable=False) # 0 read-only, 1 read-write, 2 admin 69 70user = db.relationship("User", back_populates="repoAccess") 71repo = db.relationship("Repo", back_populates="repoAccess") 72 73__table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),) 74 75def __init__(self, user, repo, level): 76self.userUsername = user.username 77self.repoRoute = repo.route 78self.accessLevel = level 79 80 81class User(db.Model): 82username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 83displayName = db.Column(db.Unicode(128), unique=False, nullable=True) 84bio = db.Column(db.Unicode(512), unique=False, nullable=True) 85passwordHashed = db.Column(db.String(60), nullable=False) 86email = db.Column(db.String(254), nullable=True) 87company = db.Column(db.Unicode(64), nullable=True) 88companyURL = db.Column(db.String(256), nullable=True) 89URL = db.Column(db.String(256), nullable=True) 90showMail = db.Column(db.Boolean, default=False, nullable=False) 91location = db.Column(db.Unicode(64), nullable=True) 92creationDate = db.Column(db.DateTime, default=datetime.utcnow) 93 94repositories = db.relationship("Repo", back_populates="owner") 95repoAccess = db.relationship("RepoAccess", back_populates="user") 96 97def __init__(self, username, password, email=None, displayName=None): 98self.username = username 99self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") 100self.email = email 101self.displayName = displayName 102 103# Create the user's directory 104if not os.path.exists(os.path.join(config.REPOS_PATH, username)): 105os.makedirs(os.path.join(config.REPOS_PATH, username)) 106if not os.path.exists(os.path.join(config.USERDATA_PATH, username)): 107os.makedirs(os.path.join(config.USERDATA_PATH, username)) 108 109avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH)) 110if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"): 111cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), 112write_to="/tmp/roundabout-avatar.png") 113avatar = Image.open("/tmp/roundabout-avatar.png") 114else: 115avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName)) 116avatar.thumbnail(config.AVATAR_SIZE) 117avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png")) 118 119 120class Repo(db.Model): 121route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) 122ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 123name = db.Column(db.String(64), nullable=False) 124owner = db.relationship("User", back_populates="repositories") 125visibility = db.Column(db.SmallInteger(), nullable=False) 126info = db.Column(db.Unicode(512), nullable=True) 127URL = db.Column(db.String(256), nullable=True) 128creationDate = db.Column(db.DateTime, default=datetime.utcnow) 129 130defaultBranch = db.Column(db.String(64), nullable=True, default="") 131 132commits = db.relationship("Commit", back_populates="repo") 133repoAccess = db.relationship("RepoAccess", back_populates="repo") 134 135def __init__(self, owner, name, visibility): 136self.route = f"/{owner.username}/{name}" 137self.name = name 138self.ownerName = owner.username 139self.owner = owner 140self.visibility = visibility 141 142# Add the owner as an admin 143repoAccess = RepoAccess(owner, self, 2) 144db.session.add(repoAccess) 145 146 147class Commit(db.Model): 148identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 149sha = db.Column(db.String(128), nullable=False) 150repoName = db.Column(db.String(97), db.ForeignKey("repo.route"), nullable=False) 151ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 152ownerIdentity = db.Column(db.String(321)) 153receiveDate = db.Column(db.DateTime, default=datetime.now) 154authorDate = db.Column(db.DateTime) 155message = db.Column(db.UnicodeText) 156repo = db.relationship("Repo", back_populates="commits") 157 158def __init__(self, sha, owner, repo, date, message, ownerIdentity): 159self.identifier = f"/{owner.username}/{repo.name}/{sha}" 160self.sha = sha 161self.repoName = repo.route 162self.repo = repo 163self.ownerName = owner.username 164self.owner = owner 165self.authorDate = datetime.fromtimestamp(int(date)) 166self.message = message 167self.ownerIdentity = ownerIdentity 168 169 170def getPermissionLevel(loggedIn, username, repository): 171user = User.query.filter_by(username=loggedIn).first() 172repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 173 174if user and repo: 175permission = RepoAccess.query.filter_by(user=user, repo=repo).first() 176if permission: 177return permission.accessLevel 178 179return None 180 181 182def getVisibility(username, repository): 183repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 184 185if repo: 186return repo.visibility 187 188return None 189 190 191import gitHTTP 192import jinjaUtils 193 194 195def humanSize(value, decimals=2, scale=1024, 196units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 197for unit in units: 198if value < scale: 199break 200value /= scale 201if int(value) == value: 202# do not return decimals, if the value is already round 203return int(value), unit 204return round(value * 10 ** decimals) / 10 ** decimals, unit 205 206 207def guessMIME(path): 208if os.path.isdir(path): 209mimetype = "inode/directory" 210elif magic.from_file(path, mime=True): 211mimetype = magic.from_file(path, mime=True) 212else: 213mimetype = "application/octet-stream" 214return mimetype 215 216 217def convertToHTML(path): 218with open(path, "r") as f: 219contents = f.read() 220return contents 221 222 223@app.context_processor 224def default(): 225username = flask.session.get("username") 226 227return {"loggedInUser": username} 228 229 230@app.route("/") 231def main(): 232return flask.render_template("home.html") 233 234 235@app.route("/about/") 236def about(): 237return flask.render_template("about.html", platform=platform) 238 239 240@app.route("/settings/", methods=["GET", "POST"]) 241def settings(): 242if flask.request.method == "GET": 243if not flask.session.get("username"): 244flask.abort(401) 245user = User.query.filter_by(username=flask.session.get("username")).first() 246 247return flask.render_template("user-settings.html", user=user) 248else: 249user = User.query.filter_by(username=flask.session.get("username")).first() 250 251user.displayName = flask.request.form["displayname"] 252user.URL = flask.request.form["url"] 253user.company = flask.request.form["company"] 254user.companyURL = flask.request.form["companyurl"] 255user.location = flask.request.form["location"] 256user.showMail = flask.request.form.get("showmail", user.showMail) 257 258db.session.commit() 259 260flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 261return flask.redirect(f"/{flask.session.get('username')}", code=303) 262 263 264@app.route("/accounts/", methods=["GET", "POST"]) 265def login(): 266if flask.request.method == "GET": 267return flask.render_template("login.html") 268else: 269if "login" in flask.request.form: 270username = flask.request.form["username"] 271password = flask.request.form["password"] 272 273user = User.query.filter_by(username=username).first() 274 275if user and bcrypt.check_password_hash(user.passwordHashed, password): 276flask.session["username"] = user.username 277flask.flash( 278Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 279category="success") 280return flask.redirect("/", code=303) 281elif not user: 282flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 283category="alert") 284return flask.render_template("login.html") 285else: 286flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 287category="error") 288return flask.render_template("login.html") 289if "signup" in flask.request.form: 290username = flask.request.form["username"] 291password = flask.request.form["password"] 292password2 = flask.request.form["password2"] 293email = flask.request.form.get("email") 294email2 = flask.request.form.get("email2") # repeat email is a honeypot 295name = flask.request.form.get("name") 296 297if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 298flask.flash(Markup( 299"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 300category="error") 301return flask.render_template("login.html") 302 303if username in config.RESERVED_NAMES: 304flask.flash( 305Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 306category="error") 307return flask.render_template("login.html") 308 309userCheck = User.query.filter_by(username=username).first() 310if userCheck: 311flask.flash( 312Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 313category="error") 314return flask.render_template("login.html") 315 316if password2 != password: 317flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 318category="error") 319return flask.render_template("login.html") 320 321user = User(username, password, email, name) 322db.session.add(user) 323db.session.commit() 324flask.session["username"] = user.username 325flask.flash(Markup( 326f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 327category="success") 328return flask.redirect("/", code=303) 329 330 331@app.route("/newrepo/", methods=["GET", "POST"]) 332def newRepo(): 333if flask.request.method == "GET": 334return flask.render_template("new-repo.html") 335else: 336name = flask.request.form["name"] 337visibility = int(flask.request.form["visibility"]) 338 339if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 340flask.flash(Markup( 341"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 342category="error") 343return flask.render_template("new-repo.html") 344 345user = User.query.filter_by(username=flask.session.get("username")).first() 346 347repo = Repo(user, name, visibility) 348db.session.add(repo) 349db.session.commit() 350 351if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 352subprocess.run(["git", "init", repo.name], 353cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 354 355flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 356category="success") 357return flask.redirect(repo.route, code=303) 358 359 360@app.route("/logout") 361def logout(): 362flask.session.clear() 363flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 364return flask.redirect("/", code=303) 365 366 367@app.route("/<username>/") 368def userProfile(username): 369user = User.query.filter_by(username=username).first() 370repos = Repo.query.filter_by(ownerName=username, visibility=2) 371return flask.render_template("user-profile.html", user=user, repos=repos) 372 373 374@app.route("/<username>/<repository>/") 375def repositoryIndex(username, repository): 376return flask.redirect("./tree", code=302) 377 378 379@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 380def repositoryRaw(username, repository, branch, subpath): 381if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("user"), username, 382repository) is not None): 383flask.abort(403) 384 385serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 386 387app.logger.info(f"Loading {serverRepoLocation}") 388 389if not os.path.exists(serverRepoLocation): 390app.logger.error(f"Cannot load {serverRepoLocation}") 391return flask.render_template("not-found.html"), 404 392 393repo = git.Repo(serverRepoLocation) 394repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 395if not repoData.defaultBranch: 396if repo.heads: 397repoData.defaultBranch = repo.heads[0].name 398else: 399return flask.render_template("empty.html", 400remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 401if not branch: 402branch = repoData.defaultBranch 403return flask.redirect(f"./{branch}", code=302) 404 405if branch.startswith("tag:"): 406ref = f"tags/{branch[4:]}" 407elif branch.startswith("~"): 408ref = branch[1:] 409else: 410ref = f"heads/{branch}" 411 412ref = ref.replace("~", "/") # encode slashes for URL support 413 414try: 415repo.git.checkout("-f", ref) 416except git.exc.GitCommandError: 417return flask.render_template("not-found.html"), 404 418 419return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 420 421 422@app.route("/info/<username>/avatar") 423def userAvatar(username): 424serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 425 426if not os.path.exists(serverUserdataLocation): 427return flask.render_template("not-found.html"), 404 428 429return flask.send_from_directory(serverUserdataLocation, "avatar.png") 430 431 432@app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 433@app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 434@app.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 435def repositoryTree(username, repository, branch, subpath): 436if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 437repository) is not None): 438flask.abort(403) 439 440serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 441 442app.logger.info(f"Loading {serverRepoLocation}") 443 444if not os.path.exists(serverRepoLocation): 445app.logger.error(f"Cannot load {serverRepoLocation}") 446return flask.render_template("not-found.html"), 404 447 448repo = git.Repo(serverRepoLocation) 449repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 450if not repoData.defaultBranch: 451if repo.heads: 452repoData.defaultBranch = repo.heads[0].name 453else: 454return flask.render_template("empty.html", 455remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 456if not branch: 457branch = repoData.defaultBranch 458return flask.redirect(f"./{branch}", code=302) 459 460if branch.startswith("tag:"): 461ref = f"tags/{branch[4:]}" 462elif branch.startswith("~"): 463ref = branch[1:] 464else: 465ref = f"heads/{branch}" 466 467ref = ref.replace("~", "/") # encode slashes for URL support 468 469try: 470repo.git.checkout("-f", ref) 471except git.exc.GitCommandError: 472return flask.render_template("not-found.html"), 404 473 474branches = repo.heads 475 476allRefs = [] 477for ref in repo.heads: 478allRefs.append((ref, "head")) 479for ref in repo.tags: 480allRefs.append((ref, "tag")) 481 482if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 483files = [] 484blobs = [] 485 486for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 487if not os.path.basename(entry) == ".git": 488files.append(os.path.join(subpath, entry)) 489 490infos = [] 491 492for file in files: 493path = os.path.join(serverRepoLocation, file) 494mimetype = guessMIME(path) 495 496text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 497 498sha = text.split("\n")[0] 499identifier = f"/{username}/{repository}/{sha}" 500lastCommit = Commit.query.filter_by(identifier=identifier).first() 501 502info = { 503"name": os.path.basename(file), 504"serverPath": path, 505"relativePath": file, 506"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 507"size": humanSize(os.path.getsize(path)), 508"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 509"commit": lastCommit, 510"shaSize": 7, 511} 512 513specialIcon = config.matchIcon(os.path.basename(file)) 514if specialIcon: 515info["icon"] = specialIcon 516elif os.path.isdir(path): 517info["icon"] = config.folderIcon 518elif mimetypes.guess_type(path)[0] in config.fileIcons: 519info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 520else: 521info["icon"] = config.unknownIcon 522 523if os.path.isdir(path): 524infos.insert(0, info) 525else: 526infos.append(info) 527 528return flask.render_template( 529"repo-tree.html", 530username=username, 531repository=repository, 532files=infos, 533subpath=os.path.join("/", subpath), 534branches=allRefs, 535current=branch 536) 537else: 538path = os.path.join(serverRepoLocation, subpath) 539 540if not os.path.exists(path): 541return flask.render_template("not-found.html"), 404 542 543mimetype = guessMIME(path) 544mode = mimetype.split("/", 1)[0] 545size = humanSize(os.path.getsize(path)) 546 547specialIcon = config.matchIcon(os.path.basename(path)) 548if specialIcon: 549icon = specialIcon 550elif os.path.isdir(path): 551icon = config.folderIcon 552elif mimetypes.guess_type(path)[0] in config.fileIcons: 553icon = config.fileIcons[mimetypes.guess_type(path)[0]] 554else: 555icon = config.unknownIcon 556 557contents = None 558if mode == "text": 559contents = convertToHTML(path) 560 561return flask.render_template( 562"repo-file.html", 563username=username, 564repository=repository, 565file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 566branches=allRefs, 567current=branch, 568mode=mode, 569mimetype=mimetype, 570detailedtype=magic.from_file(path), 571size=size, 572icon=icon, 573subpath=os.path.join("/", subpath), 574basename=os.path.basename(path), 575contents=contents 576) 577 578 579@app.route("/<username>/<repository>/forum/") 580def repositoryForum(username, repository): 581if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 582repository) is not None): 583flask.abort(403) 584 585return flask.render_template("repo-forum.html", username=username, repository=repository) 586 587 588@app.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 589def repositoryUsers(username, repository): 590if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 591repository) is not None): 592flask.abort(403) 593 594serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 595 596app.logger.info(f"Loading {serverRepoLocation}") 597 598if not os.path.exists(serverRepoLocation): 599app.logger.error(f"Cannot load {serverRepoLocation}") 600return flask.render_template("not-found.html"), 404 601 602repo = git.Repo(serverRepoLocation) 603repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 604user = User.query.filter_by(username=username).first() 605relationships = RepoAccess.query.filter_by(repo=repoData) 606userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 607 608if flask.request.method == "GET": 609return flask.render_template("repo-users.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship) 610else: 611if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 612flask.abort(401) 613 614if flask.request.form.get("new-username"): 615# Create new relationship 616newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 617relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 618db.session.add(relationship) 619db.session.commit() 620if flask.request.form.get("update-username"): 621# Create new relationship 622updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 623relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 624if flask.request.form.get("update-level") == -1: 625relationship.delete() 626else: 627relationship.accessLevel = flask.request.form.get("update-level") 628db.session.commit() 629 630return flask.redirect(app.url_for("repositoryUsers", username=username, repository=repository)) 631 632 633@app.route("/<username>/<repository>/branches/") 634def repositoryBranches(username, repository): 635if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 636repository) is not None): 637flask.abort(403) 638 639serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 640 641app.logger.info(f"Loading {serverRepoLocation}") 642 643if not os.path.exists(serverRepoLocation): 644app.logger.error(f"Cannot load {serverRepoLocation}") 645return flask.render_template("not-found.html"), 404 646 647repo = git.Repo(serverRepoLocation) 648repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 649 650return flask.render_template("repo-branches.html", username=username, repository=repository, repoData=repoData, repo=repo) 651 652 653@app.route("/<username>/<repository>/log/") 654def repositoryLog(username, repository): 655if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 656repository) is not None): 657flask.abort(403) 658 659serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 660 661app.logger.info(f"Loading {serverRepoLocation}") 662 663if not os.path.exists(serverRepoLocation): 664app.logger.error(f"Cannot load {serverRepoLocation}") 665return flask.render_template("not-found.html"), 404 666 667repo = git.Repo(serverRepoLocation) 668repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 669commits = Commit.query.filter_by(repo=repoData) 670 671return flask.render_template("repo-log.html", username=username, repository=repository, repoData=repoData, repo=repo, commits=commits) 672 673 674@app.route("/<username>/<repository>/settings/") 675def repositorySettings(username, repository): 676if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 677flask.abort(401) 678 679return flask.render_template("repo-settings.html", username=username, repository=repository) 680 681 682@app.errorhandler(404) 683def e404(error): 684return flask.render_template("not-found.html"), 404 685 686 687@app.errorhandler(401) 688def e401(error): 689return flask.render_template("unauthorised.html"), 401 690 691 692@app.errorhandler(403) 693def e403(error): 694return flask.render_template("forbidden.html"), 403 695 696 697@app.errorhandler(418) 698def e418(error): 699return flask.render_template("teapot.html"), 418 700 701 702@app.errorhandler(405) 703def e405(error): 704return flask.render_template("method-not-allowed.html"), 405 705 706 707if __name__ == "__main__": 708app.run(debug=True, port=8080, host="0.0.0.0") 709