initial commit

This commit is contained in:
Anonymous 2024-02-24 10:09:14 +08:00
commit 79c1cdde2f
23 changed files with 1210 additions and 0 deletions

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
# for caddy
CLOUDFLARE_DNS_API_TOKEN=xxxx
# for caddy and mirrors
BASE_DOMAIN=local.homeinfra.org
# for aria2 and mirrors
RPC_SECRET=changeit
# for all, if you want set proxy for all services
# aria2 need the lowercase one, FYI https://aria2.github.io/manual/en/html/aria2c.html#environment
# all_proxy=

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
data/*
!data/cache/.gitkeep
caddy/caddy_data
.idea
.env
node_modules

63
README.md Normal file
View File

@ -0,0 +1,63 @@
<div style="text-align: center">
# LightMirrors
LightMirrors是一个开源的缓存镜像站服务用于加速软件包下载和镜像拉取。
目前支持DockerHub、PyPI、PyTorch、NPM等镜像缓存服务。
<a href='https://github.com/NoCLin/LightMirrors/'><img src='https://img.shields.io/badge/Light-Mirrors-green'></a>
<a href='https://github.com/homeinfra-org/infra'><img src='https://img.shields.io/static/v1?label=Home&message=Infra&color=orange'></a>
[![GitHub](https://img.shields.io/github/stars/NoCLin/LightMirrors?style=social)](https://github.com/NoCLin/LightMirrors)
[![GitHub](https://img.shields.io/github/forks/NoCLin/LightMirrors?style=social)](https://github.com/NoCLin/LightMirrors)
</div>
---
## Quick Start
### Prerequisites
- docker + docker-compose.
- 一个域名,设置 `*.local.homeinfra.org` 的A记录指向您的服务器.
- 代理服务器(如有必要).
- 一个Cloudflare账户非强制也可以使用其他DNS服务请自行修改Caddy
### Deployment
修改 `.env` 文件,设置下列参数:
- `BASE_DOMAIN`: 基础域名,如 `local.homeinfra.org`,镜像站将会使用 `*.local.homeinfra.org` 的子域名。
- `CLOUDFLARE_DNS_API_TOKEN`Cloudflare的API Token用于管理DNS申请HTTPS证书。
- `RPC_SECRET`Aria2的RPC密钥。
- `all_proxy`:代理服务器地址,如有必要。
```bash
docker-compose up
```
## Design
LightMirrors依赖于三个组件
- aria2 + Aria2Ng : 下载器与管理UI。
- mirrors: 镜像HTTP服务器。
- caddy: HTTP网关。
## Test
> 假设我们的域名为 local.homeinfra.org
| subdomain | source | test command |
|-----------|---------------------------------|-----------------------------------------------------------------|
| pypi | https://pypi.org | pip3 download -i https://pypi.local.homeinfra.org/simple jinja2 |
| torch | https://download.pytorch.org | pip3 download -i https://pypi.local.homeinfra.org/whl/ torch |
| dockerhub | https://registry-1.docker.io/v2 | docker pull docker.local.homeinfra.org/alpine |
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=NoCLin/LightMirrors&type=Date)](https://star-history.com/#NoCLin/LightMirrors&Date)

3
ROADMAP.md Normal file
View File

@ -0,0 +1,3 @@
- Test
- Publish to ghcr and dockerhub
- Configurable upstreams

13
aria2/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM alpine:3.19.1
# Optimization for China
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk update && apk add --no-cache aria2
ADD entrypoint.sh /
ADD aria2.conf /
EXPOSE 6800
CMD ["/entrypoint.sh"]

54
aria2/aria2.conf Normal file
View File

@ -0,0 +1,54 @@
dir=/app/cache
disk-cache=64M
file-allocation=none
no-file-allocation-limit=64M
continue=true
always-resume=false
max-resume-failure-tries=0
remote-time=true
input-file=/data/aria2.session
save-session=/data/aria2.session
save-session-interval=1
auto-save-interval=20
# remove .aria2 when download is done
# we leverage this to detech if download is done
force-save=false
max-file-not-found=3
max-tries=0
retry-wait=10
connect-timeout=10
timeout=10
max-concurrent-downloads=5
max-connection-per-server=16
split=64
min-split-size=4M
piece-length=1M
allow-piece-length-change=true
lowest-speed-limit=0
max-overall-download-limit=0
max-download-limit=0
disable-ipv6=true
http-accept-gzip=true
reuse-uri=false
no-netrc=true
allow-overwrite=true
auto-file-renaming=false
content-disposition-default-utf8=true
enable-rpc=true
rpc-allow-origin-all=false
rpc-listen-all=true
rpc-listen-port=6800
rpc-max-request-size=10M
log=-
log-level=warn
console-log-level=notice
quiet=false
summary-interval=0

4
aria2/entrypoint.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
touch /data/aria2.session
aria2c --conf-path=/aria2.conf --rpc-secret $RPC_SECRET

15
caddy/Caddyfile Normal file
View File

@ -0,0 +1,15 @@
*.{$BASE_DOMAIN} {
tls {
dns cloudflare {env.CLOUDFLARE_DNS_API_TOKEN}
}
reverse_proxy http://lightmirrors:8080
@aria2 host aria2.{$BASE_DOMAIN}
handle @aria2 {
root * /wwwroot
file_server
reverse_proxy /jsonrpc http://aria2:6800
}
}

23
caddy/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM caddy:2.7.6-builder-alpine AS builder
# Optimization for China
RUN go env -w GOPROXY=https://goproxy.cn,direct
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:2.7.6-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
RUN mkdir -p /wwwroot
WORKDIR /wwwroot
# Optimization for China as the project is aimed at Chinese users
ADD https://hub.gitmirror.com/https://github.com/mayswind/AriaNg/releases/download/1.3.7/AriaNg-1.3.7.zip /wwwroot/
RUN unzip AriaNg-1.3.7.zip && rm AriaNg-1.3.7.zip
ENTRYPOINT ["caddy"]
CMD ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

44
docker-compose.yml Normal file
View File

@ -0,0 +1,44 @@
services:
lightmirrors:
image: lightmirrors/mirrors
build: ./mirrors
volumes:
- ./mirrors:/app
- ./data/cache:/app/cache
env_file:
- .env
networks:
- app
restart: unless-stopped
# for linux
extra_hosts:
- "host.docker.internal:host-gateway"
caddy:
image: lightmirrors/caddy
build: ./caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/caddy_data:/data/caddy
env_file:
- .env
networks:
- app
restart: unless-stopped
aria2:
image: lightmirrors/aria2
build: ./aria2
volumes:
- ./aria2/aria2.conf:/aria2.conf
- ./data/cache:/app/cache
- ./data/aria2:/data/
networks:
- app
env_file:
- .env
restart: unless-stopped
networks:
app:
driver: bridge

13
mirrors/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM python:3.11-alpine
ADD requirements.txt /app/requirements.txt
RUN pip install -r /app/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
WORKDIR /app
EXPOSE 8080
CMD ["python", "server.py"]

67
mirrors/aria2_api.py Normal file
View File

@ -0,0 +1,67 @@
import json
import logging
import uuid
import httpx
from config import RPC_SECRET, ARIA2_RPC_URL
logger = logging.getLogger(__name__)
async def send_request(method, params=None):
payload = {
'jsonrpc': '2.0',
'id': uuid.uuid4().hex,
'method': method,
'params': [f'token:{RPC_SECRET}'] + (params or [])
}
# specify the internal API call don't use proxy
async with httpx.AsyncClient(mounts={
"all://": httpx.AsyncHTTPTransport()
}) as client:
response = await client.post(ARIA2_RPC_URL, json=payload)
logger.info(f"aria2 request: {method} {params} -> {response.status_code} {response.text}")
try:
return response.json()
except json.JSONDecodeError as e:
logger.warning(f"aria2 response: {response.status_code} {response.text}")
raise e
async def add_download(url, save_dir='/app/cache'):
method = 'aria2.addUri'
params = [[url],
{'dir': save_dir,
'header': []
}]
response = await send_request(method, params)
return response['result']
async def pause_download(gid):
method = 'aria2.pause'
params = [gid]
response = await send_request(method, params)
return response['result']
async def resume_download(gid):
method = 'aria2.unpause'
params = [gid]
response = await send_request(method, params)
return response['result']
async def get_status(gid):
method = 'aria2.tellStatus'
params = [gid]
response = await send_request(method, params)
return response['result']
async def list_downloads():
method = 'aria2.tellActive'
response = await send_request(method)
return response['result']

11
mirrors/config.py Normal file
View File

@ -0,0 +1,11 @@
import os
ARIA2_RPC_URL = os.environ.get("ARIA2_RPC_URL", 'http://aria2:6800/jsonrpc')
RPC_SECRET = os.environ.get("RPC_SECRET", '')
BASE_DOMAIN = os.environ.get("BASE_DOMAIN", '127.0.0.1.nip.io')
PROXY = os.environ.get("PROXY", None)
CACHE_DIR = os.environ.get("CACHE_DIR", "/app/cache/")
EXTERNAL_HOST_ARIA2 = f"aria2." + BASE_DOMAIN
EXTERNAL_URL_ARIA2 = f"https://" + EXTERNAL_HOST_ARIA2

344
mirrors/poetry.lock generated Normal file
View File

@ -0,0 +1,344 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.6.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
files = [
{file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
]
[[package]]
name = "anyio"
version = "4.2.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"]
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.109.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"},
{file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.35.0,<0.36.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "1.0.2"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
version = "0.26.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
[[package]]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "pydantic"
version = "2.6.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"},
{file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.16.1"
typing-extensions = ">=4.6.1"
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.16.1"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"},
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"},
{file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"},
{file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"},
{file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"},
{file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"},
{file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"},
{file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"},
{file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"},
{file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"},
{file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"},
{file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"},
{file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"},
{file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"},
{file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "starlette"
version = "0.35.1"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
{file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"},
{file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"},
]
[package.dependencies]
anyio = ">=3.4.0,<5"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
[[package]]
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[[package]]
name = "uvicorn"
version = "0.27.0.post1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"},
{file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "3f85c224110f9432dab85aaffc90d271b06475dfb7ab97e89d8817d2afeab223"

107
mirrors/proxy/cached.py Normal file
View File

@ -0,0 +1,107 @@
import logging
import os
import pathlib
import typing
from asyncio import sleep
from enum import Enum
from urllib.parse import urlparse, quote
import httpx
from starlette.requests import Request
from starlette.responses import Response
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR, HTTP_504_GATEWAY_TIMEOUT
from aria2_api import add_download
from config import CACHE_DIR, EXTERNAL_URL_ARIA2, PROXY
logger = logging.getLogger(__name__)
def get_cache_file_and_folder(url: str) -> typing.Tuple[str, str]:
parsed_url = urlparse(url)
base_dir = pathlib.Path(CACHE_DIR)
assert parsed_url.path[0] == "/"
assert parsed_url.path[-1] != "/"
cache_file = (base_dir / parsed_url.hostname / parsed_url.path[1:]).resolve()
assert cache_file.is_relative_to(base_dir)
return str(cache_file), os.path.dirname(cache_file)
class DownloadingStatus(Enum):
DOWNLOADING = 1
DOWNLOADED = 2
NOT_FOUND = 3
def lookup_cache(url: str) -> DownloadingStatus:
cache_file, _ = get_cache_file_and_folder(url)
cache_file_aria2 = f"{cache_file}.aria2"
if os.path.exists(cache_file_aria2):
return DownloadingStatus.DOWNLOADING
if os.path.exists(cache_file):
assert not os.path.isdir(cache_file)
return DownloadingStatus.DOWNLOADED
return DownloadingStatus.NOT_FOUND
def make_cached_response(url):
cache_file, _ = get_cache_file_and_folder(url)
assert os.path.exists(cache_file)
assert not os.path.isdir(cache_file)
with open(cache_file, "rb") as f:
content = f.read()
return Response(content=content, status_code=200)
async def get_url_content_length(url):
async with httpx.AsyncClient(proxy=PROXY, verify=False) as client:
head_response = await client.head(url)
content_len = (head_response.headers.get("content-length", None))
return content_len
async def try_get_cache(
request: Request,
target_url: str,
download_wait_time: int = 60,
post_process: typing.Callable[[Request, Response], Response] = None,
) -> typing.Optional[Response]:
cache_status = lookup_cache(target_url)
if cache_status == DownloadingStatus.DOWNLOADED:
resp = make_cached_response(target_url)
if post_process:
resp = post_process(request, resp)
return resp
if cache_status == DownloadingStatus.DOWNLOADING:
logger.info(f"Download is not finished, return 503 for {target_url}")
return Response(content=f"This file is downloading, view it at {EXTERNAL_URL_ARIA2}",
status_code=HTTP_504_GATEWAY_TIMEOUT)
assert cache_status == DownloadingStatus.NOT_FOUND
cache_file, cache_file_dir = get_cache_file_and_folder(target_url)
print("prepare to download", target_url, cache_file, cache_file_dir)
processed_url = quote(target_url, safe='/:?=&')
try:
await add_download(processed_url, save_dir=cache_file_dir)
except Exception as e:
logger.error(f"Download error, return 503500 for {target_url}", exc_info=e)
return Response(content=f"Failed to add download: {e}", status_code=HTTP_500_INTERNAL_SERVER_ERROR)
# wait for download finished
for _ in range(download_wait_time):
await sleep(1)
cache_status = lookup_cache(target_url)
if cache_status == DownloadingStatus.DOWNLOADED:
return make_cached_response(target_url)
logger.info(f"Download is not finished, return 503 for {target_url}")
return Response(content=f"This file is downloading, view it at {EXTERNAL_URL_ARIA2}",
status_code=HTTP_504_GATEWAY_TIMEOUT)

47
mirrors/proxy/direct.py Normal file
View File

@ -0,0 +1,47 @@
import typing
import httpx
import starlette.requests
import starlette.responses
from config import PROXY
async def direct_proxy(
request: starlette.requests.Request,
target_url: str,
pre_process: typing.Callable[[starlette.requests.Request, httpx.Request], httpx.Request] = None,
post_process: typing.Callable[[starlette.requests.Request, httpx.Response], httpx.Response] = None,
) -> typing.Optional[starlette.responses.Response]:
async with httpx.AsyncClient(proxy=PROXY, verify=False) as client:
headers = request.headers.mutablecopy()
for key in headers.keys():
if key not in ["user-agent", "accept"]:
del headers[key]
httpx_req = client.build_request(request.method, target_url, headers=headers, )
if pre_process:
httpx_req = pre_process(request, httpx_req)
upstream_response = await client.send(httpx_req)
# TODO: move to post_process
if upstream_response.status_code == 307:
location = upstream_response.headers["location"]
print("catch redirect", location)
headers = upstream_response.headers
cl = headers.pop("content-length", None)
ce = headers.pop("content-encoding", None)
# print(target_url, cl, ce)
content = upstream_response.content
response = starlette.responses.Response(
headers=headers,
content=content,
status_code=upstream_response.status_code)
if post_process:
response = post_process(request, response)
return response

17
mirrors/pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[tool.poetry]
name = "light-mirrors"
version = "0.1.0"
description = ""
authors = ["'Anonymous' <'<>'>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9"
fastapi = "^0.109.0"
uvicorn = "^0.27.0.post1"
httpx = "^0.26.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

128
mirrors/requirements.txt Normal file
View File

@ -0,0 +1,128 @@
annotated-types==0.6.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \
--hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d
anyio==4.2.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee \
--hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f
certifi==2024.2.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1
click==8.1.7 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows" \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
exceptiongroup==1.2.0 ; python_version >= "3.9" and python_version < "3.11" \
--hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
--hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
fastapi==0.109.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093 \
--hash=sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191
h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
httpcore==1.0.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7 \
--hash=sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535
httpx==0.26.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf \
--hash=sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd
idna==3.6 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
pydantic-core==2.16.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7 \
--hash=sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca \
--hash=sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51 \
--hash=sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da \
--hash=sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc \
--hash=sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae \
--hash=sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4 \
--hash=sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b \
--hash=sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0 \
--hash=sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e \
--hash=sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118 \
--hash=sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506 \
--hash=sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798 \
--hash=sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f \
--hash=sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d \
--hash=sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948 \
--hash=sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f \
--hash=sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9 \
--hash=sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137 \
--hash=sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640 \
--hash=sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f \
--hash=sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff \
--hash=sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706 \
--hash=sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d \
--hash=sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f \
--hash=sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c \
--hash=sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8 \
--hash=sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1 \
--hash=sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7 \
--hash=sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95 \
--hash=sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60 \
--hash=sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253 \
--hash=sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e \
--hash=sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c \
--hash=sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc \
--hash=sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3 \
--hash=sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8 \
--hash=sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9 \
--hash=sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c \
--hash=sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388 \
--hash=sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95 \
--hash=sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91 \
--hash=sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818 \
--hash=sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8 \
--hash=sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f \
--hash=sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394 \
--hash=sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13 \
--hash=sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17 \
--hash=sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7 \
--hash=sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06 \
--hash=sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f \
--hash=sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196 \
--hash=sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66 \
--hash=sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf \
--hash=sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c \
--hash=sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76 \
--hash=sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0 \
--hash=sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212 \
--hash=sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f \
--hash=sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49 \
--hash=sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206 \
--hash=sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48 \
--hash=sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c \
--hash=sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2 \
--hash=sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05 \
--hash=sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610 \
--hash=sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd \
--hash=sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76 \
--hash=sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1 \
--hash=sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60 \
--hash=sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34 \
--hash=sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4 \
--hash=sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864 \
--hash=sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66 \
--hash=sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c \
--hash=sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e \
--hash=sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54 \
--hash=sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8 \
--hash=sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e
pydantic==2.6.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae \
--hash=sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \
--hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384
starlette==0.35.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc \
--hash=sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25
typing-extensions==4.9.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
uvicorn==0.27.0.post1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f \
--hash=sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907

58
mirrors/server.py Normal file
View File

@ -0,0 +1,58 @@
import base64
import signal
import urllib.parse
import uvicorn
from fastapi import FastAPI
from starlette.requests import Request
from config import BASE_DOMAIN, RPC_SECRET, EXTERNAL_URL_ARIA2, EXTERNAL_HOST_ARIA2
from sites.docker import docker
from sites.npm import npm
from sites.pypi import pypi
from sites.torch import torch
app = FastAPI()
@app.middleware("http")
async def capture_request(request: Request, call_next: callable):
hostname = request.url.hostname
if not hostname.endswith(f".{BASE_DOMAIN}"):
return await call_next(request)
if hostname.startswith("pypi."):
return await pypi(request)
if hostname.startswith("torch."):
return await torch(request)
if hostname.startswith("docker."):
return await docker(request)
if hostname.startswith("npm."):
return await npm(request)
return await call_next(request)
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal.SIG_DFL)
port = 8080
print(f"Server started at https://*.{BASE_DOMAIN})")
for dn in ["pypi", "torch", "docker", "npm"]:
print(f" - https://{dn}.{BASE_DOMAIN}")
aria2_secret = base64.b64encode(RPC_SECRET.encode()).decode()
params = {
'protocol': 'https',
'host': EXTERNAL_HOST_ARIA2,
'port': '443',
'interface': 'jsonrpc',
'secret': aria2_secret
}
query_string = urllib.parse.urlencode(params)
aria2_url_with_auth = EXTERNAL_URL_ARIA2 + "/#!/settings/rpc/set?" + query_string
print(f"Download manager (Aria2) at {aria2_url_with_auth}")
uvicorn.run(app="server:app", host="0.0.0.0", port=port, reload=True, proxy_headers=True, forwarded_allow_ips="*")

100
mirrors/sites/docker.py Normal file
View File

@ -0,0 +1,100 @@
import base64
import json
import re
import time
import httpx
from starlette.requests import Request
from starlette.responses import Response
from proxy.direct import direct_proxy
BASE_URL = "https://registry-1.docker.io"
cached_token = {
}
# https://github.com/opencontainers/distribution-spec/blob/main/spec.md
name_regex = "[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"
reference_regex = "[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}"
def try_extract_image_name(path):
pattern = rf"^/v2/(.*)/([a-zA-Z]+)/(.*)$"
match = re.search(pattern, path)
if match:
assert len(match.groups()) == 3
name, operation, reference = match.groups()
assert re.match(name_regex, name)
assert re.match(reference_regex, reference)
assert operation in ["manifests", "blobs"]
return name, operation, reference
return None, None, None
def get_docker_token(name):
cached = cached_token.get(name, {})
exp = cached.get("exp", 0)
if exp > time.time():
return cached.get("token", 0)
url = "https://auth.docker.io/token"
params = {
"scope": f"repository:{name}:pull",
"service": "registry.docker.io",
}
response = httpx.get(url, params=params, verify=False)
response.raise_for_status()
token_data = response.json()
token = token_data["token"]
payload = (token.split(".")[1])
padding = len(payload) % 4
payload += "=" * padding
payload = json.loads(base64.b64decode(payload))
assert payload["iss"] == "auth.docker.io"
assert len(payload["access"]) > 0
cached_token[name] = {
"exp": payload["exp"],
"token": token
}
return token
async def docker(request: Request):
path = request.url.path
print("[request]", request.method, request.url)
if not path.startswith("/v2/"):
return Response(content="Not Found", status_code=404)
if path == "/v2/":
return Response(content="OK")
# return await direct_proxy(request, BASE_URL + '/v2/')
name, operation, reference = try_extract_image_name(path)
if not name:
return Response(content='404 Not Found', status_code=404)
# support docker pull xxx which name without library
if '/' not in name:
name = f"library/{name}"
target_url = BASE_URL + f"/v2/{name}/{operation}/{reference}"
print('[PARSED]', path, name, operation, reference, target_url)
def inject_token(req, httpx_req):
docker_token = get_docker_token(f"{name}")
httpx_req.headers["Authorization"] = f"Bearer {docker_token}"
return httpx_req
return await direct_proxy(request, target_url, pre_process=inject_token)

11
mirrors/sites/npm.py Normal file
View File

@ -0,0 +1,11 @@
from starlette.requests import Request
from proxy.direct import direct_proxy
BASE_URL = "https://registry.npmjs.org/"
async def npm(request: Request):
path = request.url.path
return await direct_proxy(request, BASE_URL + path)

44
mirrors/sites/pypi.py Normal file
View File

@ -0,0 +1,44 @@
import re
from starlette.requests import Request
from starlette.responses import Response
from proxy.direct import direct_proxy
from proxy.cached import try_get_cache
pypi_file_base_url = "https://files.pythonhosted.org"
pypi_base_url = "https://pypi.org"
def pypi_replace(request: Request, response: Response) -> Response:
is_detail_page = re.search(r"/simple/([^/]+)/", request.url.path) is not None
if not is_detail_page:
return response
if is_detail_page:
mirror_url = f"{request.url.scheme}://{request.url.netloc}"
content = response.body
content = content.replace(pypi_file_base_url.encode(), mirror_url.encode())
response.body = content
del response.headers["content-length"]
del response.headers["content-encoding"]
return response
async def pypi(request: Request) -> Response:
# TODO: a debug flag to show origin url
path = request.url.path
if path == "/simple":
path = "/simple/"
if path.startswith("/simple/"):
target_url = pypi_base_url + path
elif path.startswith("/packages/"):
target_url = pypi_file_base_url + path
else:
return Response(content="Not Found", status_code=404)
if path.endswith(".whl") or path.endswith(".tar.gz"):
return await try_get_cache(request, target_url)
return await direct_proxy(request, target_url, post_process=pypi_replace)

24
mirrors/sites/torch.py Normal file
View File

@ -0,0 +1,24 @@
from starlette.requests import Request
from starlette.responses import Response
from proxy.cached import try_get_cache
from proxy.direct import direct_proxy
BASE_URL = "https://download.pytorch.org"
async def torch(request: Request):
path = request.url.path
if not path.startswith("/whl/"):
return Response(content="Not Found", status_code=404)
if path == "/whl":
path = "/whl/"
target_url = BASE_URL + path
if path.endswith(".whl") or path.endswith(".tar.gz"):
return await try_get_cache(request, target_url)
return await direct_proxy(request, target_url, )