Compare commits

...

21 Commits

Author SHA1 Message Date
Jimmy 0c2af63888 Implement logs and stats 2022-01-17 20:51:13 +13:00
Jimmy 18ef42023f Change test user 2022-01-17 20:50:32 +13:00
Jimmy 49a49f7ea6 Use server side events 2022-01-17 05:27:26 +00:00
Jimmy d8b8c0f801 Add cors midleware 2022-01-17 05:26:59 +00:00
Jimmy 9cdc0ad923 Add dependencies 2022-01-17 05:26:27 +00:00
Jimmy af0954b6ff Add dependencies and use uvicorn 2022-01-17 05:25:02 +00:00
Jimmy 6df31460e8 Add production file 2022-01-17 05:23:16 +00:00
Jimmy b00e0faedc Remove ports, tty and stdin_open 2022-01-16 07:10:06 +00:00
Jimmy 02097426ba Add user creation script 2022-01-16 07:09:23 +00:00
Jimmy bd6afffd77 Ignore users.json 2022-01-16 07:07:55 +00:00
Jimmy f2e4478edc Move and rename 2022-01-16 07:06:45 +00:00
Jimmy 3a5716a2f9 Remove requirements.txt 2022-01-16 07:05:00 +00:00
Jimmy 26490887ee Add users, docker.sock 2022-01-16 07:04:39 +00:00
Jimmy 9a254575f2 Add pip requirements 2022-01-15 07:35:11 +00:00
Jimmy db218cb8c2 Add server test 2022-01-13 03:13:04 +00:00
Jimmy fd5fc0a85d Remove items 2022-01-13 03:12:19 +00:00
Jimmy 7307abf7d8 Add enva to disable auth 2022-01-13 03:11:59 +00:00
Jimmy b6f41f4d0a Rename 2022-01-13 03:11:02 +00:00
Jimmy 759bab3ff3 Initial version 2022-01-12 07:48:18 +00:00
Jimmy d09c207c97 Add servers 2022-01-11 04:18:26 +00:00
Jimmy e5b2fb06f3 Update to 3.9 2022-01-11 04:18:03 +00:00
17 changed files with 239 additions and 71 deletions

3
.gitignore vendored
View File

@ -4,7 +4,8 @@ __pycache__/
*.py[cod]
*$py.class
requirements.txt
users.json
app/users.json
# C extensions
*.so

View File

@ -1,6 +1,14 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-slim
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim
ENV DOCKER=1
COPY requirements.txt /app/requirements.txt
RUN pip install -r /app/requirements.txt
COPY ./app /app/app
RUN pip install \
python-jose \
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"]

View File

@ -10,6 +10,9 @@ uvicorn = {extras = ["standard"], version = "*"}
python-jose = {extras = ["cryptography"], version = "*"}
passlib = "*"
python-multipart = "*"
docker = "*"
aiodocker = "*"
sse-starlette = "*"
[dev-packages]
pytest = "*"
@ -19,8 +22,8 @@ black = "*"
mypy = "*"
[requires]
python_version = "3.8"
python_version = "3.9"
[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"

View File

@ -3,11 +3,14 @@ import json
from datetime import timedelta, datetime
from typing import Optional
from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, status, APIRouter
from jose import JWTError, jwt
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:
# 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)):
if current_user.disabled:
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
View File

View File

@ -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
from fastapi.security import OAuth2PasswordRequestForm
from app.models import Token, User
from app.auth import get_current_active_user, create_access_token, authenticate_user, fake_users_db, ACCESS_TOKEN_EXPIRE_MINUTES
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}]
dependencies = list()
if not getenv('DISABLE_AUTH'):
dependencies.append(Depends(auth.authorise))
app.include_router(auth.router)
app.include_router(user.router)
app.include_router(server.router, dependencies=dependencies)

View File

@ -12,9 +12,9 @@ class TokenData(BaseModel):
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
servers: list
class UserInDB(User):
hashed_password: str

76
app/server.py Normal file
View 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)

View File

@ -8,7 +8,8 @@
from fastapi.testclient import TestClient
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)

21
app/test/test_server.py Normal file
View 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
View 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
View 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)

9
app/users.json Normal file → Executable file
View File

@ -1,9 +0,0 @@
{
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": "False"
}
}

View File

@ -1,14 +1,14 @@
version: '3.7'
services:
app:
api:
build: .
env_file:
- .env
ports:
- 8000:8000
tty: true
stdin_open: true
volumes:
- ./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

View File

@ -5,10 +5,7 @@ services:
build: .
env_file:
- .env
ports:
- 8000:80
tty: true
stdin_open: true
volumes:
- ./app:/app/app
- /var/run/docker.sock:/var/run/docker.sock
command: pytest app/test/test_main.py -s

View File

@ -1,13 +1,33 @@
version: '3.7'
services:
app:
build: .
services:
api:
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
ports:
- 8000:80
tty: true
stdin_open: true
- .env
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
View File

@ -0,0 +1,10 @@
{
"test": {
"username": "test",
"hashed_password": "$2b$12$VS1k1fdA4x2EeF1a/LMIyex.evEQGF5EsviIcG22S55YO8uUQCT7q",
"disabled": false,
"servers": [
"test"
]
}
}