@weibaohui/dsh-git-server 0.1.0

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.
Files changed (248) hide show
  1. package/README.md +43 -0
  2. package/client/bundle.js +2356 -0
  3. package/client/index.js +2342 -0
  4. package/cordis.patch.yml +11 -0
  5. package/package.json +70 -0
  6. package/server/dist/api/v1.js +2029 -0
  7. package/server/dist/authx/password.js +43 -0
  8. package/server/dist/authx/um.js +157 -0
  9. package/server/dist/conf.js +323 -0
  10. package/server/dist/context.js +701 -0
  11. package/server/dist/db/actions.js +202 -0
  12. package/server/dist/db/db.js +838 -0
  13. package/server/dist/dshapi.js +1416 -0
  14. package/server/dist/editorconfig.js +104 -0
  15. package/server/dist/gitx/git.js +580 -0
  16. package/server/dist/gitx/http.js +232 -0
  17. package/server/dist/gitx/paths.js +28 -0
  18. package/server/dist/gitx/service.js +251 -0
  19. package/server/dist/gosprintf.js +167 -0
  20. package/server/dist/hook.js +53 -0
  21. package/server/dist/index.js +112 -0
  22. package/server/dist/lfsx.js +265 -0
  23. package/server/dist/mailer.js +127 -0
  24. package/server/dist/markup.js +317 -0
  25. package/server/dist/metrics.js +18 -0
  26. package/server/dist/mirror.js +203 -0
  27. package/server/dist/multipart.js +33 -0
  28. package/server/dist/repox.js +99 -0
  29. package/server/dist/router.js +185 -0
  30. package/server/dist/serv.js +43 -0
  31. package/server/dist/server.js +382 -0
  32. package/server/dist/sshx/dispatch.js +160 -0
  33. package/server/dist/sshx/lfs-transfer.js +289 -0
  34. package/server/dist/sshx/server.js +107 -0
  35. package/server/dist/toolx.js +108 -0
  36. package/server/dist/twofactor.js +160 -0
  37. package/server/dist/webapi.js +377 -0
  38. package/server/dist/webhook.js +128 -0
  39. package/server/package.json +34 -0
  40. package/server/public/img/avatar_default.png +0 -0
  41. package/server/tsconfig.json +19 -0
  42. package/server/vendored-conf/app.ini +538 -0
  43. package/server/vendored-conf/gitignore/Actionscript +11 -0
  44. package/server/vendored-conf/gitignore/Ada +5 -0
  45. package/server/vendored-conf/gitignore/Agda +1 -0
  46. package/server/vendored-conf/gitignore/Android +32 -0
  47. package/server/vendored-conf/gitignore/Anjuta +3 -0
  48. package/server/vendored-conf/gitignore/AppEngine +2 -0
  49. package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
  50. package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
  51. package/server/vendored-conf/gitignore/Archives +27 -0
  52. package/server/vendored-conf/gitignore/Autotools +14 -0
  53. package/server/vendored-conf/gitignore/BricxCC +4 -0
  54. package/server/vendored-conf/gitignore/C +32 -0
  55. package/server/vendored-conf/gitignore/C Sharp +108 -0
  56. package/server/vendored-conf/gitignore/C++ +28 -0
  57. package/server/vendored-conf/gitignore/CFWheels +12 -0
  58. package/server/vendored-conf/gitignore/CMake +6 -0
  59. package/server/vendored-conf/gitignore/CUDA +6 -0
  60. package/server/vendored-conf/gitignore/CakePHP +13 -0
  61. package/server/vendored-conf/gitignore/ChefCookbook +9 -0
  62. package/server/vendored-conf/gitignore/Cloud9 +3 -0
  63. package/server/vendored-conf/gitignore/CodeIgniter +6 -0
  64. package/server/vendored-conf/gitignore/CodeKit +3 -0
  65. package/server/vendored-conf/gitignore/CommonLisp +3 -0
  66. package/server/vendored-conf/gitignore/Composer +6 -0
  67. package/server/vendored-conf/gitignore/Concrete5 +3 -0
  68. package/server/vendored-conf/gitignore/Coq +3 -0
  69. package/server/vendored-conf/gitignore/CraftCMS +3 -0
  70. package/server/vendored-conf/gitignore/DM +5 -0
  71. package/server/vendored-conf/gitignore/Dart +15 -0
  72. package/server/vendored-conf/gitignore/DartEditor +2 -0
  73. package/server/vendored-conf/gitignore/Delphi +57 -0
  74. package/server/vendored-conf/gitignore/Dreamweaver +3 -0
  75. package/server/vendored-conf/gitignore/Drupal +36 -0
  76. package/server/vendored-conf/gitignore/EPiServer +4 -0
  77. package/server/vendored-conf/gitignore/Eagle +36 -0
  78. package/server/vendored-conf/gitignore/Eclipse +39 -0
  79. package/server/vendored-conf/gitignore/EiffelStudio +2 -0
  80. package/server/vendored-conf/gitignore/Elisp +5 -0
  81. package/server/vendored-conf/gitignore/Elixir +4 -0
  82. package/server/vendored-conf/gitignore/Emacs +32 -0
  83. package/server/vendored-conf/gitignore/Ensime +4 -0
  84. package/server/vendored-conf/gitignore/Erlang +10 -0
  85. package/server/vendored-conf/gitignore/Espresso +1 -0
  86. package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
  87. package/server/vendored-conf/gitignore/ExtJs +4 -0
  88. package/server/vendored-conf/gitignore/Fancy +2 -0
  89. package/server/vendored-conf/gitignore/Finale +13 -0
  90. package/server/vendored-conf/gitignore/FlexBuilder +3 -0
  91. package/server/vendored-conf/gitignore/ForceDotCom +4 -0
  92. package/server/vendored-conf/gitignore/FuelPHP +2 -0
  93. package/server/vendored-conf/gitignore/GWT +28 -0
  94. package/server/vendored-conf/gitignore/Gcov +5 -0
  95. package/server/vendored-conf/gitignore/GitBook +16 -0
  96. package/server/vendored-conf/gitignore/Go +24 -0
  97. package/server/vendored-conf/gitignore/Gradle +8 -0
  98. package/server/vendored-conf/gitignore/Grails +33 -0
  99. package/server/vendored-conf/gitignore/Haskell +16 -0
  100. package/server/vendored-conf/gitignore/IGORPro +5 -0
  101. package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
  102. package/server/vendored-conf/gitignore/Idris +2 -0
  103. package/server/vendored-conf/gitignore/JDeveloper +13 -0
  104. package/server/vendored-conf/gitignore/Java +12 -0
  105. package/server/vendored-conf/gitignore/Jboss +19 -0
  106. package/server/vendored-conf/gitignore/Jekyll +3 -0
  107. package/server/vendored-conf/gitignore/JetBrains +46 -0
  108. package/server/vendored-conf/gitignore/Joomla +525 -0
  109. package/server/vendored-conf/gitignore/KDevelop4 +2 -0
  110. package/server/vendored-conf/gitignore/Kate +3 -0
  111. package/server/vendored-conf/gitignore/KiCAD +13 -0
  112. package/server/vendored-conf/gitignore/Kohana +2 -0
  113. package/server/vendored-conf/gitignore/LabVIEW +16 -0
  114. package/server/vendored-conf/gitignore/Laravel +4 -0
  115. package/server/vendored-conf/gitignore/Lazarus +30 -0
  116. package/server/vendored-conf/gitignore/Leiningen +12 -0
  117. package/server/vendored-conf/gitignore/LemonStand +21 -0
  118. package/server/vendored-conf/gitignore/LibreOffice +2 -0
  119. package/server/vendored-conf/gitignore/Lilypond +6 -0
  120. package/server/vendored-conf/gitignore/Linux +7 -0
  121. package/server/vendored-conf/gitignore/Lithium +2 -0
  122. package/server/vendored-conf/gitignore/Lua +41 -0
  123. package/server/vendored-conf/gitignore/LyX +4 -0
  124. package/server/vendored-conf/gitignore/Magento +126 -0
  125. package/server/vendored-conf/gitignore/Matlab +16 -0
  126. package/server/vendored-conf/gitignore/Maven +9 -0
  127. package/server/vendored-conf/gitignore/Mercurial +6 -0
  128. package/server/vendored-conf/gitignore/Mercury +13 -0
  129. package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
  130. package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
  131. package/server/vendored-conf/gitignore/ModelSim +23 -0
  132. package/server/vendored-conf/gitignore/Momentics +8 -0
  133. package/server/vendored-conf/gitignore/MonoDevelop +8 -0
  134. package/server/vendored-conf/gitignore/Nanoc +10 -0
  135. package/server/vendored-conf/gitignore/NetBeans +8 -0
  136. package/server/vendored-conf/gitignore/Nim +1 -0
  137. package/server/vendored-conf/gitignore/Ninja +2 -0
  138. package/server/vendored-conf/gitignore/Node +28 -0
  139. package/server/vendored-conf/gitignore/NotepadPP +2 -0
  140. package/server/vendored-conf/gitignore/OCaml +20 -0
  141. package/server/vendored-conf/gitignore/Objective-C +43 -0
  142. package/server/vendored-conf/gitignore/Opa +13 -0
  143. package/server/vendored-conf/gitignore/OpenCart +11 -0
  144. package/server/vendored-conf/gitignore/OracleForms +8 -0
  145. package/server/vendored-conf/gitignore/Packer +5 -0
  146. package/server/vendored-conf/gitignore/Perl +20 -0
  147. package/server/vendored-conf/gitignore/Phalcon +2 -0
  148. package/server/vendored-conf/gitignore/PhpStorm +63 -0
  149. package/server/vendored-conf/gitignore/PlayFramework +17 -0
  150. package/server/vendored-conf/gitignore/Plone +18 -0
  151. package/server/vendored-conf/gitignore/Prestashop +25 -0
  152. package/server/vendored-conf/gitignore/Processing +7 -0
  153. package/server/vendored-conf/gitignore/Python +58 -0
  154. package/server/vendored-conf/gitignore/Qooxdoo +5 -0
  155. package/server/vendored-conf/gitignore/Qt +34 -0
  156. package/server/vendored-conf/gitignore/R +16 -0
  157. package/server/vendored-conf/gitignore/ROS +47 -0
  158. package/server/vendored-conf/gitignore/Rails +35 -0
  159. package/server/vendored-conf/gitignore/Redcar +1 -0
  160. package/server/vendored-conf/gitignore/Redis +3 -0
  161. package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
  162. package/server/vendored-conf/gitignore/Ruby +36 -0
  163. package/server/vendored-conf/gitignore/Rust +11 -0
  164. package/server/vendored-conf/gitignore/SBT +9 -0
  165. package/server/vendored-conf/gitignore/SCons +2 -0
  166. package/server/vendored-conf/gitignore/SVN +1 -0
  167. package/server/vendored-conf/gitignore/Sass +2 -0
  168. package/server/vendored-conf/gitignore/Scala +17 -0
  169. package/server/vendored-conf/gitignore/Scrivener +7 -0
  170. package/server/vendored-conf/gitignore/Sdcc +8 -0
  171. package/server/vendored-conf/gitignore/SeamGen +26 -0
  172. package/server/vendored-conf/gitignore/SketchUp +1 -0
  173. package/server/vendored-conf/gitignore/SlickEdit +11 -0
  174. package/server/vendored-conf/gitignore/Stella +12 -0
  175. package/server/vendored-conf/gitignore/SublimeText +14 -0
  176. package/server/vendored-conf/gitignore/SugarCRM +25 -0
  177. package/server/vendored-conf/gitignore/Swift +43 -0
  178. package/server/vendored-conf/gitignore/Symfony +37 -0
  179. package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
  180. package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
  181. package/server/vendored-conf/gitignore/Tags +14 -0
  182. package/server/vendored-conf/gitignore/TeX +136 -0
  183. package/server/vendored-conf/gitignore/TextMate +3 -0
  184. package/server/vendored-conf/gitignore/Textpattern +11 -0
  185. package/server/vendored-conf/gitignore/TortoiseGit +2 -0
  186. package/server/vendored-conf/gitignore/TurboGears2 +20 -0
  187. package/server/vendored-conf/gitignore/Typo3 +20 -0
  188. package/server/vendored-conf/gitignore/Umbraco +17 -0
  189. package/server/vendored-conf/gitignore/Unity +21 -0
  190. package/server/vendored-conf/gitignore/UnrealEngine +76 -0
  191. package/server/vendored-conf/gitignore/VVVV +6 -0
  192. package/server/vendored-conf/gitignore/Vagrant +1 -0
  193. package/server/vendored-conf/gitignore/Vim +6 -0
  194. package/server/vendored-conf/gitignore/VirtualEnv +10 -0
  195. package/server/vendored-conf/gitignore/VisualStudio +217 -0
  196. package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
  197. package/server/vendored-conf/gitignore/Waf +3 -0
  198. package/server/vendored-conf/gitignore/WebMethods +14 -0
  199. package/server/vendored-conf/gitignore/WebStorm +63 -0
  200. package/server/vendored-conf/gitignore/Windows +18 -0
  201. package/server/vendored-conf/gitignore/WordPress +18 -0
  202. package/server/vendored-conf/gitignore/Xcode +23 -0
  203. package/server/vendored-conf/gitignore/XilinxISE +57 -0
  204. package/server/vendored-conf/gitignore/Xojo +11 -0
  205. package/server/vendored-conf/gitignore/Yeoman +6 -0
  206. package/server/vendored-conf/gitignore/Yii +6 -0
  207. package/server/vendored-conf/gitignore/ZendFramework +20 -0
  208. package/server/vendored-conf/gitignore/Zephir +26 -0
  209. package/server/vendored-conf/gitignore/macOS +25 -0
  210. package/server/vendored-conf/label/Default +7 -0
  211. package/server/vendored-conf/license/Abstyles License +10 -0
  212. package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
  213. package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
  214. package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
  215. package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
  216. package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
  217. package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
  218. package/server/vendored-conf/license/Apache License 1.0 +19 -0
  219. package/server/vendored-conf/license/Apache License 1.1 +20 -0
  220. package/server/vendored-conf/license/Apache License 2.0 +72 -0
  221. package/server/vendored-conf/license/Artistic License 1.0 +48 -0
  222. package/server/vendored-conf/license/Artistic License 2.0 +84 -0
  223. package/server/vendored-conf/license/BSD 2-clause License +8 -0
  224. package/server/vendored-conf/license/BSD 3-clause License +10 -0
  225. package/server/vendored-conf/license/BSD 4-clause License +13 -0
  226. package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
  227. package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
  228. package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
  229. package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
  230. package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
  231. package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
  232. package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
  233. package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
  234. package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
  235. package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
  236. package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
  237. package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
  238. package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
  239. package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
  240. package/server/vendored-conf/license/ISC license +6 -0
  241. package/server/vendored-conf/license/MIT License +8 -0
  242. package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
  243. package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
  244. package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
  245. package/server/vendored-conf/readme/Default +3 -0
  246. package/src/dsh-host.js +168 -0
  247. package/src/index.js +682 -0
  248. package/src/runner.js +364 -0
@@ -0,0 +1,160 @@
1
+ // Two-factor authentication: TOTP (RFC 6238), AES-GCM secret storage compatible
2
+ // with gogs cryptox, and recovery codes.
3
+ import * as crypto from 'node:crypto';
4
+ import * as db from './db/db.js';
5
+ import { conf } from './conf.js';
6
+ import { randomChars } from './db/db.js';
7
+ // ---------------------------------------------------------------- crypto (cryptox compat)
8
+ function md5Bytes(str) {
9
+ return crypto.createHash('md5').update(str).digest();
10
+ }
11
+ export function aesGcmEncrypt(key, plaintext) {
12
+ // golang.org/x/crypto GCM: 12-byte nonce prepended, tag appended
13
+ const nonce = crypto.randomBytes(12);
14
+ const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
15
+ const enc = Buffer.concat([cipher.update(plaintext), cipher.final()]);
16
+ return Buffer.concat([nonce, enc, cipher.getAuthTag()]);
17
+ }
18
+ export function aesGcmDecrypt(key, ciphertext) {
19
+ const nonce = ciphertext.subarray(0, 12);
20
+ const tag = ciphertext.subarray(ciphertext.length - 16);
21
+ const data = ciphertext.subarray(12, ciphertext.length - 16);
22
+ const decipher = crypto.createDecipheriv('aes-128-gcm', key, nonce);
23
+ decipher.setAuthTag(tag);
24
+ return Buffer.concat([decipher.update(data), decipher.final()]);
25
+ }
26
+ function secretKey() {
27
+ return md5Bytes(conf.secretKey);
28
+ }
29
+ // ---------------------------------------------------------------- TOTP (RFC 6238)
30
+ const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
31
+ function base32Encode(buf) {
32
+ let bits = 0;
33
+ let value = 0;
34
+ let out = '';
35
+ for (const byte of buf) {
36
+ value = (value << 8) | byte;
37
+ bits += 8;
38
+ while (bits >= 5) {
39
+ out += B32[(value >>> (bits - 5)) & 31];
40
+ bits -= 5;
41
+ }
42
+ }
43
+ if (bits > 0)
44
+ out += B32[(value << (5 - bits)) & 31];
45
+ return out;
46
+ }
47
+ function base32Decode(str) {
48
+ const clean = str.toUpperCase().replace(/[=\s]/g, '');
49
+ let bits = 0;
50
+ let value = 0;
51
+ const out = [];
52
+ for (const ch of clean) {
53
+ const idx = B32.indexOf(ch);
54
+ if (idx < 0)
55
+ continue;
56
+ value = (value << 5) | idx;
57
+ bits += 5;
58
+ if (bits >= 8) {
59
+ out.push((value >>> (bits - 8)) & 0xff);
60
+ bits -= 8;
61
+ }
62
+ }
63
+ return Buffer.from(out);
64
+ }
65
+ export function totpGenerate(issuer, accountName) {
66
+ const raw = crypto.randomBytes(20);
67
+ const secret = base32Encode(raw);
68
+ const url = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
69
+ return { secret, url };
70
+ }
71
+ export function totpCode(secret, timeStep = Math.floor(Date.now() / 30000)) {
72
+ const key = base32Decode(secret);
73
+ const counter = Buffer.alloc(8);
74
+ counter.writeUInt32BE(Math.floor(timeStep / 2 ** 32), 0);
75
+ counter.writeUInt32BE(timeStep >>> 0, 4);
76
+ const hmac = crypto.createHmac('sha1', key).update(counter).digest();
77
+ const offset = hmac[hmac.length - 1] & 0xf;
78
+ const code = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) | (hmac[offset + 2] << 8) | hmac[offset + 3];
79
+ return String(code % 1000000).padStart(6, '0');
80
+ }
81
+ /** totp.Validate: accepts ±1 time step for clock skew, exactly 6 digits. */
82
+ export function totpValidate(passcode, secret) {
83
+ if (!/^\d{6}$/.test(passcode))
84
+ return false;
85
+ const step = Math.floor(Date.now() / 30000);
86
+ for (const t of [step - 1, step, step + 1]) {
87
+ if (totpCode(secret, t) === passcode)
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ // ---------------------------------------------------------------- storage
93
+ function encryptSecret(secret) {
94
+ return aesGcmEncrypt(secretKey(), Buffer.from(secret)).toString('base64');
95
+ }
96
+ function decryptSecret(stored) {
97
+ return aesGcmDecrypt(secretKey(), Buffer.from(stored, 'base64')).toString();
98
+ }
99
+ export function isTwoFactorEnabled(userID) {
100
+ return !!db.db().prepare('SELECT 1 FROM two_factor WHERE user_id = ?').get(userID);
101
+ }
102
+ export function getTwoFactorByUserID(userID) {
103
+ return db.db().prepare('SELECT * FROM two_factor WHERE user_id = ?').get(userID) ?? null;
104
+ }
105
+ export function createTwoFactor(userID, secret) {
106
+ const now = Math.floor(Date.now() / 1000);
107
+ db.db()
108
+ .prepare('INSERT INTO two_factor (user_id, secret, created_unix) VALUES (?,?,?) ON CONFLICT(user_id) DO UPDATE SET secret = excluded.secret')
109
+ .run(userID, encryptSecret(secret), now);
110
+ // 10 recovery codes like gogs
111
+ for (let i = 0; i < 10; i++) {
112
+ const code = randomChars(10);
113
+ db.db().prepare('INSERT INTO two_factor_recovery_code (user_id, code, is_used) VALUES (?,?,0)').run(userID, code.slice(0, 5).toLowerCase() + '-' + code.slice(5).toLowerCase());
114
+ }
115
+ }
116
+ export function deleteTwoFactor(userID) {
117
+ db.db().prepare('DELETE FROM two_factor WHERE user_id = ?').run(userID);
118
+ db.db().prepare('DELETE FROM two_factor_recovery_code WHERE user_id = ?').run(userID);
119
+ }
120
+ export function validateTOTP(userID, passcode) {
121
+ const t = getTwoFactorByUserID(userID);
122
+ if (!t)
123
+ return false;
124
+ try {
125
+ return totpValidate(passcode, decryptSecret(t.secret));
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ export function listRecoveryCodes(userID) {
132
+ return db.db().prepare('SELECT * FROM two_factor_recovery_code WHERE user_id = ? ORDER BY id').all(userID);
133
+ }
134
+ export function regenerateRecoveryCodes(userID) {
135
+ db.db().prepare('DELETE FROM two_factor_recovery_code WHERE user_id = ?').run(userID);
136
+ for (let i = 0; i < 10; i++) {
137
+ const code = randomChars(10);
138
+ db.db().prepare('INSERT INTO two_factor_recovery_code (user_id, code, is_used) VALUES (?,?,0)').run(userID, code.slice(0, 5).toLowerCase() + '-' + code.slice(5).toLowerCase());
139
+ }
140
+ }
141
+ export function useRecoveryCode(userID, codeInput) {
142
+ const code = String(codeInput).toLowerCase().trim();
143
+ const row = db.db().prepare('SELECT * FROM two_factor_recovery_code WHERE user_id = ? AND code = ? AND is_used = 0').get(userID, code);
144
+ if (!row)
145
+ return false;
146
+ db.db().prepare('UPDATE two_factor_recovery_code SET is_used = 1 WHERE id = ?').run(row.id);
147
+ return true;
148
+ }
149
+ // passcode reuse cache (60s) like gogs cache.TwoFactorCacheKey
150
+ const usedPasscodes = new Set();
151
+ export function passcodeRecentlyUsed(userID, passcode) {
152
+ const key = `${userID}:${passcode}`;
153
+ return usedPasscodes.has(key);
154
+ }
155
+ export function markPasscodeUsed(userID, passcode) {
156
+ const key = `${userID}:${passcode}`;
157
+ usedPasscodes.add(key);
158
+ setTimeout(() => usedPasscodes.delete(key), 60000);
159
+ }
160
+ //# sourceMappingURL=twofactor.js.map
@@ -0,0 +1,377 @@
1
+ // /api/web/* JSON endpoints consumed by the React SPA (sign-in/up, MFA,
2
+ // activate, reset password, repo header/watch/star/commit).
3
+ import * as db from './db/db.js';
4
+ import { conf } from './conf.js';
5
+ import { completeSignIn } from './context.js';
6
+ import { getCommit } from './gitx/git.js';
7
+ function errBody(error, fields) {
8
+ const out = {};
9
+ if (error)
10
+ out.error = error;
11
+ if (fields)
12
+ out.fields = fields;
13
+ return out;
14
+ }
15
+ function isAlphaDashDot(s) {
16
+ return !/[^\d\w\-_.]/.test(s);
17
+ }
18
+ export async function handleWebAPI(c, subPath) {
19
+ // subPath begins after /api/web
20
+ const method = c.Method();
21
+ if (subPath === '/user/info' && method === 'GET') {
22
+ if (!c.User) {
23
+ c.Status(204);
24
+ c.res.end();
25
+ c.rendered = true;
26
+ return true;
27
+ }
28
+ c.JSONSuccess({
29
+ username: c.User.name,
30
+ avatarURL: c.User.AvatarURL(),
31
+ isAdmin: c.User.is_admin === 1,
32
+ canCreateOrganization: c.User.CanCreateOrganization(),
33
+ });
34
+ return true;
35
+ }
36
+ if (subPath === '/user/sign-up') {
37
+ if (method === 'GET') {
38
+ c.JSONSuccess({
39
+ registrationDisabled: conf.disableRegistration,
40
+ captchaEnabled: conf.enableRegistrationCaptcha,
41
+ });
42
+ return true;
43
+ }
44
+ if (method === 'POST') {
45
+ const req = await c.form();
46
+ const userName = String(req.userName ?? '');
47
+ const email = String(req.email ?? '');
48
+ const password = String(req.password ?? '');
49
+ if (conf.disableRegistration) {
50
+ c.JSON(403, errBody(c.Tr('auth.disable_register_prompt')));
51
+ return true;
52
+ }
53
+ if (conf.enableRegistrationCaptcha) {
54
+ const { validateCaptcha } = await import('./toolx.js');
55
+ const captchaID = c.GetCookie('gogs_captcha');
56
+ if (!validateCaptcha(captchaID, String(req.captcha ?? ''))) {
57
+ const msg = c.Tr('form.captcha_incorrect');
58
+ c.JSON(401, errBody(undefined, { captcha: msg }));
59
+ return true;
60
+ }
61
+ }
62
+ if (!userName || !isAlphaDashDot(userName) || userName.length > 35) {
63
+ c.JSON(400, errBody(undefined, { userName: c.Tr('form.username') + c.Tr('form.alpha_dash_dot_error') }));
64
+ return true;
65
+ }
66
+ if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || email.length > 254) {
67
+ c.JSON(400, errBody(undefined, { email: c.Tr('form.email') + c.Tr('form.email_error') }));
68
+ return true;
69
+ }
70
+ if (!password || password.length > 255) {
71
+ c.JSON(400, errBody(undefined, { password: c.Tr('form.password') + c.Tr('form.require_error') }));
72
+ return true;
73
+ }
74
+ let user;
75
+ try {
76
+ user = db.createUser(userName, email, { password, activated: !conf.requireEmailConfirmation });
77
+ }
78
+ catch (e) {
79
+ if (e instanceof db.AlreadyExistError) {
80
+ if (String(e.message).includes('email')) {
81
+ c.JSON(422, errBody(undefined, { email: c.Tr('form.email_been_used') }));
82
+ }
83
+ else {
84
+ c.JSON(422, errBody(undefined, { userName: c.Tr('form.username_been_taken') }));
85
+ }
86
+ return true;
87
+ }
88
+ if (e instanceof db.NameNotAllowedError) {
89
+ c.JSON(400, errBody(undefined, { userName: c.Tr('user.form.name_not_allowed', userName) }));
90
+ return true;
91
+ }
92
+ throw e;
93
+ }
94
+ // first user becomes admin & activated
95
+ const count = db.db().prepare('SELECT COUNT(*) AS c FROM user WHERE type = 0').get().c;
96
+ if (count === 1) {
97
+ db.updateUserColumns(user.id, { is_active: 1, is_admin: 1 });
98
+ }
99
+ c.JSONSuccess({});
100
+ return true;
101
+ }
102
+ }
103
+ if (subPath === '/user/sign-in') {
104
+ if (method === 'GET') {
105
+ // local auth only in this port; no external login sources yet
106
+ c.JSONSuccess({ loginSources: [] });
107
+ return true;
108
+ }
109
+ if (method === 'POST') {
110
+ const req = await c.form();
111
+ const username = String(req.username ?? '');
112
+ const password = String(req.password ?? '');
113
+ const user = db.getUserByUsername(username) ?? db.getUserByEmail(username);
114
+ const { verifyPassword } = await import('./authx/password.js');
115
+ // 兜底管理员:永远允许本地登录(插件自有紧急账号)
116
+ const bootstrapName = (process.env.DSH_BOOTSTRAP_ADMIN || '').split(':')[0] || 'root';
117
+ if (user && user.type === 0 && user.name === bootstrapName && verifyPassword(password, user.salt, user.passwd)) {
118
+ completeSignIn(c, user);
119
+ c.JSONSuccess({});
120
+ return true;
121
+ }
122
+ // 账户单一来源:UM service 启用时登录只走 service;本地密码仅当
123
+ // service 不可用时兜底(防影子账号残留的旧同步密码绕过)
124
+ const um = await import('./authx/um.js');
125
+ if (um.umServiceEnabled()) {
126
+ const check = await um.umCheckLogin(username, password);
127
+ if (check.ok) {
128
+ const shadow = um.ensureShadowUser(check.user);
129
+ if (shadow && shadow.type === 0) {
130
+ completeSignIn(c, shadow);
131
+ c.JSONSuccess({});
132
+ return true;
133
+ }
134
+ }
135
+ if (!check.unavailable) {
136
+ c.JSON(401, errBody(c.Tr('form.username_password_incorrect'), { username: null, password: null }));
137
+ return true;
138
+ }
139
+ }
140
+ const authed = !!(user && user.type === 0 && verifyPassword(password, user.salt, user.passwd));
141
+ if (!authed) {
142
+ c.JSON(401, errBody(c.Tr('form.username_password_incorrect'), { username: null, password: null }));
143
+ return true;
144
+ }
145
+ completeSignIn(c, user);
146
+ c.JSONSuccess({});
147
+ return true;
148
+ }
149
+ }
150
+ if (subPath === '/user/sign-out' && method === 'POST') {
151
+ c.session.Clear();
152
+ c.session.Release();
153
+ c.SetCookie(conf.cookieUserName, '', 0);
154
+ c.NoContent();
155
+ return true;
156
+ }
157
+ if (subPath === '/user/reset-password') {
158
+ if (method === 'GET') {
159
+ const code = c.Query('code');
160
+ let valid = false;
161
+ if (code) {
162
+ const { verifyUserFromCode } = await import('./toolx.js');
163
+ const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
164
+ valid = !!parsed?.valid;
165
+ }
166
+ c.JSONSuccess({ emailEnabled: conf.emailEnabled, valid });
167
+ return true;
168
+ }
169
+ if (method === 'POST') {
170
+ if (!conf.emailEnabled) {
171
+ c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
172
+ return true;
173
+ }
174
+ const req = await c.form();
175
+ const email = String(req.email ?? '').toLowerCase().trim();
176
+ const user = db.getUserByEmail(email);
177
+ if (!user) {
178
+ c.JSONSuccess({ hours: Math.floor(conf.activateCodeLives / 60) });
179
+ return true;
180
+ }
181
+ if (user.type !== 0) {
182
+ const msg = c.Tr('auth.non_local_account');
183
+ c.JSON(403, errBody(undefined, { email: msg }));
184
+ return true;
185
+ }
186
+ try {
187
+ const { createActivateCode } = await import('./toolx.js');
188
+ const { sendResetPasswordMail } = await import('./mailer.js');
189
+ const code = createActivateCode(user, conf.resetPwdCodeLives) + Buffer.from(user.name).toString('hex');
190
+ await sendResetPasswordMail(user, code);
191
+ c.JSONSuccess({ hours: Math.floor(conf.resetPwdCodeLives / 60) });
192
+ }
193
+ catch (e) {
194
+ console.error('[mailer] reset mail:', e?.message ?? e);
195
+ c.JSONSuccess({ hours: Math.floor(conf.resetPwdCodeLives / 60) });
196
+ }
197
+ return true;
198
+ }
199
+ }
200
+ if (subPath === '/user/reset-password/complete' && method === 'POST') {
201
+ const req = await c.form();
202
+ const code = String(req.code ?? '');
203
+ const { verifyUserFromCode } = await import('./toolx.js');
204
+ const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
205
+ if (!parsed || !parsed.valid) {
206
+ c.JSON(400, errBody(c.Tr('auth.invalid_code')));
207
+ return true;
208
+ }
209
+ const password = String(req.password ?? '');
210
+ if (password.length < 6) {
211
+ const msg = c.Tr('auth.password_too_short');
212
+ c.JSON(400, errBody(undefined, { password: msg }));
213
+ return true;
214
+ }
215
+ const { encodePassword, randomSalt } = await import('./authx/password.js');
216
+ const salt = randomSalt();
217
+ db.updateUserColumns(parsed.user.id, { passwd: encodePassword(password, salt), salt });
218
+ c.Status(204);
219
+ c.res.end();
220
+ c.rendered = true;
221
+ return true;
222
+ }
223
+ if (subPath === '/user/activate') {
224
+ if (!c.User) {
225
+ c.Status(401);
226
+ c.res.end();
227
+ c.rendered = true;
228
+ return true;
229
+ }
230
+ if (method === 'POST') {
231
+ if (!conf.requireEmailConfirmation) {
232
+ c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
233
+ return true;
234
+ }
235
+ if (!conf.emailEnabled) {
236
+ c.JSON(403, errBody(c.Tr('auth.disable_register_mail')));
237
+ return true;
238
+ }
239
+ try {
240
+ const { createActivateCode } = await import('./toolx.js');
241
+ const { sendActivateMail } = await import('./mailer.js');
242
+ const code = createActivateCode(c.User, conf.activateCodeLives) + Buffer.from(c.User.name).toString('hex');
243
+ await sendActivateMail(c.User, code);
244
+ c.JSONSuccess({ codeLifetimeHours: Math.floor(conf.activateCodeLives / 60) });
245
+ }
246
+ catch (e) {
247
+ console.error('[mailer] activate mail:', e?.message ?? e);
248
+ c.JSON(500, errBody(String(e?.message ?? e)));
249
+ }
250
+ return true;
251
+ }
252
+ c.JSONSuccess({ email: c.User.email, codeLifetimeHours: Math.floor(conf.activateCodeLives / 60) });
253
+ return true;
254
+ }
255
+ if (subPath === '/user/activate/complete' && method === 'POST') {
256
+ const req = await c.form();
257
+ const code = String(req.code ?? '');
258
+ const { verifyUserFromCode } = await import('./toolx.js');
259
+ const parsed = verifyUserFromCode(code, (u) => db.getUserByUsername(u));
260
+ if (!parsed || !parsed.valid) {
261
+ c.JSON(400, errBody(c.Tr('auth.invalid_code')));
262
+ return true;
263
+ }
264
+ const { randomSalt } = await import('./authx/password.js');
265
+ const salt = randomSalt();
266
+ db.updateUserColumns(parsed.user.id, { is_active: 1, rands: salt });
267
+ completeSignIn(c, parsed.user);
268
+ c.Status(204);
269
+ c.res.end();
270
+ c.rendered = true;
271
+ return true;
272
+ }
273
+ // repo endpoints
274
+ const repoMatch = /^\/([^/]+)\/([^/]+)(\/.*)?$/.exec(subPath);
275
+ if (repoMatch) {
276
+ const [, ownerName, repoName, rest] = repoMatch;
277
+ const owner = db.getUserByUsername(ownerName);
278
+ const repo = owner ? db.getRepoByOwnerAndName(owner, repoName) : null;
279
+ if (owner && repo) {
280
+ const viewerID = c.UserID();
281
+ const mode = db.accessMode(viewerID, repo);
282
+ const canRead = mode >= db.AccessMode.READ || (!repo.is_private && !conf.requireSigninView);
283
+ if (rest === '/header' && method === 'GET') {
284
+ if (!canRead) {
285
+ c.Status(404);
286
+ c.res.end();
287
+ c.rendered = true;
288
+ return true;
289
+ }
290
+ c.JSONSuccess({
291
+ id: repo.id,
292
+ owner: owner.name,
293
+ name: repo.name,
294
+ avatarURL: owner.AvatarURL(),
295
+ visibility: repo.is_private ? 'private' : 'public',
296
+ watchCount: repo.num_watches,
297
+ starCount: repo.num_stars,
298
+ forkCount: repo.num_forks,
299
+ issuesEnabled: repo.enable_issues === 1,
300
+ openIssueCount: repo.num_issues - repo.num_closed_issues,
301
+ pullRequestsEnabled: repo.enable_pulls === 1,
302
+ openPullRequestCount: repo.num_pulls - repo.num_closed_pulls,
303
+ wikiEnabled: repo.enable_wiki === 1,
304
+ viewerCanAdminister: mode >= db.AccessMode.ADMIN,
305
+ viewerIsWatching: !!db.isWatching(viewerID, repo.id),
306
+ viewerIsStarring: !!db.isStaring(viewerID, repo.id),
307
+ });
308
+ return true;
309
+ }
310
+ if (rest === '/watch') {
311
+ if (method === 'POST') {
312
+ db.watchRepo(viewerID, repo.id, true);
313
+ c.NoContent();
314
+ return true;
315
+ }
316
+ if (method === 'DELETE') {
317
+ db.watchRepo(viewerID, repo.id, false);
318
+ c.NoContent();
319
+ return true;
320
+ }
321
+ }
322
+ if (rest === '/star') {
323
+ if (method === 'POST') {
324
+ db.starRepo(viewerID, repo.id, true);
325
+ c.NoContent();
326
+ return true;
327
+ }
328
+ if (method === 'DELETE') {
329
+ db.starRepo(viewerID, repo.id, false);
330
+ c.NoContent();
331
+ return true;
332
+ }
333
+ }
334
+ const commitMatch = /^\/commit\/([0-9a-f]{7,40})$/.exec(rest ?? '');
335
+ if (commitMatch && method === 'GET') {
336
+ if (!canRead) {
337
+ c.Status(404);
338
+ c.res.end();
339
+ c.rendered = true;
340
+ return true;
341
+ }
342
+ const commit = await getCommit(repo.RepoPath(), commitMatch[1]);
343
+ if (!commit) {
344
+ c.Status(404);
345
+ c.res.end();
346
+ c.rendered = true;
347
+ return true;
348
+ }
349
+ const authorUser = db.getUserByEmail(commit.author.email);
350
+ c.JSONSuccess({
351
+ sha: commit.id,
352
+ subject: commit.Summary(),
353
+ body: commit.message.slice(commit.Summary().length).replace(/^\n/, ''),
354
+ author: {
355
+ name: commit.author.name,
356
+ email: commit.author.email,
357
+ when: commit.author.when.toISOString(),
358
+ avatarURL: avatarURLForEmail(commit.author.email),
359
+ ...(authorUser ? { profileURL: authorUser.HomeURLPath() } : {}),
360
+ },
361
+ parents: commit.parents,
362
+ });
363
+ return true;
364
+ }
365
+ }
366
+ }
367
+ return false;
368
+ }
369
+ function avatarURLForEmail(email) {
370
+ return conf.subpath + '/user/avatar/' + require_md5(email);
371
+ }
372
+ // small helper to avoid import cycle overhead
373
+ import { md5 } from './authx/password.js';
374
+ function require_md5(s) {
375
+ return md5(s.trim().toLowerCase());
376
+ }
377
+ //# sourceMappingURL=webapi.js.map
@@ -0,0 +1,128 @@
1
+ // Webhook delivery (hook_task persistence + async HTTP POST), mirroring gogs
2
+ // webhook.go deliverHooks with HMAC-SHA256 signature.
3
+ import * as http from 'node:http';
4
+ import * as https from 'node:https';
5
+ import * as crypto from 'node:crypto';
6
+ import * as db from './db/db.js';
7
+ import { newUUID } from './db/db.js';
8
+ import { conf } from './conf.js';
9
+ export function deliverWebhook(hook, eventType, payload, repo) {
10
+ const hookTaskType = hook.hook_task_type ?? 1;
11
+ const isJSON = hook.content_type === 1;
12
+ let payloadContent = '';
13
+ if (hookTaskType === 1) {
14
+ payloadContent = JSON.stringify(payload);
15
+ }
16
+ else if (hookTaskType === 2) {
17
+ // slack
18
+ const meta = JSON.parse(hook.meta ?? '{}');
19
+ payloadContent = JSON.stringify({
20
+ channel: meta.channel,
21
+ username: meta.username ?? 'Gogs',
22
+ icon_url: meta.icon_url ?? conf.externalURL + 'img/favicon.png',
23
+ text: slackText(eventType, payload),
24
+ });
25
+ }
26
+ else if (hookTaskType === 3) {
27
+ const meta = JSON.parse(hook.meta ?? '{}');
28
+ payloadContent = JSON.stringify({
29
+ username: meta.username ?? 'Gogs',
30
+ content: `\`\`\`${slackText(eventType, payload)}\`\`\``,
31
+ });
32
+ }
33
+ else {
34
+ payloadContent = JSON.stringify({ msgtype: 'text', text: { content: slackText(eventType, payload) } });
35
+ }
36
+ const body = isJSON ? payloadContent : new URLSearchParams({ payload: payloadContent }).toString();
37
+ const signature = crypto
38
+ .createHmac('sha256', hook.secret ?? '')
39
+ .update(payloadContent)
40
+ .digest('hex');
41
+ const task = db
42
+ .db()
43
+ .prepare(`INSERT INTO hook_task (repo_id, hook_id, uuid, type, url, signature, payload_content, content_type, event_type, is_ssl, is_delivered, delivered, is_succeed)
44
+ VALUES (?,?,?,?,?,?,?,?,?,?,0,?,0)`)
45
+ .run(repo.id, hook.id, newUUID(), hookTaskType, hook.url, signature, payloadContent, hook.content_type ?? 1, eventType, hook.is_ssl ?? 0, process.hrtime.bigint() // delivered UnixNano (placeholder until completion)
46
+ );
47
+ const taskID = Number(task.lastInsertRowid);
48
+ db.updateWebhookLastStatus(hook.id, 2); // pending → failed until success
49
+ const delivery = (async () => {
50
+ const started = process.hrtime.bigint();
51
+ try {
52
+ const { status } = await postURL(String(hook.url), Buffer.from(body), isJSON ? 'application/json' : 'application/x-www-form-urlencoded', {
53
+ 'X-Gogs-Delivery': String(taskID),
54
+ 'X-Gogs-Event': eventType,
55
+ 'X-Gogs-Event-Type': eventType,
56
+ 'X-Gogs-Signature': signature,
57
+ 'X-GitHub-Delivery': String(taskID),
58
+ 'X-GitHub-Event': eventType,
59
+ });
60
+ const delivered = process.hrtime.bigint();
61
+ db.db()
62
+ .prepare(`UPDATE hook_task SET is_delivered = 1, delivered = ?, is_succeed = ?, request_content = ?, response_content = ? WHERE id = ?`)
63
+ .run(delivered, status >= 200 && status < 300 ? 1 : 0, JSON.stringify({ headers: {} }), JSON.stringify({ status, headers: {}, body: '' }), taskID);
64
+ if (status >= 200 && status < 300) {
65
+ db.updateWebhookLastStatus(hook.id, 1);
66
+ }
67
+ }
68
+ catch (e) {
69
+ db.db()
70
+ .prepare(`UPDATE hook_task SET is_delivered = 1, delivered = ?, is_succeed = 0, response_content = ? WHERE id = ?`)
71
+ .run(process.hrtime.bigint(), JSON.stringify({ err: String(e?.message ?? e) }), taskID);
72
+ }
73
+ })();
74
+ pendingDeliveries.add(delivery);
75
+ void delivery.finally(() => pendingDeliveries.delete(delivery));
76
+ }
77
+ /** In-flight deliveries — short-lived processes (git hooks) must await these before exit. */
78
+ export const pendingDeliveries = new Set();
79
+ function slackText(eventType, payload) {
80
+ const repo = payload?.repository?.full_name ?? '';
81
+ const who = payload?.sender?.username ?? payload?.pusher?.username ?? '';
82
+ switch (eventType) {
83
+ case 'push': {
84
+ const commits = payload?.commits ?? [];
85
+ return `[${repo}] ${who} pushed ${commits.length} commit(s):\n` + commits.map((c) => ` - ${c.message}`).join('\n');
86
+ }
87
+ case 'issues':
88
+ return `[${repo}] Issue #${payload?.number}: "${payload?.issue?.title}" ${payload?.action ?? 'opened'} by ${who}`;
89
+ case 'pull_request':
90
+ return `[${repo}] Pull request #${payload?.number}: "${payload?.issue?.title}" ${payload?.action ?? 'opened'} by ${who}`;
91
+ case 'issue_comment':
92
+ return `[${repo}] New comment on #${payload?.number} by ${who}`;
93
+ case 'create':
94
+ return `[${repo}] ${payload?.ref_type} "${payload?.ref}" created by ${who}`;
95
+ case 'delete':
96
+ return `[${repo}] ${payload?.ref_type} "${payload?.ref}" deleted by ${who}`;
97
+ case 'fork':
98
+ return `[${repo}] forked by ${who}`;
99
+ case 'release':
100
+ return `[${repo}] Release "${payload?.release?.tag_name}" ${payload?.action ?? 'published'} by ${who}`;
101
+ default:
102
+ return `[${repo}] ${eventType} by ${who}`;
103
+ }
104
+ }
105
+ function postURL(url, body, contentType, headers) {
106
+ return new Promise((resolve, reject) => {
107
+ const u = new URL(url);
108
+ const mod = u.protocol === 'https:' ? https : http;
109
+ const req = mod.request({
110
+ method: 'POST',
111
+ hostname: u.hostname,
112
+ port: u.port || (u.protocol === 'https:' ? 443 : 80),
113
+ path: u.pathname + u.search,
114
+ headers: { 'Content-Type': contentType, 'Content-Length': String(body.length), ...headers },
115
+ timeout: conf.deliverTimeout * 1000,
116
+ rejectUnauthorized: !conf.skipVerify,
117
+ }, (res) => {
118
+ res.resume();
119
+ res.on('end', () => resolve({ status: res.statusCode ?? 0 }));
120
+ });
121
+ req.on('error', reject);
122
+ req.on('timeout', () => {
123
+ req.destroy(new Error('timeout'));
124
+ });
125
+ req.end(body);
126
+ });
127
+ }
128
+ //# sourceMappingURL=webhook.js.map