├── 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 | 
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 |
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 |
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 |
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 |
23 |
24 | {% for note in notes %}
25 |
26 |
27 |
35 |
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 |
--------------------------------------------------------------------------------