Compare commits

...

7 Commits

Author SHA1 Message Date
18c6a97ae7 Update parcel 2 2022-01-11 02:52:48 +00:00
617c44d40e Add sample users.json 2022-01-08 22:59:43 +13:00
17eaab6158 Update Readme 2021-10-30 23:58:31 +00:00
85c2a3142e Update 'README.md'
Add do not use
2021-10-30 12:44:19 +00:00
905cb9412c On start or server select open new websocket rather than reloading 2021-09-22 21:38:03 +12:00
5d8ba91865 Intial commit 2021-01-09 22:14:53 +13:00
d5282f1c99 Intial commit 2021-01-09 22:13:34 +13:00
16 changed files with 14682 additions and 133 deletions

141
.gitignore vendored
View File

@@ -1,131 +1,10 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
public/
users.json
.vscode/
frontend/node_modules/
frontend/.cache/
__pychache__
.parcel-cache/
frontend/.parcel-cache/
frontend/dist/
package-lock.json

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3
RUN apt update && apt install -y python3-pip &&\
/usr/bin/python3 -m pip install aiohttp aiodocker docker
RUN mkdir /app
COPY ./public /app/public
COPY ./src /app/src
EXPOSE 8080
WORKDIR /app
ENTRYPOINT [ "/usr/bin/python3", "/app/src/main.py" ]

View File

@@ -1,3 +1,8 @@
# console
# Minecraft Web Console
Front end for starting, stopping, sending command and viewing logs
Not for public use.
This is currently be rewritten
Front end for starting, stopping, sending command and viewing logs

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: '3.7'
services:
console:
build: ./
container_name: console
image: magmise/console
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./users.json:/app/config/users.json
restart: unless-stopped
networks:
- caddy
labels:
caddy: console
caddy.reverse_proxy: "{{upstreams 8080}}"
#caddy.tls: "admin@chch.tech"
caddy.tls: "internal"
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- 80:80
- 443:443
labels: # Global options
caddy.email: caddy@jimmy.nz
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- caddy
restart: unless-stopped
networks:
- caddy
networks:
caddy:
name: caddy

14115
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
frontend/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "client",
"version": "1.0.0",
"dependencies": {
"xterm": "latest",
"xterm-addon-attach": "latest",
"xterm-addon-fit": "latest",
"xterm-addon-search": "latest"
},
"devDependencies": {
"parcel": "^2.0.0"
},
"source": "src/index.html",
"scripts": {
"start": "parcel",
"build": "parcel build src/index.html --dist-dir ../public",
"watch": "parcel watch src/index.html --dist-dir ../public"
},
"browserslist": [
"last 3 and_chr versions",
"last 3 chrome versions",
"last 3 opera versions",
"last 3 ios_saf versions",
"last 3 safari versions"
]
}

30
frontend/src/console.js Normal file
View File

@@ -0,0 +1,30 @@
import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';
import { FitAddon } from 'xterm-addon-fit';
window.customElements.define('console-component', class extends HTMLElement {
constructor(host, server, token) {
super();
this.shadow = this.attachShadow({mode: 'open'});
this.render();
this.term = new Terminal();
const fitAddon = new FitAddon();
this.term.loadAddon(fitAddon);
fitAddon.fit();
term.open(document.getElementById('terminal'));
this.connect();
}
connect() {
this.socket = new WebSocket(`wss://${host}/server/${server}/logs?token=${token}`);
const attachAddon = new AttachAddon(socket);
term.loadAddon(attachAddon);
}
render() {
this.shadow.innerHTML = `
<div id="terminal"></div>
`;
}
});

23
frontend/src/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../node_modules/xterm/css/xterm.css" />
</head>
<body>
<select id="serverselect"></select>
<button id="start">Start</button>
<button id="stop">Stop</button>
<label id="server"></label>
<div id="terminal"></div>
<form id="send">
<input type="submit" value="Send">
<input type="text" name="send" value="">
</form>
<script type="module" src="main.js"></script>
</body>
</html>

172
frontend/src/main.js Normal file
View File

@@ -0,0 +1,172 @@
import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
function main() {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if(token==null) {
alert("You need a token to use this.");
return;
}
const host = `${window.location.hostname}`;
var server = window.location.hash.replace('#', '');
var serverselect = document.getElementById('serverselect')
var serverlabel = document.getElementById('server');
var socket;
var term = new Terminal();
const fitAddon = new FitAddon();
const searchAddon = new SearchAddon();
term.loadAddon(fitAddon);
term.loadAddon(searchAddon);
fitAddon.fit();
serverlabel.innerText = server;
getServers(token, host).then(servers => {
if(servers==null) {
alert("Invalid token");
return;
}
servers.forEach(element => {
let opt = document.createElement('option');
opt.text = element;
serverselect.add(opt);
});
if(server == "") {
server = serverselect.options[0].value;
window.location.hash = serverselect.value;
}
serverselect.value = server;
console.log(server, servers, servers.includes(server));
if(servers.includes(server)) {
term.open(document.getElementById('terminal'));
try {
socket = new WebSocket(`wss://${host}/server/${server}/logs?token=${token}`);
const attachAddon = new AttachAddon(socket);
term.loadAddon(attachAddon);
} catch(err) {
alert("You are not allowed to use this server");
return;
}
}
});
serverselect.addEventListener("click", (event) => {
console.log(serverselect.value);
window.location.hash = serverselect.value;
server = serverselect.value;
socket.close();
try {
socket = new WebSocket(`wss://${host}/server/${server}/logs?token=${token}`);
const attachAddon = new AttachAddon(socket);
term.loadAddon(attachAddon);
} catch(err) {
alert("You are not allowed to use this server");
return;
}
//location.reload();
})
window.onbeforeunload = function() {
socket.close();
console.log("Closing");
alert("Closing");
}
const start = document.getElementById('start');
const stop = document.getElementById('stop');
const send = document.getElementById('send');
start.addEventListener('click', async _ => {
try {
const response = await fetch(`https://${host}/server/${server}/start?token=${token}`, {
method: 'post',
body: {
// Your body
}
});
console.log('Completed!', response);
if(await response.status==401) {
alert("You are not allowed to start this server");
return
}
socket.close();
try {
socket = new WebSocket(`wss://${host}/server/${server}/logs?token=${token}`);
const attachAddon = new AttachAddon(socket);
term.loadAddon(attachAddon);
} catch(err) {
alert("You are not allowed to use this server");
return;
}
//location.reload();
} catch(err) {
console.error(`Error: ${err}`);
}
});
stop.addEventListener('click', async _ => {
try {
const response = await fetch(`https://${host}/server/${server}/stop?token=${token}`, {
method: 'post',
body: {
// Your body
}
});
console.log('Completed!', response);
if(await response.status==401) {
alert("You are not allowed to stop this server");
return
}
} catch(err) {
console.error(`Error: ${err}`);
}
socket.close();
});
send.onsubmit = async (e) => {
e.preventDefault();
let formdata = new FormData(send);
let command = btoa(formdata.get('send'));
console.log(command);
//let command = btoa('say hello')
try {
let response = await fetch(`https://${host}/server/${server}/command/${command}?token=${token}`, {
method: 'POST'
});
if(await response.status==401) {
alert("You are not allowed to send commands to this server");
return
}
} catch(err) {
console.error(`Error: ${err}`);
}
};
}
async function getServers(token, host) {
try {
console.log(`https://${host}/user/servers?token=${token}`);
let response = await fetch(`https://${host}/user/servers?token=${token}`, {
method: 'GET'
});
if(response.ok)
return JSON.parse(await response.json());
else return null;
} catch(err) {
console.error(`Error: ${err}`);
}
}
window.onload = main();

34
src/auth.py Normal file
View File

@@ -0,0 +1,34 @@
import json
from aiohttp import web
def authenticate(request):
if('token' not in request.query):
return None
token = request.query['token']
user = loadUser(token)
return user
def authorise(request):
user = authenticate(request)
if(user is None):
return False
server = server = request.match_info['server']
return server in user['servers']
def loadUser(token):
with open('/app/config/users.json') as f:
data = json.load(f)['users']
if(token in data):
return data[token]
return None
def getServers(request):
user = authenticate(request)
if(user is not none):
return user['servers']
return none
if __name__ == "__main__":
app = web.Application()
app.add_routes([web.post('/{server}', authorise)])
web.run_app(app)

22
src/handlers.py Normal file
View File

@@ -0,0 +1,22 @@
from aiohttp import web
import server
def start(request):
#await server.start(request.match_info['server'])
pass
def stop(request):
pass
def status(request):
pass
def command(request):
pass
def logs(request):
pass
async def index(request):
return web.FileResponse('/app/public/index.html')

26
src/main.py Normal file
View File

@@ -0,0 +1,26 @@
from aiohttp import web
import server
import base64
import handlers
import user
# client = docker.from_env()
# container = client.containers.get('mc')
# for line in container.logs(stream=True):
# print(line.decode('utf8'))
def main():
print(base64.urlsafe_b64encode(b"say Hello"))
app = web.Application()
app.add_routes([web.post('/server/{server}/start', server.start),
web.post('/server/{server}/stop', server.stop),
web.post('/server/{server}/status', server.status),
web.post('/server/{server}/command/{command}', server.command),
web.get('/server/{server}/logs', server.logs),
web.get('/user/servers', user.servers),
web.get('/', handlers.index)])
app.router.add_static('/', "/app/public")
web.run_app(app)
if __name__ == "__main__":
main()

90
src/server.py Normal file
View File

@@ -0,0 +1,90 @@
from aiohttp import web
import docker
import aiodocker
import base64
import auth
client = docker.from_env()
docker = aiodocker.Docker()
async def start(request):
if(not auth.authorise(request)):
print("Not authorised")
return web.Response(status=401)
try:
container = await getContainer(request)
await container.start()
status=200
except:
status = 500
return web.Response(status=status)
async def stop(request):
if(not auth.authorise(request)):
print("Not authorised")
return web.Response(status=401)
try:
container = await getContainer(request)
await container.stop()
status=200
except:
status=500
return web.Response(status=status)
async def status(request):
# if(not auth.authorise(request)):
# print("Not authorised")
# return web.Response(status=401)
try:
running="error"
running = getContainer(request).status
s=200
except:
s=500
running = ""
finally:
return web.Response(status=s, body=running)
async def command(request):
if(not auth.authorise(request)):
print("Not authorised")
return web.Response(status=401)
server = request.match_info['server']
try:
container = client.containers.get(server)
b64cmd = request.match_info['command']
print(b64cmd)
cmd = base64.urlsafe_b64decode(b64cmd).decode('utf_8')
print(cmd)
container.exec_run(cmd="/usr/local/bin/cmd " + str(cmd))
s= 200
running="Success"
except:
s=500
print("Failed Command")
finally:
return web.Response(status=s)
async def logs(request):
if(not auth.authorise(request)):
print("Not authorised")
return web.Response(status=401)
ws = web.WebSocketResponse()
await ws.prepare(request)
server = request.match_info['server']
docker = aiodocker.Docker()
container = await docker.containers.get(server)
async for line in container.log(stdout=True, follow=True, tail=5000):
if ws.closed:
break
#print(line)
await ws.send_str(line)
await docker.close()
print("Closed")
return ws
async def getContainer(request):
server = request.match_info['server']
return await docker.containers.get(server)

57
src/test.py Normal file
View File

@@ -0,0 +1,57 @@
import unittest
import docker
import aiohttp
import asyncio
import server
import time
class TestServer(unittest.IsolatedAsyncioTestCase):
async def testStart(self):
self.container.stop()
time.sleep(5)
async with self.session.post('http://localhost:8080/mc_test/start') as resp:
assert(resp.status == 200)
async def testStop(self):
async with self.session.post('http://localhost:8080/mc_test/stop') as resp:
assert(resp.status == 200)
async def testStatus(self):
async with self.session.post('http://localhost:8080/mc_test/status') as resp:
assert(resp.status == 200)
async def testCommand(self):
async with self.session.post('http://localhost:8080/mc_test/command/c2F5IEhlbGxv') as resp:
assert(resp.status == 200)
async def testLogs(self):
pass
async def asyncSetUp(self):
self.client = docker.from_env()
self.container = self.client.containers.run("mc",detach=True, name="mc_test")
self.session = aiohttp.ClientSession()
#self.req = make_mocked_request('POST', '/{server}/start', match_info={"server": "mc_test"})
async def asyncTearDown(self):
self.container.stop()
self.container.remove(force=True)
await self.session.close()
self.client.close()
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def startServer(command):
return await command()
if __name__ == "__main__":
unittest.main()

10
src/user.py Normal file
View File

@@ -0,0 +1,10 @@
from aiohttp import web
import auth
import json
async def servers(request):
user = auth.authenticate(request)
if(user is not None):
data = json.dumps(user['servers'])
return web.json_response(data=data ,status=200)
return web.json_response(status=403)

8
users.json.sample Normal file
View File

@@ -0,0 +1,8 @@
{
"users": {
"test": {
"name": "test",
"servers": ["test"]
}
}
}