├── app ├── sqlite.db ├── init.py ├── Dockerfile ├── schema.sql ├── templates │ ├── activity.html │ ├── base.html │ ├── index.html │ └── notes.html └── app.py ├── admin ├── flag.txt ├── Dockerfile └── admin.py ├── .gitattributes ├── screenshot.png ├── DO_NOT_ACCESS └── README.md ├── docker-compose.yaml ├── README.md ├── chrome └── Dockerfile └── .gitignore /app/sqlite.db: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/flag.txt: -------------------------------------------------------------------------------- 1 | CSCG{TESTFLAGTESTFLAGTESTFLAG} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveOverflow/ctf-screenshotter/HEAD/screenshot.png -------------------------------------------------------------------------------- /DO_NOT_ACCESS/README.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | The [`DO_NOT_ACCESS`](/DO_NOT_ACCESS) folder contains the solution when you checkout the `solution` branch with `git checkout solution`. -------------------------------------------------------------------------------- /app/init.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import uuid 3 | from app import DATABASE 4 | 5 | db = sqlite3.connect(DATABASE) 6 | 7 | with open('schema.sql', 'r') as f: 8 | db.cursor().executescript(f.read()) 9 | db.commit() 10 | 11 | with open('secret', 'wb') as f: 12 | f.write(uuid.uuid4().bytes) -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN apk update 4 | RUN apk add py-pip 5 | RUN pip install flask gunicorn requests pyppeteer logzero 6 | ENV LIBRARY_PATH=/lib:/usr/lib 7 | 8 | ADD . /app/ 9 | WORKDIR /app/ 10 | 11 | ENV PORT=1024 BIND_ADDR=0.0.0.0 12 | 13 | RUN python init.py 14 | ENTRYPOINT gunicorn -w 8 -b "${BIND_ADDR}:${PORT}" --access-logfile - --error-logfile - app:app -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | chrome: 4 | platform: linux/x86_64 5 | build: 6 | context: chrome/ 7 | #ports: 8 | # - 9222:9222 9 | 10 | screenshotter: 11 | build: 12 | context: app/ 13 | depends_on: 14 | - "chrome" 15 | ports: 16 | - 5000:1024 17 | 18 | admin: 19 | platform: linux/x86_64 20 | build: 21 | context: admin/ 22 | depends_on: 23 | - "screenshotter" -------------------------------------------------------------------------------- /admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | RUN apt-get update && apt-get install -yq gnupg curl 4 | RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 5 | echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list && \ 6 | # Cypress dependencies 7 | apt-get update && apt-get install -yq google-chrome-stable 8 | 9 | 10 | RUN apt-get install -yq python3 python3-pip 11 | RUN python3 -m pip install pyppeteer logzero 12 | 13 | ADD . /app/ 14 | WORKDIR /app/ 15 | 16 | ENTRYPOINT python3 admin.py -------------------------------------------------------------------------------- /app/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `uuid` varchar(36) PRIMARY KEY NOT NULL, 3 | `username` varchar(32) NOT NULL default '', 4 | `password` varchar(64) NOT NULL default '' 5 | ); 6 | CREATE UNIQUE INDEX `useruuid` ON `users`(`uuid`); 7 | 8 | CREATE TABLE `notes` ( 9 | `uuid` varchar(36) PRIMARY KEY NOT NULL, 10 | `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP, 11 | `user` varchar(36) NOT NULL, 12 | `title` varchar(64) NOT NULL default '', 13 | `body` TEXT NOT NULL default '', 14 | `data` TEXT NOT NULL default '' 15 | ); 16 | CREATE UNIQUE INDEX `noteuuid` ON `notes`(`uuid`); 17 | CREATE INDEX `noteuser` ON `notes`(`user`); 18 | 19 | CREATE TABLE `logs` ( 20 | `id` INTEGER PRIMARY KEY, 21 | `msg` varchar(512) NOT NULL default '', 22 | `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP 23 | ); 24 | CREATE UNIQUE INDEX `logid` ON `logs`(`id`); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # screenshotter (web) 2 | 3 | A CTF web challenge about making screenshots. It is inspired by a bug found in real life. 4 | The challenge was created by [@LiveOverflow](https://twitter.com/LiveOverflow) for [https://cscg.de/](https://cscg.de/). 5 | 6 | Watch the video writeup here: https://www.youtube.com/watch?v=FCjMoPpOPYI 7 | 8 | ![screenshot of the screenshotter challenge](/screenshot.png?raw=true "screenshot of the screenshotter challenge") 9 | 10 | ## Run the challenge 11 | 12 | To run the challenge you have to install [`docker-compose`](https://docs.docker.com/compose/install/): 13 | 14 | ``` 15 | docker-compose up 16 | ``` 17 | 18 | Once the servicses are running, you should be able to access [http://127.0.0.1:5000](http://127.0.0.1:5000). 19 | 20 | ## Solution 21 | 22 | The [`DO_NOT_ACCESS`](/DO_NOT_ACCESS) folder contains the solution when you checkout the `solution` branch with `git checkout solution`. 23 | -------------------------------------------------------------------------------- /app/templates/activity.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 | 5 |
6 | {% for log in logs %} 7 |
8 |
9 | {{ log.timestamp|timesince }} 10 | {{ log.msg }} 11 |
12 |
13 | {% endfor %} 14 | {% if len_logs == 15 %} 15 |
16 |
17 | ... 18 |
19 |
20 | {% endif %} 21 |
22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /chrome/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | RUN apt-get update && apt-get install -yq gnupg curl 4 | RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 5 | echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list && \ 6 | # Cypress dependencies 7 | apt-get update && apt-get install -yq google-chrome-stable 8 | 9 | # Add chrome user 10 | RUN groupadd -r chrome && useradd -r -g chrome -G audio,video chrome \ 11 | && mkdir -p /home/chrome/Downloads && chown -R chrome:chrome /home/chrome 12 | 13 | WORKDIR /home/chrome 14 | USER chrome 15 | 16 | # Expose port 9222 17 | EXPOSE 9222 18 | 19 | ENTRYPOINT [ "/usr/bin/google-chrome", \ 20 | "--headless", "--no-sandbox", "--disable-gpu", \ 21 | "--remote-debugging-address=0.0.0.0", \ 22 | "--remote-debugging-port=9222", \ 23 | "--use-mock-keychain", \ 24 | "--password-store=basic", \ 25 | "--disable-dev-shm-usage", \ 26 | "--test-type=webdriver", \ 27 | "--enable-automation", \ 28 | "--disable-hang-monitor", \ 29 | "--window-size=1280,650", \ 30 | "--disable-background-networking", \ 31 | "--disable-default-apps", \ 32 | "--disable-extensions", \ 33 | "--disable-sync", \ 34 | "--disable-web-resources", \ 35 | "--disable-notifications", \ 36 | "--disable-translate", \ 37 | "--hide-scrollbars", \ 38 | "--metrics-recording-only", \ 39 | "--mute-audio", \ 40 | "--no-first-run", \ 41 | "--safebrowsing-disable-auto-update", \ 42 | "--user-data-dir=/home/chrome" \ 43 | ] -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | screenshotter 8 | 9 | 10 |
11 | 12 | {% if g.user %} 13 | 14 | {% else %} 15 | 16 | {% endif %} 17 | screenshotter DEMO 18 | 19 | 20 | activity 21 | {% if g.user %} 22 | {{g.user.username}} 23 | 24 | 27 | 28 | {% else %} 29 | {% endif %} 30 | 31 |
32 |
33 | {% block content %} 34 | 35 | {% endblock %} 36 |
37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 |

Login

7 | 8 |
9 |
10 | 13 | 14 |
15 |
16 | 19 | 20 |
21 |
22 |
23 | 26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 | Demo version:
will only process screenshots for http://cscg.de 37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/notes.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 | 15 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | {% for note in notes %} 25 |
26 |
27 |
28 | 29 |
36 |
37 |
38 | {% if note.data.startswith('data:image/') %} 39 | {{ 40 | {% else %} 41 |
{{ note.body }}
42 | {% endif %} 43 |
44 | 45 |
46 |
47 |

48 | {{ note.title }} 49 |

50 |
51 |
52 |
53 |
54 | {% endfor %} 55 | {% endblock %} -------------------------------------------------------------------------------- /admin/admin.py: -------------------------------------------------------------------------------- 1 | import pyppeteer 2 | import asyncio 3 | import uuid 4 | from logzero import logger 5 | import json 6 | import socket 7 | 8 | FLAG = open('flag.txt').read() 9 | try: 10 | HOSTNAME = socket.getaddrinfo('screenshotter',0)[0][4][0] 11 | except socket.gaierror: 12 | HOSTNAME = '127.0.0.1' 13 | HOSTNAME += ":1024" 14 | FLAGGER_PW = uuid.uuid4().hex 15 | 16 | CHROME_ARGS = ["--no-sandbox", "--disable-gpu", 17 | "--use-mock-keychain", 18 | "--password-store=basic", 19 | "--disable-dev-shm-usage", 20 | "--test-type=webdriver", 21 | "--enable-automation", 22 | "--disable-hang-monitor", 23 | "--window-size=1280,650", 24 | "--disable-background-networking", 25 | "--disable-default-apps", 26 | "--disable-extensions", 27 | "--disable-sync", 28 | "--disable-web-resources", 29 | "--disable-notifications", 30 | "--disable-translate", 31 | "--hide-scrollbars", 32 | "--metrics-recording-only", 33 | "--mute-audio", 34 | "--no-first-run", 35 | "--safebrowsing-disable-auto-update", 36 | "--user-data-dir=/home/chrome" 37 | ] 38 | 39 | async def simulate_user(context): 40 | page = await context.newPage() 41 | await asyncio.wait([ 42 | page.goto(f'http://{HOSTNAME}'), 43 | page.waitForNavigation(), 44 | ]) 45 | 46 | await asyncio.sleep(2) 47 | await page.type('#username', 'flagger') 48 | await page.type('#password', FLAGGER_PW) 49 | await asyncio.wait([ 50 | page.click('#login'), 51 | page.waitForNavigation(), 52 | ]) 53 | 54 | await asyncio.sleep(2) 55 | if FLAG not in await page.content(): 56 | logger.info(f'adding flag note') 57 | await page.type('#title', 'flag') 58 | await page.type('#body', FLAG) 59 | await asyncio.wait([ 60 | page.click('#submit'), 61 | page.waitForNavigation(), 62 | ]) 63 | 64 | await asyncio.sleep(2) 65 | logger.info(f'requesting screenshot') 66 | await page.type('#body', 'http://cscg.de') 67 | await asyncio.wait([ 68 | page.click('#submit'), 69 | page.waitForNavigation(), 70 | ]) 71 | 72 | 73 | # wait for 2 minutes 74 | for _ in range(0, 4): 75 | await asyncio.sleep(10) 76 | await asyncio.wait([ 77 | page.reload(), 78 | page.waitForNavigation(), 79 | ]) 80 | 81 | await asyncio.sleep(2) 82 | resp = await page.goto(f'http://{HOSTNAME}/notes.json', {'waitUntil': 'networkidle0'}) 83 | notes = await resp.json() 84 | 85 | await asyncio.sleep(2) 86 | await asyncio.wait([ 87 | page.goto(f'http://{HOSTNAME}/notes'), 88 | page.waitForNavigation(), 89 | ]) 90 | 91 | logger.info(f'cleanup notes') 92 | for note in notes: 93 | if note['title'] != 'flag': 94 | 95 | await asyncio.sleep(2) 96 | await asyncio.wait([ 97 | page.click(f"#delete_{note['uuid']}"), 98 | page.waitForNavigation(), 99 | ]) 100 | await page.close() 101 | 102 | async def main(): 103 | await asyncio.sleep(30) 104 | while True: 105 | try: 106 | logger.info(f'credentials flager:{FLAGGER_PW}') 107 | browser = await pyppeteer.launch({ 108 | #'executablePath': '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', 109 | 'executablePath': '/usr/bin/google-chrome', 110 | 'headless': True, 'args': CHROME_ARGS}) 111 | context = await browser.createIncognitoBrowserContext() 112 | while True: 113 | try: 114 | await simulate_user(context) 115 | except pyppeteer.errors.PageError: 116 | logger.warning('pyppeteer.errors.PageError') 117 | await asyncio.sleep(30) 118 | 119 | except pyppeteer.errors.BrowserError: 120 | logger.warning('pyppeteer.errors.BrowserError') 121 | await asyncio.sleep(60) 122 | 123 | asyncio.get_event_loop().run_until_complete(main()) -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import hmac 4 | import time 5 | import uuid 6 | import pyppeteer 7 | import base64 8 | import socket 9 | from datetime import datetime 10 | from logzero import logger 11 | from functools import wraps 12 | import sqlite3 13 | from threading import Thread 14 | from flask import Flask, render_template, g, url_for, request, Response, copy_current_request_context, jsonify 15 | 16 | 17 | DATABASE = 'sqlite.db' 18 | # hopefully this gets the chrome service's IP, because chrome debug doesn't allow access via hostname 19 | try: 20 | CHROME_IP = socket.getaddrinfo('chrome',0)[0][4][0] 21 | except socket.gaierror: 22 | CHROME_IP = '127.0.0.1' 23 | try: 24 | SECRET = open('secret', 'rb').read() 25 | except FileNotFoundError: 26 | SECRET = uuid.uuid4().bytes 27 | 28 | app = Flask(__name__) 29 | 30 | 31 | def redirect(location): 32 | "drop-in replacement for flask's redirect that doesn't sanitize the redirect target URL" 33 | response = Response('Redirecting...', 302, mimetype="text/html") 34 | response.headers["Location"] = location 35 | response.autocorrect_location_header = False 36 | return response 37 | 38 | # https://github.com/fengsp/flask-snippets/blob/master/templatetricks/timesince_filter.py 39 | @app.template_filter() 40 | def timesince(dt, default="just now"): 41 | now = datetime.utcnow() 42 | # 2021-03-03 13:34:58 43 | diff = now - datetime.strptime(dt, '%Y-%m-%d %H:%M:%S') 44 | periods = ( 45 | (int(diff.days / 365), "year", "years"), 46 | (int(diff.days / 30), "month", "months"), 47 | (int(diff.days / 7), "week", "weeks"), 48 | (diff.days, "day", "days"), 49 | (int(diff.seconds / 3600), "hour", "hours"), 50 | (int(diff.seconds / 60), "minute", "minutes"), 51 | (diff.seconds, "second", "seconds"), 52 | ) 53 | 54 | for period, singular, plural in periods: 55 | 56 | if period: 57 | return "%d %s ago" % (period, singular if period == 1 else plural) 58 | 59 | return default 60 | 61 | def signature(s): 62 | ''' 63 | generate a hmac signature for a given string 64 | ''' 65 | 66 | m = hmac.new(SECRET, digestmod=hashlib.sha256) 67 | m.update(s.encode('ascii')) 68 | return m.hexdigest() 69 | 70 | def get_db(): 71 | ''' 72 | helper function to get a sqlite database connection 73 | ''' 74 | db = getattr(g, '_database', None) 75 | if db is None: 76 | db = g._database = sqlite3.connect(DATABASE) 77 | db.row_factory = sqlite3.Row 78 | return db 79 | 80 | @app.teardown_appcontext 81 | def close_connection(exception): 82 | ''' 83 | helper function to close the database connection 84 | ''' 85 | db = getattr(g, '_database', None) 86 | if db is not None: 87 | db.close() 88 | 89 | def query_db(query, args=(), one=False): 90 | ''' 91 | helper function to do a SQL query like select 92 | ''' 93 | #logger.info(f'{query} | {args}') 94 | cur = get_db().execute(query, args) 95 | rv = cur.fetchall() 96 | cur.close() 97 | return (rv[0] if rv else None) if one else rv 98 | 99 | def commit_db(query, args=()): 100 | ''' 101 | helper function to do SQl queries like insert into 102 | ''' 103 | #logger.info(f'{query} | {args}') 104 | get_db().cursor().execute(query, args) 105 | get_db().commit() 106 | 107 | def login_required(f): 108 | ''' 109 | login required decorator to ensure g.user exists 110 | ''' 111 | @wraps(f) 112 | def decorated_function(*args, **kwargs): 113 | if 'user' not in g or g.user == None: 114 | return redirect('/logout') 115 | return f(*args, **kwargs) 116 | return decorated_function 117 | 118 | def public_log(msg): 119 | logger.info(msg) 120 | commit_db('insert into logs (msg) values (?)', [msg]) 121 | 122 | @app.before_request 123 | def before_request(): 124 | ''' 125 | session middleware. checks if we have a valid session and sets g.user 126 | ''' 127 | # request - flask.request 128 | if 'session' not in request.cookies: 129 | return None 130 | session = request.cookies['session'].split('.') 131 | if not len(session) == 2: 132 | return None 133 | 134 | key, sig = session 135 | if not hmac.compare_digest(sig, signature(key)): 136 | return None 137 | g.user= query_db('select * from users where uuid = ?', 138 | [key], one=True) 139 | 140 | 141 | async def screenshot(username, note_uuid, url): 142 | try: 143 | 144 | browser = await pyppeteer.connect(browserURL=f'http://{CHROME_IP}:9222') 145 | context = await browser.createIncognitoBrowserContext() 146 | page = await context.newPage() 147 | await page.goto(url) 148 | await asyncio.sleep(10) # wait until page is fully loaded 149 | title = await page.title() 150 | shot = await page.screenshot() 151 | await context.close() 152 | 153 | data = 'data:image/png;base64,'+base64.b64encode(shot).decode('ascii') 154 | commit_db('update notes set data = ?, title = ? where uuid = ?', [data, title, note_uuid]) 155 | public_log(f"awesome! screenshot processed for {username}") 156 | except: 157 | public_log(f"sorry {username} :( your screenshot failed") 158 | commit_db('delete from notes where uuid = ?', [note_uuid]) 159 | 160 | 161 | 162 | 163 | @app.route('/') 164 | def index(): 165 | return render_template('index.html') 166 | 167 | @app.route('/logout') 168 | def logout(): 169 | response = redirect("/") 170 | response.set_cookie('session', '', expires=0) 171 | return response 172 | 173 | @app.route('/notes') 174 | @login_required 175 | def notes(): 176 | notes = query_db('select * from notes where user = ? order by timestamp desc', [g.user['uuid']]) 177 | return render_template('notes.html', user=g.user, notes=notes) 178 | 179 | @app.route('/activity.json') 180 | def activity_json(): 181 | log = query_db('select * from logs order by timestamp desc LIMIT 15') 182 | log_dict = [ 183 | {'id': l['id'], 'timestamp': l['timestamp'], 'msg': l['msg']} 184 | for l in log] 185 | return jsonify(log_dict) 186 | 187 | @app.route('/activity') 188 | def activity(): 189 | logs = query_db('select * from logs order by timestamp desc LIMIT 15') 190 | 191 | return render_template('activity.html', logs=logs, len_logs=len(logs)) 192 | 193 | @app.route('/notes.json') 194 | @login_required 195 | def notes_json(): 196 | notes = query_db('select * from notes where user = ? order by timestamp desc', [g.user['uuid']]) 197 | notes_dict = [ 198 | {'uuid': n['uuid'], 'body': n['body'], 'title': n['title'], 'timestamp': n['timestamp'], 'data': n['data']} 199 | for n in notes] 200 | return jsonify(notes_dict) 201 | 202 | @app.route('/delete_note', methods=['POST']) 203 | @login_required 204 | def delete_note(): 205 | user = g.user['uuid'] 206 | note_uuid = request.form['uuid'] 207 | 208 | commit_db('delete from notes where uuid = ? and user = ?', [note_uuid, user]) 209 | public_log(f"{g.user['username']} deleted a note") 210 | return redirect('/notes') 211 | 212 | @app.route('/add_note', methods=['POST']) 213 | @login_required 214 | def add_note(): 215 | new_note_uuid = uuid.uuid4().hex 216 | user = g.user['uuid'] 217 | title = request.form['title'] 218 | body = request.form['body'] 219 | data = '' 220 | 221 | if body.startswith('https://www.cscg.de') or body.startswith('http://cscg.de'): 222 | 223 | @copy_current_request_context 224 | def screenshot_task(username, note_uuid, url): 225 | asyncio.set_event_loop(asyncio.SelectorEventLoop()) 226 | asyncio.get_event_loop().run_until_complete(screenshot(username, note_uuid, url)) 227 | 228 | title = 'processing screenshot...' 229 | data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2O4e/fufwAIyQOXgDhBOwAAAABJRU5ErkJggg==' 230 | thread = Thread(target=screenshot_task, args=(g.user['username'], new_note_uuid, body)) 231 | thread.daemon = True 232 | thread.start() 233 | worker_name = base64.b64encode(CHROME_IP.encode('ascii')).decode('ascii').strip('=') 234 | public_log(f"{g.user['username']} requested a screenshot via worker chrome:{worker_name}") 235 | else: 236 | public_log(f"nice! {g.user['username']} added a note") 237 | commit_db('insert into notes (uuid, user, title, body, data) values (?, ?, ?, ?, ?)', 238 | [new_note_uuid, user, title, body, data]) 239 | return redirect('/notes') 240 | 241 | @app.route('/registerlogin', methods=['POST']) 242 | def registerlogin(): 243 | username = request.form['username'] 244 | password = request.form['password'] 245 | pwhash = hashlib.sha256(password.encode('utf-8')).hexdigest() 246 | user = query_db('select * from users where username = ? and password = ?', 247 | [username, pwhash], one=True) 248 | 249 | if not user: 250 | # new user. let's create it in the database 251 | new_user_uuid = uuid.uuid4().hex 252 | commit_db('insert into users (uuid, username, password) values (?, ?, ?)', 253 | [new_user_uuid, username, pwhash]) 254 | user= query_db('select * from users where uuid = ?', [new_user_uuid], one=True) 255 | 256 | # calculate signature for cookie 257 | key = user['uuid'] 258 | sig = signature(user['uuid']) 259 | response = redirect('/notes') 260 | response.set_cookie('session', f'{key}.{sig}') 261 | return response 262 | 263 | --------------------------------------------------------------------------------