Merge branch 'main' into feat/push-notifications
# Conflicts: # src/snek/app.py
This commit is contained in:
commit
d04ea8549d
pyproject.toml
src/snek
@ -39,6 +39,7 @@ dependencies = [
|
||||
"Pillow",
|
||||
"pillow-heif",
|
||||
"IP2Location",
|
||||
"bleach"
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
@ -29,7 +29,7 @@ from snek.service import get_services
|
||||
from snek.system import http
|
||||
from snek.system.cache import Cache
|
||||
from snek.system.markdown import MarkdownExtension
|
||||
from snek.system.middleware import auth_middleware, cors_middleware
|
||||
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
|
||||
from snek.system.profiler import profiler_handler
|
||||
from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
|
||||
from snek.view.about import AboutHTMLView, AboutMDView
|
||||
@ -66,9 +66,11 @@ from snek.view.settings.containers import (
|
||||
ContainersDeleteView,
|
||||
)
|
||||
from snek.webdav import WebdavApplication
|
||||
from snek.system.template import sanitize_html
|
||||
from snek.sgit import GitApplication
|
||||
|
||||
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||
from snek.system.template import whitelist_attributes
|
||||
|
||||
|
||||
@web.middleware
|
||||
@ -119,6 +121,7 @@ class Application(BaseApplication):
|
||||
cors_middleware,
|
||||
web.normalize_path_middleware(merge_slashes=True),
|
||||
ip2location_middleware,
|
||||
csp_middleware
|
||||
]
|
||||
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
|
||||
self.static_path = pathlib.Path(__file__).parent.joinpath("static")
|
||||
@ -136,6 +139,7 @@ class Application(BaseApplication):
|
||||
self.jinja2_env.add_extension(LinkifyExtension)
|
||||
self.jinja2_env.add_extension(PythonExtension)
|
||||
self.jinja2_env.add_extension(EmojiExtension)
|
||||
self.jinja2_env.filters['sanitize'] = sanitize_html
|
||||
self.time_start = datetime.now()
|
||||
self.ssh_host = "0.0.0.0"
|
||||
self.ssh_port = 2242
|
||||
@ -297,9 +301,10 @@ class Application(BaseApplication):
|
||||
# self.router.add_get("/{file_path:.*}", self.static_handler)
|
||||
|
||||
async def handle_test(self, request):
|
||||
return await self.render_template(
|
||||
|
||||
return await whitelist_attributes(self.render_template(
|
||||
"test.html", request, context={"name": "retoor"}
|
||||
)
|
||||
))
|
||||
|
||||
async def handle_http_get(self, request: web.Request):
|
||||
url = request.query.get("url")
|
||||
@ -370,6 +375,8 @@ class Application(BaseApplication):
|
||||
|
||||
self.jinja2_env.loader = self.original_loader
|
||||
|
||||
#rendered.text = whitelist_attributes(rendered.text)
|
||||
#rendered.headers['Content-Lenght'] = len(rendered.text)
|
||||
return rendered
|
||||
|
||||
async def static_handler(self, request):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from snek.system.service import BaseService
|
||||
from snek.system.template import whitelist_attributes
|
||||
|
||||
|
||||
class ChannelMessageService(BaseService):
|
||||
@ -28,6 +29,7 @@ class ChannelMessageService(BaseService):
|
||||
try:
|
||||
template = self.app.jinja2_env.get_template("message.html")
|
||||
model["html"] = template.render(**context)
|
||||
model["html"] = whitelist_attributes(model["html"])
|
||||
except Exception as ex:
|
||||
print(ex, flush=True)
|
||||
|
||||
@ -65,6 +67,7 @@ class ChannelMessageService(BaseService):
|
||||
)
|
||||
template = self.app.jinja2_env.get_template("message.html")
|
||||
model["html"] = template.render(**context)
|
||||
model["html"] = whitelist_attributes(model["html"])
|
||||
return await super().save(model)
|
||||
|
||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||
|
@ -174,7 +174,15 @@ export class App extends EventHandler {
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false;
|
||||
}
|
||||
|
||||
ntsh(times,message) {
|
||||
if(!message)
|
||||
message = "Nothing to see here!"
|
||||
if(!times)
|
||||
times=100
|
||||
for(let x = 0; x < times; x++){
|
||||
this.rpc.sendMessage("293ecf12-08c9-494b-b423-48ba1a2d12c2",message)
|
||||
}
|
||||
}
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
}
|
||||
|
@ -221,6 +221,55 @@ footer {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.message-content .spoiler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
/*color: transparent;*/
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
height: 2.5rem;
|
||||
overflow: hidden;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.message-content .spoiler * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.spoiler:hover, .spoiler:focus, .spoiler:focus-within, .spoiler:active {
|
||||
/*color: #e6e6e6;*/
|
||||
/*transition: color 0.3s ease-in;*/
|
||||
height: unset;
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
@keyframes delay-pointer-events {
|
||||
0% {
|
||||
visibility: hidden;
|
||||
}
|
||||
50% {
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.spoiler:hover * {
|
||||
animation: unset;
|
||||
}
|
||||
|
||||
.spoiler:hover *, .spoiler:focus *, .spoiler:focus-within *, .spoiler:active * {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
animation: delay-pointer-events 0.2s linear;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ class ChatInputComponent extends HTMLElement {
|
||||
_value = ""
|
||||
lastUpdateEvent = null
|
||||
expiryTimer = null;
|
||||
queuedMessage = null;
|
||||
lastMessagePromise = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -38,11 +40,11 @@ class ChatInputComponent extends HTMLElement {
|
||||
return Object.assign({}, this.autoCompletions, this.hiddenCompletions)
|
||||
}
|
||||
|
||||
resolveAutoComplete() {
|
||||
resolveAutoComplete(input) {
|
||||
let value = null;
|
||||
|
||||
for (const key of Object.keys(this.allAutoCompletions)) {
|
||||
if (key.startsWith(this.value.split(" ", 1)[0])) {
|
||||
if (key.startsWith(input.split(" ", 1)[0])) {
|
||||
if (value) {
|
||||
return null;
|
||||
}
|
||||
@ -193,7 +195,7 @@ class ChatInputComponent extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.finalizeMessage()
|
||||
this.finalizeMessage(this.messageUid)
|
||||
|
||||
return;
|
||||
}
|
||||
@ -203,19 +205,25 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
this.textarea.addEventListener("keydown", (e) => {
|
||||
this.value = e.target.value;
|
||||
|
||||
let autoCompletion = null;
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
autoCompletion = this.resolveAutoComplete();
|
||||
autoCompletion = this.resolveAutoComplete(this.value);
|
||||
if (autoCompletion) {
|
||||
e.target.value = autoCompletion;
|
||||
this.value = autoCompletion;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.repeat) {
|
||||
this.updateFromInput(e.target.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener("upload", (e) => {
|
||||
@ -256,17 +264,28 @@ class ChatInputComponent extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
finalizeMessage() {
|
||||
if (!this.messageUid) {
|
||||
finalizeMessage(messageUid) {
|
||||
if (!messageUid) {
|
||||
if (this.value.trim() === "") {
|
||||
return;
|
||||
}
|
||||
this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(this.value), !this.liveType);
|
||||
} else if (messageUid.startsWith("?")) {
|
||||
const lastQueuedMessage = this.queuedMessage;
|
||||
|
||||
this.lastMessagePromise?.then((uid) => {
|
||||
const updatePromise = lastQueuedMessage ? app.rpc.updateMessageText(uid, lastQueuedMessage) : Promise.resolve();
|
||||
return updatePromise.finally(() => {
|
||||
return app.rpc.finalizeMessage(uid);
|
||||
})
|
||||
})
|
||||
} else {
|
||||
app.rpc.finalizeMessage(this.messageUid)
|
||||
app.rpc.finalizeMessage(messageUid)
|
||||
}
|
||||
this.value = "";
|
||||
this.messageUid = null;
|
||||
this.queuedMessage = null;
|
||||
this.lastMessagePromise = null
|
||||
}
|
||||
|
||||
updateFromInput(value) {
|
||||
@ -281,18 +300,33 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
if (this.liveType && value[0] !== "/") {
|
||||
this.expiryTimer = setTimeout(() => {
|
||||
this.finalizeMessage()
|
||||
this.finalizeMessage(this.messageUid)
|
||||
}, this.liveTypeInterval * 1000);
|
||||
|
||||
if (this.messageUid === "?") {
|
||||
|
||||
const messageText = this.replaceMentionsWithAuthors(value);
|
||||
if (this.messageUid?.startsWith("?")) {
|
||||
this.queuedMessage = messageText;
|
||||
} else if (this.messageUid) {
|
||||
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
||||
app.rpc.updateMessageText(this.messageUid, messageText).then((d) => {
|
||||
if (!d.success) {
|
||||
this.messageUid = null
|
||||
this.updateFromInput(value)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.messageUid = "?"; // Indicate that a message is being sent
|
||||
this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(value), !this.liveType).then((uid) => {
|
||||
if (this.liveType) {
|
||||
const placeHolderId = "?" + crypto.randomUUID();
|
||||
this.messageUid = placeHolderId;
|
||||
|
||||
this.lastMessagePromise = this.sendMessage(this.channelUid, messageText, !this.liveType).then(async (uid) => {
|
||||
if (this.liveType && this.messageUid === placeHolderId) {
|
||||
if (this.queuedMessage && this.queuedMessage !== messageText) {
|
||||
await app.rpc.updateMessageText(uid, this.queuedMessage)
|
||||
}
|
||||
this.messageUid = uid;
|
||||
}
|
||||
|
||||
return uid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ from pygments.lexers import get_lexer_by_name
|
||||
|
||||
class MarkdownRenderer(HTMLRenderer):
|
||||
|
||||
_allow_harmful_protocols = True
|
||||
_allow_harmful_protocols = False
|
||||
|
||||
def __init__(self, app, template):
|
||||
super().__init__(False, True)
|
||||
@ -26,8 +26,8 @@ class MarkdownRenderer(HTMLRenderer):
|
||||
formatter = html.HtmlFormatter()
|
||||
self.env.globals["highlight_styles"] = formatter.get_style_defs()
|
||||
|
||||
def _escape(self, str):
|
||||
return str ##escape(str)
|
||||
#def _escape(self, str):
|
||||
# return str ##escape(str)
|
||||
|
||||
def get_lexer(self, lang, default="bash"):
|
||||
try:
|
||||
|
@ -7,8 +7,30 @@
|
||||
# MIT License: This code is distributed under the MIT License.
|
||||
|
||||
from aiohttp import web
|
||||
import secrets
|
||||
|
||||
|
||||
csp_policy = (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' https://*.cloudflare.com https://molodetz.nl 'nonce-{nonce}'; "
|
||||
"style-src 'self' https://*.cloudflare.com https://molodetz.nl; "
|
||||
"img-src 'self' https://*.cloudflare.com https://molodetz.nl data:; "
|
||||
"connect-src 'self' https://*.cloudflare.com https://molodetz.nl;"
|
||||
)
|
||||
|
||||
|
||||
def generate_nonce():
|
||||
return secrets.token_hex(16)
|
||||
|
||||
@web.middleware
|
||||
async def csp_middleware(request, handler):
|
||||
|
||||
response = await handler(request)
|
||||
return response
|
||||
nonce = generate_nonce()
|
||||
response.headers['Content-Security-Policy'] = csp_policy.format(nonce=nonce)
|
||||
return response
|
||||
|
||||
@web.middleware
|
||||
async def no_cors_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
|
@ -10,6 +10,8 @@ from bs4 import BeautifulSoup
|
||||
from jinja2 import TemplateSyntaxError, nodes
|
||||
from jinja2.ext import Extension
|
||||
from jinja2.nodes import Const
|
||||
import bleach
|
||||
|
||||
|
||||
emoji.EMOJI_DATA['<img src="/emoji/snek1.gif" />'] = {
|
||||
"en": ":snek1:",
|
||||
@ -78,6 +80,36 @@ emoji.EMOJI_DATA[
|
||||
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
|
||||
|
||||
|
||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
||||
"img", "video", "audio", "source", "iframe", "picture", "span"
|
||||
]
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
|
||||
"img": ["src", "alt", "title", "width", "height"],
|
||||
"a": ["href", "title", "target", "rel", "referrerpolicy", "class"],
|
||||
"iframe": ["src", "width", "height", "frameborder", "allow", "allowfullscreen", "title", "referrerpolicy", "style"],
|
||||
"video": ["src", "controls", "width", "height"],
|
||||
"audio": ["src", "controls"],
|
||||
"source": ["src", "type"],
|
||||
"span": ["class"],
|
||||
"picture": [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def sanitize_html(value):
|
||||
return bleach.clean(
|
||||
value,
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
|
||||
strip=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def set_link_target_blank(text):
|
||||
soup = BeautifulSoup(text, "html.parser")
|
||||
|
||||
@ -89,6 +121,26 @@ def set_link_target_blank(text):
|
||||
|
||||
return str(soup)
|
||||
|
||||
SAFE_ATTRIBUTES = {
|
||||
'href', 'src', 'alt', 'title', 'width', 'height', 'style', 'id', 'class',
|
||||
'rel', 'type', 'name', 'value', 'placeholder', 'aria-hidden', 'aria-label', 'srcset'
|
||||
}
|
||||
|
||||
def whitelist_attributes(html):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
for tag in soup.find_all():
|
||||
if hasattr(tag, 'attrs'):
|
||||
if tag.name in ['script','form','input']:
|
||||
tag.replace_with('')
|
||||
continue
|
||||
attrs = dict(tag.attrs)
|
||||
for attr in list(attrs):
|
||||
# Check if attribute is in the safe list or is a data-* attribute
|
||||
if not (attr in SAFE_ATTRIBUTES or attr.startswith('data-')):
|
||||
del tag.attrs[attr]
|
||||
return str(soup)
|
||||
|
||||
|
||||
def embed_youtube(text):
|
||||
soup = BeautifulSoup(text, "html.parser")
|
||||
|
Loading…
Reference in New Issue
Block a user