Merge branch 'main' of http://1.117.67.95:3000/team/hyapi-server
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -49,4 +49,8 @@ resources/Pure_Component/
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
cmd/api/__debug_bin*
|
||||
cmd/api/__debug_bin*
|
||||
|
||||
# 戎行 OpenVPN 机密(仅保留 secrets/openvpn/.gitkeep)
|
||||
secrets/openvpn/*
|
||||
!secrets/openvpn/.gitkeep
|
||||
@@ -860,6 +860,8 @@ rongxing:
|
||||
app_id: "hykj"
|
||||
private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw=="
|
||||
timeout: 10s
|
||||
# 仅戎行请求走 VPN SOCKS;本机已连 OpenVPN GUI 时可留空。生产 Docker:socks5://rongxing-vpn:1080
|
||||
proxy: ""
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
|
||||
@@ -279,6 +279,8 @@ rongxing:
|
||||
app_id: "hykj"
|
||||
private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw=="
|
||||
timeout: 10s
|
||||
# 开发机用 OpenVPN GUI 直连时可留空;若走本地 Docker VPN 可填 socks5://rongxing-vpn:1080
|
||||
proxy: ""
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
|
||||
@@ -245,6 +245,8 @@ rongxing:
|
||||
app_id: "hykj"
|
||||
private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw=="
|
||||
timeout: 10s
|
||||
# 生产:戎行请求经 Docker 内 rongxing-vpn 的 SOCKS5 进企业内网
|
||||
proxy: "socks5://rongxing-vpn:1080"
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
|
||||
35
deployments/openvpn/Dockerfile
Normal file
35
deployments/openvpn/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# 戎行专用:OpenVPN 客户端 + microsocks SOCKS5(走系统路由/tun0)
|
||||
FROM alpine:3.19 AS socks-builder
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk add --no-cache build-base
|
||||
|
||||
WORKDIR /src
|
||||
COPY third_party/microsocks/ ./
|
||||
RUN make && strip microsocks
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk add --no-cache \
|
||||
openvpn \
|
||||
bash \
|
||||
iproute2 \
|
||||
curl
|
||||
|
||||
COPY --from=socks-builder /src/microsocks /usr/local/bin/microsocks
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/microsocks
|
||||
|
||||
ENV VPN_DIR=/vpn \
|
||||
SOCKS_HOST=0.0.0.0 \
|
||||
SOCKS_PORT=1080 \
|
||||
RONGXING_HOST=192.168.3.43
|
||||
|
||||
VOLUME ["/vpn"]
|
||||
EXPOSE 1080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=5 \
|
||||
CMD ip link show tun0 >/dev/null 2>&1 || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
14
deployments/openvpn/README.md
Normal file
14
deployments/openvpn/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Rongxing OpenVPN(部署附件)
|
||||
|
||||
完整方案见:[docs/戎行OpenVPN接入方案.md](../../docs/戎行OpenVPN接入方案.md)
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `Dockerfile` | OpenVPN + 自编译 microsocks |
|
||||
| `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` |
|
||||
| `third_party/microsocks/` | SOCKS5 源码(避免 Alpine 无包 / Dante 绑错网卡) |
|
||||
| `tiany0721.ovpn.example` | ovpn 模板 |
|
||||
| `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) |
|
||||
| `upload-checklist.txt` | 上传清单 |
|
||||
|
||||
**真实证书放到服务器 secrets 目录或 `./secrets/openvpn/`,不要提交 Git。**
|
||||
31
deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml
Normal file
31
deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# 已并入 docker-compose.prod.yml 的 rongxing-vpn 服务。
|
||||
# 保留本文件便于 1Panel 单独编排时参考。
|
||||
#
|
||||
# 1Panel 机密目录示例:
|
||||
# RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn
|
||||
#
|
||||
# 独立栈时需 external 网络(名称以 docker network ls 为准):
|
||||
# networks:
|
||||
# hyapi-network:
|
||||
# external: true
|
||||
# name: hyapi-server_hyapi-network
|
||||
|
||||
services:
|
||||
rongxing-vpn:
|
||||
build:
|
||||
context: ./deployments/openvpn
|
||||
dockerfile: Dockerfile
|
||||
image: hyapi-rongxing-vpn:local
|
||||
container_name: hyapi-rongxing-vpn
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
RONGXING_HOST: "192.168.3.43"
|
||||
volumes:
|
||||
- ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
networks:
|
||||
- hyapi-network
|
||||
restart: unless-stopped
|
||||
85
deployments/openvpn/entrypoint.sh
Normal file
85
deployments/openvpn/entrypoint.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# OpenVPN 连通后拉起 microsocks;出口跟随系统路由(含 tun0),避免 Dante 绑错网卡。
|
||||
set -euo pipefail
|
||||
|
||||
VPN_DIR="${VPN_DIR:-/vpn}"
|
||||
SOCKS_HOST="${SOCKS_HOST:-0.0.0.0}"
|
||||
SOCKS_PORT="${SOCKS_PORT:-1080}"
|
||||
RONGXING_HOST="${RONGXING_HOST:-192.168.3.43}"
|
||||
|
||||
cd "$VPN_DIR"
|
||||
|
||||
CONF=""
|
||||
for f in *.conf *.ovpn; do
|
||||
if [ -f "$f" ]; then
|
||||
CONF="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$CONF" ]; then
|
||||
echo "[rongxing-vpn] 未在 ${VPN_DIR} 找到 .ovpn/.conf,请挂载证书目录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[rongxing-vpn] 使用配置: ${CONF}"
|
||||
echo "[rongxing-vpn] 仅路由 ${RONGXING_HOST}/32,SOCKS5 ${SOCKS_HOST}:${SOCKS_PORT}"
|
||||
|
||||
openvpn \
|
||||
--config "$CONF" \
|
||||
--cd "$VPN_DIR" \
|
||||
--route-nopull \
|
||||
--route "$RONGXING_HOST" 255.255.255.255 \
|
||||
--daemon \
|
||||
--writepid /var/run/openvpn.pid \
|
||||
--log /var/log/openvpn.log \
|
||||
--verb 3
|
||||
|
||||
for i in $(seq 1 90); do
|
||||
if ip link show tun0 >/dev/null 2>&1; then
|
||||
echo "[rongxing-vpn] tun0 已就绪 (${i}s)"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 90 ]; then
|
||||
echo "[rongxing-vpn] 等待 tun0 超时,OpenVPN 日志:"
|
||||
cat /var/log/openvpn.log || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[rongxing-vpn] 路由表:"
|
||||
ip route || true
|
||||
|
||||
# 直连自检(不阻断)
|
||||
if curl -sS -m 5 -o /tmp/rx_probe.out -w "[rongxing-vpn] 直连探测 HTTP %{http_code}\n" "http://${RONGXING_HOST}:7007/" ; then
|
||||
head -c 200 /tmp/rx_probe.out 2>/dev/null || true
|
||||
echo
|
||||
else
|
||||
echo "[rongxing-vpn] 直连探测失败: http://${RONGXING_HOST}:7007/"
|
||||
fi
|
||||
|
||||
microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" &
|
||||
SOCKS_PID=$!
|
||||
sleep 1
|
||||
if ! kill -0 "$SOCKS_PID" 2>/dev/null; then
|
||||
echo "[rongxing-vpn] microsocks 启动失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (microsocks pid=${SOCKS_PID})"
|
||||
|
||||
cleanup() {
|
||||
kill "$SOCKS_PID" 2>/dev/null || true
|
||||
if [ -f /var/run/openvpn.pid ]; then
|
||||
kill "$(cat /var/run/openvpn.pid)" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
while kill -0 "$(cat /var/run/openvpn.pid)" 2>/dev/null && kill -0 "$SOCKS_PID" 2>/dev/null; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "[rongxing-vpn] OpenVPN 或 SOCKS 已退出"
|
||||
cat /var/log/openvpn.log || true
|
||||
exit 1
|
||||
6
deployments/openvpn/env.example
Normal file
6
deployments/openvpn/env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# 可选:复制为 .env 供 docker compose 读取
|
||||
# 1Panel 推荐:
|
||||
RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn
|
||||
|
||||
# 本地默认(不设则用 compose 内 ./secrets/openvpn):
|
||||
# RONGXING_VPN_CONFIG=./secrets/openvpn
|
||||
3
deployments/openvpn/third_party/microsocks/.gitignore
vendored
Normal file
3
deployments/openvpn/third_party/microsocks/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.o
|
||||
*.out
|
||||
|
||||
24
deployments/openvpn/third_party/microsocks/COPYING
vendored
Normal file
24
deployments/openvpn/third_party/microsocks/COPYING
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
microSocks is licensed under the following standard MIT license:
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Copyright © 2017 rofl0r.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
----------------------------------------------------------------------
|
||||
35
deployments/openvpn/third_party/microsocks/Makefile
vendored
Normal file
35
deployments/openvpn/third_party/microsocks/Makefile
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# if you want to change/override some variables, do so in a file called
|
||||
# config.mak, which is gets included automatically if it exists.
|
||||
|
||||
prefix = /usr/local
|
||||
bindir = $(prefix)/bin
|
||||
|
||||
PROG = microsocks
|
||||
SRCS = sockssrv.c server.c sblist.c sblist_delete.c
|
||||
OBJS = $(SRCS:.c=.o)
|
||||
|
||||
LIBS = -lpthread
|
||||
|
||||
CFLAGS += -Wall -std=c99
|
||||
|
||||
INSTALL = ./install.sh
|
||||
|
||||
-include config.mak
|
||||
|
||||
all: $(PROG)
|
||||
|
||||
install: $(PROG)
|
||||
$(INSTALL) -D -m 755 $(PROG) $(DESTDIR)$(bindir)/$(PROG)
|
||||
|
||||
clean:
|
||||
rm -f $(PROG)
|
||||
rm -f $(OBJS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(INC) $(PIC) -c -o $@ $<
|
||||
|
||||
$(PROG): $(OBJS)
|
||||
$(CC) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
.PHONY: all clean install
|
||||
|
||||
65
deployments/openvpn/third_party/microsocks/README.md
vendored
Normal file
65
deployments/openvpn/third_party/microsocks/README.md
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
MicroSocks - multithreaded, small, efficient SOCKS5 server.
|
||||
===========================================================
|
||||
|
||||
a SOCKS5 service that you can run on your remote boxes to tunnel connections
|
||||
through them, if for some reason SSH doesn't cut it for you.
|
||||
|
||||
It's very lightweight, and very light on resources too:
|
||||
|
||||
for every client, a thread with a stack size of 8KB is spawned.
|
||||
the main process basically doesn't consume any resources at all.
|
||||
|
||||
the only limits are the amount of file descriptors and the RAM.
|
||||
|
||||
It's also designed to be robust: it handles resource exhaustion
|
||||
gracefully by simply denying new connections, instead of calling abort()
|
||||
as most other programs do these days.
|
||||
|
||||
another plus is ease-of-use: no config file necessary, everything can be
|
||||
done from the command line and doesn't even need any parameters for quick
|
||||
setup.
|
||||
|
||||
History
|
||||
-------
|
||||
|
||||
This is the successor of "rocksocks5", and it was written with
|
||||
different goals in mind:
|
||||
|
||||
- prefer usage of standard libc functions over homegrown ones
|
||||
- no artificial limits
|
||||
- do not aim for minimal binary size, but for minimal source code size,
|
||||
and maximal readability, reusability, and extensibility.
|
||||
|
||||
as a result of that, ipv4, dns, and ipv6 is supported out of the box
|
||||
and can use the same code, while rocksocks5 has several compile time
|
||||
defines to bring down the size of the resulting binary to extreme values
|
||||
like 10 KB static linked when only ipv4 support is enabled.
|
||||
|
||||
still, if optimized for size, *this* program when static linked against musl
|
||||
libc is not even 50 KB. that's easily usable even on the cheapest routers.
|
||||
|
||||
command line options
|
||||
--------------------
|
||||
|
||||
microsocks -1 -i listenip -p port -u user -P password -b bindaddr
|
||||
|
||||
all arguments are optional.
|
||||
by default listenip is 0.0.0.0 and port 1080.
|
||||
|
||||
option -1 activates auth_once mode: once a specific ip address
|
||||
authed successfully with user/pass, it is added to a whitelist
|
||||
and may use the proxy without auth.
|
||||
this is handy for programs like firefox that don't support
|
||||
user/pass auth. for it to work you'd basically make one connection
|
||||
with another program that supports it, and then you can use firefox too.
|
||||
for example, authenticate once using curl:
|
||||
|
||||
curl --socks5 user:password@listenip:port anyurl
|
||||
|
||||
|
||||
Supported SOCKS5 Features
|
||||
-------------------------
|
||||
- authentication: none, password, one-time
|
||||
- IPv4, IPv6, DNS
|
||||
- TCP (no UDP at this time)
|
||||
|
||||
28
deployments/openvpn/third_party/microsocks/create-dist.sh
vendored
Normal file
28
deployments/openvpn/third_party/microsocks/create-dist.sh
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
if [ -z "$VER" ] ; then
|
||||
echo set VER!
|
||||
exit
|
||||
fi
|
||||
me=`pwd`
|
||||
|
||||
proj=microsocks
|
||||
projver=${proj}-${VER}
|
||||
|
||||
tempdir=/tmp/${proj}-0000
|
||||
rm -rf "$tempdir"
|
||||
mkdir -p "$tempdir"
|
||||
|
||||
cd $tempdir
|
||||
GITDIR=https://github.com/rofl0r/$proj
|
||||
GITDIR=$me
|
||||
git clone "$GITDIR" $projver
|
||||
|
||||
rm -rf $projver/.git
|
||||
rm -rf $projver/docs
|
||||
rm -f $projver/.gitignore
|
||||
rm -f $projver/create-dist.sh
|
||||
|
||||
tar cf $proj.tar $projver/
|
||||
xz -z -9 -e $proj.tar
|
||||
mv $proj.tar.xz $me/$projver.tar.xz
|
||||
rm -rf "$tempdir"
|
||||
67
deployments/openvpn/third_party/microsocks/install.sh
vendored
Normal file
67
deployments/openvpn/third_party/microsocks/install.sh
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Written by Rich Felker, originally as part of musl libc.
|
||||
# Multi-licensed under MIT, 0BSD, and CC0.
|
||||
#
|
||||
# This is an actually-safe install command which installs the new
|
||||
# file atomically in the new location, rather than overwriting
|
||||
# existing files.
|
||||
#
|
||||
|
||||
usage() {
|
||||
printf "usage: %s [-D] [-l] [-m mode] src dest\n" "$0" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdirp=
|
||||
symlink=
|
||||
mode=755
|
||||
|
||||
while getopts Dlm: name ; do
|
||||
case "$name" in
|
||||
D) mkdirp=yes ;;
|
||||
l) symlink=yes ;;
|
||||
m) mode=$OPTARG ;;
|
||||
?) usage ;;
|
||||
esac
|
||||
done
|
||||
shift $(($OPTIND - 1))
|
||||
|
||||
test "$#" -eq 2 || usage
|
||||
src=$1
|
||||
dst=$2
|
||||
tmp="$dst.tmp.$$"
|
||||
|
||||
case "$dst" in
|
||||
*/) printf "%s: %s ends in /\n", "$0" "$dst" 1>&2 ; exit 1 ;;
|
||||
esac
|
||||
|
||||
set -C
|
||||
set -e
|
||||
|
||||
if test "$mkdirp" ; then
|
||||
umask 022
|
||||
case "$2" in
|
||||
*/*) mkdir -p "${dst%/*}" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
trap 'rm -f "$tmp"' EXIT INT QUIT TERM HUP
|
||||
|
||||
umask 077
|
||||
|
||||
if test "$symlink" ; then
|
||||
ln -s "$1" "$tmp"
|
||||
else
|
||||
cat < "$1" > "$tmp"
|
||||
chmod "$mode" "$tmp"
|
||||
fi
|
||||
|
||||
mv -f "$tmp" "$2"
|
||||
test -d "$2" && {
|
||||
rm -f "$2/$tmp"
|
||||
printf "%s: %s is a directory\n" "$0" "$dst" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
73
deployments/openvpn/third_party/microsocks/sblist.c
vendored
Normal file
73
deployments/openvpn/third_party/microsocks/sblist.c
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
#undef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "sblist.h"
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define MY_PAGE_SIZE 4096
|
||||
|
||||
sblist* sblist_new(size_t itemsize, size_t blockitems) {
|
||||
sblist* ret = (sblist*) malloc(sizeof(sblist));
|
||||
sblist_init(ret, itemsize, blockitems);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void sblist_clear(sblist* l) {
|
||||
l->items = NULL;
|
||||
l->capa = 0;
|
||||
l->count = 0;
|
||||
}
|
||||
|
||||
void sblist_init(sblist* l, size_t itemsize, size_t blockitems) {
|
||||
if(l) {
|
||||
l->blockitems = blockitems ? blockitems : MY_PAGE_SIZE / itemsize;
|
||||
l->itemsize = itemsize;
|
||||
sblist_clear(l);
|
||||
}
|
||||
}
|
||||
|
||||
void sblist_free_items(sblist* l) {
|
||||
if(l) {
|
||||
if(l->items) free(l->items);
|
||||
sblist_clear(l);
|
||||
}
|
||||
}
|
||||
|
||||
void sblist_free(sblist* l) {
|
||||
if(l) {
|
||||
sblist_free_items(l);
|
||||
free(l);
|
||||
}
|
||||
}
|
||||
|
||||
char* sblist_item_from_index(sblist* l, size_t idx) {
|
||||
return l->items + (idx * l->itemsize);
|
||||
}
|
||||
|
||||
void* sblist_get(sblist* l, size_t item) {
|
||||
if(item < l->count) return (void*) sblist_item_from_index(l, item);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int sblist_set(sblist* l, void* item, size_t pos) {
|
||||
if(pos >= l->count) return 0;
|
||||
memcpy(sblist_item_from_index(l, pos), item, l->itemsize);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sblist_grow_if_needed(sblist* l) {
|
||||
char* temp;
|
||||
if(l->count == l->capa) {
|
||||
temp = realloc(l->items, (l->capa + l->blockitems) * l->itemsize);
|
||||
if(!temp) return 0;
|
||||
l->capa += l->blockitems;
|
||||
l->items = temp;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sblist_add(sblist* l, void* item) {
|
||||
if(!sblist_grow_if_needed(l)) return 0;
|
||||
l->count++;
|
||||
return sblist_set(l, item, l->count - 1);
|
||||
}
|
||||
92
deployments/openvpn/third_party/microsocks/sblist.h
vendored
Normal file
92
deployments/openvpn/third_party/microsocks/sblist.h
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
#ifndef SBLIST_H
|
||||
#define SBLIST_H
|
||||
|
||||
/* this file is part of libulz, as of commit 8ab361a27743aaf025323ee43b8b8876dc054fdd
|
||||
modified for direct inclusion in microsocks. */
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
/*
|
||||
* simple buffer list.
|
||||
*
|
||||
* this thing here is basically a generic dynamic array
|
||||
* will realloc after every blockitems inserts
|
||||
* can store items of any size.
|
||||
*
|
||||
* so think of it as a by-value list, as opposed to a typical by-ref list.
|
||||
* you typically use it by having some struct on the stack, and pass a pointer
|
||||
* to sblist_add, which will copy the contents into its internal memory.
|
||||
*
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
size_t itemsize;
|
||||
size_t blockitems;
|
||||
size_t count;
|
||||
size_t capa;
|
||||
char* items;
|
||||
} sblist;
|
||||
|
||||
#define sblist_getsize(X) ((X)->count)
|
||||
#define sblist_get_count(X) ((X)->count)
|
||||
#define sblist_empty(X) ((X)->count == 0)
|
||||
|
||||
// for dynamic style
|
||||
sblist* sblist_new(size_t itemsize, size_t blockitems);
|
||||
void sblist_free(sblist* l);
|
||||
|
||||
//for static style
|
||||
void sblist_init(sblist* l, size_t itemsize, size_t blockitems);
|
||||
void sblist_free_items(sblist* l);
|
||||
|
||||
// accessors
|
||||
void* sblist_get(sblist* l, size_t item);
|
||||
// returns 1 on success, 0 on OOM
|
||||
int sblist_add(sblist* l, void* item);
|
||||
int sblist_set(sblist* l, void* item, size_t pos);
|
||||
void sblist_delete(sblist* l, size_t item);
|
||||
char* sblist_item_from_index(sblist* l, size_t idx);
|
||||
int sblist_grow_if_needed(sblist* l);
|
||||
int sblist_insert(sblist* l, void* item, size_t pos);
|
||||
/* same as sblist_add, but returns list index of new item, or -1 */
|
||||
size_t sblist_addi(sblist* l, void* item);
|
||||
void sblist_sort(sblist *l, int (*compar)(const void *, const void *));
|
||||
/* insert element into presorted list, returns listindex of new entry or -1*/
|
||||
size_t sblist_insert_sorted(sblist* l, void* o, int (*compar)(const void *, const void *));
|
||||
|
||||
#ifndef __COUNTER__
|
||||
#define __COUNTER__ __LINE__
|
||||
#endif
|
||||
|
||||
#define __sblist_concat_impl( x, y ) x##y
|
||||
#define __sblist_macro_concat( x, y ) __sblist_concat_impl( x, y )
|
||||
#define __sblist_iterator_name __sblist_macro_concat(sblist_iterator, __COUNTER__)
|
||||
|
||||
// use with custom iterator variable
|
||||
#define sblist_iter_counter(LIST, ITER, PTR) \
|
||||
for(size_t ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++)
|
||||
|
||||
// use with custom iterator variable, which is predeclared
|
||||
#define sblist_iter_counter2(LIST, ITER, PTR) \
|
||||
for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++)
|
||||
|
||||
// use with custom iterator variable, which is predeclared and signed
|
||||
// useful for a loop which can delete items from the list, and then decrease the iterator var.
|
||||
#define sblist_iter_counter2s(LIST, ITER, PTR) \
|
||||
for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < (ssize_t) sblist_getsize(LIST); ITER++)
|
||||
|
||||
|
||||
// uses "magic" iterator variable
|
||||
#define sblist_iter(LIST, PTR) sblist_iter_counter(LIST, __sblist_iterator_name, PTR)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma RcB2 DEP "sblist.c" "sblist_delete.c"
|
||||
|
||||
#endif
|
||||
9
deployments/openvpn/third_party/microsocks/sblist_delete.c
vendored
Normal file
9
deployments/openvpn/third_party/microsocks/sblist_delete.c
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
#include "sblist.h"
|
||||
#include <string.h>
|
||||
|
||||
void sblist_delete(sblist* l, size_t item) {
|
||||
if (l->count && item < l->count) {
|
||||
memmove(sblist_item_from_index(l, item), sblist_item_from_index(l, item + 1), (sblist_getsize(l) - (item + 1)) * l->itemsize);
|
||||
l->count--;
|
||||
}
|
||||
}
|
||||
64
deployments/openvpn/third_party/microsocks/server.c
vendored
Normal file
64
deployments/openvpn/third_party/microsocks/server.c
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "server.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int resolve(const char *host, unsigned short port, struct addrinfo** addr) {
|
||||
struct addrinfo hints = {
|
||||
.ai_family = AF_UNSPEC,
|
||||
.ai_socktype = SOCK_STREAM,
|
||||
.ai_flags = AI_PASSIVE,
|
||||
};
|
||||
char port_buf[8];
|
||||
snprintf(port_buf, sizeof port_buf, "%u", port);
|
||||
return getaddrinfo(host, port_buf, &hints, addr);
|
||||
}
|
||||
|
||||
int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res) {
|
||||
struct addrinfo *ainfo = 0;
|
||||
int ret;
|
||||
SOCKADDR_UNION_AF(res) = AF_UNSPEC;
|
||||
if((ret = resolve(host, port, &ainfo))) return ret;
|
||||
memcpy(res, ainfo->ai_addr, ainfo->ai_addrlen);
|
||||
freeaddrinfo(ainfo);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bindtoip(int fd, union sockaddr_union *bindaddr) {
|
||||
socklen_t sz = SOCKADDR_UNION_LENGTH(bindaddr);
|
||||
if(sz)
|
||||
return bind(fd, (struct sockaddr*) bindaddr, sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int server_waitclient(struct server *server, struct client* client) {
|
||||
socklen_t clen = sizeof client->addr;
|
||||
return ((client->fd = accept(server->fd, (void*)&client->addr, &clen)) == -1)*-1;
|
||||
}
|
||||
|
||||
int server_setup(struct server *server, const char* listenip, unsigned short port) {
|
||||
struct addrinfo *ainfo = 0;
|
||||
if(resolve(listenip, port, &ainfo)) return -1;
|
||||
struct addrinfo* p;
|
||||
int listenfd = -1;
|
||||
for(p = ainfo; p; p = p->ai_next) {
|
||||
if((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
|
||||
continue;
|
||||
int yes = 1;
|
||||
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
|
||||
if(bind(listenfd, p->ai_addr, p->ai_addrlen) < 0) {
|
||||
close(listenfd);
|
||||
listenfd = -1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
freeaddrinfo(ainfo);
|
||||
if(listenfd < 0) return -2;
|
||||
if(listen(listenfd, SOMAXCONN) < 0) {
|
||||
close(listenfd);
|
||||
return -3;
|
||||
}
|
||||
server->fd = listenfd;
|
||||
return 0;
|
||||
}
|
||||
49
deployments/openvpn/third_party/microsocks/server.h
vendored
Normal file
49
deployments/openvpn/third_party/microsocks/server.h
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef SERVER_H
|
||||
#define SERVER_H
|
||||
|
||||
#undef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#pragma RcB2 DEP "server.c"
|
||||
|
||||
union sockaddr_union {
|
||||
struct sockaddr_in v4;
|
||||
struct sockaddr_in6 v6;
|
||||
};
|
||||
|
||||
#define SOCKADDR_UNION_AF(PTR) (PTR)->v4.sin_family
|
||||
|
||||
#define SOCKADDR_UNION_LENGTH(PTR) ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? sizeof((PTR)->v4) : ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? sizeof((PTR)->v6) : 0 ) )
|
||||
|
||||
#define SOCKADDR_UNION_ADDRESS(PTR) ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (void*) &(PTR)->v4.sin_addr : ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (void*) &(PTR)->v6.sin6_addr : (void*) 0 ) )
|
||||
|
||||
#define SOCKADDR_UNION_PORT(PTR) ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (PTR)->v4.sin_port : ( \
|
||||
( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (PTR)->v6.sin6_port : 0 ) )
|
||||
|
||||
struct client {
|
||||
union sockaddr_union addr;
|
||||
int fd;
|
||||
};
|
||||
|
||||
struct server {
|
||||
int fd;
|
||||
};
|
||||
|
||||
int resolve(const char *host, unsigned short port, struct addrinfo** addr);
|
||||
int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res);
|
||||
int bindtoip(int fd, union sockaddr_union *bindaddr);
|
||||
|
||||
int server_waitclient(struct server *server, struct client* client);
|
||||
int server_setup(struct server *server, const char* listenip, unsigned short port);
|
||||
|
||||
#endif
|
||||
|
||||
482
deployments/openvpn/third_party/microsocks/sockssrv.c
vendored
Normal file
482
deployments/openvpn/third_party/microsocks/sockssrv.c
vendored
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
MicroSocks - multithreaded, small, efficient SOCKS5 server.
|
||||
|
||||
Copyright (C) 2017 rofl0r.
|
||||
|
||||
This is the successor of "rocksocks5", and it was written with
|
||||
different goals in mind:
|
||||
|
||||
- prefer usage of standard libc functions over homegrown ones
|
||||
- no artificial limits
|
||||
- do not aim for minimal binary size, but for minimal source code size,
|
||||
and maximal readability, reusability, and extensibility.
|
||||
|
||||
as a result of that, ipv4, dns, and ipv6 is supported out of the box
|
||||
and can use the same code, while rocksocks5 has several compile time
|
||||
defines to bring down the size of the resulting binary to extreme values
|
||||
like 10 KB static linked when only ipv4 support is enabled.
|
||||
|
||||
still, if optimized for size, *this* program when static linked against musl
|
||||
libc is not even 50 KB. that's easily usable even on the cheapest routers.
|
||||
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <unistd.h>
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <poll.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include "server.h"
|
||||
#include "sblist.h"
|
||||
|
||||
/* timeout in microseconds on resource exhaustion to prevent excessive
|
||||
cpu usage. */
|
||||
#ifndef FAILURE_TIMEOUT
|
||||
#define FAILURE_TIMEOUT 64
|
||||
#endif
|
||||
|
||||
#ifndef MAX
|
||||
#define MAX(x, y) ((x) > (y) ? (x) : (y))
|
||||
#endif
|
||||
|
||||
#ifdef PTHREAD_STACK_MIN
|
||||
#define THREAD_STACK_SIZE MAX(8*1024, PTHREAD_STACK_MIN)
|
||||
#else
|
||||
#define THREAD_STACK_SIZE 64*1024
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#undef THREAD_STACK_SIZE
|
||||
#define THREAD_STACK_SIZE 64*1024
|
||||
#elif defined(__GLIBC__) || defined(__FreeBSD__)
|
||||
#undef THREAD_STACK_SIZE
|
||||
#define THREAD_STACK_SIZE 32*1024
|
||||
#endif
|
||||
|
||||
static const char* auth_user;
|
||||
static const char* auth_pass;
|
||||
static sblist* auth_ips;
|
||||
static pthread_rwlock_t auth_ips_lock = PTHREAD_RWLOCK_INITIALIZER;
|
||||
static const struct server* server;
|
||||
static union sockaddr_union bind_addr = {.v4.sin_family = AF_UNSPEC};
|
||||
|
||||
enum socksstate {
|
||||
SS_1_CONNECTED,
|
||||
SS_2_NEED_AUTH, /* skipped if NO_AUTH method supported */
|
||||
SS_3_AUTHED,
|
||||
};
|
||||
|
||||
enum authmethod {
|
||||
AM_NO_AUTH = 0,
|
||||
AM_GSSAPI = 1,
|
||||
AM_USERNAME = 2,
|
||||
AM_INVALID = 0xFF
|
||||
};
|
||||
|
||||
enum errorcode {
|
||||
EC_SUCCESS = 0,
|
||||
EC_GENERAL_FAILURE = 1,
|
||||
EC_NOT_ALLOWED = 2,
|
||||
EC_NET_UNREACHABLE = 3,
|
||||
EC_HOST_UNREACHABLE = 4,
|
||||
EC_CONN_REFUSED = 5,
|
||||
EC_TTL_EXPIRED = 6,
|
||||
EC_COMMAND_NOT_SUPPORTED = 7,
|
||||
EC_ADDRESSTYPE_NOT_SUPPORTED = 8,
|
||||
};
|
||||
|
||||
struct thread {
|
||||
pthread_t pt;
|
||||
struct client client;
|
||||
enum socksstate state;
|
||||
volatile int done;
|
||||
};
|
||||
|
||||
#ifndef CONFIG_LOG
|
||||
#define CONFIG_LOG 1
|
||||
#endif
|
||||
#if CONFIG_LOG
|
||||
/* we log to stderr because it's not using line buffering, i.e. malloc which would need
|
||||
locking when called from different threads. for the same reason we use dprintf,
|
||||
which writes directly to an fd. */
|
||||
#define dolog(...) dprintf(2, __VA_ARGS__)
|
||||
#else
|
||||
static void dolog(const char* fmt, ...) { }
|
||||
#endif
|
||||
|
||||
static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* bind_addr) {
|
||||
int af = SOCKADDR_UNION_AF(bind_addr);
|
||||
if(af == AF_UNSPEC) return list;
|
||||
struct addrinfo* p;
|
||||
for(p=list; p; p=p->ai_next)
|
||||
if(p->ai_family == af) return p;
|
||||
return list;
|
||||
}
|
||||
|
||||
static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) {
|
||||
if(n < 5) return -EC_GENERAL_FAILURE;
|
||||
if(buf[0] != 5) return -EC_GENERAL_FAILURE;
|
||||
if(buf[1] != 1) return -EC_COMMAND_NOT_SUPPORTED; /* we support only CONNECT method */
|
||||
if(buf[2] != 0) return -EC_GENERAL_FAILURE; /* malformed packet */
|
||||
|
||||
int af = AF_INET;
|
||||
size_t minlen = 4 + 4 + 2, l;
|
||||
char namebuf[256];
|
||||
struct addrinfo* remote;
|
||||
|
||||
switch(buf[3]) {
|
||||
case 4: /* ipv6 */
|
||||
af = AF_INET6;
|
||||
minlen = 4 + 2 + 16;
|
||||
/* fall through */
|
||||
case 1: /* ipv4 */
|
||||
if(n < minlen) return -EC_GENERAL_FAILURE;
|
||||
if(namebuf != inet_ntop(af, buf+4, namebuf, sizeof namebuf))
|
||||
return -EC_GENERAL_FAILURE; /* malformed or too long addr */
|
||||
break;
|
||||
case 3: /* dns name */
|
||||
l = buf[4];
|
||||
minlen = 4 + 2 + l + 1;
|
||||
if(n < 4 + 2 + l + 1) return -EC_GENERAL_FAILURE;
|
||||
memcpy(namebuf, buf+4+1, l);
|
||||
namebuf[l] = 0;
|
||||
break;
|
||||
default:
|
||||
return -EC_ADDRESSTYPE_NOT_SUPPORTED;
|
||||
}
|
||||
unsigned short port;
|
||||
port = (buf[minlen-2] << 8) | buf[minlen-1];
|
||||
/* there's no suitable errorcode in rfc1928 for dns lookup failure */
|
||||
if(resolve(namebuf, port, &remote)) return -EC_GENERAL_FAILURE;
|
||||
struct addrinfo* raddr = addr_choose(remote, &bind_addr);
|
||||
int fd = socket(raddr->ai_family, SOCK_STREAM, 0);
|
||||
if(fd == -1) {
|
||||
eval_errno:
|
||||
if(fd != -1) close(fd);
|
||||
freeaddrinfo(remote);
|
||||
switch(errno) {
|
||||
case ETIMEDOUT:
|
||||
return -EC_TTL_EXPIRED;
|
||||
case EPROTOTYPE:
|
||||
case EPROTONOSUPPORT:
|
||||
case EAFNOSUPPORT:
|
||||
return -EC_ADDRESSTYPE_NOT_SUPPORTED;
|
||||
case ECONNREFUSED:
|
||||
return -EC_CONN_REFUSED;
|
||||
case ENETDOWN:
|
||||
case ENETUNREACH:
|
||||
return -EC_NET_UNREACHABLE;
|
||||
case EHOSTUNREACH:
|
||||
return -EC_HOST_UNREACHABLE;
|
||||
case EBADF:
|
||||
default:
|
||||
perror("socket/connect");
|
||||
return -EC_GENERAL_FAILURE;
|
||||
}
|
||||
}
|
||||
if(SOCKADDR_UNION_AF(&bind_addr) == raddr->ai_family &&
|
||||
bindtoip(fd, &bind_addr) == -1)
|
||||
goto eval_errno;
|
||||
if(connect(fd, raddr->ai_addr, raddr->ai_addrlen) == -1)
|
||||
goto eval_errno;
|
||||
|
||||
freeaddrinfo(remote);
|
||||
if(CONFIG_LOG) {
|
||||
char clientname[256];
|
||||
af = SOCKADDR_UNION_AF(&client->addr);
|
||||
void *ipdata = SOCKADDR_UNION_ADDRESS(&client->addr);
|
||||
inet_ntop(af, ipdata, clientname, sizeof clientname);
|
||||
dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int is_authed(union sockaddr_union *client, union sockaddr_union *authedip) {
|
||||
int af = SOCKADDR_UNION_AF(authedip);
|
||||
if(af == SOCKADDR_UNION_AF(client)) {
|
||||
size_t cmpbytes = af == AF_INET ? 4 : 16;
|
||||
void *cmp1 = SOCKADDR_UNION_ADDRESS(client);
|
||||
void *cmp2 = SOCKADDR_UNION_ADDRESS(authedip);
|
||||
if(!memcmp(cmp1, cmp2, cmpbytes)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_in_authed_list(union sockaddr_union *caddr) {
|
||||
size_t i;
|
||||
for(i=0;i<sblist_getsize(auth_ips);i++)
|
||||
if(is_authed(caddr, sblist_get(auth_ips, i)))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void add_auth_ip(union sockaddr_union *caddr) {
|
||||
sblist_add(auth_ips, caddr);
|
||||
}
|
||||
|
||||
static enum authmethod check_auth_method(unsigned char *buf, size_t n, struct client*client) {
|
||||
if(buf[0] != 5) return AM_INVALID;
|
||||
size_t idx = 1;
|
||||
if(idx >= n ) return AM_INVALID;
|
||||
int n_methods = buf[idx];
|
||||
idx++;
|
||||
while(idx < n && n_methods > 0) {
|
||||
if(buf[idx] == AM_NO_AUTH) {
|
||||
if(!auth_user) return AM_NO_AUTH;
|
||||
else if(auth_ips) {
|
||||
int authed = 0;
|
||||
if(pthread_rwlock_rdlock(&auth_ips_lock) == 0) {
|
||||
authed = is_in_authed_list(&client->addr);
|
||||
pthread_rwlock_unlock(&auth_ips_lock);
|
||||
}
|
||||
if(authed) return AM_NO_AUTH;
|
||||
}
|
||||
} else if(buf[idx] == AM_USERNAME) {
|
||||
if(auth_user) return AM_USERNAME;
|
||||
}
|
||||
idx++;
|
||||
n_methods--;
|
||||
}
|
||||
return AM_INVALID;
|
||||
}
|
||||
|
||||
static void send_auth_response(int fd, int version, enum authmethod meth) {
|
||||
unsigned char buf[2];
|
||||
buf[0] = version;
|
||||
buf[1] = meth;
|
||||
write(fd, buf, 2);
|
||||
}
|
||||
|
||||
static void send_error(int fd, enum errorcode ec) {
|
||||
/* position 4 contains ATYP, the address type, which is the same as used in the connect
|
||||
request. we're lazy and return always IPV4 address type in errors. */
|
||||
char buf[10] = { 5, ec, 0, 1 /*AT_IPV4*/, 0,0,0,0, 0,0 };
|
||||
write(fd, buf, 10);
|
||||
}
|
||||
|
||||
static void copyloop(int fd1, int fd2) {
|
||||
struct pollfd fds[2] = {
|
||||
[0] = {.fd = fd1, .events = POLLIN},
|
||||
[1] = {.fd = fd2, .events = POLLIN},
|
||||
};
|
||||
|
||||
while(1) {
|
||||
/* inactive connections are reaped after 15 min to free resources.
|
||||
usually programs send keep-alive packets so this should only happen
|
||||
when a connection is really unused. */
|
||||
switch(poll(fds, 2, 60*15*1000)) {
|
||||
case 0:
|
||||
return;
|
||||
case -1:
|
||||
if(errno == EINTR || errno == EAGAIN) continue;
|
||||
else perror("poll");
|
||||
return;
|
||||
}
|
||||
int infd = (fds[0].revents & POLLIN) ? fd1 : fd2;
|
||||
int outfd = infd == fd2 ? fd1 : fd2;
|
||||
char buf[1024];
|
||||
ssize_t sent = 0, n = read(infd, buf, sizeof buf);
|
||||
if(n <= 0) return;
|
||||
while(sent < n) {
|
||||
ssize_t m = write(outfd, buf+sent, n-sent);
|
||||
if(m < 0) return;
|
||||
sent += m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static enum errorcode check_credentials(unsigned char* buf, size_t n) {
|
||||
if(n < 5) return EC_GENERAL_FAILURE;
|
||||
if(buf[0] != 1) return EC_GENERAL_FAILURE;
|
||||
unsigned ulen, plen;
|
||||
ulen=buf[1];
|
||||
if(n < 2 + ulen + 2) return EC_GENERAL_FAILURE;
|
||||
plen=buf[2+ulen];
|
||||
if(n < 2 + ulen + 1 + plen) return EC_GENERAL_FAILURE;
|
||||
char user[256], pass[256];
|
||||
memcpy(user, buf+2, ulen);
|
||||
memcpy(pass, buf+2+ulen+1, plen);
|
||||
user[ulen] = 0;
|
||||
pass[plen] = 0;
|
||||
if(!strcmp(user, auth_user) && !strcmp(pass, auth_pass)) return EC_SUCCESS;
|
||||
return EC_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
static void* clientthread(void *data) {
|
||||
struct thread *t = data;
|
||||
t->state = SS_1_CONNECTED;
|
||||
unsigned char buf[1024];
|
||||
ssize_t n;
|
||||
int ret;
|
||||
int remotefd = -1;
|
||||
enum authmethod am;
|
||||
while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) {
|
||||
switch(t->state) {
|
||||
case SS_1_CONNECTED:
|
||||
am = check_auth_method(buf, n, &t->client);
|
||||
if(am == AM_NO_AUTH) t->state = SS_3_AUTHED;
|
||||
else if (am == AM_USERNAME) t->state = SS_2_NEED_AUTH;
|
||||
send_auth_response(t->client.fd, 5, am);
|
||||
if(am == AM_INVALID) goto breakloop;
|
||||
break;
|
||||
case SS_2_NEED_AUTH:
|
||||
ret = check_credentials(buf, n);
|
||||
send_auth_response(t->client.fd, 1, ret);
|
||||
if(ret != EC_SUCCESS)
|
||||
goto breakloop;
|
||||
t->state = SS_3_AUTHED;
|
||||
if(auth_ips && !pthread_rwlock_wrlock(&auth_ips_lock)) {
|
||||
if(!is_in_authed_list(&t->client.addr))
|
||||
add_auth_ip(&t->client.addr);
|
||||
pthread_rwlock_unlock(&auth_ips_lock);
|
||||
}
|
||||
break;
|
||||
case SS_3_AUTHED:
|
||||
ret = connect_socks_target(buf, n, &t->client);
|
||||
if(ret < 0) {
|
||||
send_error(t->client.fd, ret*-1);
|
||||
goto breakloop;
|
||||
}
|
||||
remotefd = ret;
|
||||
send_error(t->client.fd, EC_SUCCESS);
|
||||
copyloop(t->client.fd, remotefd);
|
||||
goto breakloop;
|
||||
|
||||
}
|
||||
}
|
||||
breakloop:
|
||||
|
||||
if(remotefd != -1)
|
||||
close(remotefd);
|
||||
|
||||
close(t->client.fd);
|
||||
t->done = 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void collect(sblist *threads) {
|
||||
size_t i;
|
||||
for(i=0;i<sblist_getsize(threads);) {
|
||||
struct thread* thread = *((struct thread**)sblist_get(threads, i));
|
||||
if(thread->done) {
|
||||
pthread_join(thread->pt, 0);
|
||||
sblist_delete(threads, i);
|
||||
free(thread);
|
||||
} else
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static int usage(void) {
|
||||
dprintf(2,
|
||||
"MicroSocks SOCKS5 Server\n"
|
||||
"------------------------\n"
|
||||
"usage: microsocks -1 -i listenip -p port -u user -P password -b bindaddr\n"
|
||||
"all arguments are optional.\n"
|
||||
"by default listenip is 0.0.0.0 and port 1080.\n\n"
|
||||
"option -b specifies which ip outgoing connections are bound to\n"
|
||||
"option -1 activates auth_once mode: once a specific ip address\n"
|
||||
"authed successfully with user/pass, it is added to a whitelist\n"
|
||||
"and may use the proxy without auth.\n"
|
||||
"this is handy for programs like firefox that don't support\n"
|
||||
"user/pass auth. for it to work you'd basically make one connection\n"
|
||||
"with another program that supports it, and then you can use firefox too.\n"
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* prevent username and password from showing up in top. */
|
||||
static void zero_arg(char *s) {
|
||||
size_t i, l = strlen(s);
|
||||
for(i=0;i<l;i++) s[i] = 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int ch;
|
||||
const char *listenip = "0.0.0.0";
|
||||
unsigned port = 1080;
|
||||
while((ch = getopt(argc, argv, ":1b:i:p:u:P:")) != -1) {
|
||||
switch(ch) {
|
||||
case '1':
|
||||
auth_ips = sblist_new(sizeof(union sockaddr_union), 8);
|
||||
break;
|
||||
case 'b':
|
||||
resolve_sa(optarg, 0, &bind_addr);
|
||||
break;
|
||||
case 'u':
|
||||
auth_user = strdup(optarg);
|
||||
zero_arg(optarg);
|
||||
break;
|
||||
case 'P':
|
||||
auth_pass = strdup(optarg);
|
||||
zero_arg(optarg);
|
||||
break;
|
||||
case 'i':
|
||||
listenip = optarg;
|
||||
break;
|
||||
case 'p':
|
||||
port = atoi(optarg);
|
||||
break;
|
||||
case ':':
|
||||
dprintf(2, "error: option -%c requires an operand\n", optopt);
|
||||
/* fall through */
|
||||
case '?':
|
||||
return usage();
|
||||
}
|
||||
}
|
||||
if((auth_user && !auth_pass) || (!auth_user && auth_pass)) {
|
||||
dprintf(2, "error: user and pass must be used together\n");
|
||||
return 1;
|
||||
}
|
||||
if(auth_ips && !auth_pass) {
|
||||
dprintf(2, "error: auth-once option must be used together with user/pass\n");
|
||||
return 1;
|
||||
}
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
struct server s;
|
||||
sblist *threads = sblist_new(sizeof (struct thread*), 8);
|
||||
if(server_setup(&s, listenip, port)) {
|
||||
perror("server_setup");
|
||||
return 1;
|
||||
}
|
||||
server = &s;
|
||||
|
||||
while(1) {
|
||||
collect(threads);
|
||||
struct client c;
|
||||
struct thread *curr = malloc(sizeof (struct thread));
|
||||
if(!curr) goto oom;
|
||||
curr->done = 0;
|
||||
if(server_waitclient(&s, &c)) {
|
||||
dolog("failed to accept connection\n");
|
||||
free(curr);
|
||||
usleep(FAILURE_TIMEOUT);
|
||||
continue;
|
||||
}
|
||||
curr->client = c;
|
||||
if(!sblist_add(threads, &curr)) {
|
||||
close(curr->client.fd);
|
||||
free(curr);
|
||||
oom:
|
||||
dolog("rejecting connection due to OOM\n");
|
||||
usleep(FAILURE_TIMEOUT); /* prevent 100% CPU usage in OOM situation */
|
||||
continue;
|
||||
}
|
||||
pthread_attr_t *a = 0, attr;
|
||||
if(pthread_attr_init(&attr) == 0) {
|
||||
a = &attr;
|
||||
pthread_attr_setstacksize(a, THREAD_STACK_SIZE);
|
||||
}
|
||||
if(pthread_create(&curr->pt, a, clientthread, curr) != 0)
|
||||
dolog("pthread_create failed. OOM?\n");
|
||||
if(a) pthread_attr_destroy(&attr);
|
||||
}
|
||||
}
|
||||
23
deployments/openvpn/tiany0721.ovpn.example
Normal file
23
deployments/openvpn/tiany0721.ovpn.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# 复制为服务器上的 tiany0721.ovpn,并与 ca.crt / 证书 / 密钥同目录放置。
|
||||
# 勿将真实证书提交到 Git。
|
||||
|
||||
client
|
||||
dev tun
|
||||
proto tcp
|
||||
remote 111.31.14.226 32199
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
ca ca.crt
|
||||
cert tiany0721.crt
|
||||
key tiany0721.key
|
||||
remote-cert-tls server
|
||||
tls-auth ta.key 1
|
||||
cipher AES-256-CBC
|
||||
compress lz4-v2
|
||||
verb 3
|
||||
|
||||
# 仅路由戎行主机,不接管默认网关(其它流量仍走容器原出口)
|
||||
route-nopull
|
||||
route 192.168.3.43 255.255.255.255
|
||||
17
deployments/openvpn/upload-checklist.txt
Normal file
17
deployments/openvpn/upload-checklist.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
# 从本机 C:\Users\a1726\Desktop\11\OpenVPN\config 上传到服务器:
|
||||
# /opt/1panel/apps/hyapi-secrets/openvpn/
|
||||
#
|
||||
# 需要:
|
||||
# [ ] tiany0721.ovpn (上传后按 example 加上 route-nopull + route)
|
||||
# [ ] ca.crt
|
||||
# [ ] tiany0721.crt
|
||||
# [ ] tiany0721.key
|
||||
# [ ] ta.key
|
||||
#
|
||||
# 不需要:
|
||||
# [x] bin/、doc/、Uninstall.exe、openvpn-gui.exe 等 Windows 安装文件
|
||||
#
|
||||
# 权限:
|
||||
# chmod 700 /opt/1panel/apps/hyapi-secrets /opt/1panel/apps/hyapi-secrets/openvpn
|
||||
# chmod 600 *.key
|
||||
# chmod 644 *.crt *.ovpn
|
||||
@@ -38,6 +38,45 @@ services:
|
||||
ports:
|
||||
- "25010:5432"
|
||||
|
||||
# 戎行 OpenVPN 客户端 + SOCKS5(仅 192.168.3.43 走隧道)
|
||||
# 证书目录:默认 ./secrets/openvpn;1Panel 建议:
|
||||
# export RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn
|
||||
rongxing-vpn:
|
||||
build:
|
||||
context: ./deployments/openvpn
|
||||
dockerfile: Dockerfile
|
||||
image: hyapi-rongxing-vpn:local
|
||||
container_name: hyapi-rongxing-vpn
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
RONGXING_HOST: "192.168.3.43"
|
||||
SOCKS_HOST: "0.0.0.0"
|
||||
SOCKS_PORT: "1080"
|
||||
volumes:
|
||||
- ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
networks:
|
||||
- hyapi-network
|
||||
# 禁止把 1080 暴露到宿主机公网
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ip link show tun0 >/dev/null 2>&1 || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: "0.3"
|
||||
reservations:
|
||||
memory: 64M
|
||||
cpus: "0.1"
|
||||
|
||||
# Redis 缓存 (生产环境)
|
||||
redis:
|
||||
image: redis:8.0.2
|
||||
@@ -101,6 +140,8 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rongxing-vpn:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
@@ -134,6 +175,8 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rongxing-vpn:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD", "ps", "aux", "|", "grep", "worker"]
|
||||
interval: 30s
|
||||
|
||||
262
docs/戎行OpenVPN接入方案.md
Normal file
262
docs/戎行OpenVPN接入方案.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 戎行数据源 OpenVPN 接入方案(1Panel)
|
||||
|
||||
> 目标:**仅** `rongxing`(`http://192.168.3.43:7007`)走企业 VPN;Postgres / Redis / 其它外部 API 仍走原出口。
|
||||
> 方案:**OpenVPN 容器 + SOCKS5 代理 + 戎行 HTTP 客户端单独走代理**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构
|
||||
|
||||
```
|
||||
hyapi-network(现有 bridge)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ hyapi-app / hyapi-worker │
|
||||
│ ├─ DB / Redis / 其它 API ──► 默认出口(不经 VPN) │
|
||||
│ └─ 戎行 Client ──► socks5://rongxing-vpn:1080 │
|
||||
│ │ │
|
||||
│ rongxing-vpn(openvpn + microsocks)│ │
|
||||
│ TUN ──► OpenVPN Server 111.31.14.226:32199 │
|
||||
│ ──► 企业内网 192.168.3.43:7007 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 业务容器网络身份不变(端口、健康检查、服务发现保持现状)。
|
||||
- VPN 只负责进企业网;对外暴露的是 Docker 内网 SOCKS5,不映射到公网。
|
||||
- OpenVPN 使用 `route-nopull`,只加 `192.168.3.43`,避免抢走默认路由。
|
||||
|
||||
---
|
||||
|
||||
## 2. VPN 材料要不要上传?
|
||||
|
||||
**要上传,但只上传「客户端配置与证书」,不要上传整个 Windows OpenVPN 安装目录。**
|
||||
|
||||
| 本地路径(你机器) | 是否上传 | 说明 |
|
||||
|--------------------|----------|------|
|
||||
| `OpenVPN\config\tiany0721.ovpn` | ✅ | 连接配置(服务器上会再加路由控制指令) |
|
||||
| `OpenVPN\config\ca.crt` | ✅ | CA |
|
||||
| `OpenVPN\config\tiany0721.crt` | ✅ | 客户端证书 |
|
||||
| `OpenVPN\config\tiany0721.key` | ✅ | 客户端私钥(敏感) |
|
||||
| `OpenVPN\config\ta.key` | ✅ | tls-auth |
|
||||
| `OpenVPN\bin\`、`doc\`、`Uninstall.exe` 等 | ❌ | Windows 安装包,容器不用 |
|
||||
|
||||
服务器上使用 **Linux 版 OpenVPN 镜像**,复用上述 5 个文件即可。
|
||||
|
||||
---
|
||||
|
||||
## 3. 1Panel 推荐目录(最优落点)
|
||||
|
||||
假设 1Panel 数据盘常用根为 `/opt/1panel`(若你改过安装目录,把前缀换成实际路径)。
|
||||
|
||||
**推荐:密钥与业务代码分离。**
|
||||
|
||||
```text
|
||||
/opt/1panel/apps/hyapi/ # 业务 Compose / 代码(可随发布更新)
|
||||
├── docker-compose.prod.yml
|
||||
├── config.yaml / configs/...
|
||||
├── ...
|
||||
└── (不要把 .key 放进 git 仓库目录)
|
||||
|
||||
/opt/1panel/apps/hyapi-secrets/openvpn/ # ★ VPN 机密目录(推荐)
|
||||
├── tiany0721.ovpn # 服务器侧改过的版本(见下文)
|
||||
├── ca.crt
|
||||
├── tiany0721.crt
|
||||
├── tiany0721.key # 权限 600
|
||||
└── ta.key # 权限 600
|
||||
```
|
||||
|
||||
备选(同样可以):
|
||||
|
||||
```text
|
||||
/data/hyapi/secrets/openvpn/ # 若 1Panel 数据盘挂在 /data
|
||||
```
|
||||
|
||||
**不要放:**
|
||||
|
||||
- 网站静态目录 `/opt/1panel/www/...`(易被误暴露)
|
||||
- 仓库内 `hyapi-server/config/` 或会随 CI 同步的路径(防泄漏)
|
||||
|
||||
**权限建议(SSH 上执行):**
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/1panel/apps/hyapi-secrets/openvpn
|
||||
sudo chmod 700 /opt/1panel/apps/hyapi-secrets
|
||||
sudo chmod 700 /opt/1panel/apps/hyapi-secrets/openvpn
|
||||
# 上传文件后:
|
||||
sudo chmod 600 /opt/1panel/apps/hyapi-secrets/openvpn/*.key
|
||||
sudo chmod 644 /opt/1panel/apps/hyapi-secrets/openvpn/*.{crt,ovpn}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 服务器侧 OpenVPN 配置(准备好再挂载)
|
||||
|
||||
把本地 `tiany0721.ovpn` 拷到服务器后,**追加**以下行(保留原有 `remote` / 证书引用):
|
||||
|
||||
```conf
|
||||
# --- 仅路由戎行内网,不接管默认网关 ---
|
||||
route-nopull
|
||||
route 192.168.3.43 255.255.255.255
|
||||
```
|
||||
|
||||
完整示例见仓库:`deployments/openvpn/tiany0721.ovpn.example`。
|
||||
|
||||
证书相对路径保持与 `.ovpn` 同目录即可(`ca ca.crt` 等),挂载整个目录到容器 `/vpn`。
|
||||
|
||||
---
|
||||
|
||||
## 5. Compose 片段(接入现有 prod)
|
||||
|
||||
参考文件:`deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml`
|
||||
|
||||
核心服务示意:
|
||||
|
||||
```yaml
|
||||
rongxing-vpn:
|
||||
image: ghcr.io/linuxserver/openvpn-as:latest # 不推荐 AS;见下方推荐镜像
|
||||
```
|
||||
|
||||
**推荐用轻量客户端镜像**(文档配套 snippet 已按此写):
|
||||
|
||||
- 镜像:`dperson/openvpn-client` **或** 自建 `alpine + openvpn + microsocks`
|
||||
- 能力:`cap_add: [NET_ADMIN]`,`devices: [/dev/net/tun]`
|
||||
- 挂载:`/opt/1panel/apps/hyapi-secrets/openvpn:/vpn:ro`
|
||||
- 网络:加入现有 `hyapi-network`
|
||||
- **不要**把 `1080` 映射到宿主机公网端口
|
||||
|
||||
业务侧:
|
||||
|
||||
- `hyapi-app` /(若异步也会调戎行)`hyapi-worker` 保持原 `networks: [hyapi-network]`
|
||||
- 配置增加代理地址:`socks5://rongxing-vpn:1080`
|
||||
|
||||
---
|
||||
|
||||
## 6. 应用配置(仅戎行)
|
||||
|
||||
在 `rongxing` 段增加代理字段(实现代码时再接线;配置先预留):
|
||||
|
||||
```yaml
|
||||
rongxing:
|
||||
url: "http://192.168.3.43:7007"
|
||||
account: "..."
|
||||
password: "..."
|
||||
app_id: "..."
|
||||
private_key: "..."
|
||||
timeout: 10s
|
||||
# 仅戎行走 VPN 旁路代理;留空则直连(开发机已连 VPN 时可留空)
|
||||
proxy: "socks5://rongxing-vpn:1080"
|
||||
logging:
|
||||
# ...
|
||||
```
|
||||
|
||||
开发机若已用 GUI 连上 VPN,`proxy` 留空即可,无需起 Docker VPN。
|
||||
|
||||
---
|
||||
|
||||
## 7. 代码改动清单(后续开发)
|
||||
|
||||
当前 `RongxingService` 使用裸 `http.Client`,需:
|
||||
|
||||
1. `RongxingConfig` / `serviceConfig` 增加 `Proxy string`
|
||||
2. `NewRongxingServiceWithConfig` 传入 `cfg.Rongxing.Proxy`
|
||||
3. 创建 `http.Client` 时:若 `Proxy` 非空,用 `http.ProxyURL` 或 SOCKS5 dialer(`golang.org/x/net/proxy`)
|
||||
4. **只改戎行包**,其它外部服务不动
|
||||
|
||||
验证点:未配 `proxy` 时行为与现在一致。
|
||||
|
||||
---
|
||||
|
||||
## 8. 1Panel 上操作清单
|
||||
|
||||
### 8.1 一次性准备
|
||||
|
||||
1. SSH 登录服务器,创建 `/opt/1panel/apps/hyapi-secrets/openvpn/`
|
||||
2. 用 SFTP / `scp` 上传 5 个文件(见第 2 节)
|
||||
3. 编辑服务器上的 `tiany0721.ovpn`,加上 `route-nopull` + `route 192.168.3.43 ...`
|
||||
4. 设好目录/文件权限(第 3 节)
|
||||
5. 确认内核允许 TUN:多数 1Panel/Linux 默认有 `/dev/net/tun`;若容器报错再装 `tun` 模块
|
||||
|
||||
### 8.2 接入 Compose
|
||||
|
||||
任选其一:
|
||||
|
||||
- **A. 1Panel「编排」**:打开现有 hyapi 的 Compose,粘贴 `rongxing-vpn` 服务片段,挂载 secrets 目录,重新部署
|
||||
- **B. 独立编排**:单独建一个 `rongxing-vpn` 栈,但必须加入与 hyapi **同一个** Docker network(`hyapi-network` 的外部网络名以 `docker network ls` 为准)
|
||||
|
||||
查现有网络名:
|
||||
|
||||
```bash
|
||||
docker network ls | grep hyapi
|
||||
docker inspect hyapi-app-prod --format '{{json .NetworkSettings.Networks}}'
|
||||
```
|
||||
|
||||
若网络是 compose 项目前缀(如 `hyapi-server_hyapi-network`),独立栈里要:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
hyapi-network:
|
||||
external: true
|
||||
name: hyapi-server_hyapi-network # 以实际名为准
|
||||
```
|
||||
|
||||
### 8.3 发布后验证
|
||||
|
||||
```bash
|
||||
# 1) VPN 容器是否拿到 tun / 路由
|
||||
docker exec -it <rongxing-vpn容器名> ip route
|
||||
# 应能看到 192.168.3.43 经 tun0
|
||||
|
||||
# 2) 在 VPN 容器内测内网(镜像若有 curl)
|
||||
docker exec -it <rongxing-vpn容器名> wget -qO- --timeout=5 http://192.168.3.43:7007/ || true
|
||||
|
||||
# 3) 从业务容器经 SOCKS5 测(需业务镜像有 curl,或临时 alpine)
|
||||
docker run --rm --network <hyapi网络名> curlimages/curl:8.5.0 \
|
||||
-x socks5h://rongxing-vpn:1080 -m 10 -v http://192.168.3.43:7007/
|
||||
|
||||
# 4) 确认「不走代理」时业务仍能访问公网/库(对照现有 health)
|
||||
curl -f http://127.0.0.1:25000/health
|
||||
```
|
||||
|
||||
业务侧再打一笔戎行真实查询,看 `logs/external_services` 下戎行日志是否成功。
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全与运维
|
||||
|
||||
- `.key` / `ta.key` / 账号密码:**不进 Git**;secrets 目录不挂到可下载的 Web 根。
|
||||
- SOCKS5 **仅**监听容器网络,不 `-p 1080:1080` 到宿主机。
|
||||
- VPN 证书到期前找对方续期,替换 secrets 目录文件后 `docker compose restart rongxing-vpn`。
|
||||
- 断线策略:容器 `restart: unless-stopped`;OpenVPN 侧已有 `resolv-retry infinite` / `persist-tun`。
|
||||
- 监控:可对 `rongxing-vpn` 做 1Panel 容器存活告警;业务侧戎行错误率告警。
|
||||
|
||||
---
|
||||
|
||||
## 10. 本地文件 → 服务器对照表
|
||||
|
||||
| 本地 | 服务器推荐路径 |
|
||||
|------|----------------|
|
||||
| `...\OpenVPN\config\tiany0721.ovpn` | `/opt/1panel/apps/hyapi-secrets/openvpn/tiany0721.ovpn`(改过路由) |
|
||||
| `...\OpenVPN\config\ca.crt` | 同目录 |
|
||||
| `...\OpenVPN\config\tiany0721.crt` | 同目录 |
|
||||
| `...\OpenVPN\config\tiany0721.key` | 同目录(600) |
|
||||
| `...\OpenVPN\config\ta.key` | 同目录(600) |
|
||||
| Windows `bin/` 整包 | **不上传** |
|
||||
|
||||
---
|
||||
|
||||
## 11. 实施状态与顺序
|
||||
|
||||
**代码与 Compose 已落地**(见仓库当前变更):
|
||||
|
||||
- `rongxing` 支持 `proxy`(SOCKS5/HTTP)
|
||||
- `docker-compose.prod.yml` 已加入 `rongxing-vpn` 服务
|
||||
- `deployments/openvpn/` 自建镜像(OpenVPN + microsocks)
|
||||
- 生产配置 `configs/env.production.yaml`:`proxy: "socks5://rongxing-vpn:1080"`
|
||||
|
||||
**1Panel 服务器仍需人工完成:**
|
||||
|
||||
1. 创建 `/opt/1panel/apps/hyapi-secrets/openvpn/`,上传 5 个证书/配置文件(可用本机已备好的 `secrets/openvpn/` 内容)
|
||||
2. 设置环境变量:`RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn`
|
||||
3. 拉取/构建并重新部署 Compose(确保服务器能访问 Docker Hub / 镜像源以下载 `alpine`)
|
||||
4. 验证:`docker exec hyapi-rongxing-vpn ip route` 含 `192.168.3.43`;业务侧打一笔戎行查询
|
||||
@@ -846,6 +846,7 @@ type RongxingConfig struct {
|
||||
AppID string `mapstructure:"app_id"` // 应用 ID
|
||||
PrivateKey string `mapstructure:"private_key"` // RSA 私钥(PEM 或 PKCS#8 Base64)
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
Proxy string `mapstructure:"proxy"` // 可选,仅戎行请求走此代理,如 socks5://rongxing-vpn:1080
|
||||
|
||||
Logging RongxingLoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
@@ -233,6 +233,13 @@ type JRZQH6M3Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQ5J5CReq 无间司南-纯黑版C10 金融黑名单
|
||||
type JRZQ5J5CReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
// JRZQW3L8Req 信用司南
|
||||
type JRZQW3L8Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
@@ -1350,6 +1357,10 @@ type FLXGC4CTReq struct {
|
||||
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
|
||||
}
|
||||
|
||||
type FLXLLD77Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
type QYGLLUCMReq struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
|
||||
@@ -1360,3 +1371,37 @@ type JRZQ0OO1Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type JRZQT2C1Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type JRZQ2PV2Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type QCXGV20OReq struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
}
|
||||
|
||||
type QCXGVP00Req struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
}
|
||||
|
||||
type QCXGVJ70Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
|
||||
type QCXG2Y8XReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"omitempty"`
|
||||
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||
VinCode string `json:"vin_code" validate:"omitempty"`
|
||||
}
|
||||
type QCXG74YTReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"omitempty"`
|
||||
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||
VinCode string `json:"vin_code" validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"FLXGJI17": flxg.ProcessFLXGJI17Request, //董监高司法综合信息核验
|
||||
"FLXGMMG7": flxg.ProcessFLXGMMG7Request, //个人诉讼定制版
|
||||
"FLXGC4CT": flxg.ProcessFLXGC4CTRequest, //个人涉诉案件查询汇博
|
||||
"FLXLLD77": flxg.ProcessFLXLLD77Request, //劳动仲裁查询天远
|
||||
// JRZQ系列处理器
|
||||
"JRZQOICN": jrzq.ProcessJRZQOICNRequest, // 银行卡四要素
|
||||
"JRZQMDQ1": jrzq.ProcessJRZQMDQ1Request, // 银行卡OCR数卖
|
||||
@@ -209,6 +210,12 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQW3L8": jrzq.ProcessJRZQW3L8Request, //信用司南
|
||||
"JRZQP8D2": jrzq.ProcessJRZQP8D2Request, //全景雷达BH
|
||||
"JRZQ0OO1": jrzq.ProcessJRZQ0OO1Request, //戎行贷后信息
|
||||
"JRZQT2C1": jrzq.ProcessJRZQT2C1Request, //探针C
|
||||
"JRZQT2A1": jrzq.ProcessJRZQT2A1Request, //探针A
|
||||
"JRZQS74D": jrzq.ProcessJRZQS74DRequest, //申请借贷
|
||||
"JRZQ2F4W": jrzq.ProcessJRZQ2F4WRequest, //支付行为
|
||||
"JRZQ2PV2": jrzq.ProcessJRZQ2PV2Request, //租赁通用版v2
|
||||
"JRZQ5J5C": jrzq.ProcessJRZQ5J5CRequest, //无间司南-纯黑版C10 金融黑名单
|
||||
|
||||
// QYGL系列处理器
|
||||
"QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
||||
@@ -223,6 +230,11 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QCXGCP77": qcxg.ProcessQCXGCP77Request, //全国车辆配置查验(车辆详情)
|
||||
"QCXGX2X6": qcxg.ProcessQCXGX2X6Request, //行驶证信息核验V2
|
||||
"QCXG1S2L": qcxg.ProcessQCXG1S2LRequest, //汽车车辆五项
|
||||
"QCXGV20O": qcxg.ProcessQCXGV20ORequest, //车VIN查询估值
|
||||
"QCXGVP00": qcxg.ProcessQCXGVP00Request, //车VIN查车牌号
|
||||
"QCXGVJ70": qcxg.ProcessQCXGVJ70Request, //车牌号查vin
|
||||
"QCXG2Y8X": qcxg.ProcessQCXG2Y8XRequest, //商业险有效性
|
||||
"QCXG74YT": qcxg.ProcessQCXG74YTRequest, //交强险有效性
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSY0YYV": yysy.ProcessYYSY0YYVRequest, //运营商二要素查询
|
||||
|
||||
@@ -320,7 +320,18 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZX7J9": &dto.IVYZX7J9Req{}, //学籍核验
|
||||
"QCXG6U5G": &dto.QCXG6U5GReq{}, //车辆核验
|
||||
"JRZQ0OO1": &dto.JRZQ0OO1Req{}, //戎行贷后信息 Info360
|
||||
|
||||
"JRZQT2C1": &dto.JRZQT2C1Req{}, //探针C
|
||||
"JRZQT2A1": &dto.JRZQT2C1Req{}, //探针A
|
||||
"JRZQS74D": &dto.JRZQT2C1Req{}, //申请借贷
|
||||
"JRZQ2F4W": &dto.JRZQT2C1Req{}, //支付行为
|
||||
"JRZQ2PV2": &dto.JRZQ2PV2Req{}, //租赁通用版v2
|
||||
"FLXLLD77": &dto.FLXLLD77Req{}, //劳动仲裁查询天远
|
||||
"QCXGV20O": &dto.QCXGV20OReq{}, //车VIN查询估值
|
||||
"QCXGVP00": &dto.QCXGVP00Req{}, //车VIN查车牌号
|
||||
"QCXGVJ70": &dto.QCXGVJ70Req{}, //车牌号查vin
|
||||
"QCXG2Y8X": &dto.QCXG2Y8XReq{}, //商业险有效性
|
||||
"QCXG74YT": &dto.QCXG74YTReq{}, //交强险有效性
|
||||
"JRZQ5J5C": &dto.JRZQ5J5CReq{}, //无间司南-纯黑版C10 金融黑名单
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package flxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
)
|
||||
|
||||
// ProcessFLXLLD77Request FLXLLD77 API处理方法 - 劳动仲裁查询天远
|
||||
func ProcessFLXLLD77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXLLD77Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
reqdata := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"id_card": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
respBytes, err := deps.TianyuanapiService.CallAPI(ctx, "IVYZ0S0D", reqdata)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -42,5 +42,9 @@ func ProcessJRZQ0OO1Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
transformed, err := transformJRZQ0OO1Response(respBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return transformed, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JRZQ0OO1 字段命名约定:{窗口}_{机构类型}_{事件}_{指标}
|
||||
// 窗口: m1/m3/m6/m12
|
||||
// 机构类型: 省略=全部贷款;bank=银行;cfc=消金;sloan=小贷
|
||||
// 事件: total/severe_overdue/overdue/repay_fail/repay_ok/repay_remind/
|
||||
//
|
||||
// disburse_fail/disburse_ok/audit_fail/audit_ok/apply/register/verify_code/marketing/other
|
||||
//
|
||||
// 指标: cnt=次数;org=机构数
|
||||
var (
|
||||
jrzq0OO1Periods = []string{"m1", "m3", "m6", "m12"}
|
||||
jrzq0OO1InstTypes = []string{"", "bank", "cfc", "sloan"}
|
||||
jrzq0OO1Events = []string{
|
||||
"total", "severe_overdue", "overdue", "repay_fail", "repay_ok",
|
||||
"repay_remind", "disburse_fail", "disburse_ok", "audit_fail", "audit_ok",
|
||||
"apply", "register", "verify_code", "marketing", "other",
|
||||
}
|
||||
jrzq0OO1Metrics = []string{"cnt", "org"}
|
||||
|
||||
// jrzq0OO1FieldMap B001-B480 -> 语义化扁平字段名
|
||||
jrzq0OO1FieldMap = buildJRZQ0OO1FieldMap()
|
||||
)
|
||||
|
||||
func buildJRZQ0OO1FieldMap() map[string]string {
|
||||
m := make(map[string]string, 480)
|
||||
n := 1
|
||||
for _, period := range jrzq0OO1Periods {
|
||||
for _, inst := range jrzq0OO1InstTypes {
|
||||
for _, event := range jrzq0OO1Events {
|
||||
for _, metric := range jrzq0OO1Metrics {
|
||||
key := fmt.Sprintf("B%03d", n)
|
||||
parts := []string{period}
|
||||
if inst != "" {
|
||||
parts = append(parts, inst)
|
||||
}
|
||||
parts = append(parts, event, metric)
|
||||
m[key] = strings.Join(parts, "_")
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// transformJRZQ0OO1Response 将 Info360 的 Bxxx 字段重命名为语义化扁平字段。
|
||||
// 未在映射表中的键原样保留。
|
||||
func transformJRZQ0OO1Response(respBytes []byte) ([]byte, error) {
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(respBytes, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]interface{}, len(raw))
|
||||
for k, v := range raw {
|
||||
if renamed, ok := jrzq0OO1FieldMap[k]; ok {
|
||||
out[renamed] = v
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildJRZQ0OO1FieldMap_CountAndBounds(t *testing.T) {
|
||||
if len(jrzq0OO1FieldMap) != 480 {
|
||||
t.Fatalf("expected 480 mappings, got %d", len(jrzq0OO1FieldMap))
|
||||
}
|
||||
if got, want := jrzq0OO1FieldMap["B001"], "m1_total_cnt"; got != want {
|
||||
t.Fatalf("B001: got %q want %q", got, want)
|
||||
}
|
||||
if got, want := jrzq0OO1FieldMap["B480"], "m12_sloan_other_org"; got != want {
|
||||
t.Fatalf("B480: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJRZQ0OO1FieldMap_DocSpotChecks(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"B001": "m1_total_cnt", // 最近1个月贷款类总次数
|
||||
"B009": "m1_repay_ok_cnt", // 最近1个月还款成功次数
|
||||
"B031": "m1_bank_total_cnt", // 最近1个月银行类贷款总次数
|
||||
"B129": "m3_repay_ok_cnt", // 最近3个月还款成功次数
|
||||
"B130": "m3_repay_ok_org", // 最近3个月还款成功机构数
|
||||
"B133": "m3_disburse_fail_cnt", // 最近3个月放款失败次数
|
||||
"B134": "m3_disburse_fail_org", // 最近3个月放款失败机构数
|
||||
"B157": "m3_bank_repay_fail_cnt", // 最近3个月银行类还款失败次数
|
||||
"B158": "m3_bank_repay_fail_org", // 最近3个月银行类还款失败机构数
|
||||
"B219": "m3_sloan_repay_ok_cnt", // 最近3个月小贷类还款成功次数
|
||||
"B220": "m3_sloan_repay_ok_org", // 最近3个月小贷类还款成功机构数
|
||||
"B223": "m3_sloan_disburse_fail_cnt", // 最近3个月小贷类放款失败次数
|
||||
"B224": "m3_sloan_disburse_fail_org", // 最近3个月小贷类放款失败机构数
|
||||
"B247": "m6_repay_fail_cnt", // 最近6个月还款失败次数
|
||||
"B248": "m6_repay_fail_org", // 最近6个月还款失败机构数
|
||||
"B257": "m6_audit_fail_cnt", // 最近6个月审核失败次数
|
||||
"B258": "m6_audit_fail_org", // 最近6个月审核失败机构数
|
||||
"B261": "m6_apply_cnt", // 最近6个月贷款申请次数
|
||||
"B262": "m6_apply_org", // 最近6个月贷款申请机构数
|
||||
"B285": "m6_bank_disburse_ok_cnt", // 最近6个月银行类放款成功次数
|
||||
"B286": "m6_bank_disburse_ok_org", // 最近6个月银行类放款成功机构数
|
||||
"B347": "m6_sloan_audit_fail_cnt", // 最近6个月小贷类审核失败次数
|
||||
"B348": "m6_sloan_audit_fail_org", // 最近6个月小贷类审核失败机构数
|
||||
"B351": "m6_sloan_apply_cnt", // 最近6个月小贷类贷款申请次数
|
||||
"B352": "m6_sloan_apply_org", // 最近6个月小贷类贷款申请机构数
|
||||
"B375": "m12_disburse_ok_cnt", // 最近12个月放款成功次数
|
||||
"B376": "m12_disburse_ok_org", // 最近12个月放款成功机构数
|
||||
"B061": "m1_cfc_total_cnt", // 最近1个月持牌消金类贷款总次数
|
||||
"B301": "m6_cfc_total_cnt", // 最近6个月消金类贷款总次数
|
||||
}
|
||||
for src, want := range cases {
|
||||
if got := jrzq0OO1FieldMap[src]; got != want {
|
||||
t.Errorf("%s: got %q want %q", src, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformJRZQ0OO1Response_Sample(t *testing.T) {
|
||||
in := []byte(`{
|
||||
"B129": 6,
|
||||
"B130": 3,
|
||||
"B133": 4,
|
||||
"B134": 2,
|
||||
"B157": 2,
|
||||
"B158": 1,
|
||||
"B219": 6,
|
||||
"B220": 3,
|
||||
"B223": 4,
|
||||
"B224": 2,
|
||||
"B247": 2,
|
||||
"B248": 1,
|
||||
"B257": 6,
|
||||
"B258": 3,
|
||||
"B261": 4,
|
||||
"B262": 2,
|
||||
"B285": 2,
|
||||
"B286": 1,
|
||||
"B347": 6,
|
||||
"B348": 3,
|
||||
"B351": 4,
|
||||
"B352": 2,
|
||||
"B375": 2,
|
||||
"B376": 1
|
||||
}`)
|
||||
|
||||
outBytes, err := transformJRZQ0OO1Response(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out map[string]interface{}
|
||||
if err := json.Unmarshal(outBytes, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]float64{
|
||||
"m3_repay_ok_cnt": 6,
|
||||
"m3_repay_ok_org": 3,
|
||||
"m3_disburse_fail_cnt": 4,
|
||||
"m3_disburse_fail_org": 2,
|
||||
"m3_bank_repay_fail_cnt": 2,
|
||||
"m3_bank_repay_fail_org": 1,
|
||||
"m3_sloan_repay_ok_cnt": 6,
|
||||
"m3_sloan_repay_ok_org": 3,
|
||||
"m3_sloan_disburse_fail_cnt": 4,
|
||||
"m3_sloan_disburse_fail_org": 2,
|
||||
"m6_repay_fail_cnt": 2,
|
||||
"m6_repay_fail_org": 1,
|
||||
"m6_audit_fail_cnt": 6,
|
||||
"m6_audit_fail_org": 3,
|
||||
"m6_apply_cnt": 4,
|
||||
"m6_apply_org": 2,
|
||||
"m6_bank_disburse_ok_cnt": 2,
|
||||
"m6_bank_disburse_ok_org": 1,
|
||||
"m6_sloan_audit_fail_cnt": 6,
|
||||
"m6_sloan_audit_fail_org": 3,
|
||||
"m6_sloan_apply_cnt": 4,
|
||||
"m6_sloan_apply_org": 2,
|
||||
"m12_disburse_ok_cnt": 2,
|
||||
"m12_disburse_ok_org": 1,
|
||||
}
|
||||
|
||||
if len(out) != len(want) {
|
||||
t.Fatalf("field count: got %d want %d, out=%v", len(out), len(want), out)
|
||||
}
|
||||
for k, w := range want {
|
||||
got, ok := out[k]
|
||||
if !ok {
|
||||
t.Errorf("missing key %s", k)
|
||||
continue
|
||||
}
|
||||
num, ok := got.(float64)
|
||||
if !ok || num != w {
|
||||
t.Errorf("%s: got %#v want %v", k, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformJRZQ0OO1Response_KeepUnknown(t *testing.T) {
|
||||
in := []byte(`{"B001":1,"extra":"x"}`)
|
||||
outBytes, err := transformJRZQ0OO1Response(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out map[string]interface{}
|
||||
if err := json.Unmarshal(outBytes, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := out["m1_total_cnt"]; !ok {
|
||||
t.Fatal("expected m1_total_cnt")
|
||||
}
|
||||
if out["extra"] != "x" {
|
||||
t.Fatalf("expected extra kept, got %#v", out["extra"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQ2F4WRequest 支付行为(上游 jy000017;body encryptType=1 MD5 + timestamp)
|
||||
func ProcessJRZQ2F4WRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQT2C1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"name": paramsDto.Name,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000039"
|
||||
apiPath := "/api/v1/payment/index"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQ2PV2Request 租赁通用版v2
|
||||
func ProcessJRZQ2PV2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ2PV2Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000049"
|
||||
apiPath := "/api/v1/generlea/agreev2"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQ5J5CRequest 无间司南-纯黑A版(上游 jy000052)
|
||||
func ProcessJRZQ5J5CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ5J5CReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"name": paramsDto.Name,
|
||||
"idCard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000055"
|
||||
apiPath := "/api/v1/blacklist/c10/verify"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQS74DRequest 申请借贷(上游 jy000017;body encryptType=1 MD5 + timestamp)
|
||||
func ProcessJRZQS74DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQT2C1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"name": paramsDto.Name,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000048"
|
||||
apiPath := "/api/v1/pd/loan/behavior"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQT2A1Request 探针A(上游 jy000017;body encryptType=1 MD5 + timestamp)
|
||||
func ProcessJRZQT2A1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQT2C1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"name": jiyi.MD5Encrypt(paramsDto.Name),
|
||||
"idCard": jiyi.MD5Encrypt(paramsDto.IDCard),
|
||||
"mobile": jiyi.MD5Encrypt(paramsDto.MobileNo),
|
||||
}
|
||||
|
||||
apiKey := "jy000017"
|
||||
apiPath := "/api/v1/probe/a/verify/encrypt"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.CaveInvestOptions(1))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessJRZQT2C1Request 探针C(上游 jy000008;顶层 encryptionType=2 MD5)
|
||||
func ProcessJRZQT2C1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQT2C1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"name": jiyi.MD5Encrypt(paramsDto.Name),
|
||||
"idCard": jiyi.MD5Encrypt(paramsDto.IDCard),
|
||||
"mobile": jiyi.MD5Encrypt(paramsDto.MobileNo),
|
||||
}
|
||||
|
||||
apiKey := "jy000008"
|
||||
apiPath := "/api/v1/probe/c/verify"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(2))
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ProcessQCXG2Y8XRequest 商业险有效性
|
||||
func ProcessQCXG2Y8XRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG2Y8XReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.PlateNo == "" && paramsDto.IDCard == "" && paramsDto.VinCode == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("plate_no、id_card、vin_code 至少需要传其中一个"))
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"licenseNo": paramsDto.PlateNo,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"vin": paramsDto.VinCode,
|
||||
"nonce": uuid.New().String(),
|
||||
}
|
||||
|
||||
apiKey := "jy000019"
|
||||
apiPath := "/api/v1/commercial/validity"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ProcessQCXG74YTRequest 交强险有效性
|
||||
func ProcessQCXG74YTRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG74YTReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.PlateNo == "" && paramsDto.IDCard == "" && paramsDto.VinCode == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("plate_no、id_card、vin_code 至少需要传其中一个"))
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"licenseNo": paramsDto.PlateNo,
|
||||
"idNo": paramsDto.IDCard,
|
||||
"vin": paramsDto.VinCode,
|
||||
"nonce": uuid.New().String(),
|
||||
}
|
||||
|
||||
apiKey := "jy000020"
|
||||
apiPath := "/api/v1/compulsory/validity"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessQCXGV20ORequest 车VIN查询估值
|
||||
func ProcessQCXGV20ORequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGV20OReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"vin": paramsDto.VinCode,
|
||||
}
|
||||
|
||||
apiKey := "jy000025"
|
||||
apiPath := "/api/v1/vehicle/valuation"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessQCXGVJ70Request 车牌号查vin
|
||||
func ProcessQCXGVJ70Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGVJ70Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"plateNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
apiKey := "jy000018"
|
||||
apiPath := "/api/v1/car/vin"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hyapi-server/internal/domains/api/dto"
|
||||
"hyapi-server/internal/domains/api/services/processors"
|
||||
"hyapi-server/internal/infrastructure/external/jiyi"
|
||||
)
|
||||
|
||||
// ProcessQCXGVP00Request 车VIN查车牌号
|
||||
func ProcessQCXGVP00Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGVP00Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"vin": paramsDto.VinCode,
|
||||
}
|
||||
|
||||
apiKey := "jy000028"
|
||||
apiPath := "/api/v1/plateNumber/vin"
|
||||
|
||||
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
|
||||
if err != nil {
|
||||
if errors.Is(err, jiyi.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, jiyi.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -19,8 +19,14 @@ const defaultRequestTimeout = 4 * time.Second
|
||||
|
||||
// queryBillingAPIKeys 查询计费接口:未查得/空结果仍按成功返回空数据,由平台侧计费
|
||||
var queryBillingAPIKeys = map[string]struct{}{
|
||||
"jy000008": {}, // 探针C
|
||||
"jy000017": {}, // 探针A
|
||||
"jy000019": {}, // 商业险有效性
|
||||
"jy000020": {}, // 交强险有效性
|
||||
"jy000022": {}, // 洞侦1.0
|
||||
"jy000028": {}, // 车VIN查车牌号
|
||||
"jy000042": {}, // 借贷意向验证3.0
|
||||
"jy000048": {}, // 申请借贷
|
||||
"jy000052": {}, // 无间司南-纯黑A版
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,21 @@ import (
|
||||
)
|
||||
|
||||
// generateCurlCommand 生成可直接复现的 curl 命令,便于联调排查。
|
||||
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
|
||||
// proxyURL 非空时附加 -x,便于确认线上是否应走 SOCKS。
|
||||
func generateCurlCommand(method, requestURL string, headers map[string]string, body, proxyURL string) string {
|
||||
var cmd strings.Builder
|
||||
cmd.WriteString("curl -X ")
|
||||
cmd.WriteString(method)
|
||||
cmd.WriteString(" '")
|
||||
cmd.WriteString(url)
|
||||
cmd.WriteString(requestURL)
|
||||
cmd.WriteString("'")
|
||||
|
||||
if p := strings.TrimSpace(proxyURL); p != "" {
|
||||
cmd.WriteString(" \\\n -x '")
|
||||
cmd.WriteString(escapeSingleQuotes(p))
|
||||
cmd.WriteString("'")
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
cmd.WriteString(" \\\n -H '")
|
||||
cmd.WriteString(key)
|
||||
|
||||
47
internal/infrastructure/external/rongxing/http_client_test.go
vendored
Normal file
47
internal/infrastructure/external/rongxing/http_client_test.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package rongxing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewHTTPClient_Direct(t *testing.T) {
|
||||
client, err := newHTTPClient(5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if client.Timeout != 5*time.Second {
|
||||
t.Fatalf("timeout = %v", client.Timeout)
|
||||
}
|
||||
if client.Transport != nil && client.Transport != http.DefaultTransport {
|
||||
// 直连允许 Transport 为 nil(使用 DefaultTransport)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_Socks5(t *testing.T) {
|
||||
client, err := newHTTPClient(3*time.Second, "socks5://rongxing-vpn:1080")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if client.Transport == nil {
|
||||
t.Fatal("expected custom transport for socks5")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_HTTPProxy(t *testing.T) {
|
||||
client, err := newHTTPClient(3*time.Second, "http://127.0.0.1:8888")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if client.Transport == nil {
|
||||
t.Fatal("expected custom transport for http proxy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_UnsupportedScheme(t *testing.T) {
|
||||
_, err := newHTTPClient(time.Second, "ftp://x")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsupported scheme")
|
||||
}
|
||||
}
|
||||
@@ -46,5 +46,6 @@ func NewRongxingServiceWithConfig(cfg *config.Config) (*RongxingService, error)
|
||||
AppID: cfg.Rongxing.AppID,
|
||||
PrivateKey: cfg.Rongxing.PrivateKey,
|
||||
Timeout: timeout,
|
||||
}, logger), nil
|
||||
Proxy: cfg.Rongxing.Proxy,
|
||||
}, logger)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -15,6 +18,7 @@ import (
|
||||
"hyapi-server/internal/shared/external_logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,15 +36,17 @@ type serviceConfig struct {
|
||||
AppID string
|
||||
PrivateKey string
|
||||
Timeout time.Duration
|
||||
Proxy string
|
||||
}
|
||||
|
||||
// RongxingService 戎行数据源服务
|
||||
type RongxingService struct {
|
||||
config serviceConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
client *http.Client
|
||||
|
||||
tokenMu sync.RWMutex
|
||||
cachedToken string
|
||||
// loginMu 避免并发登录打爆对方;不缓存 token,每次业务调用重新登录。
|
||||
loginMu sync.Mutex
|
||||
}
|
||||
|
||||
// apiResponse 戎行统一响应。code 可能是数字或字符串;扣费以 consumeFlag 为准。
|
||||
@@ -65,12 +71,52 @@ func (r apiResponse) code() string {
|
||||
}
|
||||
|
||||
// NewRongxingService 创建戎行服务实例
|
||||
func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) *RongxingService {
|
||||
func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) (*RongxingService, error) {
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = defaultRequestTimeout
|
||||
}
|
||||
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
||||
return &RongxingService{config: cfg, logger: logger}
|
||||
client, err := newHTTPClient(cfg.Timeout, cfg.Proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RongxingService{config: cfg, logger: logger, client: client}, nil
|
||||
}
|
||||
|
||||
// newHTTPClient 构建 HTTP 客户端;proxyURL 支持 socks5/socks5h/http/https,空则直连。
|
||||
func newHTTPClient(timeout time.Duration, proxyURL string) (*http.Client, error) {
|
||||
client := &http.Client{Timeout: timeout}
|
||||
proxyURL = strings.TrimSpace(proxyURL)
|
||||
if proxyURL == "" {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 proxy 失败: %w", err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "socks5", "socks5h":
|
||||
dialer, err := proxy.FromURL(u, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 SOCKS 代理失败: %w", err)
|
||||
}
|
||||
transport := &http.Transport{}
|
||||
if cd, ok := dialer.(proxy.ContextDialer); ok {
|
||||
transport.DialContext = cd.DialContext
|
||||
} else {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(network, addr)
|
||||
}
|
||||
}
|
||||
client.Transport = transport
|
||||
case "http", "https":
|
||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的 proxy scheme: %s(支持 socks5/socks5h/http/https)", u.Scheme)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetConfig 获取运行时配置
|
||||
@@ -80,7 +126,7 @@ func (s *RongxingService) GetConfig() serviceConfig {
|
||||
|
||||
// CallAPI 通用业务接口调用。
|
||||
// apiPath 为相对路径(如 /third/loan/info360),reqData 为已组装好的请求体。
|
||||
// Token 获取与 Header 注入由服务内部处理;401/403 时自动刷新 Token 并重试一次。
|
||||
// 每次调用重新登录取 Token(不本地缓存);若业务码/HTTP 仍为 401/403,再登录重试一次。
|
||||
func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData map[string]interface{}) ([]byte, error) {
|
||||
apiKey := strings.Trim(apiPath, "/")
|
||||
|
||||
@@ -118,7 +164,7 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
|
||||
"Content-Type": "application/json",
|
||||
headerDmsToken: token,
|
||||
}
|
||||
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
|
||||
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
@@ -139,7 +185,6 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
|
||||
respStr := string(respBody)
|
||||
|
||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||
s.clearToken()
|
||||
if attempt == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -162,6 +207,11 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
|
||||
}
|
||||
|
||||
code := resp.code()
|
||||
// 对方常以 HTTP 200 + body.code=401 表示 token 无效(非 HTTP 401)
|
||||
if isTokenInvalidCode(code) && attempt == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := extractBusinessPayload(resp.Data)
|
||||
|
||||
// 扣费只看 consumeFlag:1 扣费(按成功返回),0 不扣费
|
||||
@@ -180,32 +230,20 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
|
||||
return nil, errors.Join(ErrDatasource, errors.New("请求失败"))
|
||||
}
|
||||
|
||||
// getToken 每次重新登录,不缓存 token。
|
||||
func (s *RongxingService) getToken(ctx context.Context, transactionID string) (string, error) {
|
||||
s.tokenMu.RLock()
|
||||
token := s.cachedToken
|
||||
s.tokenMu.RUnlock()
|
||||
if token != "" {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
if s.cachedToken != "" {
|
||||
return s.cachedToken, nil
|
||||
}
|
||||
|
||||
token, err := s.login(ctx, transactionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.cachedToken = token
|
||||
return token, nil
|
||||
s.loginMu.Lock()
|
||||
defer s.loginMu.Unlock()
|
||||
return s.login(ctx, transactionID)
|
||||
}
|
||||
|
||||
func (s *RongxingService) clearToken() {
|
||||
s.tokenMu.Lock()
|
||||
s.cachedToken = ""
|
||||
s.tokenMu.Unlock()
|
||||
func isTokenInvalidCode(code string) bool {
|
||||
switch strings.TrimSpace(code) {
|
||||
case "401", "403":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RongxingService) login(ctx context.Context, transactionID string) (string, error) {
|
||||
@@ -253,7 +291,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
|
||||
bodyStr := string(bodyBytes)
|
||||
|
||||
headers := map[string]string{"Content-Type": "application/json"}
|
||||
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
|
||||
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
@@ -309,10 +347,9 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
|
||||
}
|
||||
|
||||
func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
|
||||
client := &http.Client{Timeout: s.config.Timeout}
|
||||
resp, err := client.Do(req)
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, s.wrapTransportError(req.URL.String(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -323,6 +360,49 @@ func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
|
||||
return body, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// wrapTransportError 把超时/拒连/代理失败等包装成可读诊断,便于区分「联不通」原因。
|
||||
func (s *RongxingService) wrapTransportError(targetURL string, err error) error {
|
||||
diag := classifyTransportError(err)
|
||||
proxyMode := strings.TrimSpace(s.config.Proxy)
|
||||
if proxyMode == "" {
|
||||
proxyMode = "(直连,未配置 proxy)"
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"戎行 HTTP 失败 target=%s proxy=%s diagnosis=%s cause=%w",
|
||||
targetURL, proxyMode, diag, err,
|
||||
)
|
||||
}
|
||||
|
||||
func classifyTransportError(err error) string {
|
||||
if err == nil {
|
||||
return "unknown"
|
||||
}
|
||||
msg := err.Error()
|
||||
|
||||
switch {
|
||||
case os.IsTimeout(err) || errors.Is(err, context.DeadlineExceeded) ||
|
||||
strings.Contains(msg, "Client.Timeout") || strings.Contains(msg, "deadline exceeded"):
|
||||
return "请求超时(未在 timeout 内收到响应头;可能:目标 192.168.3.43:7007 不可达、VPN/SOCKS 未转发、或服务无响应)"
|
||||
case strings.Contains(msg, "connection refused"):
|
||||
return "连接被拒绝(端口未监听或代理/目标拒绝)"
|
||||
case strings.Contains(msg, "no such host") || strings.Contains(msg, "lookup"):
|
||||
return "DNS/主机名解析失败(检查 rongxing-vpn 服务名或目标域名)"
|
||||
case strings.Contains(msg, "network is unreachable") || strings.Contains(msg, "no route to host"):
|
||||
return "网络不可达(无路由;直连内网 IP 时常见于未走 VPN/代理)"
|
||||
case strings.Contains(msg, "i/o timeout") || strings.Contains(msg, "TLS handshake timeout"):
|
||||
return "传输层超时(链路通但握手/读写超时)"
|
||||
case strings.Contains(msg, "proxy") || strings.Contains(msg, "socks"):
|
||||
return "代理链路异常(检查 socks5://rongxing-vpn:1080 与 VPN 容器)"
|
||||
}
|
||||
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return "网络超时"
|
||||
}
|
||||
// 完整原始错误在同一条日志的 error/cause 字段中,不在别的文件
|
||||
return "其他网络错误(见本条日志 error 全文)"
|
||||
}
|
||||
|
||||
func (s *RongxingService) validateConfig() error {
|
||||
if s.config.BaseURL == "" {
|
||||
return errors.New("戎行 url 未配置")
|
||||
@@ -385,12 +465,35 @@ func (s *RongxingService) logErrorWithCurl(transactionID, apiKey string, err err
|
||||
if s.logger == nil {
|
||||
return
|
||||
}
|
||||
proxyMode := strings.TrimSpace(s.config.Proxy)
|
||||
if proxyMode == "" {
|
||||
proxyMode = "(直连,未配置 proxy)"
|
||||
}
|
||||
s.logger.LogErrorWithFields("rongxing API错误",
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("api_code", apiKey),
|
||||
zap.String("base_url", s.config.BaseURL),
|
||||
zap.String("proxy", proxyMode),
|
||||
zap.String("diagnosis", extractDiagnosis(err)),
|
||||
zap.Error(err),
|
||||
zap.Any("params", payload),
|
||||
zap.String("curl", curlCmd),
|
||||
zap.String("response_body", respBody),
|
||||
)
|
||||
}
|
||||
|
||||
func extractDiagnosis(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
const marker = "diagnosis="
|
||||
msg := err.Error()
|
||||
if i := strings.Index(msg, marker); i >= 0 {
|
||||
rest := msg[i+len(marker):]
|
||||
if j := strings.Index(rest, " cause="); j >= 0 {
|
||||
return rest[:j]
|
||||
}
|
||||
return rest
|
||||
}
|
||||
return classifyTransportError(err)
|
||||
}
|
||||
|
||||
43
internal/infrastructure/external/rongxing/transport_error_test.go
vendored
Normal file
43
internal/infrastructure/external/rongxing/transport_error_test.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package rongxing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClassifyTransportError_Timeout(t *testing.T) {
|
||||
err := errors.New(`Post "http://192.168.3.43:7007/auth/login": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`)
|
||||
got := classifyTransportError(err)
|
||||
if !strings.Contains(got, "请求超时") {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyTransportError_Deadline(t *testing.T) {
|
||||
got := classifyTransportError(context.DeadlineExceeded)
|
||||
if !strings.Contains(got, "请求超时") {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapTransportError_IncludesProxy(t *testing.T) {
|
||||
svc, err := NewRongxingService(serviceConfig{
|
||||
BaseURL: "http://192.168.3.43:7007",
|
||||
Timeout: time.Second,
|
||||
Proxy: "socks5://rongxing-vpn:1080",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapped := svc.wrapTransportError("http://192.168.3.43:7007/auth/login", context.DeadlineExceeded)
|
||||
msg := wrapped.Error()
|
||||
if !strings.Contains(msg, "proxy=socks5://rongxing-vpn:1080") {
|
||||
t.Fatalf("missing proxy in error: %s", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "diagnosis=") {
|
||||
t.Fatalf("missing diagnosis in error: %s", msg)
|
||||
}
|
||||
}
|
||||
3
secrets/openvpn/.gitkeep
Normal file
3
secrets/openvpn/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# 本地/仓库内占位:真实证书请放到此目录(勿提交 *.key / *.crt / *.ovpn)
|
||||
# 1Panel 推荐改用绝对路径:/opt/1panel/apps/hyapi-secrets/openvpn
|
||||
# Compose 通过环境变量 RONGXING_VPN_CONFIG 覆盖挂载源目录。
|
||||
Reference in New Issue
Block a user