docs: add Typecho red-team case study and Nutstore push helper
notebook-ci / build-push-deploy (push) Successful in 49s
notebook-ci / build-push-deploy (push) Successful in 49s
Summarize the Typecho XML-RPC to RCE to root attack chain as a publishable security note, and add a small WebDAV PUT script to sync individual notes to Jianguoyun KnowledgeBase.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 将本地一篇(或多篇)笔记 PUT 到坚果云 WebDAV
|
||||
* 用法: node scripts/push-to-nutstore.mjs <local-md-path> [remote-rel-path]
|
||||
*
|
||||
* remote-rel-path 相对 NUTSTORE_WEBDAV_PATH,默认与 posts 下相对路径一致
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadEnv } from './lib/load-env.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
loadEnv(root);
|
||||
|
||||
const localArg = process.argv[2];
|
||||
if (!localArg) {
|
||||
console.error('用法: node scripts/push-to-nutstore.mjs <local-md-path> [remote-rel-path]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const localPath = path.isAbsolute(localArg) ? localArg : path.join(root, localArg);
|
||||
if (!fs.existsSync(localPath)) {
|
||||
console.error('文件不存在:', localPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const postsOut = path.join(root, 'src/content/posts');
|
||||
let remoteRel = process.argv[3];
|
||||
if (!remoteRel) {
|
||||
const rel = path.relative(postsOut, localPath);
|
||||
if (rel.startsWith('..')) {
|
||||
remoteRel = path.basename(localPath);
|
||||
} else {
|
||||
remoteRel = rel.split(path.sep).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
const DAV_BASE = (process.env.NUTSTORE_WEBDAV_URL || 'https://dav.jianguoyun.com/dav').replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
const sub = (process.env.NUTSTORE_WEBDAV_PATH || '').replace(/^\/dav\/?/i, '').replace(/^\/+|\/+$/g, '');
|
||||
const user = process.env.NUTSTORE_WEBDAV_USER || process.env.NUTSTORE_USER;
|
||||
const pass = process.env.NUTSTORE_WEBDAV_PASS || process.env.NUTSTORE_PASS;
|
||||
if (!user || !pass) {
|
||||
console.error('缺少 NUTSTORE_WEBDAV_USER / NUTSTORE_WEBDAV_PASS');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const auth = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
||||
|
||||
function joinDav(...parts) {
|
||||
return parts
|
||||
.filter(Boolean)
|
||||
.map((p, i) => (i === 0 ? p.replace(/\/$/, '') : String(p).replace(/^\/+|\/+$/g, '')))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
async function ensureDir(davDirUrl) {
|
||||
// MKCOL each segment under base
|
||||
const base = joinDav(DAV_BASE, sub);
|
||||
if (!davDirUrl.startsWith(base)) return;
|
||||
let rel = davDirUrl.slice(base.length).replace(/^\/+/, '');
|
||||
if (!rel) return;
|
||||
const segs = rel.split('/').filter(Boolean);
|
||||
let cur = base;
|
||||
for (const seg of segs) {
|
||||
cur = joinDav(cur, encodeURIComponent(seg).replace(/%2F/gi, '/'));
|
||||
// try without encode for chinese paths - 坚果云 often wants raw utf8 path encoded per segment
|
||||
const tryUrl = joinDav(base, ...segs.slice(0, segs.indexOf(seg) + 1).map(encodeURIComponent));
|
||||
const res = await fetch(tryUrl + (tryUrl.endsWith('/') ? '' : '/'), {
|
||||
method: 'MKCOL',
|
||||
headers: { Authorization: auth },
|
||||
});
|
||||
// 201 created, 405 exists, 409 parent missing handled by order
|
||||
if (![201, 405, 301, 200].includes(res.status) && res.status !== 409) {
|
||||
// 409 can mean exists as collection on some servers
|
||||
if (res.status !== 409) {
|
||||
const t = await res.text().catch(() => '');
|
||||
// ignore "already exists" style
|
||||
if (res.status !== 405) {
|
||||
console.warn(`MKCOL ${res.status} ${tryUrl} ${t.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function mkcolRecursive(remoteRelPath) {
|
||||
const parts = remoteRelPath.split('/').filter(Boolean);
|
||||
// only dirs
|
||||
const dirParts = parts.slice(0, -1);
|
||||
let built = [];
|
||||
for (const p of dirParts) {
|
||||
built.push(p);
|
||||
const url =
|
||||
joinDav(DAV_BASE, sub, ...built.map((s) => encodeURIComponent(s))) + '/';
|
||||
const res = await fetch(url, { method: 'MKCOL', headers: { Authorization: auth } });
|
||||
console.log(`MKCOL ${res.status} ${decodeURIComponent(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fileName = path.posix.basename(remoteRel.split(path.sep).join('/'));
|
||||
const remotePosix = remoteRel.split(path.sep).join('/');
|
||||
await mkcolRecursive(remotePosix);
|
||||
|
||||
const url =
|
||||
joinDav(DAV_BASE, sub, ...remotePosix.split('/').map(encodeURIComponent));
|
||||
const body = fs.readFileSync(localPath);
|
||||
const res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Content-Length': String(body.length),
|
||||
},
|
||||
body,
|
||||
});
|
||||
const t = await res.text().catch(() => '');
|
||||
console.log(`PUT ${res.status} ${decodeURIComponent(url)}`);
|
||||
if (!res.ok) {
|
||||
console.error(t.slice(0, 400));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('已同步到坚果云:', path.posix.join(sub, remotePosix));
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user