Add Content Security Policy meta tags
このコミットが含まれているのは:
コミット
4bab173237
|
@ -5,11 +5,16 @@ from anonstream.segments import segments
|
||||||
from anonstream.stream import is_online, get_stream_uptime
|
from anonstream.stream import is_online, get_stream_uptime
|
||||||
from anonstream.user import watched
|
from anonstream.user import watched
|
||||||
from anonstream.routes.wrappers import with_user_from, auth_required
|
from anonstream.routes.wrappers import with_user_from, auth_required
|
||||||
|
from anonstream.utils.security import generate_csp
|
||||||
|
|
||||||
@current_app.route('/')
|
@current_app.route('/')
|
||||||
@with_user_from(request)
|
@with_user_from(request)
|
||||||
async def home(user):
|
async def home(user):
|
||||||
return await render_template('home.html', user=user)
|
return await render_template(
|
||||||
|
'home.html',
|
||||||
|
csp=generate_csp(),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
@current_app.route('/stream.mp4')
|
@current_app.route('/stream.mp4')
|
||||||
@with_user_from(request)
|
@with_user_from(request)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from anonstream.routes.wrappers import with_user_from, render_template_with_etag
|
||||||
from anonstream.helpers.chat import get_scrollback
|
from anonstream.helpers.chat import get_scrollback
|
||||||
from anonstream.helpers.user import get_default_name
|
from anonstream.helpers.user import get_default_name
|
||||||
from anonstream.utils.chat import generate_nonce
|
from anonstream.utils.chat import generate_nonce
|
||||||
|
from anonstream.utils.security import generate_csp
|
||||||
from anonstream.utils.user import concatenate_for_notice
|
from anonstream.utils.user import concatenate_for_notice
|
||||||
|
|
||||||
CONFIG = current_app.config
|
CONFIG = current_app.config
|
||||||
|
@ -18,6 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
|
||||||
async def nojs_stream(user):
|
async def nojs_stream(user):
|
||||||
return await render_template(
|
return await render_template(
|
||||||
'nojs_stream.html',
|
'nojs_stream.html',
|
||||||
|
csp=generate_csp(),
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ async def nojs_info(user):
|
||||||
uptime, viewership = get_stream_uptime_and_viewership()
|
uptime, viewership = get_stream_uptime_and_viewership()
|
||||||
return await render_template(
|
return await render_template(
|
||||||
'nojs_info.html',
|
'nojs_info.html',
|
||||||
|
csp=generate_csp(),
|
||||||
user=user,
|
user=user,
|
||||||
viewership=viewership,
|
viewership=viewership,
|
||||||
uptime=uptime,
|
uptime=uptime,
|
||||||
|
@ -40,6 +43,7 @@ async def nojs_info(user):
|
||||||
async def nojs_chat_messages(user):
|
async def nojs_chat_messages(user):
|
||||||
return await render_template_with_etag(
|
return await render_template_with_etag(
|
||||||
'nojs_chat_messages.html',
|
'nojs_chat_messages.html',
|
||||||
|
{'csp': generate_csp()},
|
||||||
user=user,
|
user=user,
|
||||||
users_by_token=USERS_BY_TOKEN,
|
users_by_token=USERS_BY_TOKEN,
|
||||||
messages=get_scrollback(current_app.messages),
|
messages=get_scrollback(current_app.messages),
|
||||||
|
@ -58,6 +62,7 @@ async def nojs_chat_users(user):
|
||||||
users_by_presence = get_users_by_presence()
|
users_by_presence = get_users_by_presence()
|
||||||
return await render_template_with_etag(
|
return await render_template_with_etag(
|
||||||
'nojs_chat_users.html',
|
'nojs_chat_users.html',
|
||||||
|
{'csp': generate_csp()},
|
||||||
user=user,
|
user=user,
|
||||||
get_default_name=get_default_name,
|
get_default_name=get_default_name,
|
||||||
users_watching=users_by_presence[Presence.WATCHING],
|
users_watching=users_by_presence[Presence.WATCHING],
|
||||||
|
@ -73,6 +78,7 @@ async def nojs_chat_form(user):
|
||||||
prefer_chat_form = request.args.get('landing') != 'appearance'
|
prefer_chat_form = request.args.get('landing') != 'appearance'
|
||||||
return await render_template(
|
return await render_template(
|
||||||
'nojs_chat_form.html',
|
'nojs_chat_form.html',
|
||||||
|
csp=generate_csp(),
|
||||||
user=user,
|
user=user,
|
||||||
state=state,
|
state=state,
|
||||||
prefer_chat_form=prefer_chat_form,
|
prefer_chat_form=prefer_chat_form,
|
||||||
|
|
|
@ -86,11 +86,16 @@ def with_user_from(context):
|
||||||
|
|
||||||
return with_user_from_context
|
return with_user_from_context
|
||||||
|
|
||||||
async def render_template_with_etag(*args, **kwargs):
|
async def render_template_with_etag(template, deferred_kwargs, **kwargs):
|
||||||
rendered_template = await render_template(*args, **kwargs)
|
render = await render_template(template, **kwargs)
|
||||||
tag = hashlib.sha256(rendered_template.encode()).hexdigest()
|
tag = hashlib.sha256(render.encode()).hexdigest()
|
||||||
etag = f'W/"{tag}"'
|
etag = f'W/"{tag}"'
|
||||||
if request.if_none_match.contains_weak(tag):
|
if request.if_none_match.contains_weak(tag):
|
||||||
return '', 304, {'ETag': etag}
|
return '', 304, {'ETag': etag}
|
||||||
else:
|
else:
|
||||||
|
rendered_template = await render_template(
|
||||||
|
template,
|
||||||
|
**deferred_kwargs,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
return rendered_template, {'ETag': etag}
|
return rendered_template, {'ETag': etag}
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
const TOKEN = document.body.dataset.token;
|
const TOKEN = document.body.dataset.token;
|
||||||
const TOKEN_HASH = document.body.dataset.tokenHash;
|
const TOKEN_HASH = document.body.dataset.tokenHash;
|
||||||
|
|
||||||
|
/* Content Security Policy nonce */
|
||||||
|
const CSP = document.body.dataset.csp;
|
||||||
|
|
||||||
/* insert js-only markup */
|
/* insert js-only markup */
|
||||||
const jsmarkup_style_color = '<style id="style-color"></style>'
|
|
||||||
const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></style>'
|
|
||||||
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
|
||||||
const jsmarkup_stream = `<video id="stream_js" src="/stream.mp4?token=${encodeURIComponent(TOKEN)}" autoplay controls></video>`
|
const jsmarkup_stream = `<video id="stream_js" src="/stream.mp4?token=${encodeURIComponent(TOKEN)}" autoplay controls></video>`
|
||||||
const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
|
const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
|
||||||
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
|
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
|
||||||
|
@ -57,18 +57,24 @@ const jsmarkup_chat_form = `\
|
||||||
</div>
|
</div>
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
||||||
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
|
const insert_jsmarkup = () => {
|
||||||
if (document.getElementById("style-color") === null) {
|
if (document.getElementById("style-color") === null) {
|
||||||
const parent = document.head;
|
const style_color = document.createElement("style");
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
|
style_color.id = "style-color";
|
||||||
|
style_color.nonce = CSP;
|
||||||
|
document.head.insertAdjacentElement("beforeend", style_color);
|
||||||
}
|
}
|
||||||
if (document.getElementById("style-tripcode-display") === null) {
|
if (document.getElementById("style-tripcode-display") === null) {
|
||||||
const parent = document.head;
|
const style_tripcode_display = document.createElement("style");
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_display);
|
style_tripcode_display.id = "style-tripcode-display";
|
||||||
|
style_tripcode_display.nonce = CSP;
|
||||||
|
document.head.insertAdjacentElement("beforeend", style_tripcode_display);
|
||||||
}
|
}
|
||||||
if (document.getElementById("style-tripcode-colors") === null) {
|
if (document.getElementById("style-tripcode-colors") === null) {
|
||||||
const parent = document.head;
|
const style_tripcode_colors = document.createElement("style");
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_colors);
|
style_tripcode_colors.id = "style-tripcode-colors";
|
||||||
|
style_tripcode_colors.nonce = CSP;
|
||||||
|
document.head.insertAdjacentElement("beforeend", style_tripcode_colors);
|
||||||
}
|
}
|
||||||
if (document.getElementById("stream_js") === null) {
|
if (document.getElementById("stream_js") === null) {
|
||||||
const parent = document.getElementById("stream");
|
const parent = document.getElementById("stream");
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="content-security-policy" content="default-src 'none'; connect-src 'self'; img-src 'self'; frame-src 'self'; media-src 'self'; script-src 'self'; style-src 'self' 'nonce-{{ csp }}';">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
|
||||||
</head>
|
</head>
|
||||||
<body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}">
|
<body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}" data-csp="{{ csp }}">
|
||||||
<article id="stream">
|
<article id="stream">
|
||||||
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
|
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<b class="{{ insignia_class }}" title="Broadcaster">##</b>
|
<b class="{{ insignia_class }}" title="Broadcaster">##</b>
|
||||||
{{- ' ' | safe -}}
|
{{- ' ' | safe -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<span class="{{ name_class }}" style="color:{{ user.color }};">
|
<span class="{{ name_class }}">
|
||||||
{{- user.name or get_default_name(user) -}}
|
{{- user.name or get_default_name(user) -}}
|
||||||
{%- if not user.broadcaster and user.name is none -%}
|
{%- if not user.broadcaster and user.name is none -%}
|
||||||
<sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup>
|
<sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup>
|
||||||
|
@ -22,6 +22,6 @@
|
||||||
{%- if user.tripcode -%}
|
{%- if user.tripcode -%}
|
||||||
<span class="{{ tripcode_nbsp_class }}"> </span>
|
<span class="{{ tripcode_nbsp_class }}"> </span>
|
||||||
{{- '' -}}
|
{{- '' -}}
|
||||||
<span class="{{ tripcode_class }}" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</span>
|
<span class="{{ tripcode_class }}">{{ user.tripcode.digest }}</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<meta http-equiv="content-security-policy" content="default-src 'none'; img-src 'self'; style-src 'nonce-{{ csp }}';">
|
||||||
|
<style nonce="{{ csp }}">
|
||||||
:root {
|
:root {
|
||||||
--link-color: #42a5d7;
|
--link-color: #42a5d7;
|
||||||
--padding-size: 0.5rem;
|
--padding-size: 0.5rem;
|
||||||
|
@ -41,6 +42,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#tripcode {
|
#tripcode {
|
||||||
|
background-color: {{ user.tripcode.background_color }};
|
||||||
|
color: {{ user.tripcode.foreground_color }};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.x {
|
.x {
|
||||||
|
@ -230,11 +233,11 @@
|
||||||
<input id="password-toggle" name="set-tripcode" type="checkbox" accesskey="s">
|
<input id="password-toggle" name="set-tripcode" type="checkbox" accesskey="s">
|
||||||
<input id="cleared-toggle" name="clear-tripcode" type="checkbox"{% if user.tripcode != none %} accesskey="c"{% endif %}>
|
<input id="cleared-toggle" name="clear-tripcode" type="checkbox"{% if user.tripcode != none %} accesskey="c"{% endif %}>
|
||||||
<div id="password-column">
|
<div id="password-column">
|
||||||
{% if user.tripcode == none %}
|
{% if user.tripcode is none %}
|
||||||
<span class="tripcode">(no tripcode)</span>
|
<span class="tripcode">(no tripcode)</span>
|
||||||
<label for="password-toggle" class="show-password pseudolink">set</label>
|
<label for="password-toggle" class="show-password pseudolink">set</label>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label id="tripcode" for="password-toggle" class="show-password tripcode" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</label>
|
<label id="tripcode" for="password-toggle" class="show-password tripcode">{{ user.tripcode.digest }}</label>
|
||||||
<label id="show-cleared" for="cleared-toggle" class="pseudolink x">✗</label>
|
<label id="show-cleared" for="cleared-toggle" class="pseudolink x">✗</label>
|
||||||
<div id="cleared" class="tripcode">(cleared)</div>
|
<div id="cleared" class="tripcode">(cleared)</div>
|
||||||
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>
|
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||||
<meta http-equiv="refresh" content="4">
|
<meta http-equiv="refresh" content="4">
|
||||||
<meta http-equiv="refresh" content="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
|
<meta http-equiv="refresh" content="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
|
||||||
<style>
|
<style nonce="{{ csp }}">
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -128,6 +129,20 @@
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% for token in messages | map(attribute='token') | list | unique %}
|
||||||
|
{% with user = users_by_token[token] %}
|
||||||
|
[data-token-hash="{{ user.token_hash }}"] > .chat-message__name {
|
||||||
|
color: {{ user.color }};
|
||||||
|
}
|
||||||
|
{% if user.tripcode %}
|
||||||
|
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
|
||||||
|
background-color: {{ user.tripcode.background_color }};
|
||||||
|
color: {{ user.tripcode.foreground_color }};
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||||
<meta http-equiv="refresh" content="6">
|
<meta http-equiv="refresh" content="6">
|
||||||
<style>
|
<style nonce="{{ csp }}">
|
||||||
html {
|
html {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -80,6 +81,18 @@
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% for user in users_watching + users_notwatching %}
|
||||||
|
[data-token-hash="{{ user.token_hash }}"] > .user__name {
|
||||||
|
color: {{ user.color }};
|
||||||
|
}
|
||||||
|
{% if user.tripcode %}
|
||||||
|
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
|
||||||
|
background-color: {{ user.tripcode.background_color }};
|
||||||
|
color: {{ user.tripcode.foreground_color }};
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -93,7 +106,7 @@
|
||||||
<h5>Watching ({{ users_watching | length }})</h5>
|
<h5>Watching ({{ users_watching | length }})</h5>
|
||||||
<ul>
|
<ul>
|
||||||
{% for user_listed in users_watching %}
|
{% for user_listed in users_watching %}
|
||||||
<li class="user">
|
<li class="user" data-token-hash="{{ user.token_hash }}">
|
||||||
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
||||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||||
</li>
|
</li>
|
||||||
|
@ -103,7 +116,7 @@
|
||||||
<h5>Not watching ({{ users_notwatching | length }})</h5>
|
<h5>Not watching ({{ users_notwatching | length }})</h5>
|
||||||
<ul>
|
<ul>
|
||||||
{% for user_listed in users_notwatching %}
|
{% for user_listed in users_notwatching %}
|
||||||
<li class="user">
|
<li class="user" data-token-hash="{{ user.token_hash }}">
|
||||||
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
||||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||||
<meta http-equiv="refresh" content="6">
|
<meta http-equiv="refresh" content="6">
|
||||||
<style>
|
<style nonce="{{ csp }}">
|
||||||
body {
|
body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 0.75ch 1.25ch;
|
margin: 0.75ch 1.25ch;
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<meta http-equiv="content-security-policy" content="default-src 'none'; media-src 'self'; style-src 'nonce-{{ csp }}';">
|
||||||
|
<style nonce="{{ csp }}">
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
def generate_csp():
|
||||||
|
'''
|
||||||
|
Generate a random Content Secuity Policy nonce.
|
||||||
|
'''
|
||||||
|
return secrets.token_urlsafe(16)
|
読み込み中…
新しいイシューから参照