File size: 5,122 Bytes
68d7816 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import { createWriteStream } from 'node:fs';
import { chmod, mkdir, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { type Entry, fromBuffer as yauzlFromBuffer } from 'yauzl';
import { Error2, ErrorCodes } from '#/errors';
export async function downloadZip(url: string, signal?: AbortSignal): Promise<Buffer> {
const controller = new AbortController();
const timeoutHandle = setTimeout(() => {
controller.abort();
}, 5 * 60 * 1000);
try {
const resp = await fetch(url, { signal: signal ?? controller.signal });
if (!resp.ok) {
throw new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
`Failed to download zip: HTTP ${resp.status} ${resp.statusText}`,
{ details: { url, status: resp.status } },
);
}
return Buffer.from(await resp.arrayBuffer());
} finally {
clearTimeout(timeoutHandle);
}
}
export async function extractZip(buffer: Buffer, destDir: string): Promise<string> {
await mkdir(destDir, { recursive: true });
const destDirResolved = path.resolve(destDir);
let settled = false;
await new Promise<void>((resolve, reject) => {
yauzlFromBuffer(buffer, { lazyEntries: true }, (openErr, zipfile) => {
if (openErr !== null || zipfile === undefined) {
reject(
new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
`Failed to open zip: ${openErr?.message ?? 'unknown error'}`,
{ cause: openErr ?? undefined },
),
);
return;
}
const onEntry = (entry: Entry): void => {
const fileName = entry.fileName;
const destPath = path.resolve(destDir, fileName);
if (destPath !== destDirResolved && !destPath.startsWith(destDirResolved + path.sep)) {
if (!settled) {
settled = true;
reject(
new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
`Path traversal detected in zip entry: ${fileName}`,
{ details: { entry: fileName } },
),
);
}
zipfile.close();
return;
}
if (fileName.endsWith('/')) {
mkdir(destPath, { recursive: true })
.then(() => {
zipfile.readEntry();
})
.catch((error) => {
if (!settled) {
settled = true;
reject(error);
}
zipfile.close();
});
return;
}
zipfile.openReadStream(entry, (streamErr, stream) => {
if (streamErr !== null || stream === undefined) {
if (!settled) {
settled = true;
reject(
new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
`Failed to read ${fileName} from archive: ${streamErr?.message ?? 'unknown error'}`,
{ cause: streamErr ?? undefined, details: { entry: fileName } },
),
);
}
zipfile.close();
return;
}
mkdir(path.dirname(destPath), { recursive: true })
.then(() => pipeline(stream, createWriteStream(destPath)))
.then(() => restoreFilePermissions(destPath, entry))
.then(() => {
zipfile.readEntry();
})
.catch((error) => {
if (!settled) {
settled = true;
reject(error);
}
zipfile.close();
});
});
};
zipfile.on('entry', onEntry);
zipfile.on('end', () => {
if (!settled) {
settled = true;
resolve();
}
});
zipfile.on('error', (err: Error) => {
if (!settled) {
settled = true;
reject(err);
}
});
zipfile.readEntry();
});
});
return detectPluginRoot(destDir);
}
async function restoreFilePermissions(destPath: string, entry: Entry): Promise<void> {
const mode = entry.externalFileAttributes >>> 16;
if (mode === 0) return;
const permissions = mode & 0o777;
if (permissions === 0) return;
await chmod(destPath, permissions);
}
async function detectPluginRoot(dir: string): Promise<string> {
if (await hasManifest(dir)) return dir;
const entries = await readdir(dir, { withFileTypes: true });
const childDirs = entries.filter((entry) => entry.isDirectory());
const childDir = childDirs.length === 1 ? childDirs[0] : undefined;
if (childDir !== undefined) {
const child = path.join(dir, childDir.name);
if (await hasManifest(child)) return child;
}
return dir;
}
async function hasManifest(dir: string): Promise<boolean> {
const rootManifest = path.join(dir, 'kimi.plugin.json');
const dirManifest = path.join(dir, '.kimi-plugin', 'plugin.json');
return (await isFile(rootManifest)) || (await isFile(dirManifest));
}
async function isFile(p: string): Promise<boolean> {
try {
return (await stat(p)).isFile();
} catch {
return false;
}
}
|