diff --git a/Dockerfile b/Dockerfile index 6ef9f83..d24c47d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,10 @@ -FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf -FROM python:3.12.8-alpine3.21 +FROM python:3.14.0a6-bookworm +RUN mkdir -p /code WORKDIR /code -ENV FLASK_APP=app.py -ENV FLASK_RUN_HOST=0.0.0.0 -RUN apk add --no-cache gcc musl-dev linux-headers git - -#WKHTMLTOPDFNEEDS -RUN apk add --no-cache \ - libstdc++ \ - libx11 \ - libxrender \ - libxext \ - libssl3 \ - ca-certificates \ - docker \ - fontconfig \ - freetype \ - ttf-dejavu \ - ttf-droid \ - ttf-freefont \ - ttf-liberation \ - # more fonts - && apk add --no-cache --virtual .build-deps \ - msttcorefonts-installer \ - # Install microsoft fonts - && update-ms-fonts \ - && fc-cache -f \ - # Clean up when done - && rm -rf /tmp/* \ - && apk del .build-deps -COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf -COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage +RUN apt update && apt install build-essential docker -y COPY pyproject.toml pyproject.toml COPY src src +RUN mkdir /drive RUN pip install --upgrade pip RUN pip install -e . -EXPOSE 8081 -CMD ["python","-m","snek.app"] -#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"] diff --git a/compose.yml b/compose.yml index 70d21ba..9d55d3b 100644 --- a/compose.yml +++ b/compose.yml @@ -7,26 +7,12 @@ services: - "8081:8081" volumes: - ./:/code - - /media/storage/snek/molodetz.nl/drive:/code/drive + - /media/storage/snek.molodetz.nl/drive:/code/drive + - /media/storage/snek.molodetz.nl/drive:/drive - /var/run/docker.sock:/var/run/docker.sock - - /media/storage/snek/molodetz.nl/drive:/drive environment: - PYTHONDONTWRITEBYTECODE=1 - PYTHONUNBUFFERED=1 entrypoint: ["gunicorn", "-w", "1", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"] #entrypoint: ["python","-m","snek.app"] - snecssh: - build: - context: . - dockerfile: DockerfileDrive - restart: always - ports: - - "2225:2225" - volumes: - - ./:/code - environment: - - PYTHONDONTWRITEBYTECODE="1" - - PYTHONUNBUFFERED="1" - entrypoint: ["python","-m","snekssh.app2"] - #["python","-m","snek.app"] diff --git a/src/snek/scripts/chat.js b/src/snek/scripts/chat.js new file mode 100644 index 0000000..a4d6782 --- /dev/null +++ b/src/snek/scripts/chat.js @@ -0,0 +1,54 @@ +const channelUid = "{{ channel.uid.value }}"; + +function initInputField(textBox) { + textBox.addEventListener('change', (e) => { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true })); + }); + + textBox.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + const message = e.target.value.trim(); + if (message) { + app.rpc.sendMessage(channelUid, message); + e.target.value = ''; + } + } + }); + textBox.focus(); +} + +function updateTimes() { + document.querySelectorAll(".time").forEach((time) => { + time.innerText = app.timeDescription(time.dataset.created_at); + }); +} + +function isElementVisible(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} + +const messagesContainer = document.querySelector(".chat-messages"); + +let isLoadingExtra = false; + +messagesContainer.addEventListener("scroll", () => { + loadExtra(); +}); + +setInterval(updateTimes, 1000); + +app.addEventListener("channel-message", (data) => { + if (data.channel_uid !== channelUid) { + if(!isMentionForSomeoneElse(data.message)){ + channelSidebar.notify(data); + } + } +}); diff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py new file mode 100644 index 0000000..2d9341e --- /dev/null +++ b/src/snek/system/terminal.py @@ -0,0 +1,48 @@ +import asyncio +import aiohttp +import aiohttp.web +import os +import pty +import shlex +import subprocess +import pathlib + +commands = { + 'alpine': 'docker run -it alpine /bin/sh', + 'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh', +} + +class TerminalSession: + def __init__(self,command): + self.master, self.slave = pty.openpty() + self.sockets =[] + self.process = subprocess.Popen( + command.split(" "), + stdin=self.slave, + stdout=self.slave, + stderr=self.slave, + bufsize=0, + universal_newlines=True + ) + + async def read_output(self, ws): + loop = asyncio.get_event_loop() + self.sockets.append(ws) + if len(self.sockets) > 1: + return + while True: + try: + data = await loop.run_in_executor(None, os.read, self.master, 1024) + if not data: + break + try: + for ws in self.sockets: await ws.send_bytes(data) # Send raw bytes for ANSI support + except: + self.sockets.remove(ws) + except Exception: + break + + async def write_input(self, data): + os.write(self.master, data.encode()) + + diff --git a/src/snek/templates/static b/src/snek/templates/static new file mode 120000 index 0000000..d9bc54d --- /dev/null +++ b/src/snek/templates/static @@ -0,0 +1 @@ +../static/ \ No newline at end of file diff --git a/src/snek/templates/terminal.html b/src/snek/templates/terminal.html new file mode 100644 index 0000000..d9ad6d1 --- /dev/null +++ b/src/snek/templates/terminal.html @@ -0,0 +1,47 @@ +{% extends "app.html" %} + +{% block sidebar %} +Reboot +{% endblock %} + +{% block main %} +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"> + <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script> + <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script> + <style> + #terminal { width: 100%; height: 480px; overflow-y: none; } + </style> + + <div class="container" id="terminal"></div> + + <script> + const term = new Terminal({ cursorBlink: true }); + const fitAddon = new FitAddon.FitAddon(); + term.loadAddon(fitAddon); + term.open(document.getElementById("terminal")); + fitAddon.fit(); + + window.addEventListener("resize", () => fitAddon.fit()); + + const schema = window.location.protocol === "https:" ? "wss" : "ws"; + const hostname = window.location.host; + const url = `${schema}://${hostname}/terminal.ws`; + + const socket = new WebSocket(url); + socket.binaryType = "arraybuffer"; // Support binary data + + socket.onopen = () => term.write("\x1b[32mConnected to Molodetz\x1b[0m\r\n"); + + socket.onmessage = (event) => { + const data = new Uint8Array(event.data); + term.write(new TextDecoder().decode(data)); + }; + + term.onData(data => socket.send(new TextEncoder().encode(data))); + + socket.onclose = () => term.write("\r\n\x1b[31mConnection closed\x1b[0m\r\n"); + </script> + + + +{% endblock main %} diff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py new file mode 100644 index 0000000..e54dfb7 --- /dev/null +++ b/src/snek/view/terminal.py @@ -0,0 +1,56 @@ +from snek.system.view import BaseView +import aiohttp +import asyncio +from snek.system.terminal import TerminalSession +import pathlib + +class TerminalSocketView(BaseView): + + login_required = True + + user_sessions = {} + + async def prepare_drive(self): + user = await self.services.user.get(uid=self.session.get("uid")) + root = pathlib.Path("drive").joinpath(user["uid"]) + root.mkdir(parents=True, exist_ok=True) + terminal_folder = pathlib.Path("terminal") + for path in terminal_folder.iterdir(): + destination_path = root.joinpath(path.name) + if not destination_path.exists(): + if not path.is_dir(): + destination_path.write_bytes(path.read_bytes()) + return root + + async def get(self): + + + + ws = aiohttp.web.WebSocketResponse() + await ws.prepare(self.request) + user = await self.services.user.get(uid=self.session.get("uid")) + root = await self.prepare_drive() + + command = f"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash" + print(command) + + session = self.user_sessions.get(user["uid"]) + if not session: + self.user_sessions[user["uid"]] = TerminalSession(command=command) + session = self.user_sessions[user["uid"]] + asyncio.create_task(session.read_output(ws)) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + await session.write_input(msg.data.decode()) + + + return ws + +class TerminalView(BaseView): + + login_required = True + + async def get(self): + request = self.request + return await self.request.app.render_template('terminal.html',self.request) diff --git a/terminal/.bashrc b/terminal/.bashrc new file mode 100644 index 0000000..2b5d791 --- /dev/null +++ b/terminal/.bashrc @@ -0,0 +1,108 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +# If not running interactively, don't do anything +[ -z "$PS1" ] && return + +# don't put duplicate lines in the history. See bash(1) for more options +# ... or force ignoredups and ignorespace +HISTCONTROL=ignoredups:ignorespace + +# append to the history file, don't overwrite it +shopt -s histappend + +# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) +HISTSIZE=1000 +HISTFILESIZE=2000 + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# make less more friendly for non-text input files, see lesspipe(1) +[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +# set variable identifying the chroot you work in (used in the prompt below) +if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then + debian_chroot=$(cat /etc/debian_chroot) +fi + +# set a fancy prompt (non-color, unless we know we "want" color) +case "$TERM" in + xterm-color) color_prompt=yes;; +esac + +# uncomment for a colored prompt, if the terminal has the capability; turned +# off by default to not distract the user: the focus in a terminal window +# should be on the output of commands, not on the prompt +#force_color_prompt=yes + +if [ -n "$force_color_prompt" ]; then + if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then + # We have color support; assume it's compliant with Ecma-48 + # (ISO/IEC-6429). (Lack of such support is extremely rare, and such + # a case would tend to support setf rather than setaf.) + color_prompt=yes + else + color_prompt= + fi +fi + +if [ "$color_prompt" = yes ]; then + PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' +else + PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' +fi +unset color_prompt force_color_prompt + +# If this is an xterm set the title to user@host:dir +case "$TERM" in +xterm*|rxvt*) + PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" + ;; +*) + ;; +esac + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" + alias ls='ls --color=auto' + #alias dir='dir --color=auto' + #alias vdir='vdir --color=auto' + + alias grep='grep --color=auto' + alias fgrep='fgrep --color=auto' + alias egrep='egrep --color=auto' +fi + +# some more ls aliases +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' + +# Alias definitions. +# You may want to put all your additions into a separate file like +# ~/.bash_aliases, instead of adding them here directly. +# See /usr/share/doc/bash-doc/examples in the bash-doc package. + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + + +cp ~/r /usr/local/bin + +chmod -x /usr/local/bin/* + +apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv -y + +echo "r is installed." + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +#if [ -f /etc/bash_completion ] && ! shopt -oq posix; then +# . /etc/bash_completion +#fi diff --git a/terminal/.profile b/terminal/.profile new file mode 100644 index 0000000..c4c7402 --- /dev/null +++ b/terminal/.profile @@ -0,0 +1,9 @@ +# ~/.profile: executed by Bourne-compatible login shells. + +if [ "$BASH" ]; then + if [ -f ~/.bashrc ]; then + . ~/.bashrc + fi +fi + +mesg n 2> /dev/null || true diff --git a/terminal/r b/terminal/r new file mode 100755 index 0000000..2cc65df Binary files /dev/null and b/terminal/r differ