summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2020-05-30 19:11:51 +0200
committerRoland Reichwein <mail@reichwein.it>2020-05-30 19:11:51 +0200
commit28e590d5c2d6d3bb38eae7cb1978af233ecb5f64 (patch)
treef5fdebc89d5420f42a12d97b1df52eb0874e3fec
First commit
-rwxr-xr-xMakefile44
-rw-r--r--debian/README.Debian11
-rw-r--r--debian/compat1
-rw-r--r--debian/control15
-rw-r--r--debian/copyright29
-rwxr-xr-xdebian/rules8
-rw-r--r--debian/source/format1
-rw-r--r--debian/webserver.install1
-rw-r--r--debian/webserver.service19
-rw-r--r--downtube.cpp182
-rw-r--r--file.cpp46
-rw-r--r--file.h15
-rwxr-xr-xstart.sh5
13 files changed, 377 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100755
index 0000000..c3a4d5f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,44 @@
+#
+# Makefile
+#
+# Environment: Debian
+#
+
+#CC=gcc-9
+#CXX=g++-9
+CC=clang-10
+CXX=clang++-10
+
+LIBS=-lfcgi
+INCLUDES=-I.
+CFLAGS=-Wall -g -O2 -fPIC
+CPPFLAGS=-Wall -g -O2 -fPIC -std=c++17 -Wpedantic
+HEADERS=file.h
+SOURCES=$(HEADERS:.h=.cpp)
+OBJECTS=$(HEADERS:.h=.o)
+TARGETS=downtube.fcgi
+
+build: $(TARGETS)
+
+all: build
+ ./start.sh
+
+install:
+
+downtube.fcgi: $(OBJECTS)
+
+# link
+%.fcgi: %.o
+ $(CXX) $(CFLAGS) $(LIBS) -o $@ $^
+
+# .cpp -> .o
+%.o: %.cpp
+ $(CXX) $(CPPFLAGS) $(INCLUDES) -c $< -o $@
+
+clean:
+ rm -f *.o *.fcgi
+
+deb:
+ dpkg-buildpackage
+
+.PHONY: clean
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644
index 0000000..35011f3
--- /dev/null
+++ b/debian/README.Debian
@@ -0,0 +1,11 @@
+webserver for Debian
+====================
+
+This package is the Debian version of webserver.
+
+
+Contact
+-------
+
+Reichwein IT <mail@reichwein.it>
+
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..48082f7
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+12
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..62c1a48
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,15 @@
+Source: downtube
+Section: web
+Priority: optional
+Maintainer: Roland Reichwein <mail@reichwein.it>
+Build-Depends: debhelper (>= 12), libboost-all-dev | libboost1.71-all-dev, clang | g++-9, node-uglify, python3-pkg-resources, htmlmin, cleancss
+Standards-Version: 4.5.0
+Homepage: http://www.reichwein.it/downtube/
+
+Package: downtube
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Homepage: http://www.reichwein.it/downtube/
+Description: Web application for video download
+ Downtube can help you download a video specified in a HTML form, transformed
+ to MP3 (audio) or MP4 (video).
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..4b3a1b3
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,29 @@
+This package is Copyright 2020 by Roland Reichwein <mail@reichwein.it>
+
+Both upstream source code and Debian packaging is licensed under the BSD
+license:
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..921ee04
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,8 @@
+#!/usr/bin/make -f
+
+%:
+ dh $@
+
+override_dh_fixperms:
+ dh_fixperms
+ chown www-data:www-data debian/webserver/var/lib/webserver
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/debian/webserver.install b/debian/webserver.install
new file mode 100644
index 0000000..b4997f3
--- /dev/null
+++ b/debian/webserver.install
@@ -0,0 +1 @@
+debian/webserver.conf etc
diff --git a/debian/webserver.service b/debian/webserver.service
new file mode 100644
index 0000000..7834650
--- /dev/null
+++ b/debian/webserver.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Webserver
+After=network.target
+
+[Service]
+Type=simple
+# Restart=always
+ExecStart=/usr/bin/webserver -c /etc/webserver.conf
+
+# Restart once a week, maybe certificates have changed
+Restart=always
+RuntimeMaxSec=604800
+
+# webserver will lower privileges to www-data:www-data
+#User=www-data
+#Group=www-data
+
+[Install]
+WantedBy=multi-user.target
diff --git a/downtube.cpp b/downtube.cpp
new file mode 100644
index 0000000..93f88ba
--- /dev/null
+++ b/downtube.cpp
@@ -0,0 +1,182 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/types.h>
+#include <fcgiapp.h>
+
+#include <functional>
+#include <filesystem>
+#include <regex>
+#include <string>
+#include <unordered_map>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/property_tree/xml_parser.hpp>
+
+#include "file.h"
+
+namespace pt = boost::property_tree;
+using namespace std::string_literals;
+namespace fs = std::filesystem;
+
+namespace {
+
+ class TempDir
+ {
+ private:
+ fs::path m_path;
+ fs::path m_oldpath;
+ public:
+ TempDir()
+ {
+ char templ[] = "/tmp/downtubeXXXXXX";
+ char *temp = mkdtemp(templ);
+ if (temp == nullptr) {
+ throw std::runtime_error("Can't create temporary directory.");
+ }
+
+ m_path = temp;
+ m_oldpath = fs::current_path();
+ fs::current_path(m_path);
+ }
+
+ ~TempDir()
+ {
+ fs::current_path(m_oldpath);
+ fs::remove_all(m_path);
+ }
+
+ const fs::path& getPath() const {return m_path;}
+ };
+
+ std::regex re{"https?://(www\\.youtube\\.com/watch\\?v=|youtu\\.be/)[[:alnum:]-]+", std::regex::extended}; // www.youtube.com/watch?v=ItjMIxS3-rI or https://youtu.be/ItjMIxS3-rI
+
+} // anonymous namespace
+
+int main(void)
+{
+ int result = FCGX_Init();
+ if (result != 0) { // error on init
+ fprintf(stderr, "Error: FCGX_Init()\n");
+ return 1;
+ }
+
+ result = FCGX_IsCGI();
+ if (result) {
+ fprintf(stderr, "Error: No FCGI environment available.\n");
+ return 1;
+ }
+
+ FCGX_Request request;
+ result = FCGX_InitRequest(&request, 0, 0);
+ if (result != 0) {
+ fprintf(stderr, "Error: FCGX_InitRequest()\n");
+ return 1;
+ }
+
+ while (FCGX_Accept_r(&request) >= 0) {
+ try {
+ char* method = FCGX_GetParam("REQUEST_METHOD", request.envp);
+
+ // POST for server actions, changes
+ if (!strcmp(method, "POST")) {
+ size_t contentLength { std::stoul(FCGX_GetParam("CONTENT_LENGTH", request.envp)) };
+ std::string postData(contentLength, '\0'); // contentLength number of bytes, initialize with 0
+ if (FCGX_GetStr(postData.data(), contentLength, request.in) != contentLength) {
+ throw std::runtime_error("Bad data read: Content length mismatch.\r\n");
+ }
+ // postData contains POST data
+ std::string contentType(FCGX_GetParam("CONTENT_TYPE", request.envp));
+
+ std::string xmlData = postData; // default: interpret whole POST data as xml request
+
+ pt::ptree xml;
+ std::istringstream ss{xmlData};
+ pt::xml_parser::read_xml(ss, xml, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace);
+
+ std::string url {xml.get<std::string>("request.url")};
+ std::string format {xml.get<std::string>("request.format")};
+ std::string command {xml.get<std::string>("request.command")};
+
+ //FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out);
+ //FCGX_FPrintF(request.out, "url: %s\r\n", url.c_str());
+ //FCGX_FPrintF(request.out, "format: %s\r\n", format.c_str()); // mp3, mp4
+
+ if (format != "mp3" && format != "mp4") {
+ throw std::runtime_error("Bad format: "s + format);
+ }
+
+ if (!std::regex_match(url, re)) {
+ throw std::runtime_error("Bad URL");
+ }
+
+ //FCGX_FPrintF(request.out, "command: %s\r\n", command.c_str());
+
+ TempDir tempDir;
+
+ //FCGX_FPrintF(request.out, "path: %s\r\n", tempDir.getPath().string().c_str());
+
+ if (command == "getfilename") {
+ std::string cmd{"youtube-dl -o '%(title)s."s + format + "' --get-filename --no-call-home --restrict-filenames "s + url + " > filename.txt"};
+ if (system(cmd.c_str()))
+ throw std::runtime_error("Can't guess filename");
+
+ std::string filename {File::getFile("filename.txt")};
+ boost::algorithm::trim(filename);
+
+ FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out);
+ FCGX_FPrintF(request.out, "%s", filename.c_str());
+ } else if (command == "getfile") {
+ if (format == "mp3") {
+ std::string cmd{"youtube-dl --no-warnings --no-call-home --no-progress -x --audio-format mp3 --embed-thumbnail -o audio.mp3 --restrict-filenames "s + url};
+ system(cmd.c_str()); // Ignore error - "ERROR: Stream #1:0 -> #0:1 (copy)" - seems to be ok
+
+ std::string filedata {File::getFile("audio.mp3")}; // may throw
+
+ FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out);
+ FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size());
+ FCGX_PutStr(filedata.c_str(), filedata.size(), request.out);
+ } else if (format == "mp4") {
+ std::string cmd{"youtube-dl --no-warnings --no-call-home --no-progress --recode-video mp4 -o video.mp4 --restrict-filenames "s + url};
+ system(cmd.c_str()); // Ignore error
+
+ std::string filedata {File::getFile("video.mp4")}; // may throw
+
+ FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out);
+ FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size());
+ FCGX_PutStr(filedata.c_str(), filedata.size(), request.out);
+ } else {
+ throw std::runtime_error("Bad format for unknown reason: "s + format); // should have been caught above already!
+ }
+ } else {
+ throw std::runtime_error("Bad command: "s + command);
+ }
+
+ // Name:
+ // youtube-dl -o "%(title)s.mp3" --get-filename --no-call-home --restrict-filenames $SOURCE > filename.txt
+ //
+ // MP3:
+ // youtube-dl --no-warnings --no-call-home --no-progress -x --audio-format mp3 --embed-thumbnail -o "%(title)s.(ext)s" --restrict-filenames $SOURCE
+ //
+ // MP4:
+ // youtube-dl --no-warnings --no-call-home --no-progress --recode-video mp4 -o "%(title)s.(ext)s" --restrict-filenames $SOURCE
+ } else {
+ throw std::runtime_error("Unsupported method.\r\n");
+ }
+ } catch (const std::runtime_error& ex) {
+ FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out);
+ FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out);
+ FCGX_FPrintF(request.out, "Error: %s\r\n", ex.what());
+ } catch (const std::exception& ex) {
+ FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out);
+ FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out);
+ FCGX_FPrintF(request.out, "Unknown exception: %s\r\n", ex.what());
+ }
+ }
+
+ return 0;
+}
+
diff --git a/file.cpp b/file.cpp
new file mode 100644
index 0000000..47ab8be
--- /dev/null
+++ b/file.cpp
@@ -0,0 +1,46 @@
+#include "file.h"
+
+#include <fstream>
+
+namespace fs = std::filesystem;
+
+using namespace std::string_literals;
+
+std::string File::getFile(const fs::path& filename)
+{
+ std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate);
+
+ if (file.is_open()) {
+ std::ifstream::pos_type fileSize = file.tellg();
+ file.seekg(0, std::ios::beg);
+
+ std::string bytes(fileSize, ' ');
+ file.read(reinterpret_cast<char*>(bytes.data()), fileSize);
+
+ return bytes;
+
+ } else {
+ throw std::runtime_error("Opening "s + filename.string() + " for reading");
+ }
+}
+
+void File::setFile(const fs::path& filename, const std::string& s)
+{
+ File::setFile(filename, s.data(), s.size());
+}
+
+void File::setFile(const fs::path& filename, const char* data, size_t size)
+{
+ std::ofstream file(filename.string(), std::ios::out | std::ios::binary);
+ if (file.is_open()) {
+ file.write(data, size);
+ } else {
+ throw std::runtime_error("Opening "s + filename.string() + " for writing");
+ }
+}
+
+void File::setFile(const fs::path& filename, const std::vector<uint8_t>& data)
+{
+ File::setFile(filename, reinterpret_cast<const char*>(data.data()), data.size());
+}
+
diff --git a/file.h b/file.h
new file mode 100644
index 0000000..e7e4cf6
--- /dev/null
+++ b/file.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cstdint>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace File {
+
+std::string getFile(const std::filesystem::path& filename);
+void setFile(const std::filesystem::path& filename, const std::string& s);
+void setFile(const std::filesystem::path& filename, const char* data, size_t size);
+void setFile(const std::filesystem::path& filename, const std::vector<uint8_t>& data);
+
+}
diff --git a/start.sh b/start.sh
new file mode 100755
index 0000000..4697c31
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Test script for debugging
+#
+spawn-fcgi -a 127.0.0.1 -p 9004 -n -- ./downtube.fcgi