@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.
- package/README.md +43 -0
- package/client/bundle.js +2356 -0
- package/client/index.js +2342 -0
- package/cordis.patch.yml +11 -0
- package/package.json +70 -0
- package/server/dist/api/v1.js +2029 -0
- package/server/dist/authx/password.js +43 -0
- package/server/dist/authx/um.js +157 -0
- package/server/dist/conf.js +323 -0
- package/server/dist/context.js +701 -0
- package/server/dist/db/actions.js +202 -0
- package/server/dist/db/db.js +838 -0
- package/server/dist/dshapi.js +1416 -0
- package/server/dist/editorconfig.js +104 -0
- package/server/dist/gitx/git.js +580 -0
- package/server/dist/gitx/http.js +232 -0
- package/server/dist/gitx/paths.js +28 -0
- package/server/dist/gitx/service.js +251 -0
- package/server/dist/gosprintf.js +167 -0
- package/server/dist/hook.js +53 -0
- package/server/dist/index.js +112 -0
- package/server/dist/lfsx.js +265 -0
- package/server/dist/mailer.js +127 -0
- package/server/dist/markup.js +317 -0
- package/server/dist/metrics.js +18 -0
- package/server/dist/mirror.js +203 -0
- package/server/dist/multipart.js +33 -0
- package/server/dist/repox.js +99 -0
- package/server/dist/router.js +185 -0
- package/server/dist/serv.js +43 -0
- package/server/dist/server.js +382 -0
- package/server/dist/sshx/dispatch.js +160 -0
- package/server/dist/sshx/lfs-transfer.js +289 -0
- package/server/dist/sshx/server.js +107 -0
- package/server/dist/toolx.js +108 -0
- package/server/dist/twofactor.js +160 -0
- package/server/dist/webapi.js +377 -0
- package/server/dist/webhook.js +128 -0
- package/server/package.json +34 -0
- package/server/public/img/avatar_default.png +0 -0
- package/server/tsconfig.json +19 -0
- package/server/vendored-conf/app.ini +538 -0
- package/server/vendored-conf/gitignore/Actionscript +11 -0
- package/server/vendored-conf/gitignore/Ada +5 -0
- package/server/vendored-conf/gitignore/Agda +1 -0
- package/server/vendored-conf/gitignore/Android +32 -0
- package/server/vendored-conf/gitignore/Anjuta +3 -0
- package/server/vendored-conf/gitignore/AppEngine +2 -0
- package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
- package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
- package/server/vendored-conf/gitignore/Archives +27 -0
- package/server/vendored-conf/gitignore/Autotools +14 -0
- package/server/vendored-conf/gitignore/BricxCC +4 -0
- package/server/vendored-conf/gitignore/C +32 -0
- package/server/vendored-conf/gitignore/C Sharp +108 -0
- package/server/vendored-conf/gitignore/C++ +28 -0
- package/server/vendored-conf/gitignore/CFWheels +12 -0
- package/server/vendored-conf/gitignore/CMake +6 -0
- package/server/vendored-conf/gitignore/CUDA +6 -0
- package/server/vendored-conf/gitignore/CakePHP +13 -0
- package/server/vendored-conf/gitignore/ChefCookbook +9 -0
- package/server/vendored-conf/gitignore/Cloud9 +3 -0
- package/server/vendored-conf/gitignore/CodeIgniter +6 -0
- package/server/vendored-conf/gitignore/CodeKit +3 -0
- package/server/vendored-conf/gitignore/CommonLisp +3 -0
- package/server/vendored-conf/gitignore/Composer +6 -0
- package/server/vendored-conf/gitignore/Concrete5 +3 -0
- package/server/vendored-conf/gitignore/Coq +3 -0
- package/server/vendored-conf/gitignore/CraftCMS +3 -0
- package/server/vendored-conf/gitignore/DM +5 -0
- package/server/vendored-conf/gitignore/Dart +15 -0
- package/server/vendored-conf/gitignore/DartEditor +2 -0
- package/server/vendored-conf/gitignore/Delphi +57 -0
- package/server/vendored-conf/gitignore/Dreamweaver +3 -0
- package/server/vendored-conf/gitignore/Drupal +36 -0
- package/server/vendored-conf/gitignore/EPiServer +4 -0
- package/server/vendored-conf/gitignore/Eagle +36 -0
- package/server/vendored-conf/gitignore/Eclipse +39 -0
- package/server/vendored-conf/gitignore/EiffelStudio +2 -0
- package/server/vendored-conf/gitignore/Elisp +5 -0
- package/server/vendored-conf/gitignore/Elixir +4 -0
- package/server/vendored-conf/gitignore/Emacs +32 -0
- package/server/vendored-conf/gitignore/Ensime +4 -0
- package/server/vendored-conf/gitignore/Erlang +10 -0
- package/server/vendored-conf/gitignore/Espresso +1 -0
- package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
- package/server/vendored-conf/gitignore/ExtJs +4 -0
- package/server/vendored-conf/gitignore/Fancy +2 -0
- package/server/vendored-conf/gitignore/Finale +13 -0
- package/server/vendored-conf/gitignore/FlexBuilder +3 -0
- package/server/vendored-conf/gitignore/ForceDotCom +4 -0
- package/server/vendored-conf/gitignore/FuelPHP +2 -0
- package/server/vendored-conf/gitignore/GWT +28 -0
- package/server/vendored-conf/gitignore/Gcov +5 -0
- package/server/vendored-conf/gitignore/GitBook +16 -0
- package/server/vendored-conf/gitignore/Go +24 -0
- package/server/vendored-conf/gitignore/Gradle +8 -0
- package/server/vendored-conf/gitignore/Grails +33 -0
- package/server/vendored-conf/gitignore/Haskell +16 -0
- package/server/vendored-conf/gitignore/IGORPro +5 -0
- package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
- package/server/vendored-conf/gitignore/Idris +2 -0
- package/server/vendored-conf/gitignore/JDeveloper +13 -0
- package/server/vendored-conf/gitignore/Java +12 -0
- package/server/vendored-conf/gitignore/Jboss +19 -0
- package/server/vendored-conf/gitignore/Jekyll +3 -0
- package/server/vendored-conf/gitignore/JetBrains +46 -0
- package/server/vendored-conf/gitignore/Joomla +525 -0
- package/server/vendored-conf/gitignore/KDevelop4 +2 -0
- package/server/vendored-conf/gitignore/Kate +3 -0
- package/server/vendored-conf/gitignore/KiCAD +13 -0
- package/server/vendored-conf/gitignore/Kohana +2 -0
- package/server/vendored-conf/gitignore/LabVIEW +16 -0
- package/server/vendored-conf/gitignore/Laravel +4 -0
- package/server/vendored-conf/gitignore/Lazarus +30 -0
- package/server/vendored-conf/gitignore/Leiningen +12 -0
- package/server/vendored-conf/gitignore/LemonStand +21 -0
- package/server/vendored-conf/gitignore/LibreOffice +2 -0
- package/server/vendored-conf/gitignore/Lilypond +6 -0
- package/server/vendored-conf/gitignore/Linux +7 -0
- package/server/vendored-conf/gitignore/Lithium +2 -0
- package/server/vendored-conf/gitignore/Lua +41 -0
- package/server/vendored-conf/gitignore/LyX +4 -0
- package/server/vendored-conf/gitignore/Magento +126 -0
- package/server/vendored-conf/gitignore/Matlab +16 -0
- package/server/vendored-conf/gitignore/Maven +9 -0
- package/server/vendored-conf/gitignore/Mercurial +6 -0
- package/server/vendored-conf/gitignore/Mercury +13 -0
- package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
- package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
- package/server/vendored-conf/gitignore/ModelSim +23 -0
- package/server/vendored-conf/gitignore/Momentics +8 -0
- package/server/vendored-conf/gitignore/MonoDevelop +8 -0
- package/server/vendored-conf/gitignore/Nanoc +10 -0
- package/server/vendored-conf/gitignore/NetBeans +8 -0
- package/server/vendored-conf/gitignore/Nim +1 -0
- package/server/vendored-conf/gitignore/Ninja +2 -0
- package/server/vendored-conf/gitignore/Node +28 -0
- package/server/vendored-conf/gitignore/NotepadPP +2 -0
- package/server/vendored-conf/gitignore/OCaml +20 -0
- package/server/vendored-conf/gitignore/Objective-C +43 -0
- package/server/vendored-conf/gitignore/Opa +13 -0
- package/server/vendored-conf/gitignore/OpenCart +11 -0
- package/server/vendored-conf/gitignore/OracleForms +8 -0
- package/server/vendored-conf/gitignore/Packer +5 -0
- package/server/vendored-conf/gitignore/Perl +20 -0
- package/server/vendored-conf/gitignore/Phalcon +2 -0
- package/server/vendored-conf/gitignore/PhpStorm +63 -0
- package/server/vendored-conf/gitignore/PlayFramework +17 -0
- package/server/vendored-conf/gitignore/Plone +18 -0
- package/server/vendored-conf/gitignore/Prestashop +25 -0
- package/server/vendored-conf/gitignore/Processing +7 -0
- package/server/vendored-conf/gitignore/Python +58 -0
- package/server/vendored-conf/gitignore/Qooxdoo +5 -0
- package/server/vendored-conf/gitignore/Qt +34 -0
- package/server/vendored-conf/gitignore/R +16 -0
- package/server/vendored-conf/gitignore/ROS +47 -0
- package/server/vendored-conf/gitignore/Rails +35 -0
- package/server/vendored-conf/gitignore/Redcar +1 -0
- package/server/vendored-conf/gitignore/Redis +3 -0
- package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
- package/server/vendored-conf/gitignore/Ruby +36 -0
- package/server/vendored-conf/gitignore/Rust +11 -0
- package/server/vendored-conf/gitignore/SBT +9 -0
- package/server/vendored-conf/gitignore/SCons +2 -0
- package/server/vendored-conf/gitignore/SVN +1 -0
- package/server/vendored-conf/gitignore/Sass +2 -0
- package/server/vendored-conf/gitignore/Scala +17 -0
- package/server/vendored-conf/gitignore/Scrivener +7 -0
- package/server/vendored-conf/gitignore/Sdcc +8 -0
- package/server/vendored-conf/gitignore/SeamGen +26 -0
- package/server/vendored-conf/gitignore/SketchUp +1 -0
- package/server/vendored-conf/gitignore/SlickEdit +11 -0
- package/server/vendored-conf/gitignore/Stella +12 -0
- package/server/vendored-conf/gitignore/SublimeText +14 -0
- package/server/vendored-conf/gitignore/SugarCRM +25 -0
- package/server/vendored-conf/gitignore/Swift +43 -0
- package/server/vendored-conf/gitignore/Symfony +37 -0
- package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
- package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
- package/server/vendored-conf/gitignore/Tags +14 -0
- package/server/vendored-conf/gitignore/TeX +136 -0
- package/server/vendored-conf/gitignore/TextMate +3 -0
- package/server/vendored-conf/gitignore/Textpattern +11 -0
- package/server/vendored-conf/gitignore/TortoiseGit +2 -0
- package/server/vendored-conf/gitignore/TurboGears2 +20 -0
- package/server/vendored-conf/gitignore/Typo3 +20 -0
- package/server/vendored-conf/gitignore/Umbraco +17 -0
- package/server/vendored-conf/gitignore/Unity +21 -0
- package/server/vendored-conf/gitignore/UnrealEngine +76 -0
- package/server/vendored-conf/gitignore/VVVV +6 -0
- package/server/vendored-conf/gitignore/Vagrant +1 -0
- package/server/vendored-conf/gitignore/Vim +6 -0
- package/server/vendored-conf/gitignore/VirtualEnv +10 -0
- package/server/vendored-conf/gitignore/VisualStudio +217 -0
- package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
- package/server/vendored-conf/gitignore/Waf +3 -0
- package/server/vendored-conf/gitignore/WebMethods +14 -0
- package/server/vendored-conf/gitignore/WebStorm +63 -0
- package/server/vendored-conf/gitignore/Windows +18 -0
- package/server/vendored-conf/gitignore/WordPress +18 -0
- package/server/vendored-conf/gitignore/Xcode +23 -0
- package/server/vendored-conf/gitignore/XilinxISE +57 -0
- package/server/vendored-conf/gitignore/Xojo +11 -0
- package/server/vendored-conf/gitignore/Yeoman +6 -0
- package/server/vendored-conf/gitignore/Yii +6 -0
- package/server/vendored-conf/gitignore/ZendFramework +20 -0
- package/server/vendored-conf/gitignore/Zephir +26 -0
- package/server/vendored-conf/gitignore/macOS +25 -0
- package/server/vendored-conf/label/Default +7 -0
- package/server/vendored-conf/license/Abstyles License +10 -0
- package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
- package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
- package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
- package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
- package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
- package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
- package/server/vendored-conf/license/Apache License 1.0 +19 -0
- package/server/vendored-conf/license/Apache License 1.1 +20 -0
- package/server/vendored-conf/license/Apache License 2.0 +72 -0
- package/server/vendored-conf/license/Artistic License 1.0 +48 -0
- package/server/vendored-conf/license/Artistic License 2.0 +84 -0
- package/server/vendored-conf/license/BSD 2-clause License +8 -0
- package/server/vendored-conf/license/BSD 3-clause License +10 -0
- package/server/vendored-conf/license/BSD 4-clause License +13 -0
- package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
- package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
- package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
- package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
- package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
- package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
- package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
- package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
- package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
- package/server/vendored-conf/license/ISC license +6 -0
- package/server/vendored-conf/license/MIT License +8 -0
- package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
- package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
- package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
- package/server/vendored-conf/readme/Default +3 -0
- package/src/dsh-host.js +168 -0
- package/src/index.js +682 -0
- package/src/runner.js +364 -0
|
@@ -0,0 +1,160 @@
|
|
|
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
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// git-lfs-transfer: pure SSH LFS transfer protocol (lfs-transfer-1) per the
|
|
2
|
+
// git-lfs spec (docs/proposals/ssh_adapter.md). pkt-line framing: flush=0000,
|
|
3
|
+
// delim=0001, data pkt = hex(len) + payload. Binary payloads are pkt-framed.
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import { conf } from '../conf.js';
|
|
7
|
+
import * as db from '../db/db.js';
|
|
8
|
+
// pkt-line control frames are FOUR ASCII chars '0000'/'0001'
|
|
9
|
+
const FLUSH = Buffer.from('0000', 'ascii');
|
|
10
|
+
const DELIM = Buffer.from('0001', 'ascii');
|
|
11
|
+
function pkt(data) {
|
|
12
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
13
|
+
const header = Buffer.from((buf.length + 4).toString(16).padStart(4, '0'), 'ascii');
|
|
14
|
+
return Buffer.concat([header, buf]);
|
|
15
|
+
}
|
|
16
|
+
function lfsObjectPath(oid) {
|
|
17
|
+
return `${conf.appDataPath}/lfs-objects/${oid.slice(0, 2)}/${oid.slice(2, 4)}/${oid}`;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve owner/repo the same way as dispatch.serveGit. */
|
|
20
|
+
function resolveRepo(repoFullName) {
|
|
21
|
+
let repoName = repoFullName.replace(/^\//, '').replace(/\.git$/, '');
|
|
22
|
+
if (repoName.endsWith('.wiki'))
|
|
23
|
+
repoName = repoName.slice(0, -5);
|
|
24
|
+
const slash = repoName.indexOf('/');
|
|
25
|
+
const ownerName = slash > 0 ? repoName.slice(0, slash) : repoName;
|
|
26
|
+
const name = slash > 0 ? repoName.slice(slash + 1) : repoName;
|
|
27
|
+
const owner = db.getUserByUsername(ownerName);
|
|
28
|
+
const repo = owner ? db.getRepoByOwnerAndName(owner, name) : null;
|
|
29
|
+
return owner && repo ? { owner, repo } : null;
|
|
30
|
+
}
|
|
31
|
+
class PktReader {
|
|
32
|
+
buf = Buffer.alloc(0);
|
|
33
|
+
closed = false;
|
|
34
|
+
waiters = [];
|
|
35
|
+
push(data) {
|
|
36
|
+
this.buf = Buffer.concat([this.buf, data]);
|
|
37
|
+
const w = this.waiters.splice(0);
|
|
38
|
+
for (const fn of w)
|
|
39
|
+
fn();
|
|
40
|
+
}
|
|
41
|
+
end() {
|
|
42
|
+
this.closed = true;
|
|
43
|
+
const w = this.waiters.splice(0);
|
|
44
|
+
for (const fn of w)
|
|
45
|
+
fn();
|
|
46
|
+
}
|
|
47
|
+
/** Returns next pkt: {type:'data',payload} | {type:'flush'} | {type:'delim'} | null(EOF). */
|
|
48
|
+
async next() {
|
|
49
|
+
while (true) {
|
|
50
|
+
if (this.buf.length >= 4) {
|
|
51
|
+
const lenStr = this.buf.toString('utf8', 0, 4);
|
|
52
|
+
if (/^[0-9a-f]{4}$/.test(lenStr)) {
|
|
53
|
+
const len = parseInt(lenStr, 16);
|
|
54
|
+
if (len === 0) {
|
|
55
|
+
this.buf = this.buf.subarray(4);
|
|
56
|
+
return { type: 'flush', payload: Buffer.alloc(0) };
|
|
57
|
+
}
|
|
58
|
+
if (len === 1) {
|
|
59
|
+
this.buf = this.buf.subarray(4);
|
|
60
|
+
return { type: 'delim', payload: Buffer.alloc(0) };
|
|
61
|
+
}
|
|
62
|
+
if (len >= 4 && this.buf.length >= len) {
|
|
63
|
+
const payload = this.buf.subarray(4, len);
|
|
64
|
+
this.buf = this.buf.subarray(len);
|
|
65
|
+
return { type: 'data', payload };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (this.closed && this.buf.length < 4)
|
|
70
|
+
return null;
|
|
71
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function runLFSTransfer(argStr, identity, io) {
|
|
76
|
+
const parts = argStr.trim().split(/\s+/);
|
|
77
|
+
const repoFullName = parts[0];
|
|
78
|
+
const operation = parts[1] ?? 'download';
|
|
79
|
+
if (!['download', 'upload'].includes(operation)) {
|
|
80
|
+
io.write(Buffer.from(`git-lfs-transfer: invalid operation: ${operation}\n`));
|
|
81
|
+
io.close(1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const resolved = resolveRepo(repoFullName);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
io.write(Buffer.from('Repository does not exist\n'));
|
|
87
|
+
io.close(1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { owner, repo } = resolved;
|
|
91
|
+
const mode = db.accessMode(identity.userID, repo);
|
|
92
|
+
const need = operation === 'upload' ? db.AccessMode.WRITE : db.AccessMode.READ;
|
|
93
|
+
if (!(operation === 'download' && !repo.is_private && !conf.requireSigninView) && mode < need) {
|
|
94
|
+
io.write(Buffer.from('Access denied\n'));
|
|
95
|
+
io.close(1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const reader = new PktReader();
|
|
99
|
+
io.onStdin((data) => reader.push(data));
|
|
100
|
+
const writePkt = (s) => io.write(pkt(s));
|
|
101
|
+
const writeFlush = () => io.write(FLUSH);
|
|
102
|
+
const writeDelim = () => io.write(DELIM);
|
|
103
|
+
const writeStatus = (code, args = [], delim = false) => {
|
|
104
|
+
io.write(pkt(`status ${code}`));
|
|
105
|
+
for (const a of args)
|
|
106
|
+
io.write(pkt(a));
|
|
107
|
+
if (delim)
|
|
108
|
+
writeDelim();
|
|
109
|
+
writeFlush();
|
|
110
|
+
};
|
|
111
|
+
// 1. capability advertisement (server speaks first)
|
|
112
|
+
io.write(pkt('capability-list'));
|
|
113
|
+
io.write(pkt('version=1'));
|
|
114
|
+
io.write(pkt('begin-upload'));
|
|
115
|
+
io.write(pkt('begin-download'));
|
|
116
|
+
writeFlush();
|
|
117
|
+
let quit = false;
|
|
118
|
+
while (!quit) {
|
|
119
|
+
const p = await reader.next();
|
|
120
|
+
if (p === null)
|
|
121
|
+
break;
|
|
122
|
+
if (p.type === 'flush')
|
|
123
|
+
continue;
|
|
124
|
+
if (p.type !== 'data')
|
|
125
|
+
continue;
|
|
126
|
+
const line = p.payload.toString('utf8').replace(/\n$/, '');
|
|
127
|
+
const sp = line.indexOf(' ');
|
|
128
|
+
const cmd = sp < 0 ? line : line.slice(0, sp);
|
|
129
|
+
const rest = sp < 0 ? '' : line.slice(sp + 1);
|
|
130
|
+
switch (cmd) {
|
|
131
|
+
case 'version': {
|
|
132
|
+
if (rest === '1')
|
|
133
|
+
writeStatus(200);
|
|
134
|
+
else
|
|
135
|
+
writeStatus(400);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'quit': {
|
|
139
|
+
writeStatus(200);
|
|
140
|
+
quit = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'batch': {
|
|
144
|
+
// collect arguments until delim, then oid lines until flush
|
|
145
|
+
const args = {};
|
|
146
|
+
const oids = [];
|
|
147
|
+
let inOids = false;
|
|
148
|
+
while (true) {
|
|
149
|
+
const q = await reader.next();
|
|
150
|
+
if (q === null || q.type === 'flush')
|
|
151
|
+
break;
|
|
152
|
+
if (q.type === 'delim') {
|
|
153
|
+
inOids = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (q.type !== 'data')
|
|
157
|
+
continue;
|
|
158
|
+
const l = q.payload.toString('utf8').replace(/\n$/, '');
|
|
159
|
+
if (!inOids) {
|
|
160
|
+
const eq = l.indexOf('=');
|
|
161
|
+
if (eq > 0)
|
|
162
|
+
args[l.slice(0, eq)] = l.slice(eq + 1);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const m = /^(\S+) (\d+)(?: (.*))?$/.exec(l);
|
|
166
|
+
if (m)
|
|
167
|
+
oids.push({ oid: m[1], size: Number(m[2]) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
io.write(pkt('status 200'));
|
|
171
|
+
writeDelim();
|
|
172
|
+
const wantUpload = operation === 'upload';
|
|
173
|
+
for (const o of oids) {
|
|
174
|
+
const row = db.db().prepare('SELECT * FROM lfs_object WHERE repo_id = ? AND oid = ?').get(repo.id, o.oid);
|
|
175
|
+
const exists = !!row && fs.existsSync(lfsObjectPath(o.oid));
|
|
176
|
+
let action;
|
|
177
|
+
if (wantUpload) {
|
|
178
|
+
// upload: offer upload when missing or size mismatch; else noop
|
|
179
|
+
action = !exists || row.size !== o.size ? 'upload' : 'noop';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (!exists) {
|
|
183
|
+
action = 'noop'; // mirrors HTTP 404 → error shape
|
|
184
|
+
io.write(pkt(`${o.oid} ${o.size} error message=[Object does not exist]`));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
action = row.size === o.size ? 'download' : 'noop';
|
|
188
|
+
}
|
|
189
|
+
io.write(pkt(`${o.oid} ${o.size} ${action}`));
|
|
190
|
+
}
|
|
191
|
+
writeFlush();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'get-object': {
|
|
195
|
+
const oid = rest.trim();
|
|
196
|
+
const file = lfsObjectPath(oid);
|
|
197
|
+
const row = db.db().prepare('SELECT * FROM lfs_object WHERE repo_id = ? AND oid = ?').get(repo.id, oid);
|
|
198
|
+
if (!row || !fs.existsSync(file)) {
|
|
199
|
+
writeStatus(404, [], true);
|
|
200
|
+
io.write(pkt('Object does not exist\n'));
|
|
201
|
+
writeFlush();
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
io.write(pkt('status 200'));
|
|
205
|
+
io.write(pkt(`size=${row.size}`));
|
|
206
|
+
writeDelim();
|
|
207
|
+
const content = fs.readFileSync(file);
|
|
208
|
+
for (let off = 0; off < content.length; off += 65516) {
|
|
209
|
+
io.write(pkt(content.subarray(off, Math.min(off + 65516, content.length))));
|
|
210
|
+
}
|
|
211
|
+
writeFlush();
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case 'put-object': {
|
|
215
|
+
const oid = rest.trim();
|
|
216
|
+
// arguments until DELIM, then binary data until FLUSH
|
|
217
|
+
let size = 0;
|
|
218
|
+
while (true) {
|
|
219
|
+
const q = await reader.next();
|
|
220
|
+
if (q === null || q.type === 'flush')
|
|
221
|
+
break;
|
|
222
|
+
if (q.type === 'delim')
|
|
223
|
+
break;
|
|
224
|
+
if (q.type !== 'data')
|
|
225
|
+
continue;
|
|
226
|
+
const l = q.payload.toString('utf8').replace(/\n$/, '');
|
|
227
|
+
const eq = l.indexOf('=');
|
|
228
|
+
if (eq > 0 && l.slice(0, eq) === 'size')
|
|
229
|
+
size = Number(l.slice(eq + 1));
|
|
230
|
+
}
|
|
231
|
+
// binary payload: collect until flush
|
|
232
|
+
const chunks = [];
|
|
233
|
+
while (true) {
|
|
234
|
+
const q = await reader.next();
|
|
235
|
+
if (q === null || q.type === 'flush')
|
|
236
|
+
break;
|
|
237
|
+
if (q.type === 'data')
|
|
238
|
+
chunks.push(q.payload);
|
|
239
|
+
}
|
|
240
|
+
const content = Buffer.concat(chunks);
|
|
241
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
242
|
+
if (hash !== oid || (size > 0 && content.length !== size)) {
|
|
243
|
+
writeStatus(422, [], true);
|
|
244
|
+
io.write(pkt('Content-Length and Oid mismatches\n'));
|
|
245
|
+
writeFlush();
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const dst = lfsObjectPath(oid);
|
|
249
|
+
fs.mkdirSync(dst.slice(0, dst.lastIndexOf('/')), { recursive: true });
|
|
250
|
+
fs.writeFileSync(dst, content);
|
|
251
|
+
db.db()
|
|
252
|
+
.prepare('INSERT OR REPLACE INTO lfs_object (repo_id, oid, size, storage, created_at) VALUES (?,?,?,?,?)')
|
|
253
|
+
.run(repo.id, oid, content.length, 'local', new Date().toISOString().replace('T', ' ').replace('Z', ''));
|
|
254
|
+
writeStatus(200);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'verify-object': {
|
|
258
|
+
const oid = rest.trim();
|
|
259
|
+
let size = 0;
|
|
260
|
+
while (true) {
|
|
261
|
+
const q = await reader.next();
|
|
262
|
+
if (q === null || q.type === 'flush')
|
|
263
|
+
break;
|
|
264
|
+
if (q.type !== 'data')
|
|
265
|
+
continue;
|
|
266
|
+
const l = q.payload.toString('utf8').replace(/\n$/, '');
|
|
267
|
+
const eq = l.indexOf('=');
|
|
268
|
+
if (eq > 0 && l.slice(0, eq) === 'size')
|
|
269
|
+
size = Number(l.slice(eq + 1));
|
|
270
|
+
}
|
|
271
|
+
const row = db.db().prepare('SELECT * FROM lfs_object WHERE repo_id = ? AND oid = ?').get(repo.id, oid);
|
|
272
|
+
if (!row) {
|
|
273
|
+
writeStatus(404);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
writeStatus(row.size === size ? 200 : 422);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
default: {
|
|
280
|
+
// unknown command → protocol error status
|
|
281
|
+
writeStatus(400, [], true);
|
|
282
|
+
io.write(pkt(`Unknown command: ${cmd}\n`));
|
|
283
|
+
writeFlush();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
io.close(0);
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=lfs-transfer.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Builtin SSH server (gogs START_SSH_SERVER): publickey auth against the
|
|
2
|
+
// public_key table, then pipes `git-upload-pack`/`git-receive-pack` sessions.
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
import { conf } from '../conf.js';
|
|
7
|
+
import * as db from '../db/db.js';
|
|
8
|
+
import { dispatchSSHCommand } from './dispatch.js';
|
|
9
|
+
function hostKeyPath() {
|
|
10
|
+
return path.join(conf.customDir, 'ssh', 'gogs_ed25519.key');
|
|
11
|
+
}
|
|
12
|
+
function ensureHostKey() {
|
|
13
|
+
const file = hostKeyPath();
|
|
14
|
+
if (fs.existsSync(file))
|
|
15
|
+
return fs.readFileSync(file, 'utf8');
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
18
|
+
// ssh2 parses PKCS#1 "RSA PRIVATE KEY" PEM reliably
|
|
19
|
+
const pem = privateKey.export({ type: 'pkcs1', format: 'pem' }).toString();
|
|
20
|
+
fs.writeFileSync(file, pem, { mode: 0o600 });
|
|
21
|
+
return pem;
|
|
22
|
+
}
|
|
23
|
+
/** Normalize a public key to "type base64" for comparison. */
|
|
24
|
+
function normalizeKey(content) {
|
|
25
|
+
const parts = content.trim().split(/\s+/);
|
|
26
|
+
if (parts.length < 2)
|
|
27
|
+
return '';
|
|
28
|
+
return `${parts[0]} ${parts[1]}`;
|
|
29
|
+
}
|
|
30
|
+
async function authorizeKey(keyData) {
|
|
31
|
+
const presented = normalizeKey(keyData.toString('utf8'));
|
|
32
|
+
if (!presented)
|
|
33
|
+
return null;
|
|
34
|
+
const rows = db.db().prepare('SELECT id, owner_id, content, type FROM public_key').all();
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
if (normalizeKey(row.content) === presented) {
|
|
37
|
+
if (row.type === 2) {
|
|
38
|
+
const dk = db.db().prepare('SELECT repo_id FROM deploy_key WHERE key_id = ?').get(row.id);
|
|
39
|
+
return { userID: row.owner_id, keyID: row.id, deployRepoID: dk?.repo_id ?? null };
|
|
40
|
+
}
|
|
41
|
+
return { userID: row.owner_id, keyID: row.id, deployRepoID: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export function startSSHServer() {
|
|
47
|
+
import('ssh2').then(({ default: ssh2 }) => {
|
|
48
|
+
const hostKey = ensureHostKey();
|
|
49
|
+
const server = new ssh2.Server({ hostKeys: [hostKey] }, (client) => {
|
|
50
|
+
let authedUser = null;
|
|
51
|
+
client
|
|
52
|
+
.on('ready', () => {
|
|
53
|
+
client.on('session', (accept) => {
|
|
54
|
+
const session = accept();
|
|
55
|
+
session.on('exec', (accept, _reject, info) => {
|
|
56
|
+
const stream = accept();
|
|
57
|
+
const user = authedUser;
|
|
58
|
+
if (!user) {
|
|
59
|
+
stream.exit(1);
|
|
60
|
+
stream.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
dispatchSSHCommand(String(info.command), user, {
|
|
64
|
+
write: (chunk) => stream.write(chunk),
|
|
65
|
+
onStdin: (cb) => stream.on('data', (d) => cb(d)),
|
|
66
|
+
close: (code) => {
|
|
67
|
+
stream.exit(code);
|
|
68
|
+
stream.end();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.on('error', () => { });
|
|
75
|
+
client.on('authentication', (ctx) => {
|
|
76
|
+
if (ctx.method !== 'publickey') {
|
|
77
|
+
return ctx.reject(['publickey']);
|
|
78
|
+
}
|
|
79
|
+
const buf = ctx.key?.data;
|
|
80
|
+
if (!buf)
|
|
81
|
+
return ctx.reject();
|
|
82
|
+
// the wire blob starts with a length-prefixed algorithm name
|
|
83
|
+
const algLen = buf.readUInt32BE(0);
|
|
84
|
+
const algName = buf.subarray(4, 4 + algLen).toString('utf8');
|
|
85
|
+
// the .pub base64 IS the full wire blob (alg prefix + body)
|
|
86
|
+
const b64 = buf.toString('base64');
|
|
87
|
+
const line = `${algName} ${b64}`;
|
|
88
|
+
authorizeKey(Buffer.from(line)).then((u) => {
|
|
89
|
+
if (u) {
|
|
90
|
+
authedUser = u;
|
|
91
|
+
ctx.accept();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
ctx.reject();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const port = conf.sshPort === 22 ? 22 : conf.sshPort;
|
|
100
|
+
const listenPort = process.env.GOGS_SSH_PORT ? Number(process.env.GOGS_SSH_PORT) : port;
|
|
101
|
+
server.listen(listenPort, '0.0.0.0', () => {
|
|
102
|
+
console.log(`SSH server started on 0.0.0.0:${listenPort}`);
|
|
103
|
+
});
|
|
104
|
+
server.on('error', (e) => console.error('[ssh]', e));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Time-limited activation/reset codes, captcha, and email-activate helpers —
|
|
2
|
+
// mirroring gogs internal/tool TimeLimitCode semantics (sha1, minute resolution).
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
4
|
+
import { conf } from './conf.js';
|
|
5
|
+
export const TIME_LIMIT_CODE_LENGTH = 12 + 6 + 40;
|
|
6
|
+
function createTimeLimitCode(data, minutes, startStr) {
|
|
7
|
+
const format = (d) => String(d.getFullYear()).padStart(4, '0') +
|
|
8
|
+
String(d.getMonth() + 1).padStart(2, '0') +
|
|
9
|
+
String(d.getDate()).padStart(2, '0') +
|
|
10
|
+
String(d.getHours()).padStart(2, '0') +
|
|
11
|
+
String(d.getMinutes()).padStart(2, '0');
|
|
12
|
+
const start = startStr ? new Date(Number(startStr.slice(0, 4)), Number(startStr.slice(4, 6)) - 1, Number(startStr.slice(6, 8)), Number(startStr.slice(8, 10)), Number(startStr.slice(10, 12))) : new Date();
|
|
13
|
+
const startFormatted = startStr ?? format(start);
|
|
14
|
+
const end = new Date(start.getTime() + minutes * 60000);
|
|
15
|
+
const endStr = format(end);
|
|
16
|
+
const sh = crypto.createHash('sha1');
|
|
17
|
+
sh.update(data + conf.secretKey + startFormatted + endStr + String(minutes));
|
|
18
|
+
const encoded = sh.digest('hex');
|
|
19
|
+
return `${startFormatted}${String(minutes).padStart(6, '0')}${encoded}`;
|
|
20
|
+
}
|
|
21
|
+
export function verifyTimeLimitCode(data, minutes, code) {
|
|
22
|
+
if (code.length <= 18)
|
|
23
|
+
return false;
|
|
24
|
+
const start = code.slice(0, 12);
|
|
25
|
+
const lives = code.slice(12, 18);
|
|
26
|
+
const parsed = Number(lives);
|
|
27
|
+
if (!Number.isNaN(parsed))
|
|
28
|
+
minutes = parsed;
|
|
29
|
+
const retCode = createTimeLimitCode(data, minutes, start);
|
|
30
|
+
if (retCode === code && minutes > 0) {
|
|
31
|
+
const before = new Date(Number(start.slice(0, 4)), Number(start.slice(4, 6)) - 1, Number(start.slice(6, 8)), Number(start.slice(8, 10)), Number(start.slice(10, 12)));
|
|
32
|
+
return before.getTime() + minutes * 60000 > Date.now();
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/** User activation code: gogs verifyUserActiveCode data format. */
|
|
37
|
+
export function createActivateCode(user, minutes) {
|
|
38
|
+
const data = String(user.id) + user.email + user.lower_name + user.passwd + user.rands;
|
|
39
|
+
return createTimeLimitCode(data, minutes, null);
|
|
40
|
+
}
|
|
41
|
+
export function verifyActivateCode(user, code) {
|
|
42
|
+
if (!user)
|
|
43
|
+
return false;
|
|
44
|
+
const data = String(user.id) + user.email + user.lower_name + user.passwd + user.rands;
|
|
45
|
+
return verifyTimeLimitCode(data, conf.activateCodeLives, code);
|
|
46
|
+
}
|
|
47
|
+
/** Verify an activation/reset code for the user named inside it (gogs parseUserFromCode + verify). */
|
|
48
|
+
export function verifyUserFromCode(code, lookup) {
|
|
49
|
+
if (code.length <= TIME_LIMIT_CODE_LENGTH)
|
|
50
|
+
return null;
|
|
51
|
+
const hexStr = code.slice(TIME_LIMIT_CODE_LENGTH);
|
|
52
|
+
let username;
|
|
53
|
+
try {
|
|
54
|
+
username = Buffer.from(hexStr, 'hex').toString('utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const user = lookup(username);
|
|
60
|
+
if (!user)
|
|
61
|
+
return { user: null, valid: false };
|
|
62
|
+
return { user, valid: verifyActivateCode(user, code.slice(0, TIME_LIMIT_CODE_LENGTH)) };
|
|
63
|
+
}
|
|
64
|
+
const captchaCache = new Map();
|
|
65
|
+
function sweepCaptcha() {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const [k, v] of captchaCache) {
|
|
68
|
+
if (v.expires < now)
|
|
69
|
+
captchaCache.delete(k);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function newCaptcha() {
|
|
73
|
+
sweepCaptcha();
|
|
74
|
+
const id = crypto.randomBytes(16).toString('hex');
|
|
75
|
+
const code = String(Math.floor(100000 + Math.random() * 900000));
|
|
76
|
+
captchaCache.set(id, { code, expires: Date.now() + 10 * 60000 });
|
|
77
|
+
// distorted-digit SVG
|
|
78
|
+
const w = 240;
|
|
79
|
+
const h = 80;
|
|
80
|
+
const colors = ['#333', '#555', '#722', '#252', '#335'];
|
|
81
|
+
let digits = '';
|
|
82
|
+
for (let i = 0; i < code.length; i++) {
|
|
83
|
+
const x = 28 + i * 34 + Math.random() * 8;
|
|
84
|
+
const y = h / 2 + Math.random() * 10 - 5;
|
|
85
|
+
const rot = Math.random() * 50 - 25;
|
|
86
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
87
|
+
digits += `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" font-size="44" font-family="monospace" font-weight="bold" fill="${color}" transform="rotate(${rot.toFixed(1)} ${x.toFixed(1)} ${y.toFixed(1)})">${code[i]}</text>`;
|
|
88
|
+
}
|
|
89
|
+
let noise = '';
|
|
90
|
+
for (let i = 0; i < 6; i++) {
|
|
91
|
+
const x1 = Math.random() * w;
|
|
92
|
+
const y1 = Math.random() * h;
|
|
93
|
+
const x2 = Math.random() * w;
|
|
94
|
+
const y2 = Math.random() * h;
|
|
95
|
+
noise += `<line x1="${x1.toFixed(0)}" y1="${y1.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="#999" stroke-width="1" opacity="0.6"/>`;
|
|
96
|
+
}
|
|
97
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><rect width="100%" height="100%" fill="#f8f8f8"/>${noise}${digits}</svg>`;
|
|
98
|
+
return { id, svg };
|
|
99
|
+
}
|
|
100
|
+
export function validateCaptcha(id, text) {
|
|
101
|
+
const entry = captchaCache.get(id);
|
|
102
|
+
if (!entry || entry.expires < Date.now())
|
|
103
|
+
return false;
|
|
104
|
+
const ok = entry.code === String(text).trim();
|
|
105
|
+
captchaCache.delete(id); // single-use
|
|
106
|
+
return ok;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=toolx.js.map
|