mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-09-29 03:46:36 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11bf9221c2 | |||
| b80f4844a4 | |||
| fc77ddb1ef | |||
| 85e47b7ce5 | |||
| 3e8ceb2b32 | |||
| 53cc1761ce | |||
| ba83a44492 | |||
| c7a7f3d146 | |||
| e4d4f33ea1 | |||
| 6e91fe9925 |
@@ -59,6 +59,9 @@ jobs:
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: 安装 nFPM
|
||||
run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.46.3
|
||||
|
||||
- name: 编译二进制文件
|
||||
run: |
|
||||
cd src
|
||||
@@ -74,24 +77,11 @@ jobs:
|
||||
upx -9 ../build/hubproxy/hubproxy-linux-amd64
|
||||
upx -9 ../build/hubproxy/hubproxy-linux-arm64
|
||||
|
||||
- name: 复制配置文件
|
||||
- name: 准备压缩包文件
|
||||
run: |
|
||||
# 复制配置文件
|
||||
cp src/config.toml build/hubproxy/
|
||||
|
||||
# 复制systemd服务文件
|
||||
cp hubproxy.service build/hubproxy/
|
||||
|
||||
# 复制安装脚本
|
||||
cp install.sh build/hubproxy/
|
||||
|
||||
# 创建README文件
|
||||
cat > build/hubproxy/README.md << 'EOF'
|
||||
# HubProxy
|
||||
|
||||
项目地址:https://github.com/sky22333/hubproxy
|
||||
EOF
|
||||
|
||||
- name: 创建压缩包
|
||||
run: |
|
||||
cd build
|
||||
@@ -99,23 +89,61 @@ jobs:
|
||||
# Linux AMD64 包
|
||||
mkdir -p linux-amd64/hubproxy
|
||||
cp hubproxy/hubproxy-linux-amd64 linux-amd64/hubproxy/hubproxy
|
||||
cp hubproxy/config.toml hubproxy/hubproxy.service hubproxy/install.sh hubproxy/README.md linux-amd64/hubproxy/
|
||||
tar -czf hubproxy-${{ steps.version.outputs.version }}-linux-amd64.tar.gz -C linux-amd64 hubproxy
|
||||
cp hubproxy/config.toml linux-amd64/hubproxy/
|
||||
tar -czf hubproxy-linux-amd64.tar.gz -C linux-amd64 hubproxy
|
||||
|
||||
# Linux ARM64 包
|
||||
mkdir -p linux-arm64/hubproxy
|
||||
cp hubproxy/hubproxy-linux-arm64 linux-arm64/hubproxy/hubproxy
|
||||
cp hubproxy/config.toml hubproxy/hubproxy.service hubproxy/install.sh hubproxy/README.md linux-arm64/hubproxy/
|
||||
tar -czf hubproxy-${{ steps.version.outputs.version }}-linux-arm64.tar.gz -C linux-arm64 hubproxy
|
||||
cp hubproxy/config.toml linux-arm64/hubproxy/
|
||||
tar -czf hubproxy-linux-arm64.tar.gz -C linux-arm64 hubproxy
|
||||
|
||||
# 列出生成的文件
|
||||
ls -la *.tar.gz
|
||||
|
||||
- name: 计算文件校验和
|
||||
|
||||
- name: 创建Linux发行版安装包
|
||||
run: |
|
||||
cd build
|
||||
sha256sum *.tar.gz > checksums.txt
|
||||
cat checksums.txt
|
||||
mkdir -p build/packages
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
NFPM_VERSION="${VERSION#v}"
|
||||
|
||||
package() {
|
||||
hubproxy_arch="$1"
|
||||
nfpm_arch="$2"
|
||||
packager="$3"
|
||||
config="$4"
|
||||
target="build/packages/hubproxy-linux-${hubproxy_arch}.${packager}"
|
||||
temp_dir="build/packages/${hubproxy_arch}-${packager}"
|
||||
binary="./build/hubproxy/hubproxy-linux-${hubproxy_arch}"
|
||||
|
||||
rm -rf "${temp_dir}"
|
||||
mkdir -p "${temp_dir}"
|
||||
rm -rf build/package-root
|
||||
mkdir -p build/package-root
|
||||
cp "${binary}" build/package-root/hubproxy
|
||||
NFPM_ARCH="${nfpm_arch}" NFPM_VERSION="${NFPM_VERSION}" nfpm package --config "${config}" --packager "${packager}" --target "${temp_dir}/"
|
||||
mv "${temp_dir}"/*.${packager} "${target}"
|
||||
rm -rf "${temp_dir}"
|
||||
rm -rf build/package-root
|
||||
}
|
||||
|
||||
# AMD64 包
|
||||
package amd64 amd64 deb packaging/nfpm.deb-rpm.yaml
|
||||
package amd64 amd64 rpm packaging/nfpm.deb-rpm.yaml
|
||||
package amd64 amd64 apk packaging/nfpm.apk.yaml
|
||||
|
||||
# ARM64 包
|
||||
package arm64 arm64 deb packaging/nfpm.deb-rpm.yaml
|
||||
package arm64 arm64 rpm packaging/nfpm.deb-rpm.yaml
|
||||
package arm64 arm64 apk packaging/nfpm.apk.yaml
|
||||
|
||||
ls -la build/packages
|
||||
|
||||
- name: 检查安装包内容
|
||||
run: |
|
||||
dpkg-deb -c build/packages/hubproxy-linux-amd64.deb
|
||||
rpm -qpl build/packages/hubproxy-linux-amd64.rpm
|
||||
tar -tf build/packages/hubproxy-linux-amd64.apk
|
||||
|
||||
- name: 创建或更新Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
@@ -128,12 +156,16 @@ jobs:
|
||||
|
||||
## 下载文件
|
||||
|
||||
- **Linux AMD64**: `hubproxy-${{ steps.version.outputs.version }}-linux-amd64.tar.gz`
|
||||
- **Linux ARM64**: `hubproxy-${{ steps.version.outputs.version }}-linux-arm64.tar.gz`
|
||||
- **Linux AMD64**: `hubproxy-linux-amd64.tar.gz`
|
||||
- **Linux ARM64**: `hubproxy-linux-arm64.tar.gz`
|
||||
- **Debian/Ubuntu**: `.deb`
|
||||
- **RHEL/CentOS/Fedora**: `.rpm`
|
||||
- **Alpine Linux**: `.apk`
|
||||
|
||||
files: |
|
||||
build/*.tar.gz
|
||||
build/checksums.txt
|
||||
build/packages/*
|
||||
overwrite_files: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
hubproxy*
|
||||
!hubproxy.service
|
||||
/hubproxy*
|
||||
*.exe
|
||||
|
||||
@@ -40,19 +40,70 @@ docker run -d \
|
||||
ghcr.io/sky22333/hubproxy
|
||||
```
|
||||
|
||||
### 一键脚本安装
|
||||
### 脚本安装
|
||||
|
||||
自动识别系统与架构,从 GitHub Releases 下载对应的 `.deb`、`.rpm` 或 `.apk` 安装包:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/sky22333/hubproxy/main/install.sh | sudo bash
|
||||
curl -fsSL https://raw.githubusercontent.com/sky22333/hubproxy/main/install.sh | sh
|
||||
```
|
||||
|
||||
支持单个二进制文件直接启动,无需其他配置,内置默认配置,支持所有功能。
|
||||
安装包会自动安装并启动 `hubproxy` 服务。
|
||||
|
||||
这个脚本会:
|
||||
- 自动检测系统架构(AMD64/ARM64)
|
||||
- 从 GitHub Releases 下载最新版本
|
||||
- 自动配置系统服务
|
||||
- 保留现有配置(升级时)
|
||||
<details>
|
||||
<summary>服务管理命令</summary>
|
||||
|
||||
#### systemd(Debian / Ubuntu / RHEL / CentOS / Fedora)
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
sudo systemctl status hubproxy
|
||||
|
||||
# 重启服务
|
||||
sudo systemctl restart hubproxy
|
||||
|
||||
# 查看实时日志
|
||||
sudo journalctl -u hubproxy -f
|
||||
|
||||
# 编辑配置文件
|
||||
sudo nano /etc/hubproxy/config.toml
|
||||
|
||||
# 卸载服务
|
||||
sudo apt remove hubproxy
|
||||
|
||||
# 连配置一起清理
|
||||
sudo apt purge hubproxy
|
||||
```
|
||||
|
||||
#### OpenRC(Alpine Linux)
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
sudo rc-service hubproxy status
|
||||
|
||||
# 重启服务
|
||||
sudo rc-service hubproxy restart
|
||||
|
||||
# 查看实时日志
|
||||
sudo tail -f /var/log/hubproxy.log
|
||||
|
||||
# 编辑配置文件
|
||||
sudo vi /etc/hubproxy/config.toml
|
||||
|
||||
# 卸载
|
||||
sudo apk del hubproxy
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 文件路径
|
||||
|
||||
- Linux 安装包配置文件:`/etc/hubproxy/config.toml`
|
||||
- Linux 安装包二进制文件:`/usr/bin/hubproxy`
|
||||
- systemd 服务文件:`/lib/systemd/system/hubproxy.service`
|
||||
- Alpine OpenRC 服务文件:`/etc/init.d/hubproxy`
|
||||
- Alpine 日志文件:`/var/log/hubproxy.log`
|
||||
- Alpine 日志轮转配置:`/etc/logrotate.d/hubproxy`
|
||||
|
||||
## 使用方法
|
||||
|
||||
@@ -202,10 +253,6 @@ defaultTTL = "20m"
|
||||
|
||||
</details>
|
||||
|
||||
容器内的配置文件位于 `/app/config.toml`
|
||||
|
||||
脚本部署配置文件位于 `/opt/hubproxy/config.toml`
|
||||
|
||||
### 环境变量(可选)
|
||||
|
||||
支持通过环境变量覆盖部分配置,优先级高于`config.toml`,以下是默认值:
|
||||
|
||||
+97
-189
@@ -1,213 +1,121 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# HubProxy 一键安装脚本
|
||||
# 支持自动下载最新版本或使用本地文件安装
|
||||
set -e
|
||||
REPO="${REPO:-sky22333/hubproxy}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
TMP_DIR="${TMP_DIR:-/tmp/hubproxy-install}"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
# 配置
|
||||
REPO="sky22333/hubproxy"
|
||||
GITHUB_API="https://api.github.com/repos/${REPO}"
|
||||
GITHUB_RELEASES="${GITHUB_API}/releases"
|
||||
SERVICE_NAME="hubproxy"
|
||||
INSTALL_DIR="/opt/hubproxy"
|
||||
CONFIG_FILE="config.toml"
|
||||
BINARY_NAME="hubproxy"
|
||||
LOG_DIR="/var/log/hubproxy"
|
||||
TEMP_DIR="/tmp/hubproxy-install"
|
||||
fail() {
|
||||
printf 'HubProxy 安装失败:%s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo -e "${BLUE}HubProxy 一键安装脚本${NC}"
|
||||
echo "================================================="
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "缺少必要命令:$1"
|
||||
}
|
||||
|
||||
# 检查是否以root权限运行
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}此脚本需要root权限运行${NC}"
|
||||
echo "请使用: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检测系统架构
|
||||
detect_arch() {
|
||||
local arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64)
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64)
|
||||
echo "amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "arm64"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}不支持的架构: $arch${NC}"
|
||||
exit 1
|
||||
fail "不支持的系统架构:$(uname -m)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ARCH=$(detect_arch)
|
||||
echo -e "${BLUE}检测到架构: linux-${ARCH}${NC}"
|
||||
|
||||
# 检查是否为本地安装模式
|
||||
if [ -f "${BINARY_NAME}" ]; then
|
||||
echo -e "${BLUE}发现本地文件,使用本地安装模式${NC}"
|
||||
LOCAL_INSTALL=true
|
||||
else
|
||||
echo -e "${BLUE}本地无文件,使用自动下载模式${NC}"
|
||||
LOCAL_INSTALL=false
|
||||
|
||||
# 检查依赖
|
||||
missing_deps=()
|
||||
for cmd in curl jq tar; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
missing_deps+=($cmd)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||
echo -e "${YELLOW}检测到缺少依赖: ${missing_deps[*]}${NC}"
|
||||
echo -e "${BLUE}正在自动安装依赖...${NC}"
|
||||
|
||||
apt update && apt install -y curl jq
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}依赖安装失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 重新检查依赖
|
||||
for cmd in curl jq tar; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
echo -e "${RED}依赖安装后仍缺少: $cmd${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}依赖安装成功${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 自动下载功能
|
||||
if [ "$LOCAL_INSTALL" = false ]; then
|
||||
echo -e "${BLUE}获取最新版本信息...${NC}"
|
||||
LATEST_RELEASE=$(curl -s "${GITHUB_RELEASES}/latest")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}无法获取版本信息${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(echo "$LATEST_RELEASE" | jq -r '.tag_name')
|
||||
if [ "$VERSION" = "null" ]; then
|
||||
echo -e "${RED}无法解析版本信息${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}最新版本: ${VERSION}${NC}"
|
||||
|
||||
# 构造下载URL
|
||||
ASSET_NAME="hubproxy-${VERSION}-linux-${ARCH}.tar.gz"
|
||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET_NAME}"
|
||||
|
||||
echo -e "${BLUE}下载: ${ASSET_NAME}${NC}"
|
||||
|
||||
# 创建临时目录并下载
|
||||
rm -rf "${TEMP_DIR}"
|
||||
mkdir -p "${TEMP_DIR}"
|
||||
cd "${TEMP_DIR}"
|
||||
|
||||
curl -L -o "${ASSET_NAME}" "${DOWNLOAD_URL}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}下载失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 解压
|
||||
tar -xzf "${ASSET_NAME}"
|
||||
if [ $? -ne 0 ] || [ ! -d "hubproxy" ]; then
|
||||
echo -e "${RED}解压失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd hubproxy
|
||||
echo -e "${GREEN}下载完成${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}开始安装 HubProxy...${NC}"
|
||||
|
||||
# 停止现有服务(如果存在)
|
||||
if systemctl is-active --quiet ${SERVICE_NAME} 2>/dev/null; then
|
||||
echo -e "${YELLOW}停止现有服务...${NC}"
|
||||
systemctl stop ${SERVICE_NAME}
|
||||
fi
|
||||
|
||||
# 备份现有配置(如果存在)
|
||||
CONFIG_BACKUP_EXISTS=false
|
||||
if [ -f "${INSTALL_DIR}/${CONFIG_FILE}" ]; then
|
||||
echo -e "${BLUE}备份现有配置...${NC}"
|
||||
cp "${INSTALL_DIR}/${CONFIG_FILE}" "${TEMP_DIR}/config.toml.backup"
|
||||
CONFIG_BACKUP_EXISTS=true
|
||||
fi
|
||||
|
||||
# 1. 创建目录结构
|
||||
echo -e "${BLUE}创建目录结构${NC}"
|
||||
mkdir -p ${INSTALL_DIR}
|
||||
mkdir -p ${LOG_DIR}
|
||||
chmod 755 ${INSTALL_DIR}
|
||||
chmod 755 ${LOG_DIR}
|
||||
|
||||
# 2. 复制二进制文件
|
||||
echo -e "${BLUE}复制二进制文件${NC}"
|
||||
cp "${BINARY_NAME}" "${INSTALL_DIR}/"
|
||||
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
|
||||
|
||||
# 3. 复制配置文件
|
||||
echo -e "${BLUE}复制配置文件${NC}"
|
||||
if [ -f "${CONFIG_FILE}" ]; then
|
||||
if [ "$CONFIG_BACKUP_EXISTS" = false ]; then
|
||||
cp "${CONFIG_FILE}" "${INSTALL_DIR}/"
|
||||
echo -e "${GREEN}配置文件复制成功${NC}"
|
||||
detect_packager() {
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo "apk"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
echo "deb"
|
||||
elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1 || command -v rpm >/dev/null 2>&1; then
|
||||
echo "rpm"
|
||||
else
|
||||
echo -e "${YELLOW}保留现有配置文件${NC}"
|
||||
fail "不支持的系统:需要 apt、dnf、yum、rpm 或 apk"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}配置文件不存在,将使用默认配置${NC}"
|
||||
}
|
||||
|
||||
asset_name() {
|
||||
packager="$1"
|
||||
arch="$2"
|
||||
|
||||
case "$packager:$arch" in
|
||||
deb:amd64|rpm:amd64|apk:amd64) echo "hubproxy-linux-amd64.${packager}" ;;
|
||||
deb:arm64|rpm:arm64|apk:arm64) echo "hubproxy-linux-arm64.${packager}" ;;
|
||||
*) fail "不支持的安装包目标:${packager}/${arch}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
asset_url() {
|
||||
asset="$1"
|
||||
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
echo "https://github.com/${REPO}/releases/latest/download/${asset}"
|
||||
else
|
||||
echo "https://github.com/${REPO}/releases/download/${VERSION}/${asset}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_package() {
|
||||
package_file="$1"
|
||||
packager="$2"
|
||||
|
||||
case "$packager" in
|
||||
deb)
|
||||
apt-get install -y "$package_file"
|
||||
;;
|
||||
rpm)
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y "$package_file"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y "$package_file"
|
||||
else
|
||||
rpm -Uvh "$package_file"
|
||||
fi
|
||||
;;
|
||||
apk)
|
||||
apk add --allow-untrusted "$package_file"
|
||||
;;
|
||||
*)
|
||||
fail "不支持的包管理器:$packager"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
fail "请使用 root 权限运行"
|
||||
fi
|
||||
|
||||
# 5. 安装systemd服务文件
|
||||
echo -e "${BLUE}安装systemd服务文件${NC}"
|
||||
cp "${SERVICE_NAME}.service" "/etc/systemd/system/"
|
||||
systemctl daemon-reload
|
||||
need_cmd curl
|
||||
|
||||
# 6. 恢复配置文件(如果有备份)
|
||||
if [ "$CONFIG_BACKUP_EXISTS" = true ]; then
|
||||
echo -e "${BLUE}恢复配置文件...${NC}"
|
||||
cp "${TEMP_DIR}/config.toml.backup" "${INSTALL_DIR}/${CONFIG_FILE}"
|
||||
fi
|
||||
ARCH="$(detect_arch)"
|
||||
PACKAGER="$(detect_packager)"
|
||||
|
||||
# 7. 启用并启动服务
|
||||
echo -e "${BLUE}启用并启动服务${NC}"
|
||||
systemctl enable ${SERVICE_NAME}
|
||||
systemctl start ${SERVICE_NAME}
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
|
||||
|
||||
# 8. 清理临时文件
|
||||
if [ "$LOCAL_INSTALL" = false ]; then
|
||||
echo -e "${BLUE}清理临时文件...${NC}"
|
||||
cd /
|
||||
rm -rf "${TEMP_DIR}"
|
||||
fi
|
||||
log "安装 HubProxy:linux/${ARCH}(${PACKAGER})"
|
||||
|
||||
# 9. 检查服务状态
|
||||
sleep 2
|
||||
if systemctl is-active --quiet ${SERVICE_NAME}; then
|
||||
echo ""
|
||||
echo -e "${GREEN}HubProxy 安装成功!${NC}"
|
||||
echo -e "${GREEN}默认运行端口: 5000${NC}"
|
||||
echo -e "${GREEN}配置文件路径: ${INSTALL_DIR}/${CONFIG_FILE}${NC}"
|
||||
else
|
||||
echo -e "${RED}服务启动失败${NC}"
|
||||
echo "查看错误日志: sudo journalctl -u ${SERVICE_NAME} -f"
|
||||
exit 1
|
||||
fi
|
||||
ASSET="$(asset_name "$PACKAGER" "$ARCH")"
|
||||
ASSET_URL="$(asset_url "$ASSET")"
|
||||
|
||||
PACKAGE_FILE="${TMP_DIR}/$(basename "$ASSET_URL")"
|
||||
log "下载安装包..."
|
||||
curl -fL -o "$PACKAGE_FILE" "$ASSET_URL" || fail "下载安装包失败"
|
||||
|
||||
log "安装软件包..."
|
||||
install_package "$PACKAGE_FILE" "$PACKAGER"
|
||||
|
||||
log "安装完成"
|
||||
log "默认端口:5000"
|
||||
log "配置文件:/etc/hubproxy/config.toml"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/var/log/hubproxy.log {
|
||||
weekly
|
||||
maxsize 50M
|
||||
rotate 4
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name="hubproxy"
|
||||
description="Docker and GitHub acceleration proxy server"
|
||||
command="/usr/bin/hubproxy"
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
output_log="/var/log/hubproxy.log"
|
||||
error_log="/var/log/hubproxy.log"
|
||||
supervisor="supervise-daemon"
|
||||
respawn_delay=5
|
||||
respawn_max=0
|
||||
|
||||
export CONFIG_PATH="/etc/hubproxy/config.toml"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after firewall
|
||||
}
|
||||
@@ -7,11 +7,10 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/opt/hubproxy
|
||||
ExecStart=/opt/hubproxy/hubproxy
|
||||
Environment=CONFIG_PATH=/etc/hubproxy/config.toml
|
||||
ExecStart=/usr/bin/hubproxy
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=hubproxy
|
||||
@@ -0,0 +1,45 @@
|
||||
name: hubproxy
|
||||
arch: ${NFPM_ARCH}
|
||||
platform: linux
|
||||
version: ${NFPM_VERSION}
|
||||
release: "1"
|
||||
section: net
|
||||
priority: optional
|
||||
maintainer: sky22333
|
||||
description: Docker and GitHub acceleration proxy server
|
||||
vendor: sky22333
|
||||
homepage: https://github.com/sky22333/hubproxy
|
||||
license: MIT
|
||||
depends:
|
||||
- logrotate
|
||||
|
||||
contents:
|
||||
- src: ./build/package-root/hubproxy
|
||||
dst: /usr/bin/hubproxy
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
- src: ./src/config.toml
|
||||
dst: /etc/hubproxy/config.toml
|
||||
type: config|noreplace
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
- src: ./packaging/hubproxy.openrc
|
||||
dst: /etc/init.d/hubproxy
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
- src: ./packaging/hubproxy.logrotate
|
||||
dst: /etc/logrotate.d/hubproxy
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
scripts:
|
||||
postinstall: ./packaging/postinstall.sh
|
||||
preremove: ./packaging/preremove.sh
|
||||
postremove: ./packaging/postremove.sh
|
||||
|
||||
apk:
|
||||
scripts:
|
||||
postupgrade: ./packaging/postinstall.sh
|
||||
@@ -0,0 +1,34 @@
|
||||
name: hubproxy
|
||||
arch: ${NFPM_ARCH}
|
||||
platform: linux
|
||||
version: ${NFPM_VERSION}
|
||||
release: "1"
|
||||
section: net
|
||||
priority: optional
|
||||
maintainer: sky22333
|
||||
description: Docker and GitHub acceleration proxy server
|
||||
vendor: sky22333
|
||||
homepage: https://github.com/sky22333/hubproxy
|
||||
license: MIT
|
||||
|
||||
contents:
|
||||
- src: ./build/package-root/hubproxy
|
||||
dst: /usr/bin/hubproxy
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
- src: ./src/config.toml
|
||||
dst: /etc/hubproxy/config.toml
|
||||
type: config|noreplace
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
- src: ./packaging/hubproxy.service
|
||||
dst: /lib/systemd/system/hubproxy.service
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
scripts:
|
||||
postinstall: ./packaging/postinstall.sh
|
||||
preremove: ./packaging/preremove.sh
|
||||
postremove: ./packaging/postremove.sh
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
warn() {
|
||||
echo "hubproxy: $1"
|
||||
}
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || warn "systemd reload failed"
|
||||
systemctl enable hubproxy >/dev/null 2>&1 || warn "systemd enable failed"
|
||||
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl restart hubproxy || systemctl start hubproxy || {
|
||||
warn "service start failed, check: journalctl -u hubproxy"
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v rc-update >/dev/null 2>&1; then
|
||||
rc-update add hubproxy default >/dev/null 2>&1 || warn "OpenRC enable failed"
|
||||
fi
|
||||
|
||||
if command -v rc-service >/dev/null 2>&1; then
|
||||
rc-service hubproxy restart || rc-service hubproxy start || {
|
||||
warn "service start failed, check: rc-service hubproxy status"
|
||||
}
|
||||
fi
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "${1:-}" in
|
||||
1|upgrade)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl stop hubproxy >/dev/null 2>&1 || true
|
||||
systemctl disable hubproxy >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if command -v rc-service >/dev/null 2>&1; then
|
||||
rc-service hubproxy stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if command -v rc-update >/dev/null 2>&1; then
|
||||
rc-update del hubproxy default >/dev/null 2>&1 || true
|
||||
fi
|
||||
+516
-469
File diff suppressed because it is too large
Load Diff
+908
-1
@@ -1,6 +1,57 @@
|
||||
package handlers
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"hubproxy/config"
|
||||
"hubproxy/utils"
|
||||
)
|
||||
|
||||
type zeroReader struct{}
|
||||
|
||||
func (zeroReader) Read(p []byte) (int, error) {
|
||||
for i := range p {
|
||||
p[i] = 0
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type discardResponseWriter struct {
|
||||
header http.Header
|
||||
status int
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func newDiscardResponseWriter() *discardResponseWriter {
|
||||
return &discardResponseWriter{header: make(http.Header)}
|
||||
}
|
||||
|
||||
func (w *discardResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *discardResponseWriter) WriteHeader(status int) {
|
||||
w.status = status
|
||||
}
|
||||
|
||||
func (w *discardResponseWriter) Write(p []byte) (int, error) {
|
||||
if w.status == 0 {
|
||||
w.status = http.StatusOK
|
||||
}
|
||||
w.bytes += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestParseRegistryPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -28,3 +79,859 @@ func TestParseRegistryPathInvalid(t *testing.T) {
|
||||
t.Fatalf("invalid path parsed as %q %q %q", image, apiType, reference)
|
||||
}
|
||||
}
|
||||
|
||||
type testEnv interface {
|
||||
Helper()
|
||||
TempDir() string
|
||||
Setenv(string, string)
|
||||
Fatal(...interface{})
|
||||
}
|
||||
|
||||
func initDockerProxyTest(t testEnv, configBody string) {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
if err := os.WriteFile(path, []byte(configBody), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("CONFIG_PATH", path)
|
||||
if err := config.LoadConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
utils.InitHTTPClients()
|
||||
}
|
||||
|
||||
func TestRewriteAuthChallengePreservesScopeAndUsesProxyRealm(t *testing.T) {
|
||||
target := registryTarget{
|
||||
Name: "ghcr.io",
|
||||
AuthService: "ghcr.io",
|
||||
}
|
||||
|
||||
got := rewriteAuthChallenge(
|
||||
`Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:owner/image:pull"`,
|
||||
target,
|
||||
"https://proxy.example.com",
|
||||
)
|
||||
|
||||
want := `Bearer realm="https://proxy.example.com/token/ghcr.io",service="ghcr.io",scope="repository:owner/image:pull"`
|
||||
if got != want {
|
||||
t.Fatalf("challenge = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthURLForDockerHubAddsLibraryScopeAndService(t *testing.T) {
|
||||
got, err := buildAuthURL(
|
||||
defaultRegistryTarget(),
|
||||
"service=ignored&scope=repository%3Aalpine%3Apull&client_id=docker",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(got, dockerHubAuthRealm+"?") {
|
||||
t.Fatalf("auth URL = %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "service=registry.docker.io") {
|
||||
t.Fatalf("auth URL missing service: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "scope=repository%3Alibrary%2Falpine%3Apull") {
|
||||
t.Fatalf("auth URL missing normalized scope: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenTargetIsInferredFromPathBasedRegistryScope(t *testing.T) {
|
||||
initDockerProxyTest(t, `
|
||||
[registries."ghcr.io"]
|
||||
upstream = "ghcr.io"
|
||||
authHost = "ghcr.io/token"
|
||||
authType = "github"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository:ghcr.io/jeessy2/ddns-go:pull&service=registry.docker.io", nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Params = gin.Params{{Key: "path", Value: "/docker.io"}}
|
||||
|
||||
target, ok := resolveTokenTarget(c)
|
||||
if !ok {
|
||||
t.Fatal("resolveTokenTarget returned false")
|
||||
}
|
||||
if target.Name != "ghcr.io" {
|
||||
t.Fatalf("target.Name = %q, want ghcr.io", target.Name)
|
||||
}
|
||||
if target.AuthService != "ghcr.io" {
|
||||
t.Fatalf("AuthService = %q, want ghcr.io", target.AuthService)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthURLStripsPathBasedRegistryPrefixForGHCR(t *testing.T) {
|
||||
target := registryTarget{
|
||||
Name: "ghcr.io",
|
||||
AuthRealm: "https://ghcr.io/token",
|
||||
AuthService: "ghcr.io",
|
||||
}
|
||||
|
||||
got, err := buildAuthURL(
|
||||
target,
|
||||
"scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(got, "service=ghcr.io") {
|
||||
t.Fatalf("auth URL missing ghcr service: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "scope=repository%3Ajeessy2%2Fddns-go%3Apull") {
|
||||
t.Fatalf("auth URL missing stripped scope: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "registry.docker.io") {
|
||||
t.Fatalf("auth URL leaked Docker Hub service: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerIODefaultTargetUsesBuiltInWhenUnconfigured(t *testing.T) {
|
||||
initDockerProxyTest(t, "")
|
||||
|
||||
target := defaultRegistryTarget()
|
||||
if target.Upstream != dockerHubUpstream {
|
||||
t.Fatalf("Upstream = %q, want %q", target.Upstream, dockerHubUpstream)
|
||||
}
|
||||
if target.AuthRealm != dockerHubAuthRealm {
|
||||
t.Fatalf("AuthRealm = %q, want %q", target.AuthRealm, dockerHubAuthRealm)
|
||||
}
|
||||
if !target.AutoLibraryPrefix {
|
||||
t.Fatal("AutoLibraryPrefix = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerIODefaultTargetCanBeOverriddenByConfig(t *testing.T) {
|
||||
initDockerProxyTest(t, `
|
||||
[registries."docker.io"]
|
||||
upstream = "mirror.local"
|
||||
authHost = "auth.mirror.local/token"
|
||||
authType = "docker"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
target := defaultRegistryTarget()
|
||||
if target.Upstream != "https://mirror.local" {
|
||||
t.Fatalf("Upstream = %q, want custom mirror", target.Upstream)
|
||||
}
|
||||
if target.AuthRealm != "https://auth.mirror.local/token" {
|
||||
t.Fatalf("AuthRealm = %q, want custom auth realm", target.AuthRealm)
|
||||
}
|
||||
if target.AuthService != dockerHubAuthService {
|
||||
t.Fatalf("AuthService = %q, want %q", target.AuthService, dockerHubAuthService)
|
||||
}
|
||||
if !target.AutoLibraryPrefix {
|
||||
t.Fatal("AutoLibraryPrefix = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerIODefaultTargetIgnoresDisabledOverride(t *testing.T) {
|
||||
initDockerProxyTest(t, `
|
||||
[registries."docker.io"]
|
||||
upstream = "mirror.local"
|
||||
authHost = "auth.mirror.local/token"
|
||||
authType = "docker"
|
||||
enabled = false
|
||||
`)
|
||||
|
||||
target := defaultRegistryTarget()
|
||||
if target.Upstream != dockerHubUpstream {
|
||||
t.Fatalf("Upstream = %q, want built-in %q", target.Upstream, dockerHubUpstream)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryTransparentlyForwardsAuthAndRewritesChallenge(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
||||
t.Fatalf("upstream path = %q", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer client-token" {
|
||||
t.Fatalf("Authorization = %q", got)
|
||||
}
|
||||
if got := r.Header.Get("Accept"); got != "application/vnd.docker.distribution.manifest.v2+json" {
|
||||
t.Fatalf("Accept = %q", got)
|
||||
}
|
||||
if got := r.Header.Get("Range"); got != "bytes=0-99" {
|
||||
t.Fatalf("Range = %q", got)
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="https://upstream.example/token",service="upstream.example",scope="repository:team/app:pull"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Host = "proxy.example.com"
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
req.Header.Set("Authorization", "Bearer client-token")
|
||||
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Set("Range", "bytes=0-99")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
wantChallenge := `Bearer realm="https://proxy.example.com/token/test.local",service="` + strings.TrimPrefix(upstream.URL, "http://") + `",scope="repository:team/app:pull"`
|
||||
if got := w.Header().Get("WWW-Authenticate"); got != wantChallenge {
|
||||
t.Fatalf("WWW-Authenticate = %q, want %q", got, wantChallenge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerV2BaseProxiesUpstreamChallenge(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/" {
|
||||
t.Fatalf("upstream path = %q", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="https://registry.example/token",service="registry.example"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."docker.io"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.example/token"
|
||||
authType = "docker"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/", nil)
|
||||
req.Host = "hub.example.com"
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
wantChallenge := `Bearer realm="https://hub.example.com/token/docker.io",service="registry.docker.io"`
|
||||
if got := w.Header().Get("WWW-Authenticate"); got != wantChallenge {
|
||||
t.Fatalf("WWW-Authenticate = %q, want %q", got, wantChallenge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerAuthForwardsBasicCredentials(t *testing.T) {
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Basic dXNlcjpwYXNz" {
|
||||
t.Fatalf("Authorization = %q", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("service"); got != "127.0.0.1" {
|
||||
t.Fatalf("service = %q", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("scope"); got != "repository:team/app:pull" {
|
||||
t.Fatalf("scope = %q", got)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"token":"secret","expires_in":3600}`))
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "https://127.0.0.1"
|
||||
authHost = "`+authServer.URL+`"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/token/*path", ProxyDockerAuthGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
||||
req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := w.Body.String(); !strings.Contains(got, `"token":"secret"`) {
|
||||
t.Fatalf("body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerAuthRoutesPathBasedGHCRScopeToGHCRAuth(t *testing.T) {
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("service"); got != "ghcr.io" {
|
||||
t.Fatalf("service = %q, want ghcr.io", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("scope"); got != "repository:jeessy2/ddns-go:pull" {
|
||||
t.Fatalf("scope = %q, want repository:jeessy2/ddns-go:pull", got)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"token":"ghcr-token","expires_in":3600}`))
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."ghcr.io"]
|
||||
upstream = "ghcr.io"
|
||||
authHost = "`+authServer.URL+`"
|
||||
authType = "github"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/token/*path", ProxyDockerAuthGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := w.Body.String(); !strings.Contains(got, `"token":"ghcr-token"`) {
|
||||
t.Fatalf("body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerHubShortNameIsProxiedWithLibraryPrefix(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/library/nginx/manifests/latest" {
|
||||
t.Fatalf("upstream path = %q", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Docker-Content-Digest", "sha256:abc")
|
||||
_, _ = w.Write([]byte(`{"schemaVersion":2}`))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
target := defaultRegistryTarget()
|
||||
target.Upstream = upstream.URL
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/nginx/manifests/latest", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
proxyRegistryHTTP(c, target, "/v2/library/nginx/manifests/latest")
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := w.Header().Get("Docker-Content-Digest"); got != "sha256:abc" {
|
||||
t.Fatalf("Docker-Content-Digest = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryHeadReturnsHeadersWithoutBody(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodHead {
|
||||
t.Fatalf("method = %s, want HEAD", r.Method)
|
||||
}
|
||||
w.Header().Set("Content-Length", "123")
|
||||
w.Header().Set("Docker-Content-Digest", "sha256:head")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodHead, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", w.Code)
|
||||
}
|
||||
if got := w.Header().Get("Docker-Content-Digest"); got != "sha256:head" {
|
||||
t.Fatalf("Docker-Content-Digest = %q", got)
|
||||
}
|
||||
if body := w.Body.String(); body != "" {
|
||||
t.Fatalf("HEAD body = %q, want empty", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryStreamsBlobAndSkipsHopByHopHeaders(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Connection", "close")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = io.WriteString(w, "layer-data")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := w.Body.String(); got != "layer-data" {
|
||||
t.Fatalf("body = %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Connection"); got != "" {
|
||||
t.Fatalf("Connection header leaked: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryCachesAnonymousManifestByAccept(t *testing.T) {
|
||||
var hits int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt32(&hits, 1)
|
||||
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||
w.Header().Set("Content-Type", r.Header.Get("Accept"))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"hit":1`) {
|
||||
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(&hits); got != 1 {
|
||||
t.Fatalf("hits = %d, want 1", got)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("second accept status = %d; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"hit":2`) {
|
||||
t.Fatalf("second accept body = %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryDoesNotCacheAuthenticatedManifest(t *testing.T) {
|
||||
var hits int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt32(&hits, 1)
|
||||
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer token")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), fmt.Sprintf(`"hit":%d`, i)) {
|
||||
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(&hits); got != 2 {
|
||||
t.Fatalf("hits = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryCachesKnownAnonymousBearerManifest(t *testing.T) {
|
||||
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
||||
anonymousTokens.RememberFromResponse([]byte(`{"token":"anonymous-token","expires_in":3600}`), time.Hour)
|
||||
|
||||
var hits int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt32(&hits, 1)
|
||||
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer anonymous-token")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"hit":1`) {
|
||||
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(&hits); got != 1 {
|
||||
t.Fatalf("hits = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryDoesNotCacheUnknownBearerManifest(t *testing.T) {
|
||||
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
||||
|
||||
var hits int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt32(&hits, 1)
|
||||
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer user-token")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), fmt.Sprintf(`"hit":%d`, i)) {
|
||||
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(&hits); got != 2 {
|
||||
t.Fatalf("hits = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryUsesNsQueryForContainerd(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
||||
t.Fatalf("upstream path = %q", r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("ns"); got != "test.local" {
|
||||
t.Fatalf("ns query = %q", got)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/team/app/manifests/latest?ns=test.local", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerAuthCachesOnlyAnonymousTokenRequests(t *testing.T) {
|
||||
var hits int32
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt32(&hits, 1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = fmt.Fprintf(w, `{"token":"token-%d","expires_in":3600}`, count)
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "https://test.local"
|
||||
authHost = "`+authServer.URL+`"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
utils.GlobalCache = &utils.UniversalCache{}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/token/*path", ProxyDockerAuthGin)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("anonymous request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
if got := w.Body.String(); !strings.Contains(got, `"token":"token-1"`) {
|
||||
t.Fatalf("anonymous request %d body = %q", i, got)
|
||||
}
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt32(&hits); got != 1 {
|
||||
t.Fatalf("anonymous token hits = %d, want 1", got)
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
||||
req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("authenticated request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt32(&hits); got != 3 {
|
||||
t.Fatalf("authenticated token hits total = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerAuthRejectsUnknownRegistry(t *testing.T) {
|
||||
initDockerProxyTest(t, "")
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/token/*path", ProxyDockerAuthGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/token/missing.local?scope=repository:team/app:pull", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryConcurrentRequests(t *testing.T) {
|
||||
const requests = 64
|
||||
var hits int32
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&hits, 1)
|
||||
if got := r.Header.Get("Authorization"); got == "" {
|
||||
t.Fatal("missing Authorization")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan string, requests)
|
||||
for i := 0; i < requests; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer token-%d", i))
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK || w.Body.String() != "ok" {
|
||||
errs <- fmt.Sprintf("request %d status=%d body=%q", i, w.Code, w.Body.String())
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for err := range errs {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := atomic.LoadInt32(&hits); got != requests {
|
||||
t.Fatalf("hits = %d, want %d", got, requests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryLargeBlobStreamsWithoutRecorderBuffer(t *testing.T) {
|
||||
const blobSize = 8 << 20
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", blobSize))
|
||||
_, _ = io.CopyN(w, zeroReader{}, blobSize)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(t, `
|
||||
[registries."test.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.test.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:large", nil)
|
||||
w := newDiscardResponseWriter()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.status != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", w.status)
|
||||
}
|
||||
if w.bytes != blobSize {
|
||||
t.Fatalf("streamed bytes = %d, want %d", w.bytes, blobSize)
|
||||
}
|
||||
if got := w.Header().Get("Content-Length"); got != fmt.Sprintf("%d", blobSize) {
|
||||
t.Fatalf("Content-Length = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkProxyDockerRegistryBlobStreaming(b *testing.B) {
|
||||
const blobSize = 1 << 20
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", blobSize))
|
||||
_, _ = io.CopyN(w, zeroReader{}, blobSize)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
initDockerProxyTest(b, `
|
||||
[registries."bench.local"]
|
||||
upstream = "`+upstream.URL+`"
|
||||
authHost = "https://auth.bench.local/token"
|
||||
authType = "anonymous"
|
||||
enabled = true
|
||||
`)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(blobSize)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/bench.local/team/app/blobs/sha256:bench", nil)
|
||||
w := newDiscardResponseWriter()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.status != http.StatusOK || w.bytes != blobSize {
|
||||
b.Fatalf("status=%d bytes=%d", w.status, w.bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -140,15 +140,10 @@ func TestGitHubNoRouteRejectsUnsupportedHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerV2PingAndInvalidPath(t *testing.T) {
|
||||
func TestDockerV2InvalidPath(t *testing.T) {
|
||||
router := newTestRouter(t, "")
|
||||
|
||||
w := performRequest(router, http.MethodGet, "/v2/", "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("/v2/ status = %d, want 200; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
w = performRequest(router, http.MethodGet, "/v2/library/nginx/unknown/latest", "")
|
||||
w := performRequest(router, http.MethodGet, "/v2/library/nginx/unknown/latest", "")
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid v2 status = %d, want 400; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
+11
-3
@@ -102,10 +102,18 @@ func ExtractTTLFromResponse(responseBody []byte) time.Duration {
|
||||
defaultTTL := 30 * time.Minute
|
||||
|
||||
if json.Unmarshal(responseBody, &tokenResp) == nil && tokenResp.ExpiresIn > 0 {
|
||||
safeTTL := time.Duration(tokenResp.ExpiresIn-300) * time.Second
|
||||
if safeTTL > 5*time.Minute {
|
||||
return safeTTL
|
||||
expires := time.Duration(tokenResp.ExpiresIn) * time.Second
|
||||
skew := expires / 10
|
||||
if skew > 5*time.Minute {
|
||||
skew = 5 * time.Minute
|
||||
}
|
||||
if skew < 10*time.Second {
|
||||
skew = 10 * time.Second
|
||||
}
|
||||
if expires > skew {
|
||||
return expires - skew
|
||||
}
|
||||
return expires / 2
|
||||
}
|
||||
|
||||
return defaultTTL
|
||||
|
||||
@@ -34,6 +34,10 @@ func TestExtractTTLFromResponse(t *testing.T) {
|
||||
t.Fatalf("TTL = %s, want 55m", ttl)
|
||||
}
|
||||
|
||||
if ttl := ExtractTTLFromResponse([]byte(`{"expires_in":300}`)); ttl != 270*time.Second {
|
||||
t.Fatalf("short TTL = %s, want 270s", ttl)
|
||||
}
|
||||
|
||||
if ttl := ExtractTTLFromResponse([]byte(`{}`)); ttl != 30*time.Minute {
|
||||
t.Fatalf("default TTL = %s", ttl)
|
||||
}
|
||||
|
||||
+17
-11
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,6 +18,8 @@ const (
|
||||
MaxIPCacheSize = 10000
|
||||
)
|
||||
|
||||
var debugRateLimitLog = strings.EqualFold(os.Getenv("DEBUG_RATE_LIMIT_LOG"), "true")
|
||||
|
||||
// IPRateLimiter IP限流器结构体
|
||||
type IPRateLimiter struct {
|
||||
ips map[string]*rateLimiterEntry
|
||||
@@ -234,17 +237,20 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
||||
|
||||
cleanIP := extractIPFromAddress(ip)
|
||||
|
||||
normalizedIP := normalizeIPForRateLimit(cleanIP)
|
||||
if cleanIP != normalizedIP {
|
||||
fmt.Printf("请求IP: %s (提纯后: %s, 限流段: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
ip, cleanIP, normalizedIP,
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"))
|
||||
} else {
|
||||
fmt.Printf("请求IP: %s (提纯后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
ip, cleanIP,
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"))
|
||||
if debugRateLimitLog {
|
||||
normalizedIP := normalizeIPForRateLimit(cleanIP)
|
||||
if cleanIP != normalizedIP {
|
||||
fmt.Printf("请求IP: %s (提纯后: %s, 限流段: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
ip, cleanIP, normalizedIP,
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"))
|
||||
} else {
|
||||
fmt.Printf("请求IP: %s (提纯后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
ip, cleanIP,
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ipLimiter, allowed := limiter.GetLimiter(cleanIP)
|
||||
|
||||
Reference in New Issue
Block a user