Intial commit
This commit is contained in:
parent
e3d27a1ba7
commit
d5282f1c99
|
@ -1,131 +1,5 @@
|
|||
# ---> 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/
|
|
@ -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" ]
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
});
|
|
@ -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>
|
|
@ -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.
|
@ -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)
|
|
@ -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')
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
@ -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()
|
|
@ -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)
|
Loading…
Reference in New Issue