init
This commit is contained in:
commit
82ab96e352
13 changed files with 435 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
build/
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
13
config.example.json
Normal file
13
config.example.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"DisallowedFilenameChars": ["\"","<",">","|","\u0000","\u0001","\u0002","\u0003","\u0004","\u0005","\u0006","\u0007","\b","\t","\n","\u000b","\f","\r","\u000e","\u000f","\u0010","\u0011","\u0012","\u0013","\u0014","\u0015","\u0016","\u0017","\u0018","\u0019","\u001a","\u001b","\u001c","\u001d","\u001e","\u001f",":","*","?","\\","/"],
|
||||
"ListenPort": 8369,
|
||||
"MaxFileSize": 104857600,
|
||||
"BlockedMD5": [],
|
||||
"RateLimit": 10,
|
||||
"VMs": [
|
||||
{
|
||||
"ID": "vm1",
|
||||
"SocketPath": "/tmp/vm1-uploads.sock"
|
||||
}
|
||||
]
|
||||
}
|
13
config.json
Normal file
13
config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"DisallowedFilenameChars": ["\"","<",">","|","\u0000","\u0001","\u0002","\u0003","\u0004","\u0005","\u0006","\u0007","\b","\t","\n","\u000b","\f","\r","\u000e","\u000f","\u0010","\u0011","\u0012","\u0013","\u0014","\u0015","\u0016","\u0017","\u0018","\u0019","\u001a","\u001b","\u001c","\u001d","\u001e","\u001f",":","*","?","\\","/"],
|
||||
"ListenPort": 8369,
|
||||
"MaxFileSize": 104857600,
|
||||
"BlockedMD5": ["038e69dec7760dbf8d52f0f2b0677280"],
|
||||
"RateLimit": 10,
|
||||
"VMs": [
|
||||
{
|
||||
"ID": "forkievm1",
|
||||
"SocketPath": "/tmp/vm1-uploads.sock"
|
||||
}
|
||||
]
|
||||
}
|
24
package.json
Normal file
24
package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "collabvm-agent-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A server to upload files to a VM",
|
||||
"main": "build/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"author": "Elijah R",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/node": "^20.10.3",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/md5": "^2.3.5",
|
||||
"async-mutex": "^0.4.0",
|
||||
"fastify": "^4.24.3",
|
||||
"md5": "^2.3.0",
|
||||
"msgpack-lite": "^0.1.26"
|
||||
}
|
||||
}
|
11
src/IConfig.ts
Normal file
11
src/IConfig.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default interface IConfig {
|
||||
DisallowedFilenameChars : string[];
|
||||
ListenPort : number;
|
||||
MaxFileSize : number;
|
||||
BlockedMD5: string[];
|
||||
RateLimit : number;
|
||||
VMs : {
|
||||
ID : string;
|
||||
SocketPath : string;
|
||||
}[]
|
||||
}
|
9
src/Protocol.ts
Normal file
9
src/Protocol.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export interface ProtocolMessage {
|
||||
Operation : ProtocolOperation;
|
||||
Filename? : string;
|
||||
FileData? : Buffer;
|
||||
}
|
||||
|
||||
export enum ProtocolOperation {
|
||||
UploadFile = 0,
|
||||
}
|
30
src/Ratelimit.ts
Normal file
30
src/Ratelimit.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export default class RateLimit {
|
||||
private cooldown : number;
|
||||
private ready : boolean;
|
||||
private interval? : NodeJS.Timeout;
|
||||
private timeRemaining : number;
|
||||
constructor(cooldown : number) {
|
||||
this.cooldown = cooldown;
|
||||
this.ready = true;
|
||||
this.timeRemaining = 0;
|
||||
}
|
||||
Limit() : boolean {
|
||||
if (this.ready) {
|
||||
this.ready = false;
|
||||
this.timeRemaining = this.cooldown;
|
||||
this.interval = setInterval(() => {
|
||||
this.timeRemaining--;
|
||||
if (this.timeRemaining <= 0) {
|
||||
this.ready = true;
|
||||
clearInterval(this.interval!);
|
||||
}
|
||||
}, 1000);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
GetRemaining() : number {
|
||||
return this.timeRemaining;
|
||||
}
|
||||
}
|
63
src/VM.ts
Normal file
63
src/VM.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Socket } from "net";
|
||||
import * as protocol from './Protocol.js'
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
import {Mutex} from 'async-mutex';
|
||||
import log from './log.js';
|
||||
|
||||
export default class VM {
|
||||
#socketpath : string;
|
||||
#socket : Socket;
|
||||
#writeLock : Mutex = new Mutex();
|
||||
connected : boolean = false;
|
||||
constructor(socketpath : string) {
|
||||
this.#socketpath = socketpath;
|
||||
this.#socket = new Socket();
|
||||
this.#socket.connect(socketpath);
|
||||
this.#socket.on('connect', () => {
|
||||
this.connected = true;
|
||||
log("INFO", `Connected to VM at ${socketpath}`);
|
||||
});
|
||||
}
|
||||
|
||||
UploadFile(filename : string, data : Buffer) : Promise<void> {
|
||||
return new Promise(async (res, rej) => {
|
||||
const msg : protocol.ProtocolMessage = {
|
||||
Operation: protocol.ProtocolOperation.UploadFile,
|
||||
Filename: filename,
|
||||
FileData: data
|
||||
};
|
||||
var payload = msgpack.encode(msg);
|
||||
var header = Buffer.alloc(4);
|
||||
header.writeUInt32LE(payload.length);
|
||||
// Make sure we dont write two messages at the same time
|
||||
var release = await this.#writeLock.acquire();
|
||||
await this.#writeData(header);
|
||||
// shit gets fucked if i dont do this
|
||||
await sleep(100);
|
||||
await this.#writeData(payload);
|
||||
// All done
|
||||
release();
|
||||
res();
|
||||
});
|
||||
}
|
||||
|
||||
#writeData(data : Buffer) : Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#socket.write(data, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms : number) : Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
res();
|
||||
}, ms);
|
||||
})
|
||||
}
|
98
src/index.ts
Normal file
98
src/index.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import Fastify, {errorCodes} from 'fastify';
|
||||
import { readFileSync } from 'fs';
|
||||
import IConfig from './IConfig.js';
|
||||
import VM from './VM.js';
|
||||
import md5 from 'md5';
|
||||
import log from './log.js';
|
||||
import RateLimit from './Ratelimit.js';
|
||||
|
||||
log("INFO", "CollabVM Agent Server Starting up...");
|
||||
// Load the config file
|
||||
var config : IConfig;
|
||||
try {
|
||||
var configraw = readFileSync("./config.json");
|
||||
config = JSON.parse(configraw.toString());
|
||||
} catch (e) {
|
||||
console.error("Failed to load config file: " + (e as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var VMs = new Map<string, VM>();
|
||||
config.VMs.forEach((v) => {
|
||||
VMs.set(v.ID, new VM(v.SocketPath));
|
||||
});
|
||||
|
||||
var IPs = new Map<string, RateLimit>();
|
||||
|
||||
const app = Fastify({
|
||||
trustProxy: true,
|
||||
bodyLimit: config.MaxFileSize,
|
||||
});
|
||||
app.setErrorHandler((err, req, res) => {
|
||||
if (err.code === "FST_ERR_CTP_BODY_TOO_LARGE") {
|
||||
res.status(413);
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.send({ success: false, result: "File too large" });
|
||||
} else {
|
||||
res.send(err);
|
||||
}
|
||||
});
|
||||
app.addContentTypeParser("application/octet-stream", {parseAs: "buffer"}, (req, body, done) => {
|
||||
done(null, body);
|
||||
});
|
||||
|
||||
app.options("/:vm/*", async (req, res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "PUT, OPTIONS");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.header("Allow", "PUT, OPTIONS");
|
||||
res.status(204);
|
||||
return;
|
||||
});
|
||||
|
||||
app.put("/:vm/:filename", async (req, res) => {
|
||||
res.header("Content-Type", "application/json");
|
||||
res.header("Access-Control-Allow-Origin", "*")
|
||||
const { vm, filename }: {vm : string, filename : string} = (req.params as any);
|
||||
log("INFO", `${vm}: ${req.ip} is uploading "${filename}"`);
|
||||
if (req.headers['content-type'] !== "application/octet-stream") {
|
||||
res.status(400);
|
||||
return { success: false, result: "Invalid content-type" };
|
||||
}
|
||||
if (parseInt(req.headers["content-length"] as string) > config.MaxFileSize) {
|
||||
res.status(400);
|
||||
return { success: false, result: "File too large" };
|
||||
}
|
||||
if (!VMs.has(vm)) {
|
||||
res.status(400);
|
||||
return { success: false, result: "Invalid VM" };
|
||||
}
|
||||
if (config.DisallowedFilenameChars.some((c) => filename.includes(c))) {
|
||||
res.status(400);
|
||||
return { success: false, result: "Filename contains disallowed characters" };
|
||||
}
|
||||
if (IPs.has(req.ip)) {
|
||||
if (!IPs.get(req.ip)!.Limit()) {
|
||||
res.status(429);
|
||||
return { success: false, result: `Please wait ${IPs.get(req.ip)!.GetRemaining()} seconds before uploading another file` }
|
||||
}
|
||||
} else {
|
||||
IPs.set(req.ip, new RateLimit(config.RateLimit));
|
||||
}
|
||||
var filedata = req.body as Buffer;
|
||||
var hash = md5(filedata);
|
||||
if (config.BlockedMD5.indexOf(hash) !== -1) {
|
||||
res.status(400);
|
||||
log("INFO", `${vm}: ${req.ip} tried to upload "${filename}" with blocked MD5 ${hash}`);
|
||||
return { success: false, result: "That file is not allowed" };
|
||||
}
|
||||
await VMs.get(vm)!.UploadFile(filename, filedata);
|
||||
log("INFO", `${vm}: ${req.ip} uploaded "${filename}" with MD5 ${hash}`);
|
||||
return { success: true, result: "File uploaded" };
|
||||
});
|
||||
|
||||
log("INFO", "Starting HTTP server on port " + config.ListenPort);
|
||||
app.listen({
|
||||
port: config.ListenPort,
|
||||
host: "127.0.0.1"
|
||||
});
|
7
src/log.ts
Normal file
7
src/log.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function log(loglevel : string, ...message : string[]) {
|
||||
console[
|
||||
(loglevel === "ERROR" || loglevel === "FATAL") ? "error" :
|
||||
(loglevel === "WARN") ? "warn" :
|
||||
"log"
|
||||
](`[${new Date().toLocaleString()}] [${loglevel}]`, ...message);
|
||||
}
|
109
tsconfig.json
Normal file
109
tsconfig.json
Normal file
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "es2022", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./build", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
55
web.js
Normal file
55
web.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
var modalel = document.createElement('div');
|
||||
modalel.innerHTML = `
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark text-light">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Upload File</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-success" role="alert" style="display:none;" id="agentSuccessAlert"></div>
|
||||
<div class="alert alert-danger" role="alert" style="display:none;" id="agentErrorAlert"></div>
|
||||
<input type="file" class="form-control" id="agentfile"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="agentUploadBtn">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modalel.classList.add('modal');
|
||||
modalel.tabIndex = -1;
|
||||
document.body.appendChild(modalel);
|
||||
var fileinput = modalel.querySelector('#agentfile');
|
||||
var uploadbtn = modalel.querySelector('#agentUploadBtn');
|
||||
var successalert = modalel.querySelector('#agentSuccessAlert');
|
||||
var erroralert = modalel.querySelector('#agentErrorAlert');
|
||||
uploadbtn.addEventListener('click', async () => {
|
||||
if (fileinput.files.length == 0) return;
|
||||
successalert.style.display = 'none';
|
||||
erroralert.style.display = 'none';
|
||||
var file = fileinput.files[0];
|
||||
var result = await fetch(`https://vmup.elijahr.dev/${window.VMName}/${file.name}`, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
var json = await result.json();
|
||||
if (json.success) {
|
||||
successalert.style.display = 'block';
|
||||
successalert.innerText = json.result;
|
||||
} else {
|
||||
erroralert.style.display = 'block';
|
||||
erroralert.innerText = json.result;
|
||||
}
|
||||
});
|
||||
var modal = new bootstrap.Modal(modalel);
|
||||
var btn = document.createElement('button');
|
||||
btn.innerHTML = '<i class="fa-solid fa-upload"></i> Upload File';
|
||||
btn.classList.add('btn', 'btn-secondary');
|
||||
btn.addEventListener('click', () => {
|
||||
modal.show();
|
||||
});
|
||||
document.getElementById('btns').appendChild(btn);
|
Loading…
Reference in a new issue