Fix + Docker
This commit is contained in:
parent
8aa4828239
commit
0e71fe7abe
10 changed files with 92 additions and 35 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,4 +3,5 @@
|
||||||
API/__pycache__
|
API/__pycache__
|
||||||
API/models/__pycache__
|
API/models/__pycache__
|
||||||
API/routers/__pycache__
|
API/routers/__pycache__
|
||||||
API/schemas/__pycache__
|
API/schemas/__pycache__
|
||||||
|
API/utils/__pycache__
|
||||||
7
API/Dockerfile
Normal file
7
API/Dockerfile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
FROM python:3.14.4-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
@ -9,10 +9,11 @@ class Server(Base):
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
|
||||||
code: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
|
code: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
|
||||||
host: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
host: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
port: Mapped[int] = mapped_column(nullable=False)
|
port: Mapped[int] = mapped_column(nullable=False)
|
||||||
user: Mapped[str] = mapped_column(String(64), nullable=False)
|
user: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
password: Mapped[str] = mapped_column(String(64), nullable=False)
|
password: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
inbound_id: Mapped[int] = mapped_column(nullable=False)
|
inbound_id: Mapped[int] = mapped_column(nullable=False)
|
||||||
|
version: Mapped[str] = mapped_column(String(16), nullable=False, default="stable")
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@ psycopg2-binary
|
||||||
asyncpg
|
asyncpg
|
||||||
passlib
|
passlib
|
||||||
bcrypt==4.3.0
|
bcrypt==4.3.0
|
||||||
httpx
|
httpx
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ async def create_config(body: ConfigCreate, current_user: User = Depends(get_cur
|
||||||
client_email_xui = f"{current_user.email}-{body.name}"
|
client_email_xui = f"{current_user.email}-{body.name}"
|
||||||
display_name = f"SpectralVPN-{body.name}"
|
display_name = f"SpectralVPN-{body.name}"
|
||||||
config_url = await xui.add_client(
|
config_url = await xui.add_client(
|
||||||
inbound_id=server.inbound_id,
|
|
||||||
client_email=client_email_xui,
|
client_email=client_email_xui,
|
||||||
display_name=display_name
|
display_name=display_name
|
||||||
)
|
)
|
||||||
|
|
@ -119,10 +118,10 @@ async def delete_config(body: ConfigDelete, current_user: User = Depends(get_cur
|
||||||
.options(selectinload(User.server))
|
.options(selectinload(User.server))
|
||||||
)
|
)
|
||||||
server = server_result.scalar_one().server
|
server = server_result.scalar_one().server
|
||||||
if server and server.inbound_id:
|
if server:
|
||||||
xui = await XUIClient.from_server(server)
|
xui = await XUIClient.from_server(server)
|
||||||
client_email_xui = f"{current_user.email}-{body.name}"
|
client_email_xui = f"{current_user.email}-{body.name}"
|
||||||
await xui.delete_client(server.inbound_id, client_email_xui)
|
await xui.delete_client(client_email_xui)
|
||||||
except:
|
except:
|
||||||
HTTPException(
|
HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,26 @@ router = APIRouter(prefix="/server")
|
||||||
|
|
||||||
@router.post("/add", status_code=201)
|
@router.post("/add", status_code=201)
|
||||||
async def add_server(body: ServerAdd, request: Request, db: AsyncSession = Depends(get_db)):
|
async def add_server(body: ServerAdd, request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
ip = request.client.host
|
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(Server).where(
|
select(Server).where(
|
||||||
(Server.name == body.name) |
|
(Server.name == body.name) |
|
||||||
(Server.code == body.code) |
|
(Server.code == body.code)
|
||||||
(Server.host == ip)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if existing.scalar_one_or_none():
|
if existing.scalar_one_or_none():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail="Name, ip or code is already exists"
|
detail="Nameor code is already exists"
|
||||||
)
|
)
|
||||||
new_server = Server(
|
new_server = Server(
|
||||||
name=body.name,
|
name=body.name,
|
||||||
code=body.code,
|
code=body.code,
|
||||||
host=ip,
|
host=body.host,
|
||||||
port=body.port,
|
port=body.port,
|
||||||
user=body.user,
|
user=body.user,
|
||||||
password=body.password,
|
password=body.password,
|
||||||
inbound_id=body.inbound_id
|
inbound_id=body.inbound_id,
|
||||||
|
version=body.version
|
||||||
)
|
)
|
||||||
db.add(new_server)
|
db.add(new_server)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
class ServerBase(BaseModel):
|
class ServerBase(BaseModel):
|
||||||
|
|
@ -7,9 +8,11 @@ class ServerBase(BaseModel):
|
||||||
password: str = Field(max_length=64)
|
password: str = Field(max_length=64)
|
||||||
|
|
||||||
class ServerAdd(ServerBase):
|
class ServerAdd(ServerBase):
|
||||||
|
host: str = Field(max_length=128)
|
||||||
port: int = Field(ge=1, le=65536)
|
port: int = Field(ge=1, le=65536)
|
||||||
code: str = Field(max_length=128)
|
code: str = Field(max_length=128)
|
||||||
inbound_id: int
|
inbound_id: int
|
||||||
|
version: Literal["legacy", "stable"]
|
||||||
|
|
||||||
class ServerDel(ServerBase):
|
class ServerDel(ServerBase):
|
||||||
pass
|
pass
|
||||||
|
|
@ -19,3 +22,4 @@ class ServerInfo(BaseModel):
|
||||||
name: str = Field(max_length=64)
|
name: str = Field(max_length=64)
|
||||||
code: str
|
code: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
version: str
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ from typing import AsyncGenerator
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker,AsyncEngine
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker,AsyncEngine
|
||||||
|
|
||||||
engine: AsyncEngine = create_async_engine(
|
engine: AsyncEngine = create_async_engine(
|
||||||
#Todo Изменить на env, плдключение к db
|
f"postgresql+asyncpg://{getenv("DB_USER")}:{getenv("DB_PASSWORD")}@db:5432/{getenv("DB_NAME")}",
|
||||||
f"postgresql+asyncpg://spectral:spectral@localhost:5432/spectral",
|
|
||||||
echo=True, #Todo удалить
|
|
||||||
pool_pre_ping=True
|
pool_pre_ping=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import random
|
import random
|
||||||
|
from urllib.parse import quote
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
class XUIClient:
|
class XUIClient:
|
||||||
def __init__(self, base_url: str, username: str, password: str):
|
def __init__(self, host: str, base_url: str, username: str, password: str, inbound_id: int, version: str):
|
||||||
|
self.host = host
|
||||||
self.base_url = base_url.rstrip("/")
|
self.base_url = base_url.rstrip("/")
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.inbound_id = inbound_id
|
||||||
self.session = AsyncClient(timeout=20.0)
|
self.session = AsyncClient(timeout=20.0)
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
|
self.version = version
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_server(cls, server):
|
async def from_server(cls, server):
|
||||||
#TODO заменить на https
|
#TODO заменить на https
|
||||||
base_url = f"http://{server.host}:{server.port}"
|
base_url = f"http://{server.host}:{server.port}"
|
||||||
return cls(base_url, server.user, server.password)
|
return cls(server.host, base_url, server.user, server.password, server.inbound_id, server.version)
|
||||||
|
|
||||||
async def _login(self):
|
async def _login(self):
|
||||||
if self.logged_in:
|
if self.logged_in:
|
||||||
|
|
@ -34,28 +38,28 @@ class XUIClient:
|
||||||
detail="Server error"
|
detail="Server error"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_client(self, inbound_id: int, client_email: str, display_name: str) -> str:
|
async def add_client(self, client_email: str, display_name: str) -> str:
|
||||||
await self._login()
|
await self._login()
|
||||||
resp = await self.session.post(f"{self.base_url}/panel/inbound/list")
|
resp = await self.session.post(f"{self.base_url}/panel/inbound/list")
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
inbounds = data.get("obj", [])
|
inbounds = data.get("obj")
|
||||||
inbound = next((i for i in inbounds if i.get("id") == inbound_id), None)
|
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
||||||
if not inbound:
|
if not inbound:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail=f"Server not found"
|
detail=f"Server not found"
|
||||||
)
|
)
|
||||||
stream_settings = json.loads(inbound.get("streamSettings", "{}"))
|
stream_settings = json.loads(inbound.get("streamSettings"))
|
||||||
reality = stream_settings.get("realitySettings", {})
|
reality = stream_settings.get("realitySettings")
|
||||||
short_ids = reality.get("shortIds", [""])
|
short_ids = reality.get("shortIds")
|
||||||
server_names = reality.get("serverNames", {})
|
server_names = reality.get("serverNames")
|
||||||
suid = random.choice(short_ids)
|
suid = random.choice(short_ids)
|
||||||
sni = random.choice(server_names)
|
sni = random.choice(server_names)
|
||||||
pbk = reality.get("settings", {}).get("publicKey", "")
|
pbk = reality.get("settings").get("publicKey")
|
||||||
client_uuid = str(uuid.uuid4())
|
client_uuid = str(uuid.uuid4())
|
||||||
add_payload = {
|
add_payload = {
|
||||||
"id": inbound_id,
|
"id": self.inbound_id,
|
||||||
"settings": json.dumps({
|
"settings": json.dumps({
|
||||||
"clients": [{
|
"clients": [{
|
||||||
"id": client_uuid,
|
"id": client_uuid,
|
||||||
|
|
@ -74,15 +78,43 @@ class XUIClient:
|
||||||
}
|
}
|
||||||
resp = await self.session.post(f"{self.base_url}/panel/inbound/addClient", json=add_payload)
|
resp = await self.session.post(f"{self.base_url}/panel/inbound/addClient", json=add_payload)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
host = inbound.get("remark")
|
resp = await self.session.post(f"{self.base_url}/panel/inbound/list")
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
inbounds = data.get("obj")
|
||||||
|
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
||||||
|
if not inbound:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server error"
|
||||||
|
)
|
||||||
|
settings = json.loads(inbound.get("settings"))
|
||||||
|
clients = settings.get("clients")
|
||||||
|
current_client = next((i for i in clients if i.get("email") == client_email), None)
|
||||||
|
if not current_client:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server error"
|
||||||
|
)
|
||||||
|
client_uuid = current_client.get("id")
|
||||||
|
sub_id = current_client.get("subId")
|
||||||
|
stream_settings = json.loads(inbound.get("streamSettings"))
|
||||||
|
reality = stream_settings.get("realitySettings")
|
||||||
|
server_names = reality.get("serverNames")
|
||||||
|
setting = reality.get("settings")
|
||||||
|
pbk = setting.get("publicKey")
|
||||||
|
fp = setting.get("fingerprint")
|
||||||
|
spx = quote(setting.get("spiderX"), safe='')
|
||||||
|
sni = random.choice(server_names)
|
||||||
|
port = inbound.get("port")
|
||||||
config_url = (
|
config_url = (
|
||||||
f"vless://{client_uuid}@{host}?"
|
f"vless://{client_uuid}@{self.host}:{port}?"
|
||||||
f"security=reality&"
|
f"security=reality&"
|
||||||
f"pbk={pbk}&"
|
f"pbk={pbk}&"
|
||||||
f"fp=random&"
|
f"fp={fp}&"
|
||||||
f"sni={sni}&"
|
f"sni={sni}&"
|
||||||
f"sid={suid}&"
|
f"sid={sub_id}&"
|
||||||
f"spx=%2F&"
|
f"spx={spx}&"
|
||||||
f"flow=xtls-rprx-vision#"
|
f"flow=xtls-rprx-vision#"
|
||||||
f"{display_name}"
|
f"{display_name}"
|
||||||
)
|
)
|
||||||
|
|
@ -101,11 +133,11 @@ class XUIClient:
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def delete_client(self, inbound_id: int, client_email: str):
|
async def delete_client(self, client_email: str):
|
||||||
await self._login()
|
await self._login()
|
||||||
try:
|
try:
|
||||||
resp = await self.session.post(
|
resp = await self.session.post(
|
||||||
f"{self.base_url}/panel/inbound/{inbound_id}/delClientByEmail/{client_email}"
|
f"{self.base_url}/panel/inbound/{self.inbound_id}/delClientByEmail/{client_email}"
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
services:
|
services:
|
||||||
#api:
|
api:
|
||||||
# Todo
|
build:
|
||||||
|
context: ./API
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: spectralvpn-api
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
extra_hosts:
|
||||||
|
- host.docker.internal:172.17.0.1
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
|
||||||
#web:
|
#web:
|
||||||
# Todo
|
# Todo
|
||||||
db:
|
db:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue