@weibaohui/dsh-git-server 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/client/bundle.js +55 -4
- package/client/index.js +55 -4
- package/package.json +5 -4
- package/src/index.js +9 -1
- package/server/dist/serv.js +0 -43
- package/server/dist/sshx/dispatch.js +0 -160
- package/server/dist/sshx/lfs-transfer.js +0 -289
- package/server/dist/sshx/server.js +0 -107
- package/server/dist/twofactor.js +0 -160
- package/server/dist/webapi.js +0 -377
package/README.md
CHANGED
|
@@ -3,33 +3,35 @@
|
|
|
3
3
|
[](https://github.com/topics/dsh-plugin)
|
|
4
4
|
[](https://www.npmjs.com/package/@weibaohui/dsh-git-server)
|
|
5
5
|
|
|
6
|
-
**Git
|
|
6
|
+
**Git 服务器插件**:把一套完整的自助 Git 服务装进 dsh——HTTP clone/push、工单、PR、wiki、发版、标签、里程碑、webhook 全功能,网页侧全程是 dsh 原生界面,账号可复用 user-management。
|
|
7
7
|
|
|
8
8
|
<video src="docs/demo-tour.webm" controls muted playsinline width="100%"></video>
|
|
9
9
|
|
|
10
10
|
## 核心功能
|
|
11
11
|
|
|
12
|
-
- **完整自助 Git 服务**:
|
|
13
|
-
- **dsh 原生管理页**:侧栏底部「Git
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- **数据自持**:数据目录 = 仓库 + 数据库 + 日志;升级插件后原样沿用,备份整个目录即备份全部
|
|
12
|
+
- **完整自助 Git 服务**:HTTP clone/push 开箱即用,仓库/工单/PR/wiki/发版/标签/里程碑一条龙,崩溃自动拉起
|
|
13
|
+
- **dsh 原生管理页**:侧栏底部「Git」进全屏页——建仓/删仓/克隆地址,仓库浏览多标签(文件树+内容、提交历史、分支、工单、PR、Wiki、发版、标签、里程碑、设置);React + dsh 主题(亮暗自动跟随)、zh/en 双语
|
|
14
|
+
- **账号打通**:账户来源 = [user-management](https://github.com/weibaohui/user-management)(必装)——git 凭据与页面登录均走 user-management 校验,免另记一套账号;网页注册关闭,账号只由 dsh 管理
|
|
15
|
+
- **一键启停**:启用/停用/端口/数据目录全在设置页,改完即生效
|
|
16
|
+
- **局域网 git**:默认监听 `0.0.0.0`,局域网内机器可直接 `git clone/push`
|
|
17
|
+
- **数据自持**:数据目录 = 仓库 + 数据库 + 日志,备份整个目录即备份全部
|
|
19
18
|
|
|
20
19
|
## 安装
|
|
21
20
|
|
|
21
|
+
本插件依赖 [user-management](https://github.com/weibaohui/user-management) 提供登录与账户,必须先装:
|
|
22
|
+
|
|
22
23
|
```bash
|
|
24
|
+
dsh plugin --profile web add @weibaohui/user-management -w
|
|
23
25
|
dsh plugin --profile web add @weibaohui/dsh-git-server -w
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
装完重启 `dsh web`
|
|
28
|
+
装完重启 `dsh web` 即生效。
|
|
27
29
|
|
|
28
30
|
## 使用
|
|
29
31
|
|
|
30
|
-
1. 打开 Web UI → **设置 → Git 服务器**——**默认已启用**(监听 `0.0.0.0:3400
|
|
32
|
+
1. 打开 Web UI → **设置 → Git 服务器**——**默认已启用**(监听 `0.0.0.0:3400`,无需勾选;要停用在设置页取消勾选),可在此改端口/数据目录/查看 `root` 密码
|
|
31
33
|
2. 侧栏底部点「**Git**」进全屏管理页:新建仓库(或导入已有),照常开发
|
|
32
|
-
3. `git clone/push` 走子进程端口,凭据用
|
|
34
|
+
3. `git clone/push` 走子进程端口,凭据用 user-management 的用户名密码:
|
|
33
35
|
|
|
34
36
|
```sh
|
|
35
37
|
git clone http://127.0.0.1:3400/root/drill-repo.git
|
package/client/bundle.js
CHANGED
|
@@ -57,6 +57,9 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
noReleases: '暂无发版', noWiki: '暂无页面——创建第一个', deletePage: '删除页面',
|
|
58
58
|
prMerged: '已合并', prOpen: '开启中', prClosed: '已关闭',
|
|
59
59
|
labels: '标签管理', milestones: '里程碑', settings: '设置',
|
|
60
|
+
stRunning: '运行中', stStopped: '已停用', stError: '错误', stRetrying: '重试中',
|
|
61
|
+
stUnreachable: '无法连接', stFetchErr: '状态接口无响应,宿主进程可能已崩溃',
|
|
62
|
+
stPid: 'PID', stCrashes: '崩溃', stDepsFailed: '依赖缺失',
|
|
60
63
|
}
|
|
61
64
|
const EN = {
|
|
62
65
|
nav: 'Git', title: 'Repositories',
|
|
@@ -78,6 +81,9 @@ window.__ModuleLoader__.load({
|
|
|
78
81
|
noReleases: 'No releases', noWiki: 'No pages yet — create one', deletePage: 'Delete page',
|
|
79
82
|
prMerged: 'Merged', prOpen: 'Open', prClosed: 'Closed',
|
|
80
83
|
labels: 'Labels', milestones: 'Milestones', settings: 'Settings',
|
|
84
|
+
stRunning: 'Running', stStopped: 'Stopped', stError: 'Error', stRetrying: 'Retrying',
|
|
85
|
+
stUnreachable: 'Unreachable', stFetchErr: 'Status endpoint not responding — host process may have crashed',
|
|
86
|
+
stPid: 'PID', stCrashes: 'crashes', stDepsFailed: 'deps missing',
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
// ── styles(dsw token 原生) ───────────────────────────────────────────────
|
|
@@ -166,6 +172,21 @@ window.__ModuleLoader__.load({
|
|
|
166
172
|
.dgs-comment{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;margin:8px 0}
|
|
167
173
|
.dgs-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:40px 0;font-size:13px}
|
|
168
174
|
.dgs-err{color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
175
|
+
.dgs-status-row{display:flex;align-items:center;gap:8px;margin-bottom:2px}
|
|
176
|
+
.dgs-status-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
177
|
+
.dgs-status-dot.running{background:var(--dsw-alias-state-success-primary)}
|
|
178
|
+
.dgs-status-dot.stopped{background:var(--dsw-alias-label-tertiary)}
|
|
179
|
+
.dgs-status-dot.error{background:var(--dsw-alias-state-error-primary)}
|
|
180
|
+
.dgs-status-dot.waiting{background:#f59e0b}
|
|
181
|
+
.dgs-status-dot.unreachable{background:var(--dsw-alias-state-error-primary);animation:dgs-pulse 1.5s ease-in-out infinite}
|
|
182
|
+
@keyframes dgs-pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
183
|
+
.dgs-status-text{font-size:13px;font-weight:600}
|
|
184
|
+
.dgs-status-text.running{color:var(--dsw-alias-state-success-primary)}
|
|
185
|
+
.dgs-status-text.stopped{color:var(--dsw-alias-label-tertiary)}
|
|
186
|
+
.dgs-status-text.error{color:var(--dsw-alias-state-error-primary)}
|
|
187
|
+
.dgs-status-text.waiting{color:#f59e0b}
|
|
188
|
+
.dgs-status-text.unreachable{color:var(--dsw-alias-state-error-primary)}
|
|
189
|
+
.dgs-status-meta{font-size:12px;color:var(--dsw-alias-label-tertiary);margin-left:4px}
|
|
169
190
|
.dgs-ico{margin-right:8px;opacity:.75}
|
|
170
191
|
.dgs-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);background:var(--dsw-alias-toast-bg);color:#fff;border-radius:8px;padding:8px 16px;font-size:12px;z-index:2147483600}
|
|
171
192
|
.dgs-settings{max-width:720px;display:flex;flex-direction:column;gap:12px;padding:12px 16px 24px;color:var(--dsw-alias-label-primary)}
|
|
@@ -2116,27 +2137,57 @@ window.__ModuleLoader__.load({
|
|
|
2116
2137
|
|
|
2117
2138
|
function SettingsSection({ t }) {
|
|
2118
2139
|
const [status, setStatus] = useState(null)
|
|
2140
|
+
const [unreachable, setUnreachable] = useState(false)
|
|
2119
2141
|
const [form, setForm] = useState({ enabled: false, host: '127.0.0.1', port: 3400, dataDir: '' })
|
|
2120
2142
|
const [busy, setBusy] = useState(false)
|
|
2121
2143
|
const [msg, setMsg] = useState('')
|
|
2122
2144
|
useEffect(() => {
|
|
2123
2145
|
fetch(API + '/status').then((r) => r.json()).then((d) => {
|
|
2124
|
-
setStatus(d)
|
|
2146
|
+
setStatus(d); setUnreachable(false)
|
|
2125
2147
|
setForm({ enabled: !!d.enabled, host: d.host || '127.0.0.1', port: d.port || 3400, dataDir: d.dataDir || '' })
|
|
2126
|
-
}).catch(() =>
|
|
2148
|
+
}).catch(() => setUnreachable(true))
|
|
2149
|
+
}, [])
|
|
2150
|
+
// 定时轮询:保持状态新鲜但不覆盖用户正在编辑的表单
|
|
2151
|
+
useEffect(() => {
|
|
2152
|
+
const id = setInterval(() => {
|
|
2153
|
+
fetch(API + '/status').then((r) => r.json()).then((d) => { setStatus(d); setUnreachable(false) }).catch(() => setUnreachable(true))
|
|
2154
|
+
}, 5000)
|
|
2155
|
+
return () => clearInterval(id)
|
|
2127
2156
|
}, [])
|
|
2128
2157
|
const save = async () => {
|
|
2129
2158
|
setBusy(true); setMsg('…')
|
|
2130
2159
|
try {
|
|
2131
2160
|
const res = await fetch(API + '/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
|
|
2132
2161
|
const d = await res.json()
|
|
2133
|
-
setStatus(d); setMsg('')
|
|
2162
|
+
setStatus(d); setUnreachable(false); setMsg('')
|
|
2134
2163
|
} catch (e) { setMsg(String(e)) } finally { setBusy(false) }
|
|
2135
2164
|
}
|
|
2136
2165
|
const field = (k) => ({ value: form[k], onChange: (e) => setForm({ ...form, [k]: e.target.value }) })
|
|
2166
|
+
// 推导徽章状态:fetch 失败优先(宿主进程可能崩溃)
|
|
2167
|
+
let stKey = 'stopped', stLabel = t('stStopped')
|
|
2168
|
+
if (unreachable) {
|
|
2169
|
+
stKey = 'unreachable'; stLabel = t('stUnreachable')
|
|
2170
|
+
} else if (status) {
|
|
2171
|
+
if (status.error) { stKey = 'error'; stLabel = t('stError') }
|
|
2172
|
+
else if (status.running) { stKey = 'running'; stLabel = t('stRunning') }
|
|
2173
|
+
else if (status.enabled) { stKey = 'waiting'; stLabel = t('stRetrying') }
|
|
2174
|
+
}
|
|
2175
|
+
const meta = []
|
|
2176
|
+
if (!unreachable && status) {
|
|
2177
|
+
if (status.running && status.pid) meta.push(t('stPid') + ' ' + status.pid)
|
|
2178
|
+
if (status.crashes > 0) meta.push(status.crashes + ' ' + t('stCrashes'))
|
|
2179
|
+
if (status.deps === 'failed') meta.push(t('stDepsFailed'))
|
|
2180
|
+
}
|
|
2137
2181
|
return h('div', { className: 'dgs-settings' },
|
|
2138
2182
|
h('div', { className: 'dgs-card' },
|
|
2139
|
-
h('div', { className: 'dgs-
|
|
2183
|
+
h('div', { className: 'dgs-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
|
|
2184
|
+
h('div', { className: 'dgs-h1' }, t('title')),
|
|
2185
|
+
(status || unreachable) ? h('div', { className: 'dgs-status-row' },
|
|
2186
|
+
h('span', { className: 'dgs-status-dot ' + stKey }),
|
|
2187
|
+
h('span', { className: 'dgs-status-text ' + stKey }, stLabel),
|
|
2188
|
+
meta.length ? h('span', { className: 'dgs-status-meta' }, meta.join(' · ')) : null) : null),
|
|
2189
|
+
unreachable ? h('div', { className: 'dgs-err', style: { marginBottom: 8 } }, t('stFetchErr')) : null,
|
|
2190
|
+
status && status.error ? h('div', { className: 'dgs-err', style: { marginBottom: 8 } }, status.error) : null,
|
|
2140
2191
|
h('div', { className: 'dgs-row' },
|
|
2141
2192
|
h('label', null, '启用'),
|
|
2142
2193
|
h('input', { type: 'checkbox', checked: form.enabled, onChange: (e) => setForm({ ...form, enabled: e.target.checked }) })),
|
package/client/index.js
CHANGED
|
@@ -47,6 +47,9 @@ const ZH = {
|
|
|
47
47
|
noReleases: '暂无发版', noWiki: '暂无页面——创建第一个', deletePage: '删除页面',
|
|
48
48
|
prMerged: '已合并', prOpen: '开启中', prClosed: '已关闭',
|
|
49
49
|
labels: '标签管理', milestones: '里程碑', settings: '设置',
|
|
50
|
+
stRunning: '运行中', stStopped: '已停用', stError: '错误', stRetrying: '重试中',
|
|
51
|
+
stUnreachable: '无法连接', stFetchErr: '状态接口无响应,宿主进程可能已崩溃',
|
|
52
|
+
stPid: 'PID', stCrashes: '崩溃', stDepsFailed: '依赖缺失',
|
|
50
53
|
}
|
|
51
54
|
const EN = {
|
|
52
55
|
nav: 'Git', title: 'Repositories',
|
|
@@ -68,6 +71,9 @@ const EN = {
|
|
|
68
71
|
noReleases: 'No releases', noWiki: 'No pages yet — create one', deletePage: 'Delete page',
|
|
69
72
|
prMerged: 'Merged', prOpen: 'Open', prClosed: 'Closed',
|
|
70
73
|
labels: 'Labels', milestones: 'Milestones', settings: 'Settings',
|
|
74
|
+
stRunning: 'Running', stStopped: 'Stopped', stError: 'Error', stRetrying: 'Retrying',
|
|
75
|
+
stUnreachable: 'Unreachable', stFetchErr: 'Status endpoint not responding — host process may have crashed',
|
|
76
|
+
stPid: 'PID', stCrashes: 'crashes', stDepsFailed: 'deps missing',
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// ── styles(dsw token 原生) ───────────────────────────────────────────────
|
|
@@ -156,6 +162,21 @@ textarea.dgs-input,.dgs-card>.dgs-input{width:100%;box-sizing:border-box;flex:no
|
|
|
156
162
|
.dgs-comment{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;margin:8px 0}
|
|
157
163
|
.dgs-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:40px 0;font-size:13px}
|
|
158
164
|
.dgs-err{color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
165
|
+
.dgs-status-row{display:flex;align-items:center;gap:8px;margin-bottom:2px}
|
|
166
|
+
.dgs-status-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
167
|
+
.dgs-status-dot.running{background:var(--dsw-alias-state-success-primary)}
|
|
168
|
+
.dgs-status-dot.stopped{background:var(--dsw-alias-label-tertiary)}
|
|
169
|
+
.dgs-status-dot.error{background:var(--dsw-alias-state-error-primary)}
|
|
170
|
+
.dgs-status-dot.waiting{background:#f59e0b}
|
|
171
|
+
.dgs-status-dot.unreachable{background:var(--dsw-alias-state-error-primary);animation:dgs-pulse 1.5s ease-in-out infinite}
|
|
172
|
+
@keyframes dgs-pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
173
|
+
.dgs-status-text{font-size:13px;font-weight:600}
|
|
174
|
+
.dgs-status-text.running{color:var(--dsw-alias-state-success-primary)}
|
|
175
|
+
.dgs-status-text.stopped{color:var(--dsw-alias-label-tertiary)}
|
|
176
|
+
.dgs-status-text.error{color:var(--dsw-alias-state-error-primary)}
|
|
177
|
+
.dgs-status-text.waiting{color:#f59e0b}
|
|
178
|
+
.dgs-status-text.unreachable{color:var(--dsw-alias-state-error-primary)}
|
|
179
|
+
.dgs-status-meta{font-size:12px;color:var(--dsw-alias-label-tertiary);margin-left:4px}
|
|
159
180
|
.dgs-ico{margin-right:8px;opacity:.75}
|
|
160
181
|
.dgs-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);background:var(--dsw-alias-toast-bg);color:#fff;border-radius:8px;padding:8px 16px;font-size:12px;z-index:2147483600}
|
|
161
182
|
.dgs-settings{max-width:720px;display:flex;flex-direction:column;gap:12px;padding:12px 16px 24px;color:var(--dsw-alias-label-primary)}
|
|
@@ -2106,27 +2127,57 @@ function GitPage({ onClose, t }) {
|
|
|
2106
2127
|
|
|
2107
2128
|
function SettingsSection({ t }) {
|
|
2108
2129
|
const [status, setStatus] = useState(null)
|
|
2130
|
+
const [unreachable, setUnreachable] = useState(false)
|
|
2109
2131
|
const [form, setForm] = useState({ enabled: false, host: '127.0.0.1', port: 3400, dataDir: '' })
|
|
2110
2132
|
const [busy, setBusy] = useState(false)
|
|
2111
2133
|
const [msg, setMsg] = useState('')
|
|
2112
2134
|
useEffect(() => {
|
|
2113
2135
|
fetch(API + '/status').then((r) => r.json()).then((d) => {
|
|
2114
|
-
setStatus(d)
|
|
2136
|
+
setStatus(d); setUnreachable(false)
|
|
2115
2137
|
setForm({ enabled: !!d.enabled, host: d.host || '127.0.0.1', port: d.port || 3400, dataDir: d.dataDir || '' })
|
|
2116
|
-
}).catch(() =>
|
|
2138
|
+
}).catch(() => setUnreachable(true))
|
|
2139
|
+
}, [])
|
|
2140
|
+
// 定时轮询:保持状态新鲜但不覆盖用户正在编辑的表单
|
|
2141
|
+
useEffect(() => {
|
|
2142
|
+
const id = setInterval(() => {
|
|
2143
|
+
fetch(API + '/status').then((r) => r.json()).then((d) => { setStatus(d); setUnreachable(false) }).catch(() => setUnreachable(true))
|
|
2144
|
+
}, 5000)
|
|
2145
|
+
return () => clearInterval(id)
|
|
2117
2146
|
}, [])
|
|
2118
2147
|
const save = async () => {
|
|
2119
2148
|
setBusy(true); setMsg('…')
|
|
2120
2149
|
try {
|
|
2121
2150
|
const res = await fetch(API + '/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
|
|
2122
2151
|
const d = await res.json()
|
|
2123
|
-
setStatus(d); setMsg('')
|
|
2152
|
+
setStatus(d); setUnreachable(false); setMsg('')
|
|
2124
2153
|
} catch (e) { setMsg(String(e)) } finally { setBusy(false) }
|
|
2125
2154
|
}
|
|
2126
2155
|
const field = (k) => ({ value: form[k], onChange: (e) => setForm({ ...form, [k]: e.target.value }) })
|
|
2156
|
+
// 推导徽章状态:fetch 失败优先(宿主进程可能崩溃)
|
|
2157
|
+
let stKey = 'stopped', stLabel = t('stStopped')
|
|
2158
|
+
if (unreachable) {
|
|
2159
|
+
stKey = 'unreachable'; stLabel = t('stUnreachable')
|
|
2160
|
+
} else if (status) {
|
|
2161
|
+
if (status.error) { stKey = 'error'; stLabel = t('stError') }
|
|
2162
|
+
else if (status.running) { stKey = 'running'; stLabel = t('stRunning') }
|
|
2163
|
+
else if (status.enabled) { stKey = 'waiting'; stLabel = t('stRetrying') }
|
|
2164
|
+
}
|
|
2165
|
+
const meta = []
|
|
2166
|
+
if (!unreachable && status) {
|
|
2167
|
+
if (status.running && status.pid) meta.push(t('stPid') + ' ' + status.pid)
|
|
2168
|
+
if (status.crashes > 0) meta.push(status.crashes + ' ' + t('stCrashes'))
|
|
2169
|
+
if (status.deps === 'failed') meta.push(t('stDepsFailed'))
|
|
2170
|
+
}
|
|
2127
2171
|
return h('div', { className: 'dgs-settings' },
|
|
2128
2172
|
h('div', { className: 'dgs-card' },
|
|
2129
|
-
h('div', { className: 'dgs-
|
|
2173
|
+
h('div', { className: 'dgs-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
|
|
2174
|
+
h('div', { className: 'dgs-h1' }, t('title')),
|
|
2175
|
+
(status || unreachable) ? h('div', { className: 'dgs-status-row' },
|
|
2176
|
+
h('span', { className: 'dgs-status-dot ' + stKey }),
|
|
2177
|
+
h('span', { className: 'dgs-status-text ' + stKey }, stLabel),
|
|
2178
|
+
meta.length ? h('span', { className: 'dgs-status-meta' }, meta.join(' · ')) : null) : null),
|
|
2179
|
+
unreachable ? h('div', { className: 'dgs-err', style: { marginBottom: 8 } }, t('stFetchErr')) : null,
|
|
2180
|
+
status && status.error ? h('div', { className: 'dgs-err', style: { marginBottom: 8 } }, status.error) : null,
|
|
2130
2181
|
h('div', { className: 'dgs-row' },
|
|
2131
2182
|
h('label', null, '启用'),
|
|
2132
2183
|
h('input', { type: 'checkbox', checked: form.enabled, onChange: (e) => setForm({ ...form, enabled: e.target.checked }) })),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weibaohui/dsh-git-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "dsh 插件 · Git 服务器:内嵌 ts-gogs(Gogs 的 TypeScript 平替),独立端口跑完整 Git 服务(HTTP clone/push、网页端、issue/PR/wiki),可复用 user-management 的用户名密码,设置页一键启停。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"cordis.patch.yml",
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.5",
|
|
29
|
+
"dsh": ">=0.1.2-rc.1"
|
|
30
|
+
},
|
|
27
31
|
"dsh": {
|
|
28
32
|
"bundle": {
|
|
29
33
|
"patch": "./cordis.patch.yml"
|
|
@@ -57,9 +61,6 @@
|
|
|
57
61
|
"ini": "^5.0.0",
|
|
58
62
|
"marked": "^15.0.0"
|
|
59
63
|
},
|
|
60
|
-
"engines": {
|
|
61
|
-
"node": ">=22.5"
|
|
62
|
-
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@types/better-sqlite3": "^7.6.12",
|
|
65
66
|
"@types/busboy": "^1.5.4",
|
package/src/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const path = require('node:path')
|
|
|
25
25
|
const engine = require('./runner')
|
|
26
26
|
|
|
27
27
|
const name = engine.PLUGIN_ID
|
|
28
|
-
const inject = ['webServer', 'settings']
|
|
28
|
+
const inject = ['webServer', 'settings', 'connection']
|
|
29
29
|
const API_PREFIX = '/' + engine.PLUGIN_ID + '/api'
|
|
30
30
|
const SETTINGS_NS = engine.PLUGIN_ID
|
|
31
31
|
|
|
@@ -452,6 +452,14 @@ module.exports = {
|
|
|
452
452
|
kind: 'prefix',
|
|
453
453
|
path: API_PREFIX,
|
|
454
454
|
handler: async (req, res) => {
|
|
455
|
+
// 与其它 host 路由一致的信任栅栏:connection 服务的 Host/Origin 检查
|
|
456
|
+
// 加浏览器认证,防止本机任意网页跨站调用。
|
|
457
|
+
const rejection = ctx.connection.requestRejection(req)
|
|
458
|
+
if (rejection !== undefined) {
|
|
459
|
+
res.writeHead(rejection)
|
|
460
|
+
res.end()
|
|
461
|
+
return
|
|
462
|
+
}
|
|
455
463
|
const url = new URL(req.url || '/', 'http://dsh.local')
|
|
456
464
|
const rest = url.pathname.slice(API_PREFIX.length)
|
|
457
465
|
try {
|
package/server/dist/serv.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// serv subcommand: entrypoint for authorized_keys mode. The system sshd runs
|
|
2
|
-
// `<ts-gogs> serv key-<id>` via a command= restriction on each public key line;
|
|
3
|
-
// the actual git command arrives in SSH_ORIGINAL_COMMAND (gogs cmd/gogs/serv.go).
|
|
4
|
-
import * as db from './db/db.js';
|
|
5
|
-
export async function runServ(keyArg) {
|
|
6
|
-
const keyID = Number(String(keyArg).replace(/^key-/, ''));
|
|
7
|
-
const key = db.getPublicKeyByID(keyID);
|
|
8
|
-
if (!key) {
|
|
9
|
-
console.error('Serv: cannot find public key:', keyArg);
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
const userID = key.owner_id;
|
|
13
|
-
const identity = {
|
|
14
|
-
userID,
|
|
15
|
-
keyID,
|
|
16
|
-
deployRepoID: null,
|
|
17
|
-
};
|
|
18
|
-
if (key.type === 2) {
|
|
19
|
-
const dk = db.db().prepare('SELECT repo_id FROM deploy_key WHERE key_id = ?').get(keyID);
|
|
20
|
-
identity.deployRepoID = dk?.repo_id ?? null;
|
|
21
|
-
}
|
|
22
|
-
const sshCmd = process.env.SSH_ORIGINAL_COMMAND ?? '';
|
|
23
|
-
if (!sshCmd) {
|
|
24
|
-
console.error('Serv: interactive shell is disabled.');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
const { dispatchSSHCommand } = await import('./sshx/dispatch.js');
|
|
28
|
-
// keep the event loop alive until close() — async handlers (LFS transfer)
|
|
29
|
-
// must never be cut off by a premature exit
|
|
30
|
-
const keepAlive = setInterval(() => { }, 1 << 30);
|
|
31
|
-
dispatchSSHCommand(sshCmd, identity, {
|
|
32
|
-
write: (chunk) => process.stdout.write(chunk),
|
|
33
|
-
onStdin: (cb) => {
|
|
34
|
-
process.stdin.on('data', (d) => cb(d));
|
|
35
|
-
process.stdin.resume();
|
|
36
|
-
},
|
|
37
|
-
close: (code) => {
|
|
38
|
-
clearInterval(keepAlive);
|
|
39
|
-
process.exit(code);
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=serv.js.map
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Shared SSH command dispatch used by both the builtin SSH server and the
|
|
2
|
-
// `serv` subcommand (authorized_keys mode). Verbs mirror gogs cmd/gogs/serv.go
|
|
3
|
-
// plus the LFS-over-SSH extensions (git-lfs-authenticate, git-lfs-transfer).
|
|
4
|
-
import * as fs from 'node:fs';
|
|
5
|
-
import * as path from 'node:path';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
|
-
import { conf } from '../conf.js';
|
|
8
|
-
import * as db from '../db/db.js';
|
|
9
|
-
import { runLFSTransfer } from './lfs-transfer.js';
|
|
10
|
-
export function parseSSHCmd(cmd) {
|
|
11
|
-
const m = /^\s*(git[-\s][a-z-]+|git-lfs-authenticate|git-lfs-transfer)\s+'(.*)'\s*$/.exec(cmd) ??
|
|
12
|
-
/^\s*(git[-\s][a-z-]+|git-lfs-authenticate|git-lfs-transfer)\s+"(.*)"\s*$/.exec(cmd) ??
|
|
13
|
-
/^\s*(git[-\s][a-z-]+|git-lfs-authenticate|git-lfs-transfer)\s+(.*)\s*$/.exec(cmd);
|
|
14
|
-
if (!m)
|
|
15
|
-
return [];
|
|
16
|
-
return [m[1], m[2].replace(/^~\//, '')];
|
|
17
|
-
}
|
|
18
|
-
function resolveRepo(repoFullName) {
|
|
19
|
-
let repoName = repoFullName.replace(/^\//, '').replace(/\.git$/, '');
|
|
20
|
-
let isWiki = false;
|
|
21
|
-
if (repoName.endsWith('.wiki')) {
|
|
22
|
-
isWiki = true;
|
|
23
|
-
repoName = repoName.slice(0, -5);
|
|
24
|
-
}
|
|
25
|
-
const slash = repoName.indexOf('/');
|
|
26
|
-
const ownerName = slash > 0 ? repoName.slice(0, slash) : repoName;
|
|
27
|
-
const name = slash > 0 ? repoName.slice(slash + 1) : repoName;
|
|
28
|
-
const owner = db.getUserByUsername(ownerName);
|
|
29
|
-
const repo = owner ? db.getRepoByOwnerAndName(owner, name) : null;
|
|
30
|
-
if (!owner || !repo)
|
|
31
|
-
return null;
|
|
32
|
-
const repoDir = isWiki ? repo.WikiPath() : repo.RepoPath();
|
|
33
|
-
if (!fs.existsSync(repoDir))
|
|
34
|
-
return null;
|
|
35
|
-
return { owner, repo, repoDir };
|
|
36
|
-
}
|
|
37
|
-
/** git-upload-pack / git-receive-pack with gogs permission semantics. */
|
|
38
|
-
export function serveGit(verb, repoFullName, identity, io) {
|
|
39
|
-
let repoName = repoFullName.replace(/^\//, '').replace(/\.git$/, '');
|
|
40
|
-
let isWiki = false;
|
|
41
|
-
if (repoName.endsWith('.wiki')) {
|
|
42
|
-
isWiki = true;
|
|
43
|
-
repoName = repoName.slice(0, -5);
|
|
44
|
-
}
|
|
45
|
-
const slash = repoName.indexOf('/');
|
|
46
|
-
const ownerName = slash > 0 ? repoName.slice(0, slash) : repoName;
|
|
47
|
-
const name = slash > 0 ? repoName.slice(slash + 1) : repoName;
|
|
48
|
-
const owner = db.getUserByUsername(ownerName);
|
|
49
|
-
const repo = owner ? db.getRepoByOwnerAndName(owner, name) : null;
|
|
50
|
-
const fail = (msg) => {
|
|
51
|
-
io.write(Buffer.from(`\r\n${msg}\r\n`));
|
|
52
|
-
io.close(1);
|
|
53
|
-
};
|
|
54
|
-
if (!owner || !repo)
|
|
55
|
-
return fail('Repository does not exist');
|
|
56
|
-
const repoDir = isWiki ? repo.WikiPath() : repo.RepoPath();
|
|
57
|
-
if (!fs.existsSync(repoDir))
|
|
58
|
-
return fail('Repository does not exist');
|
|
59
|
-
const isPull = verb === 'git-upload-pack';
|
|
60
|
-
// deploy keys: single-repo, read-only
|
|
61
|
-
if (identity.deployRepoID !== null && (!isPull || identity.deployRepoID !== repo.id)) {
|
|
62
|
-
return fail('Access denied');
|
|
63
|
-
}
|
|
64
|
-
const mode = db.accessMode(identity.userID, repo);
|
|
65
|
-
const need = isPull ? db.AccessMode.READ : db.AccessMode.WRITE;
|
|
66
|
-
if (!(isPull && !repo.is_private && !conf.requireSigninView) && mode < need) {
|
|
67
|
-
return fail('Access denied');
|
|
68
|
-
}
|
|
69
|
-
if (!isPull && repo.is_mirror)
|
|
70
|
-
return fail('Mirror repository is read-only');
|
|
71
|
-
const args = isPull ? ['upload-pack', repoDir] : ['receive-pack', repoDir];
|
|
72
|
-
const env = {};
|
|
73
|
-
if (!isPull) {
|
|
74
|
-
const user = db.getUserByID(identity.userID);
|
|
75
|
-
Object.assign(env, {
|
|
76
|
-
GOGS_AUTH_USER_ID: String(user?.id ?? 0),
|
|
77
|
-
GOGS_AUTH_USER_NAME: String(user?.name ?? ''),
|
|
78
|
-
GOGS_AUTH_USER_EMAIL: String(user?.email ?? ''),
|
|
79
|
-
GOGS_REPO_OWNER_NAME: owner.name,
|
|
80
|
-
GOGS_REPO_ID: String(repo.id),
|
|
81
|
-
GOGS_REPO_NAME: String(repo.name),
|
|
82
|
-
GOGS_REPO_CUSTOM_HOOKS_PATH: path.join(repoDir, 'custom_hooks'),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const child = spawn('git', args, { env: { ...process.env, ...env } });
|
|
86
|
-
child.stdout.on('data', (d) => io.write(d));
|
|
87
|
-
child.stderr.on('data', (d) => io.write(Buffer.from(`remote: ${d.toString()}`)));
|
|
88
|
-
io.onStdin((data) => child.stdin.write(data));
|
|
89
|
-
child.on('close', (code) => io.close(code ?? 0));
|
|
90
|
-
child.on('error', () => io.close(1));
|
|
91
|
-
}
|
|
92
|
-
/** git-lfs-authenticate <path> <download|upload> → JSON auth response on stdout. */
|
|
93
|
-
export async function serveLFSAuthenticate(argStr, identity, io) {
|
|
94
|
-
const parts = argStr.trim().split(/\s+/);
|
|
95
|
-
const repoFullName = parts[0];
|
|
96
|
-
const operation = parts[1] ?? 'download';
|
|
97
|
-
if (!['download', 'upload'].includes(operation)) {
|
|
98
|
-
io.write(Buffer.from(`lfs-authenticate: invalid operation: ${operation}\n`));
|
|
99
|
-
io.close(1);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const resolved = resolveRepo(repoFullName);
|
|
103
|
-
if (!resolved) {
|
|
104
|
-
io.write(Buffer.from('Repository does not exist\n'));
|
|
105
|
-
io.close(1);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const { owner, repo } = resolved;
|
|
109
|
-
const mode = db.accessMode(identity.userID, repo);
|
|
110
|
-
const need = operation === 'upload' ? db.AccessMode.WRITE : db.AccessMode.READ;
|
|
111
|
-
if (!(operation === 'download' && !repo.is_private && !conf.requireSigninView) && mode < need) {
|
|
112
|
-
io.write(Buffer.from('Access denied\n'));
|
|
113
|
-
io.close(1);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const { mintLFSToken } = await import('../lfsx.js');
|
|
117
|
-
const { token, expiresAt } = mintLFSToken(identity.userID, repo.id);
|
|
118
|
-
const href = `${conf.externalURL}${owner.name}/${repo.name}.git/info/lfs/objects/batch`;
|
|
119
|
-
io.write(Buffer.from(JSON.stringify({
|
|
120
|
-
href,
|
|
121
|
-
header: { Authorization: `RemoteAuth ${token}` },
|
|
122
|
-
'expires_in': 86400,
|
|
123
|
-
'expires_at': new Date(expiresAt * 1000).toISOString(),
|
|
124
|
-
}) + '\n'));
|
|
125
|
-
io.close(0);
|
|
126
|
-
}
|
|
127
|
-
/** git-lfs-transfer <path> <download|upload> — pure SSH LFS transfer protocol (lfs-transfer-1). */
|
|
128
|
-
export async function serveLFSTransfer(argStr, identity, io) {
|
|
129
|
-
await runLFSTransfer(argStr, identity, io);
|
|
130
|
-
}
|
|
131
|
-
/** Top-level dispatch: parse and run one SSH command. */
|
|
132
|
-
export function dispatchSSHCommand(command, identity, io) {
|
|
133
|
-
const [verb, args] = parseSSHCmd(command);
|
|
134
|
-
if (!verb) {
|
|
135
|
-
io.close(1);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
switch (verb) {
|
|
139
|
-
case 'git-upload-pack':
|
|
140
|
-
case 'git-receive-pack':
|
|
141
|
-
serveGit(verb, args, identity, io);
|
|
142
|
-
return;
|
|
143
|
-
case 'git-lfs-authenticate':
|
|
144
|
-
serveLFSAuthenticate(args, identity, io).catch((e) => {
|
|
145
|
-
console.error('[lfs-auth] error:', e);
|
|
146
|
-
io.close(1);
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
case 'git-lfs-transfer':
|
|
150
|
-
serveLFSTransfer(args, identity, io).catch((e) => {
|
|
151
|
-
console.error('[lfs-transfer] error:', e);
|
|
152
|
-
io.close(1);
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
default:
|
|
156
|
-
io.write(Buffer.from(`Serv: unknown command: ${verb}\n`));
|
|
157
|
-
io.close(1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
//# sourceMappingURL=dispatch.js.map
|