Compare commits
19 Commits
d09c207c97
...
master
Author | SHA1 | Date | |
---|---|---|---|
0c2af63888 | |||
18ef42023f | |||
49a49f7ea6 | |||
d8b8c0f801 | |||
9cdc0ad923 | |||
af0954b6ff | |||
6df31460e8 | |||
b00e0faedc | |||
02097426ba | |||
bd6afffd77 | |||
f2e4478edc | |||
3a5716a2f9 | |||
26490887ee | |||
9a254575f2 | |||
db218cb8c2 | |||
fd5fc0a85d | |||
7307abf7d8 | |||
b6f41f4d0a | |||
759bab3ff3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,7 +4,8 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
requirements.txt
|
requirements.txt
|
||||||
|
users.json
|
||||||
|
app/users.json
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
14
Dockerfile
14
Dockerfile
@@ -1,6 +1,14 @@
|
|||||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim
|
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim
|
||||||
ENV DOCKER=1
|
ENV DOCKER=1
|
||||||
|
|
||||||
COPY requirements.txt /app/requirements.txt
|
RUN pip install \
|
||||||
RUN pip install -r /app/requirements.txt
|
python-jose \
|
||||||
COPY ./app /app/app
|
passlib \
|
||||||
|
python-multipart \
|
||||||
|
docker \
|
||||||
|
aiodocker \
|
||||||
|
sse-starlette \
|
||||||
|
anyio
|
||||||
|
COPY ./app /app/app
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
|
||||||
|
5
Pipfile
5
Pipfile
@@ -10,6 +10,9 @@ uvicorn = {extras = ["standard"], version = "*"}
|
|||||||
python-jose = {extras = ["cryptography"], version = "*"}
|
python-jose = {extras = ["cryptography"], version = "*"}
|
||||||
passlib = "*"
|
passlib = "*"
|
||||||
python-multipart = "*"
|
python-multipart = "*"
|
||||||
|
docker = "*"
|
||||||
|
aiodocker = "*"
|
||||||
|
sse-starlette = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
@@ -22,5 +25,5 @@ mypy = "*"
|
|||||||
python_version = "3.9"
|
python_version = "3.9"
|
||||||
|
|
||||||
[scripts]
|
[scripts]
|
||||||
test = "pytest app/test/test_main.py -s"
|
test = "pytest app/test/test_auth.py app/test/test_server.py -W ignore::DeprecationWarning -s"
|
||||||
dev = "uvicorn app.main:app --reload"
|
dev = "uvicorn app.main:app --reload"
|
||||||
|
32
app/auth.py
32
app/auth.py
@@ -3,11 +3,14 @@ import json
|
|||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status, APIRouter
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from app.models import User, UserInDB, TokenData
|
from app.models import User, UserInDB, TokenData, Token
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
# to get a string like this run:
|
# to get a string like this run:
|
||||||
# openssl rand -hex 32
|
# openssl rand -hex 32
|
||||||
@@ -78,4 +81,27 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||||
if current_user.disabled:
|
if current_user.disabled:
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
def authorise(server, current_user: User = Depends(get_current_active_user)):
|
||||||
|
print(server, current_user.servers)
|
||||||
|
if server not in current_user.servers:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/token", response_model=Token)
|
||||||
|
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.username}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
0
app/file.py
Normal file
0
app/file.py
Normal file
51
app/main.py
51
app/main.py
@@ -1,35 +1,20 @@
|
|||||||
from datetime import timedelta
|
from fastapi import FastAPI, Depends
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app import auth, user, server
|
||||||
|
from os import getenv
|
||||||
|
app = FastAPI(docs_url="/")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
dependencies = list()
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
if not getenv('DISABLE_AUTH'):
|
||||||
|
dependencies.append(Depends(auth.authorise))
|
||||||
from app.models import Token, User
|
app.include_router(auth.router)
|
||||||
from app.auth import get_current_active_user, create_access_token, authenticate_user, fake_users_db, ACCESS_TOKEN_EXPIRE_MINUTES
|
app.include_router(user.router)
|
||||||
|
app.include_router(server.router, dependencies=dependencies)
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
@app.post("/token", response_model=Token)
|
|
||||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
||||||
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Incorrect username or password",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
access_token = create_access_token(
|
|
||||||
data={"sub": user.username}, expires_delta=access_token_expires
|
|
||||||
)
|
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/users/me/", response_model=User)
|
|
||||||
async def read_users_me(current_user: User = Depends(get_current_active_user)):
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/users/me/items/")
|
|
||||||
async def read_own_items(current_user: User = Depends(get_current_active_user)):
|
|
||||||
return [{"item_id": "Foo", "owner": current_user.username}]
|
|
||||||
|
76
app/server.py
Normal file
76
app/server.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status, WebSocket, Request
|
||||||
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
import base64
|
||||||
|
import docker
|
||||||
|
import aiodocker
|
||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
client = docker.from_env()
|
||||||
|
docker = aiodocker.Docker()
|
||||||
|
|
||||||
|
@router.post("/server/{server}/start")
|
||||||
|
async def start(server):
|
||||||
|
try:
|
||||||
|
container = await getContainer(server)
|
||||||
|
await container.start()
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=500)
|
||||||
|
return server
|
||||||
|
|
||||||
|
@router.post("/server/{server}/stop")
|
||||||
|
async def stop(server):
|
||||||
|
try:
|
||||||
|
container = await getContainer(server)
|
||||||
|
await container.stop()
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=500)
|
||||||
|
return server
|
||||||
|
|
||||||
|
@router.post("/server/{server}/command/{command}",
|
||||||
|
description="Take server and base64 encoded command")
|
||||||
|
async def commnd(server, command):
|
||||||
|
try:
|
||||||
|
container = client.containers.get(server)
|
||||||
|
print(command)
|
||||||
|
cmd = base64.urlsafe_b64decode(command).decode('utf_8')
|
||||||
|
print(cmd)
|
||||||
|
container.exec_run(cmd="/usr/local/bin/cmd " + str(cmd))
|
||||||
|
s= 200
|
||||||
|
running="Success"
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=500)
|
||||||
|
|
||||||
|
return f"{server} {base64.urlsafe_b64decode(command).decode('utf_8')}"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/server/{server}/logs")
|
||||||
|
async def logs(server: str, request: Request, follow: bool = True, tail: int = 1000):
|
||||||
|
container = await getContainer(server)
|
||||||
|
#event_generator = logGenerator(request, server)
|
||||||
|
if follow:
|
||||||
|
return EventSourceResponse(logStream(container, tail))
|
||||||
|
else:
|
||||||
|
return await container.log(stdout=True, follow=False, tail=tail)
|
||||||
|
|
||||||
|
async def logStream(container, tail):
|
||||||
|
async for line in container.log(stdout=True, follow=True, tail=tail):
|
||||||
|
yield line
|
||||||
|
yield '\n'
|
||||||
|
|
||||||
|
@router.get("/server/{server}/stats")
|
||||||
|
async def stats(server: str, request: Request, stream: bool = True, delay: int = 2):
|
||||||
|
container = await getContainer(server)
|
||||||
|
if stream:
|
||||||
|
return EventSourceResponse(statStream(request, container, delay))
|
||||||
|
return await container.stats(stream=False)
|
||||||
|
|
||||||
|
async def statStream(request, container, delay):
|
||||||
|
while True:
|
||||||
|
yield await container.stats(stream=False)
|
||||||
|
_delay = delay - 1 if delay else 0
|
||||||
|
print(_delay)
|
||||||
|
await sleep(_delay)
|
||||||
|
|
||||||
|
async def getContainer(server):
|
||||||
|
return await docker.containers.get(server)
|
@@ -8,7 +8,8 @@
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from app.main import app, create_access_token
|
from app.main import app
|
||||||
|
from app.auth import create_access_token
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
21
app/test/test_server.py
Normal file
21
app/test/test_server.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#curl -i -X POST http://localhost:8000/token -H "Content-Type: application/x-www-form-urlencoded" -d "username=johndoe&password=secret"
|
||||||
|
|
||||||
|
# curl -X 'GET' \
|
||||||
|
# 'http://localhost:8000/users/me/' \
|
||||||
|
# -H 'accept: application/json' \
|
||||||
|
# -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiZXhwIjoxNjMxNDQ4MjQ1fQ.DrM92jgRiry0uXBXn-61rRehATW4zDhHUWoGR6lv6Us'
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
import docker
|
||||||
|
|
||||||
|
from server import *
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
testclient = TestClient(app)
|
||||||
|
|
||||||
|
def test_start():
|
||||||
|
#response = testclient.post("/server/minecraft/start")
|
||||||
|
#assert response.status_code == 200
|
||||||
|
pass
|
||||||
|
|
11
app/user.py
Normal file
11
app/user.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import Depends, APIRouter
|
||||||
|
from app.models import User
|
||||||
|
from app.auth import get_current_active_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/users/me/", response_model=User)
|
||||||
|
async def read_users_me(current_user: User = Depends(get_current_active_user)):
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
18
app/useradd.py
Normal file
18
app/useradd.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from distutils.fancy_getopt import fancy_getopt
|
||||||
|
from site import USER_BASE
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from json import load, dump
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
with open("app/users.json", 'r+') as f:
|
||||||
|
fake_users_db = load(f)
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
fake_users_db[argv[1]] = {"username": argv[1], "hashed_password": pwd_context.hash(argv[2]),
|
||||||
|
"disabled": False, "servers": argv[3:]}
|
||||||
|
f.seek(0)
|
||||||
|
dump(fake_users_db, f, indent=2)
|
||||||
|
print(fake_users_db)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
10
app/users.json
Normal file → Executable file
10
app/users.json
Normal file → Executable file
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"johndoe": {
|
|
||||||
"username": "johndoe",
|
|
||||||
"full_name": "John Doe",
|
|
||||||
"email": "johndoe@example.com",
|
|
||||||
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
|
||||||
"disabled": "False",
|
|
||||||
"servers": ["a", "b"]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +1,14 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
api:
|
||||||
build: .
|
build: .
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app/app
|
- ./app:/app/app
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./users.json:/app/app/users.json
|
||||||
command: uvicorn app.main:app --host 0.0.0.0 --reload
|
command: uvicorn app.main:app --host 0.0.0.0 --reload
|
@@ -5,10 +5,7 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 8000:80
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app/app
|
- ./app:/app/app
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
command: pytest app/test/test_main.py -s
|
command: pytest app/test/test_main.py -s
|
@@ -1,13 +1,33 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
api:
|
||||||
build: .
|
build: ./
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./users.json:/app/app/users.json
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
labels:
|
||||||
|
caddy: console
|
||||||
|
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||||
|
#caddy.tls: "admin@localhost"
|
||||||
|
caddy.tls: "internal"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
|
||||||
- 8000:80
|
|
||||||
tty: true
|
|
||||||
stdin_open: true
|
|
||||||
|
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: lucaslorentz/caddy-docker-proxy:ci-alpine
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
name: caddy
|
10
users.json.sample
Normal file
10
users.json.sample
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"username": "test",
|
||||||
|
"hashed_password": "$2b$12$VS1k1fdA4x2EeF1a/LMIyex.evEQGF5EsviIcG22S55YO8uUQCT7q",
|
||||||
|
"disabled": false,
|
||||||
|
"servers": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user