diff --git a/.gitignore b/.gitignore index 3ed3100..3cd9bba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ tmp .vscode .DS_Store -*.html *yml *.log *.sh diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..cd0d007 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1 @@ +*.sh \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..fdf1331 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-alpine + +RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone && apk add ca-certificates + +RUN apk add --update --no-cache python3 py3-pip nginx \ + && mkdir -p /run/nginx \ + && rm -rf /var/cache/apk/* + +WORKDIR /app + +COPY ./requirements.txt /app/requirements.txt + +RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple \ + && pip config set global.trusted-host mirrors.cloud.tencent.com \ + && pip install --upgrade pip --break-system-packages \ + && pip install --user -r requirements.txt --break-system-packages + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY . /app + +CMD ["sh", "-c", "python3 app.py & nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/web/Dockerfile.zh b/web/Dockerfile.zh new file mode 100644 index 0000000..1956c4c --- /dev/null +++ b/web/Dockerfile.zh @@ -0,0 +1,23 @@ +FROM docker.proxy.yangrucheng.top/python:3.12-alpine + +RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone && apk add ca-certificates + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories \ + && apk add --update --no-cache python3 py3-pip nginx \ + && mkdir -p /run/nginx \ + && rm -rf /var/cache/apk/* + +WORKDIR /app + +COPY ./requirements.txt /app/requirements.txt + +RUN pip config set global.index-url http://mirrors.cloud.tencent.com/pypi/simple \ + && pip config set global.trusted-host mirrors.cloud.tencent.com \ + && pip install --upgrade pip --break-system-packages \ + && pip install --user -r requirements.txt --break-system-packages + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY . /app + +CMD ["sh", "-c", "python3 app.py & nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..207ee3c --- /dev/null +++ b/web/README.md @@ -0,0 +1,3 @@ +# 小程序的网页入口 + +> 为了减少小程序被封风险而添加的网页入口,没有任何功能 \ No newline at end of file diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..2df0795 --- /dev/null +++ b/web/app.py @@ -0,0 +1,140 @@ + +from fastapi.responses import JSONResponse, RedirectResponse, Response +from fastapi import FastAPI, Body, APIRouter, Request +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException +from fastapi.staticfiles import StaticFiles +from contextlib import asynccontextmanager +import datetime +import uvicorn +import socket +import httpx + + +@asynccontextmanager +async def lifespan(app: FastAPI): + yield + +app = FastAPI(lifespan=lifespan, redoc_url=None, docs_url=None) + + +@app.get("/api/login") +async def login(request: Request, username: str, password: str): + """ 登录 """ + async with httpx.AsyncClient() as client: + resp = await client.get("https://passport2-api.chaoxing.com/v11/loginregister", params={ + "cx_xxt_passport": "json", + "roleSelect": "true", + "uname": username, + "code": password, + "loginType": "1", + }) + resp2 = JSONResponse(resp.json()) + for key, value in resp.cookies.items(): + resp2.set_cookie(key, value, max_age=3600*24*7) + return resp2 + + +@app.get("/api/get_courses") +async def get_courses(request: Request): + """ 获取课程列表 """ + async with httpx.AsyncClient(cookies=dict(request.cookies)) as client: + resp = await client.get("https://mooc1-api.chaoxing.com/mycourse/backclazzdata", params={ + 'view': 'json', + 'rss': '1', + }) + return JSONResponse(resp.json()) + + +app.mount("/", StaticFiles(directory="public"), name="public") + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + if exc.status_code == 404: + return RedirectResponse("/index.html", status_code=302) + else: + return JSONResponse({ + "status": -1, + "msg": exc.detail, + "data": None, + "time": int(datetime.datetime.now().timestamp()), + }, status_code=exc.status_code) + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse({ + "status": -1, + "msg": "参数错误, 请检查参数", + "data": { + "body": exc.body, + "query": { + "raw": str(request.query_params), + "parsed": dict(request.query_params), + }, + "error": exc.errors(), + }, + "time": int(datetime.datetime.now().timestamp()), + }, status_code=422) + + +@app.exception_handler(Exception) +async def exception_handler(request: Request, exc: Exception): + return JSONResponse({ + "status": -1, + "msg": "服务器内部错误, 请联系管理员! 邮箱: admin@misaka-network.top", + "time": int(datetime.datetime.now().timestamp()), + }, status_code=500) + + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + starttime = datetime.datetime.now() + response: Response = await call_next(request) + endtime = datetime.datetime.now() + response.headers["X-Process-Time"] = str( + int((endtime - starttime).total_seconds())) + response.headers["X-Client-Host"] = request.client.host + return response + + +def get_localhost(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + except Exception as e: + return None + else: + return ip + finally: + s.close() + + +if __name__ == "__main__": + starttime = datetime.datetime.now().strftime(r"%Y-%m-%d %H:%M:%S") + from utils.logger import set_log_formatter + set_log_formatter() + import logging as lg + logging = lg.getLogger("uvicorn") + try: + logging.info(f"服务已启动, 请访问 http://{get_localhost()}:8080") + uvicorn.run( + app="app:app", + host="0.0.0.0", + port=8080, + reload=False, + forwarded_allow_ips="*", + log_config=None, + workers=1, + headers=[ + ("Server", "Misaka Network Distributed Server"), + ("X-Powered-By", "Misaka Network Studio"), + ("X-Statement", "This service is provided by Misaka Network Studio. For complaints/cooperation, please email admin@misaka-network.top"), + ("X-Copyright", "© 2024 Misaka Network Studio. All rights reserved."), + ("X-Server-Start-Time", starttime), + ], + ) + except KeyboardInterrupt: + logging.info("Ctrl+C 终止服务") diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..d38faaf --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,42 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/null main; + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; + + server { + listen 80; + server_name localhost; + + location / { + root /app/public; + index index.html; + } + + location /api { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..2c96a16 Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..6dafdd4 --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,265 @@ + + + +
+ + + +