Need to revisit this later

This commit is contained in:
KernelDeimos 2024-03-30 21:52:24 -04:00
parent dbe8b02d09
commit 752496bfe1
2 changed files with 0 additions and 274 deletions

View File

@ -1,273 +0,0 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
"use strict"
const express = require('express');
const router = express.Router();
const config = require('../config');
const axios = require('axios');
const mime = require('mime-types')
const path = require('path')
const https = require('https');
const {URL} = require('url');
const _path = require('path');
const { NodePathSelector } = require('../filesystem/node/selectors.js');
const eggspress = require('../api/eggspress.js');
const { Context } = require('../util/context');
const { HLWrite } = require('../filesystem/hl_operations/hl_write');
const FSNodeParam = require('../api/filesystem/FSNodeParam');
// TODO: eggspressify
// todo this could be abused to send get requests to any url and cause a denial of service
//
// -----------------------------------------------------------------------//
// POST /download
// -----------------------------------------------------------------------//
module.exports = eggspress('/download', {
subdomain: 'api',
verified: true,
auth: true,
fs: true,
json: true,
allowedMethods: ['POST'],
parameters: {
fsNode: new FSNodeParam('path'),
}
}, async (req, res, next) => {
const log = req.services.get('log-service').create('api:download');
// check subdomain
if(require('../helpers').subdomain(req) !== 'api')
next();
// socketio
let socketio = require('../socketio.js').getio();
// check if user is verified
if((config.strict_email_verification_required || req.user.requires_email_confirmation) && !req.user.email_confirmed)
return res.status(400).send({code: 'account_is_not_verified', message: 'Account is not verified'});
// modules
const {cp, id2path, suggest_app_for_fsentry, validate_signature_auth, is_shared_with_anyone, uuid2fsentry} = require('../helpers')
if(!req.body.url)
return res.status(400).send({code: 'url_is_required', message: 'URL is required'});
// if url doesn't have a protocol, add http://
if(!req.body.url.startsWith('http://') && !req.body.url.startsWith('https://')){
req.body.url = 'http://' + req.body.url;
}
// Ensure url is not "localhost" or a private IP range
{
const url_obj = new URL(req.body.url);
if ( url_obj.hostname === 'localhost' ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
// GitHub Copilot generated most of these
if ( url_obj.hostname.match(/^10\./) ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
if ( url_obj.hostname.match(/^192\.168\./) ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
if ( url_obj.hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
// github 127
if ( url_obj.hostname.match(/^127\./) ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
// and 100 range for tailscale
if ( url_obj.hostname.match(/^100\./) ) {
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
}
// check if `url` is a valid URL and then parse it
let url_obj;
try{
url_obj = new URL(req.body.url);
}catch(e){
return res.status(400).send({code: 'invalid_url', message: 'Invalid URL'});
}
//--------------------------------------------------
// Puter file
//--------------------------------------------------
if(url_obj.origin === config.api_base_url && url_obj.searchParams && url_obj.searchParams.get('uid') && url_obj.searchParams.get('expires') && url_obj.searchParams.get('signature')){
// authenticate
// validate URL signature
try{
validate_signature_auth(req.body.url, 'read');
}
catch(e){
console.log(e)
return res.status(403).send(e);
}
// get file
const source_item = await uuid2fsentry(url_obj.searchParams.get('uid'));
log.info('source_item', { value: source_item });
const source_path = await id2path(source_item.id);
let new_fsentries
try{
new_fsentries = await cp(source_path, req.body.path, req.user, false, true, false);
}catch(e){
return res.status(400).send(e)
}
// is_shared, dirpath, original_client_socket_id, suggested_apps,...
if(new_fsentries.length > 0){
for (let i = 0; i < new_fsentries.length; i++) {
let fse = await uuid2fsentry(new_fsentries[i].uid);
new_fsentries[i].is_shared = await is_shared_with_anyone(fse.id);
new_fsentries[i].dirpath = _path.dirname(new_fsentries[i].path);
new_fsentries[i].original_client_socket_id = req.body.original_client_socket_id;
new_fsentries[i].suggested_apps = await suggest_app_for_fsentry(fse, {user: req.user});
// associated_app
if(new_fsentries[i].associated_app_id){
const app = await get_app({id: new_fsentries[i].associated_app_id})
// remove some privileged information
delete app.id;
delete app.approved_for_listing;
delete app.approved_for_opening_items;
delete app.godmode;
delete app.owner_user_id;
// add to array
new_fsentries[i].associated_app = app;
}else{
new_fsentries[i].associated_app = {};
}
// send realtime msg to client
if(socketio){
socketio.to(req.user.id).emit('item.added', new_fsentries[i])
}
}
}
return res.status(200).send(new_fsentries);
}
//--------------------------------------------------
// non-Puter file
//--------------------------------------------------
// disable axios ssl verification when in dev env and the url is from the local Puter API
let axios_instance;
if(config.env === 'dev' && url_obj.origin === config.api_base_url){
axios_instance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
}else{
axios_instance = axios;
}
// todo if there is a way to get the file size without downloading the whole file, do that and then check if user has enough space
// get file
let file;
try{
// old implementation using buffer
file = await axios_instance.get(req.body.url, {responseType: 'arraybuffer', onDownloadProgress: (progressEvent) => {
if(req.body.socket_id){
socketio.to(req.body.socket_id).emit('download.progress', {
loaded: progressEvent.loaded,
total: progressEvent.total * 2,
operation_id: req.body.operation_id,
item_upload_id: req.body.item_upload_id,
original_client_socket_id: req.body.original_client_socket_id,
});
}
}})
}catch(error){
console.log(error)
return res.status(500).send({message: error.message});
}
file.buffer = file.data;
// get file type
await (async () => {
const { fileTypeFromBuffer } = await import('file-type');
const type = await fileTypeFromBuffer(file.buffer);
// set file type
if(type)
file.type = type.mime;
else
file.type = 'application/octet-stream';
})();
// get file name
let filename;
// extract file name from url
if(!req.body.name){
filename = req.body.name ?? req.body.url.split('/').pop().split('#')[0].split('?')[0];
// extension?
if(!path.extname(filename)){
// get extension from mime type
const ext = mime.extension(file.type);
// add extension to filename
if(ext)
filename += '.' + ext;
}
}else
filename = req.body.name;
// Setup metadata in context
{
const x = Context.get();
const operationTraceSvc = x.get('services').get('operationTrace');
const frame = (await operationTraceSvc.add_frame('api:/download'))
.attr('gui_metadata', {
original_client_socket_id: req.body.original_client_socket_id,
socket_id: req.body.socket_id,
operation_id: req.body.operation_id,
item_upload_id: req.body.item_upload_id,
user_id: req.user.id,
})
;
x.set(operationTraceSvc.ckey('frame'), frame);
}
// write file
try{
const dirNode = req.values.fsNode;
const hl_write = new HLWrite();
const response = await hl_write.run({
destination_or_parent: dirNode,
specified_name: filename,
overwrite: false,
dedupe_name: req.body.dedupe_name,
user: req.user,
file: file,
});
return res.send(response);
}catch(error){
console.log(error)
return res.contentType('application/json').status(500).send(error);
}
});

View File

@ -45,7 +45,6 @@ class FilesystemAPIService extends BaseService {
// misc
app.use(require('../routers/df'))
app.use(require('../routers/download'))
}
}