pve9-repo-fix/RDP/rdp-toggle-api.py
thug0bin c6919abf1c Add RDP Toggle API with venv support
RDP 관련 파일들을 RDP 폴더로 정리하고 API 시스템 추가

주요 변경사항:
- FastAPI 기반 RDP/Shell 모드 전환 API 서버 추가
- venv 환경을 사용하는 자동 설치 스크립트
- requirements.txt로 패키지 의존성 관리
- systemd 서비스로 자동 시작 설정
- CORS 지원으로 외부 프론트엔드 연동 가능
- 실시간 상태 모니터링 API
- 웹 기반 컨트롤 패널 포함

파일 구성:
- rdp-toggle-api.py: FastAPI REST API 서버
- install-rdp-api.sh: venv 환경 자동 설치
- requirements.txt: Python 패키지 의존성
- rdp-toggle-web.html: 웹 컨트롤 패널
- README.md: 사용 가이드

API 기능:
- GET /status: 현재 모드 확인
- POST /toggle: RDP/Shell 모드 전환
- GET /config: 설정 확인
- PUT /config: 설정 업데이트

리액트 프론트엔드에서 토글로 화면 모드 제어 가능

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 09:14:41 +09:00

276 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
RDP Toggle API Server
Control RDP/Shell display mode via REST API
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import subprocess
import os
import json
from typing import Optional
from datetime import datetime
import uvicorn
app = FastAPI(title="RDP Toggle API", version="1.0.0")
# CORS 설정 (외부에서 접근 가능)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 상태 저장 파일
STATE_FILE = "/var/lib/rdp-toggle/state.json"
CONFIG_FILE = "/var/lib/rdp-toggle/config.json"
# 기본 설정
DEFAULT_CONFIG = {
"rdp_server": "192.168.0.229",
"rdp_username": "0bin",
"rdp_password": "trajet6640",
"local_user": "rdpuser"
}
class ToggleRequest(BaseModel):
mode: str # "rdp" or "shell"
class ConfigUpdate(BaseModel):
rdp_server: Optional[str] = None
rdp_username: Optional[str] = None
rdp_password: Optional[str] = None
local_user: Optional[str] = None
class StatusResponse(BaseModel):
current_mode: str
rdp_active: bool
last_changed: str
config: dict
def ensure_directories():
"""필요한 디렉토리 생성"""
os.makedirs("/var/lib/rdp-toggle", exist_ok=True)
def load_state():
"""현재 상태 로드"""
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
return json.load(f)
return {
"current_mode": "shell",
"rdp_active": False,
"last_changed": datetime.now().isoformat()
}
def save_state(state):
"""상태 저장"""
ensure_directories()
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
def load_config():
"""설정 로드"""
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
return DEFAULT_CONFIG
def save_config(config):
"""설정 저장"""
ensure_directories()
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f)
def enable_rdp():
"""RDP 모드 활성화"""
config = load_config()
# 자동 로그인 설정
subprocess.run([
"mkdir", "-p", "/etc/systemd/system/getty@tty1.service.d"
], check=False)
override_content = f"""[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin {config['local_user']} --noclear %I $TERM
Type=idle"""
with open("/etc/systemd/system/getty@tty1.service.d/override.conf", "w") as f:
f.write(override_content)
# X 자동 시작 스크립트
bash_profile = f"""if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
startx
logout
fi"""
user_home = f"/home/{config['local_user']}"
with open(f"{user_home}/.bash_profile", "w") as f:
f.write(bash_profile)
# .xinitrc 업데이트
xinitrc_content = f"""#!/bin/bash
xset -dpms
xset s off
xset s noblank
unclutter -idle 0.1 -root &
openbox-session &
sleep 2
xfreerdp3 /v:{config['rdp_server']} /u:{config['rdp_username']} /p:"{config['rdp_password']}" +f /cert:ignore +dynamic-resolution /sound:sys:alsa +clipboard
pkill -SIGTERM Xorg"""
with open(f"{user_home}/.xinitrc", "w") as f:
f.write(xinitrc_content)
subprocess.run(["chmod", "+x", f"{user_home}/.xinitrc"])
subprocess.run(["chown", f"{config['local_user']}:{config['local_user']}",
f"{user_home}/.bash_profile", f"{user_home}/.xinitrc"])
# systemd 리로드 및 getty 재시작
subprocess.run(["systemctl", "daemon-reload"])
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
return True
def disable_rdp():
"""Shell 모드로 전환 (RDP 비활성화)"""
config = load_config()
# RDP 프로세스 종료
subprocess.run(["pkill", "-u", config['local_user'], "xfreerdp3"], check=False)
subprocess.run(["pkill", "-u", config['local_user'], "-f", "xinit|Xorg|openbox"], check=False)
# 자동 로그인 설정 제거
subprocess.run(["rm", "-f", "/etc/systemd/system/getty@tty1.service.d/override.conf"], check=False)
# 자동 시작 스크립트 제거
user_home = f"/home/{config['local_user']}"
subprocess.run(["rm", "-f", f"{user_home}/.bash_profile"], check=False)
# systemd 리로드 및 getty 재시작
subprocess.run(["systemctl", "daemon-reload"])
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
# TTY1으로 전환
subprocess.run(["chvt", "1"], check=False)
return True
@app.get("/")
async def root():
"""API 정보"""
return {
"name": "RDP Toggle API",
"version": "1.0.0",
"endpoints": {
"GET /status": "현재 상태 확인",
"POST /toggle": "모드 전환 (rdp/shell)",
"GET /config": "현재 설정 확인",
"PUT /config": "설정 업데이트"
}
}
@app.get("/status", response_model=StatusResponse)
async def get_status():
"""현재 상태 반환"""
state = load_state()
config = load_config()
# 실제 프로세스 확인
try:
result = subprocess.run(
["pgrep", "-f", "xfreerdp3"],
capture_output=True,
text=True
)
rdp_running = result.returncode == 0
state["rdp_active"] = rdp_running
state["current_mode"] = "rdp" if rdp_running else "shell"
except:
pass
return StatusResponse(
current_mode=state["current_mode"],
rdp_active=state["rdp_active"],
last_changed=state["last_changed"],
config=config
)
@app.post("/toggle")
async def toggle_mode(request: ToggleRequest):
"""모드 전환"""
if request.mode not in ["rdp", "shell"]:
raise HTTPException(status_code=400, detail="Mode must be 'rdp' or 'shell'")
state = load_state()
try:
if request.mode == "rdp":
success = enable_rdp()
if success:
state["current_mode"] = "rdp"
state["rdp_active"] = True
else: # shell
success = disable_rdp()
if success:
state["current_mode"] = "shell"
state["rdp_active"] = False
state["last_changed"] = datetime.now().isoformat()
save_state(state)
return {
"status": "success",
"mode": request.mode,
"message": f"Switched to {request.mode} mode"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/config")
async def get_config():
"""현재 설정 반환"""
return load_config()
@app.put("/config")
async def update_config(update: ConfigUpdate):
"""설정 업데이트"""
config = load_config()
if update.rdp_server:
config["rdp_server"] = update.rdp_server
if update.rdp_username:
config["rdp_username"] = update.rdp_username
if update.rdp_password is not None:
config["rdp_password"] = update.rdp_password
if update.local_user:
config["local_user"] = update.local_user
save_config(config)
# 현재 RDP 모드인 경우 재시작
state = load_state()
if state["rdp_active"]:
disable_rdp()
enable_rdp()
return {
"status": "success",
"config": config
}
if __name__ == "__main__":
# Root 권한 확인
if os.geteuid() != 0:
print("This script must be run as root")
exit(1)
ensure_directories()
# 서버 시작
uvicorn.run(app, host="0.0.0.0", port=8090)