mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Need to revisit this later
This commit is contained in:
parent
dbe8b02d09
commit
752496bfe1
@ -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);
|
||||
}
|
||||
});
|
@ -45,7 +45,6 @@ class FilesystemAPIService extends BaseService {
|
||||
|
||||
// misc
|
||||
app.use(require('../routers/df'))
|
||||
app.use(require('../routers/download'))
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user