Patched arbitrary file system access vulnerability and visual changes
This commit is contained in:
56
backend.py
56
backend.py
@@ -2,8 +2,10 @@ import threading
|
|||||||
import yt_dlp as ydl
|
import yt_dlp as ydl
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.request import urlopen
|
||||||
|
from urllib.error import URLError
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
@@ -21,9 +23,17 @@ def enqueue_download(url, update=False, ext='mp3'):
|
|||||||
|
|
||||||
|
|
||||||
def process_general(url, ext, update=False):
|
def process_general(url, ext, update=False):
|
||||||
# get current time and put in list to be displayed on /index
|
# get current time
|
||||||
current_time = datetime.now().time()
|
current_time = datetime.now().time()
|
||||||
running_downloads.append([url, str(current_time.hour) + ':' + str(current_time.minute)])
|
|
||||||
|
# parse hour and minute
|
||||||
|
hour = str(current_time.hour)
|
||||||
|
hour = hour if len(hour) > 1 else '0' + hour
|
||||||
|
minute = str(current_time.minute)
|
||||||
|
minute = minute if len(minute) > 1 else '0' + hour
|
||||||
|
|
||||||
|
# add url and time to list of queued downloads
|
||||||
|
queued_downloads.append([url, hour + ':' + minute])
|
||||||
|
|
||||||
# wait for previous thread to finish if not first / only in list
|
# wait for previous thread to finish if not first / only in list
|
||||||
current_thread = threading.current_thread()
|
current_thread = threading.current_thread()
|
||||||
@@ -45,7 +55,7 @@ def process_general(url, ext, update=False):
|
|||||||
process_download(url, ext, parent, query, current_thread)
|
process_download(url, ext, parent, query, current_thread)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
running_downloads.pop(0)
|
queued_downloads.pop(0)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
print('*** IndexError: download could not be removed from list of running downloads. ***')
|
print('*** IndexError: download could not be removed from list of running downloads. ***')
|
||||||
|
|
||||||
@@ -324,7 +334,7 @@ def downloads_path() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# updates zip in or creates new zip of given directory
|
# updates zip in or creates new zip of given directory
|
||||||
def zip_folder(full_rel_path):
|
def zip_folder(full_rel_path) -> tuple[str, str]:
|
||||||
# get playlist name
|
# get playlist name
|
||||||
parent = full_rel_path.split('/')
|
parent = full_rel_path.split('/')
|
||||||
for folder in parent:
|
for folder in parent:
|
||||||
@@ -350,7 +360,7 @@ def zip_folder(full_rel_path):
|
|||||||
# create archive
|
# create archive
|
||||||
zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'w')
|
zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'w')
|
||||||
|
|
||||||
# Open the existing zip file in append mode
|
# add remaining files to zip
|
||||||
with zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'a') as existing_zip:
|
with zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'a') as existing_zip:
|
||||||
file_list = existing_zip.namelist()
|
file_list = existing_zip.namelist()
|
||||||
file_list = [e[len(parent)+1:] for e in file_list]
|
file_list = [e[len(parent)+1:] for e in file_list]
|
||||||
@@ -358,9 +368,31 @@ def zip_folder(full_rel_path):
|
|||||||
for entry in os.scandir(downloads_path() + full_rel_path):
|
for entry in os.scandir(downloads_path() + full_rel_path):
|
||||||
if entry.is_file() and not entry.name.endswith('.zip') and entry.name not in file_list:
|
if entry.is_file() and not entry.name.endswith('.zip') and entry.name not in file_list:
|
||||||
# Add the file to the zip, preserving the directory structure
|
# Add the file to the zip, preserving the directory structure
|
||||||
existing_zip.write(entry.path, arcname=parent + '\\' + entry.name)
|
existing_zip.write(entry.path, arcname=parent + '/' + entry.name)
|
||||||
|
|
||||||
return filename
|
return full_rel_path, filename
|
||||||
|
|
||||||
|
|
||||||
|
def zip_folder_not_in_directory(zip_full_rel_path):
|
||||||
|
video_not_in_directory = query_db_threaded('SELECT path, name, ext FROM video '
|
||||||
|
'INNER JOIN collection ON video.id = collection.video '
|
||||||
|
'WHERE NOT path = playlist')
|
||||||
|
|
||||||
|
# get full path to downloads directory
|
||||||
|
downloads_folder = downloads_path()
|
||||||
|
|
||||||
|
# add remaining files to zip
|
||||||
|
with zipfile.ZipFile(downloads_folder + zip_full_rel_path, 'a') as existing_zip:
|
||||||
|
file_list_zip = existing_zip.namelist()
|
||||||
|
file_list_files = [e.split('/')[-1] for e in file_list_zip]
|
||||||
|
file_list_folder = file_list_zip[0].split('/')[0]
|
||||||
|
|
||||||
|
for video in video_not_in_directory:
|
||||||
|
file_name = video[1] + video[2]
|
||||||
|
if file_name not in file_list_files:
|
||||||
|
existing_zip.write(downloads_folder + video[0] + file_name, arcname=file_list_folder + '/' + file_name)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def directory_contains_zip(full_rel_path):
|
def directory_contains_zip(full_rel_path):
|
||||||
@@ -368,3 +400,11 @@ def directory_contains_zip(full_rel_path):
|
|||||||
if file.name.endswith('.zip'):
|
if file.name.endswith('.zip'):
|
||||||
return file.name
|
return file.name
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def internet_available(target='http://www.youtube.com'):
|
||||||
|
try:
|
||||||
|
urlopen(target)
|
||||||
|
return True
|
||||||
|
except URLError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ ids = []
|
|||||||
titles = []
|
titles = []
|
||||||
urls = []
|
urls = []
|
||||||
thread_queue = []
|
thread_queue = []
|
||||||
running_downloads = []
|
queued_downloads = []
|
||||||
|
|||||||
53
frontend.py
53
frontend.py
@@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
|||||||
from flask import Blueprint, request, render_template, flash, send_from_directory, send_file
|
from flask import Blueprint, request, render_template, flash, send_from_directory, send_file
|
||||||
|
|
||||||
from forms.download import DownloadForm
|
from forms.download import DownloadForm
|
||||||
from backend import zip_folder, downloads_path, enqueue_download
|
from backend import zip_folder, zip_folder_not_in_directory, downloads_path, enqueue_download, internet_available
|
||||||
from db_tools import query_db
|
from db_tools import query_db
|
||||||
from file_cache import *
|
from file_cache import *
|
||||||
|
|
||||||
@@ -14,9 +14,9 @@ frontend = Blueprint('frontend', __name__)
|
|||||||
# index has a list of running downloads
|
# index has a list of running downloads
|
||||||
@frontend.route('/', methods=['GET'])
|
@frontend.route('/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
if not running_downloads:
|
if not queued_downloads:
|
||||||
flash('Currently, no downloads are running.', 'primary')
|
flash('Currently, no downloads are running.', 'primary')
|
||||||
return render_template('index.html', running_downloads=running_downloads, titles=titles, urls=urls, amount=len(urls))
|
return render_template('index.html', running_downloads=queued_downloads, titles=titles, urls=urls, amount=len(urls))
|
||||||
|
|
||||||
|
|
||||||
@frontend.route('/downloader', methods=['GET', 'POST'])
|
@frontend.route('/downloader', methods=['GET', 'POST'])
|
||||||
@@ -28,7 +28,7 @@ def downloader():
|
|||||||
url = str(form.url.data)
|
url = str(form.url.data)
|
||||||
ext = str(form.ext.data)
|
ext = str(form.ext.data)
|
||||||
|
|
||||||
# check if valid link
|
# check if high likelihood of being a valid link
|
||||||
# this tool should technically work with other platforms but that is not tested
|
# this tool should technically work with other platforms but that is not tested
|
||||||
# since KeyErrors are to be expected in backend.process_download(url), it's blocked here
|
# since KeyErrors are to be expected in backend.process_download(url), it's blocked here
|
||||||
# you are invited to test and adjust the code for other platforms
|
# you are invited to test and adjust the code for other platforms
|
||||||
@@ -39,30 +39,44 @@ def downloader():
|
|||||||
valid_link = True if url == 'None' else False # if url is empty, don't show error
|
valid_link = True if url == 'None' else False # if url is empty, don't show error
|
||||||
return render_template('downloader.html', form=form, ytLink=valid_link, amount=len(urls))
|
return render_template('downloader.html', form=form, ytLink=valid_link, amount=len(urls))
|
||||||
|
|
||||||
|
if not internet_available():
|
||||||
|
flash('No internet connection available.', 'danger')
|
||||||
|
return render_template('flash-message.html')
|
||||||
|
|
||||||
# kick off download process
|
# kick off download process
|
||||||
enqueue_download(url, ext=ext)
|
enqueue_download(url, ext=ext)
|
||||||
|
|
||||||
# show download start confirmation
|
# show download start confirmation
|
||||||
flash('Download enqueued and will finish in background.', 'primary')
|
flash('Download enqueued and will finish in background.', 'primary')
|
||||||
return render_template('feedback-simple.html', amount=len(urls))
|
return render_template('flash-message.html')
|
||||||
|
|
||||||
|
|
||||||
# downloads a single file
|
# downloads a single file
|
||||||
@frontend.route('/download/<path:file_path>', methods=['GET'])
|
@frontend.route('/download/<path:file_path>', methods=['GET'])
|
||||||
def download(file_path):
|
def download(file_path: str):
|
||||||
# if the path does not end with a slash, a single file is requested
|
# if the path does not end with a slash, a single file is requested
|
||||||
if '.' in file_path:
|
if '.' in file_path:
|
||||||
|
file_folder = ''.join([x if x not in file_path.split('/')[-1] else '' for x in file_path.split('/')])
|
||||||
|
|
||||||
|
video = query_db('SELECT path, name, ext FROM video WHERE name = :name AND path = :path',
|
||||||
|
{'name': file_path.split('/')[-1].split('.')[0], 'path': file_folder + '\\' if file_folder else ''},
|
||||||
|
True)
|
||||||
|
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
downloads_path(),
|
downloads_path() + video['path'],
|
||||||
file_path
|
video['name'] + video['ext']
|
||||||
)
|
)
|
||||||
|
|
||||||
# else a directory is requested
|
# else a directory is requested
|
||||||
else:
|
else:
|
||||||
|
zip_path, zip_name = zip_folder(file_path)
|
||||||
|
print(zip_path, zip_name)
|
||||||
|
zip_folder_not_in_directory(zip_path + zip_name)
|
||||||
|
|
||||||
# zip and send
|
# zip and send
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
downloads_path(),
|
downloads_path() + zip_path,
|
||||||
file_path + zip_folder(file_path)
|
zip_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +90,7 @@ def update(url_rowid):
|
|||||||
|
|
||||||
# show download start confirmation
|
# show download start confirmation
|
||||||
flash('Update enqueued and will finish in background.', 'primary')
|
flash('Update enqueued and will finish in background.', 'primary')
|
||||||
return render_template('feedback-simple.html', titles=titles, urls=urls, amount=len(urls))
|
return render_template('flash-message.html', titles=titles, urls=urls, amount=len(urls))
|
||||||
|
|
||||||
|
|
||||||
@frontend.route('/library', methods=['GET'])
|
@frontend.route('/library', methods=['GET'])
|
||||||
@@ -93,11 +107,20 @@ def library():
|
|||||||
@frontend.route('/library-playlist', methods=['GET'])
|
@frontend.route('/library-playlist', methods=['GET'])
|
||||||
def library_playlist():
|
def library_playlist():
|
||||||
playlist = request.args.get('playlist', None)
|
playlist = request.args.get('playlist', None)
|
||||||
videos = query_db('SELECT video.name, video.ext, video.path FROM video LEFT JOIN collection ON video.id = '
|
videos = query_db('SELECT video.name, video.ext, video.path FROM video '
|
||||||
'collection.video LEFT JOIN playlist ON collection.playlist=playlist.folder WHERE '
|
'LEFT JOIN collection ON video.id = collection.video '
|
||||||
'playlist.ROWID = :playlist',
|
'LEFT JOIN playlist ON collection.playlist=playlist.folder '
|
||||||
|
'WHERE playlist.ROWID = :playlist',
|
||||||
{'playlist': playlist})
|
{'playlist': playlist})
|
||||||
return render_template('collection.html', videos=videos)
|
|
||||||
|
# get playlist path since could be empty in some entries
|
||||||
|
folder = ''
|
||||||
|
for video in videos:
|
||||||
|
if len(video['path']) > 0:
|
||||||
|
folder = video['path']
|
||||||
|
break
|
||||||
|
|
||||||
|
return render_template('collection.html', videos=videos, folder=folder)
|
||||||
|
|
||||||
|
|
||||||
@frontend.route('/player', methods=['GET'])
|
@frontend.route('/player', methods=['GET'])
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" style="padding: 1.5%">
|
<div class="container" style="padding: 1.5%">
|
||||||
<form action="/download/{{ videos[0]['path'] }}">
|
<form action="/download/{{ folder }}">
|
||||||
<input type="submit" class="btn btn-primary float-end" value="Download all"/>
|
<input type="submit" class="btn btn-primary float-end" value="Download all"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,13 +12,14 @@
|
|||||||
{{ form.url.label(class_="form-label") }}
|
{{ form.url.label(class_="form-label") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="width: fit-content">
|
<div class="row" style="width: fit-content">
|
||||||
<div class="col">
|
{{ form.url(class_="form-control") }}
|
||||||
{{ form.url(class_="form-control") }}
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row" style="width: fit-content">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
{{ form.ext(class_="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-md-auto">
|
||||||
{{ form.ext(class_="form-select") }}
|
|
||||||
</div>
|
|
||||||
<div class="col" style="width: fit-content">
|
|
||||||
{{ form.submit(class_="btn btn-primary") }}
|
{{ form.submit(class_="btn btn-primary") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{%- extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{{ super() }}
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
{% if titles %}
|
|
||||||
<div class="container">
|
|
||||||
<table id="videos" class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="text-center">New Title(s)</th>
|
|
||||||
<th scope="col" class="text-center">URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for i in range(amount) %}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center">{{ titles[i] }}</td>
|
|
||||||
<td class="text-center"> <a href="{{ urls[i] }}" target="_blank">Link</a></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<form target="/start-download"
|
|
||||||
<input type="submit"
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
|
||||||
1
templates/flash-message.html
Normal file
1
templates/flash-message.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{%- extends "base.html" %}
|
||||||
@@ -6,28 +6,22 @@
|
|||||||
{% if running_downloads %}
|
{% if running_downloads %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="list-group list-group-flush">
|
<table class="table">
|
||||||
<li class="list-group-item">
|
<thead>
|
||||||
<div class="row">
|
<tr>
|
||||||
<table id="videos" class="table">
|
<th scope="col" class="text-center">Queue</th>
|
||||||
<thead>
|
<th scope="col" class="text-center">Started at</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th scope="col" class="text-center">Queue</th>
|
</thead>
|
||||||
<th scope="col" class="text-center">Started at</th>
|
<tbody>
|
||||||
</tr>
|
{% for entry in running_downloads %}
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<td class="text-center"><a href="{{ entry[0] }}" target="_blank">{{ entry[0] }}</a></td>
|
||||||
{% for entry in running_downloads %}
|
<td class="text-center">{{ entry[1] }}</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="text-center"><a href="{{ entry[0] }}" target="_blank">{{ entry[0] }}</a></td>
|
{% endfor %}
|
||||||
<td class="text-center">{{ entry[1] }}</td>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,26 +29,22 @@
|
|||||||
|
|
||||||
{% if titles %}
|
{% if titles %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<ul class="list-group list-group-flush">
|
<div class="card-body">
|
||||||
<li class="list-group-item">
|
<table class="table">
|
||||||
<div class="row">
|
<thead>
|
||||||
<table id="videos" class="table">
|
<tr>
|
||||||
<thead>
|
<th scope="col" class="text-center">Currently processing</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th scope="col" class="text-center">Currently processing</th>
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
{% for i in range(amount) %}
|
||||||
<tbody>
|
<tr>
|
||||||
{% for i in range(amount) %}
|
<td class="text-center"><a href="{{ urls[i] }}" target="_blank">{{ titles[i] }}</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="text-center"><a href="{{ urls[i] }}" target="_blank">{{ titles[i] }}</a></td>
|
{% endfor %}
|
||||||
</tr>
|
</tbody>
|
||||||
{% endfor %}
|
</table>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user