Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a53bbe743e | |||
| 7b1f49c182 | |||
| 8c8486f9af | |||
| 7d0f4c8065 | |||
| 3d521973ab | |||
| cb8e4bddd8 | |||
| 4cfe4d0ccb | |||
| 366695ffd6 |
@@ -45,7 +45,8 @@ jobs:
|
||||
env:
|
||||
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
run: bash scripts/ci/compile-android.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/compile-android.sh "$REF_NAME"
|
||||
|
||||
- name: Upload android artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -65,7 +66,9 @@ jobs:
|
||||
|
||||
- name: Compile (Windows installer)
|
||||
shell: bash
|
||||
run: bash scripts/ci/compile-windows.sh "${{ gitea.ref_name }}"
|
||||
env:
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/compile-windows.sh "$REF_NAME"
|
||||
|
||||
- name: Upload windows artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -80,38 +83,44 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download android artifact
|
||||
# 一次性下所有 artifact(不带 name),避免同 job 内两次复用 download-artifact
|
||||
# action → act 对其只读缓存 git 仓库做二次操作时 EACCES(pack idx 444)。
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: android
|
||||
path: dist/
|
||||
path: dist-raw/
|
||||
|
||||
- name: Download windows artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows
|
||||
path: dist/
|
||||
- name: Flatten artifacts into dist/
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
find dist-raw -type f -exec cp {} dist/ \;
|
||||
echo "dist/ 内容:"; ls -la dist/
|
||||
|
||||
- name: Release → Forgejo
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
run: bash scripts/ci/release-client.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/release-client.sh "$REF_NAME"
|
||||
|
||||
- name: Deploy → pangolin1 (downloads/)
|
||||
env:
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: bash scripts/ci/deploy-client.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/deploy-client.sh "$REF_NAME"
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
run: |
|
||||
. scripts/ci/notify.sh
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
notify_ok "client ${{ gitea.ref_name }} released + deployed"
|
||||
if [ "$JOB_STATUS" = "success" ]; then
|
||||
notify_ok "client $REF_NAME released + deployed"
|
||||
else
|
||||
notify_fail "client ${{ gitea.ref_name }} pipeline failed"
|
||||
notify_fail "client $REF_NAME pipeline failed"
|
||||
fi
|
||||
|
||||
@@ -109,9 +109,17 @@ ISS="${BUILD_CLIENT}/windows/installer/pangolin.iss"
|
||||
sed -i "s/^#define MyAppVersion .*/#define MyAppVersion \"${VER}\"/" "$ISS"
|
||||
|
||||
echo "==> building installer with Inno Setup (version ${VER})"
|
||||
# MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL disable Git Bash's automatic conversion
|
||||
# of paths, matching jiu's ISCC invocation fix.
|
||||
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "$ISCC" "$ISS"
|
||||
# ISCC 是原生 Windows 程序,只认 Windows 路径(C:\...);传 MSYS 风格 /c/... 会被它
|
||||
# 当成选项 → "Unknown option: /c/...pangolin.iss"。故先用 cygpath 把 .iss 转成
|
||||
# Windows 路径再传(jiu 是直接写死 C:\ 字面量;这里用 cygpath 更稳)。
|
||||
# 不用 $() 命令替换(仓库约定):cygpath 输出落临时文件、read 读回。
|
||||
cygpath -w "$ISS" > /tmp/pangolin_iss_win.$$
|
||||
ISS_WIN=""
|
||||
read -r ISS_WIN < /tmp/pangolin_iss_win.$$
|
||||
rm -f /tmp/pangolin_iss_win.$$
|
||||
# MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL 关掉 Git Bash 对 /-开头参数的自动路径转换
|
||||
# (否则会把 .iss 里将来可能的 /D 定义也改坏)。ISS_WIN 已是 Windows 路径,原样传即可。
|
||||
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "$ISCC" "$ISS_WIN"
|
||||
|
||||
# pangolin.iss has no explicit OutputDir -> Inno defaults to {src}\Output
|
||||
# (relative to the .iss file), filename OutputBaseFilename=pangolin-setup-<ver>.
|
||||
|
||||
@@ -123,6 +123,8 @@ func main() {
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
r.Use(apierr.Middleware)
|
||||
// CORS:Web 用户中心(app.yanmeiai.com)跨域调 /v1/*;原生端不受影响。
|
||||
r.Use(httpapi.NewCORS())
|
||||
|
||||
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewCORS 返回一个 CORS 中间件:为白名单 Origin(精确匹配)补 CORS 响应头,并直接
|
||||
// 应答 OPTIONS 预检。Web 用户中心(app.yanmeiai.com,浏览器)跨域调用 /v1/*,浏览器
|
||||
// 会先发预检、并校验 Access-Control-Allow-Origin;原生移动/桌面客户端不是浏览器、
|
||||
// 不受 CORS 约束,故此前无需 CORS。
|
||||
//
|
||||
// Origin 白名单来自 CORS_ORIGINS(逗号分隔),缺省含 usercenter 的正式域与 pages.dev。
|
||||
// 认证走 Authorization: Bearer(非 cookie),所以不需要 Allow-Credentials。
|
||||
func NewCORS() func(http.Handler) http.Handler {
|
||||
raw := os.Getenv("CORS_ORIGINS")
|
||||
if raw == "" {
|
||||
raw = "https://app.yanmeiai.com,https://pangolin-usercenter.pages.dev"
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
o = strings.TrimSpace(o)
|
||||
if o != "" {
|
||||
allowed[o] = true
|
||||
}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin != "" && allowed[origin] {
|
||||
h := w.Header()
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Add("Vary", "Origin")
|
||||
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
h.Set("Access-Control-Max-Age", "600")
|
||||
}
|
||||
// 预检请求直接 204(不落到业务路由,避免 /v1/... 的 OPTIONS 404)。
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCORS(t *testing.T) {
|
||||
t.Setenv("CORS_ORIGINS", "https://app.yanmeiai.com")
|
||||
mw := NewCORS()
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
|
||||
h := mw(next)
|
||||
|
||||
// 允许的 Origin:补 Allow-Origin,普通请求继续。
|
||||
r := httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.yanmeiai.com" {
|
||||
t.Fatalf("allowed origin: want header, got %q", got)
|
||||
}
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("non-preflight should reach next, got %d", w.Code)
|
||||
}
|
||||
|
||||
// OPTIONS 预检:204,不落到业务。
|
||||
r = httptest.NewRequest("OPTIONS", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight: want 204, got %d", w.Code)
|
||||
}
|
||||
if w.Header().Get("Access-Control-Allow-Methods") == "" {
|
||||
t.Fatalf("preflight missing Allow-Methods")
|
||||
}
|
||||
|
||||
// 未白名单 Origin:不补 Allow-Origin。
|
||||
r = httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://evil.example.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("disallowed origin should get no header, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import { SITE } from '../config/site';
|
||||
interface Props { t: T }
|
||||
const { t } = Astro.props;
|
||||
|
||||
// href 缺省 = 本轮未接入下载(iOS / macOS / Linux),按钮渲染为禁用态占位。
|
||||
// href 缺省 = 本轮未接入下载(iOS / macOS / Windows / Linux),按钮渲染为禁用态占位。
|
||||
// 本轮仅 Android 有真实产物;Windows 待其 CI(需 windows runner)产出 exe 后接
|
||||
// SITE.downloads.windows(已在 site.ts 备好)。
|
||||
const plats: { icon: string; name: string; ver: string; href?: string }[] = [
|
||||
{ icon: 'smartphone', name: 'iOS', ver: 'iOS 16+' },
|
||||
{ icon: 'smartphone', name: 'Android', ver: 'Android 9+', href: SITE.downloads.android },
|
||||
{ icon: 'laptop', name: 'macOS', ver: 'macOS 12+' },
|
||||
{ icon: 'monitor', name: 'Windows', ver: 'Win 10/11', href: SITE.downloads.windows },
|
||||
{ icon: 'monitor', name: 'Windows', ver: 'Win 10/11' },
|
||||
{ icon: 'terminal', name: 'Linux', ver: 'deb / rpm' },
|
||||
];
|
||||
---
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, Menu } from 'lucide-react';
|
||||
import { SITE } from '../config/site';
|
||||
|
||||
function Mark() {
|
||||
return (
|
||||
@@ -60,8 +61,7 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
<a data-lang="zh" class={lang === 'zh' ? 'on' : undefined} href="/">中文</a>
|
||||
<a data-lang="en" class={lang === 'en' ? 'on' : undefined} href="/en/">EN</a>
|
||||
</div>
|
||||
{/* 「登录」暂移除:Web 用户中心(web/usercenter)未部署、无 URL;待 #30 CI/CD
|
||||
部署后接上 usercenter 登录地址再恢复。 */}
|
||||
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
|
||||
<a class="btn btn-primary" href="#download">
|
||||
<Download />
|
||||
<span>{t.get}</span>
|
||||
|
||||
@@ -20,6 +20,9 @@ export const SITE = {
|
||||
/** 自助发卡商店 —— 占位待定(#24)。 */
|
||||
store: { label: 'shop.pangolin.vpn' },
|
||||
|
||||
/** Web 用户中心(web/usercenter,Next.js 静态导出 → CF Pages)。登录/订阅/设备管理入口。 */
|
||||
usercenter: 'https://app.yanmeiai.com',
|
||||
|
||||
/**
|
||||
* 客户端安装包直链。控制面 pangolin-server 通过 Cloudflare Tunnel 对外暴露
|
||||
* /downloads/<file>(origin = 127.0.0.1:8080),文件由
|
||||
|
||||
Reference in New Issue
Block a user