Intial commit

This commit is contained in:
Jimmy Allen 2021-01-09 22:13:34 +13:00
parent e3d27a1ba7
commit d5282f1c99
18 changed files with 6826 additions and 131 deletions

136
.gitignore vendored
View File

@ -1,131 +1,5 @@
# ---> Python public/
# Byte-compiled / optimized / DLL files users.json
__pycache__/ .vscode/
*.py[cod] frontend/node_modules/
*$py.class frontend/.cache/
# 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/

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" ]

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
version: '3.7'
services:
console:
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

6293
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": {
"console.js": "^2.0.1",
"parcel": "^1.12.4",
"xterm": "^4.9.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-search": "^0.7.0"
},
"devDependencies": {
"parcel-bundler": "latest"
},
"scripts": {
"build": "parcel build src/index.html --out-dir ../public",
"watch": "parcel watch src/index.html --out-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>
`;
}
});

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

@ -0,0 +1,26 @@
<!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>
<console-component></console-component>
<script src="main.js"></script>
<script src="console.js"></script>
</body>
</html>

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

@ -0,0 +1,156 @@
import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import 'console.js';
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;
socket.close();
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();
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();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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)