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.user import watched
|
||||
from anonstream.routes.wrappers import with_user_from, auth_required
|
||||
from anonstream.utils.security import generate_csp
|
||||
|
||||
@current_app.route('/')
|
||||
@with_user_from(request)
|
||||
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')
|
||||
@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.user import get_default_name
|
||||
from anonstream.utils.chat import generate_nonce
|
||||
from anonstream.utils.security import generate_csp
|
||||
from anonstream.utils.user import concatenate_for_notice
|
||||
|
||||
CONFIG = current_app.config
|
||||
|
@ -18,6 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
|
|||
async def nojs_stream(user):
|
||||
return await render_template(
|
||||
'nojs_stream.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
@ -28,6 +30,7 @@ async def nojs_info(user):
|
|||
uptime, viewership = get_stream_uptime_and_viewership()
|
||||
return await render_template(
|
||||
'nojs_info.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
viewership=viewership,
|
||||
uptime=uptime,
|
||||
|
@ -40,6 +43,7 @@ async def nojs_info(user):
|
|||
async def nojs_chat_messages(user):
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_messages.html',
|
||||
{'csp': generate_csp()},
|
||||
user=user,
|
||||
users_by_token=USERS_BY_TOKEN,
|
||||
messages=get_scrollback(current_app.messages),
|
||||
|
@ -58,6 +62,7 @@ async def nojs_chat_users(user):
|
|||
users_by_presence = get_users_by_presence()
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_users.html',
|
||||
{'csp': generate_csp()},
|
||||
user=user,
|
||||
get_default_name=get_default_name,
|
||||
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'
|
||||
return await render_template(
|
||||
'nojs_chat_form.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
state=state,
|
||||
prefer_chat_form=prefer_chat_form,
|
||||
|
|
|
@ -86,11 +86,16 @@ def with_user_from(context):
|
|||
|
||||
return with_user_from_context
|
||||
|
||||
async def render_template_with_etag(*args, **kwargs):
|
||||
rendered_template = await render_template(*args, **kwargs)
|
||||
tag = hashlib.sha256(rendered_template.encode()).hexdigest()
|
||||
async def render_template_with_etag(template, deferred_kwargs, **kwargs):
|
||||
render = await render_template(template, **kwargs)
|
||||
tag = hashlib.sha256(render.encode()).hexdigest()
|
||||
etag = f'W/"{tag}"'
|
||||
if request.if_none_match.contains_weak(tag):
|
||||
return '', 304, {'ETag': etag}
|
||||
else:
|
||||
rendered_template = await render_template(
|
||||
template,
|
||||
**deferred_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
return rendered_template, {'ETag': etag}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
const TOKEN = document.body.dataset.token;
|
||||
const TOKEN_HASH = document.body.dataset.tokenHash;
|
||||
|
||||
/* Content Security Policy nonce */
|
||||
const CSP = document.body.dataset.csp;
|
||||
|
||||
/* 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_info = '<div id="info_js" data-js="true"></div>';
|
||||
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
|
||||
|
@ -57,18 +57,24 @@ const jsmarkup_chat_form = `\
|
|||
</div>
|
||||
</form>`;
|
||||
|
||||
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
|
||||
const insert_jsmarkup = () => {
|
||||
if (document.getElementById("style-color") === null) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
|
||||
const style_color = document.createElement("style");
|
||||
style_color.id = "style-color";
|
||||
style_color.nonce = CSP;
|
||||
document.head.insertAdjacentElement("beforeend", style_color);
|
||||
}
|
||||
if (document.getElementById("style-tripcode-display") === null) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_display);
|
||||
const style_tripcode_display = document.createElement("style");
|
||||
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) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_colors);
|
||||
const style_tripcode_colors = document.createElement("style");
|
||||
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) {
|
||||
const parent = document.getElementById("stream");
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</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">
|
||||
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
|
||||
</article>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<b class="{{ insignia_class }}" title="Broadcaster">##</b>
|
||||
{{- ' ' | safe -}}
|
||||
{%- endif -%}
|
||||
<span class="{{ name_class }}" style="color:{{ user.color }};">
|
||||
<span class="{{ name_class }}">
|
||||
{{- user.name or get_default_name(user) -}}
|
||||
{%- if not user.broadcaster and user.name is none -%}
|
||||
<sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup>
|
||||
|
@ -22,6 +22,6 @@
|
|||
{%- if user.tripcode -%}
|
||||
<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 -%}
|
||||
{% endmacro %}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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 {
|
||||
--link-color: #42a5d7;
|
||||
--padding-size: 0.5rem;
|
||||
|
@ -41,6 +42,8 @@
|
|||
padding: 0;
|
||||
}
|
||||
#tripcode {
|
||||
background-color: {{ user.tripcode.background_color }};
|
||||
color: {{ user.tripcode.foreground_color }};
|
||||
cursor: pointer;
|
||||
}
|
||||
.x {
|
||||
|
@ -230,11 +233,11 @@
|
|||
<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 %}>
|
||||
<div id="password-column">
|
||||
{% if user.tripcode == none %}
|
||||
{% if user.tripcode is none %}
|
||||
<span class="tripcode">(no tripcode)</span>
|
||||
<label for="password-toggle" class="show-password pseudolink">set</label>
|
||||
{% 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>
|
||||
<div id="cleared" class="tripcode">(cleared)</div>
|
||||
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -128,6 +129,20 @@
|
|||
font-size: 9pt;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
@ -80,6 +81,18 @@
|
|||
font-size: 9pt;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -93,7 +106,7 @@
|
|||
<h5>Watching ({{ users_watching | length }})</h5>
|
||||
<ul>
|
||||
{% 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') -}}
|
||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||
</li>
|
||||
|
@ -103,7 +116,7 @@
|
|||
<h5>Not watching ({{ users_notwatching | length }})</h5>
|
||||
<ul>
|
||||
{% 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') -}}
|
||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||
</li>
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
body {
|
||||
overflow-y: auto;
|
||||
margin: 0.75ch 1.25ch;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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 {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import secrets
|
||||
|
||||
def generate_csp():
|
||||
'''
|
||||
Generate a random Content Secuity Policy nonce.
|
||||
'''
|
||||
return secrets.token_urlsafe(16)
|
読み込み中…
新しいイシューから参照