// Copyright (c) 2024 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "WebServerConfig.hpp" #include "PresetBundle.hpp" #include "WebServer.hpp" #include #include #include "PiPedalConfiguration.hpp" #include "PiPedalModel.hpp" #include "Banks.hpp" #include "Ipv6Helpers.hpp" #include #include "ZipFile.hpp" #include "PiPedalUI.hpp" #include "ofstream_synced.hpp" #include "UpdaterSecurity.hpp" #include "TemporaryFile.hpp" #include "PresetBundle.hpp" #include "json.hpp" #include "HotspotManager.hpp" #include "MimeTypes.hpp" #include "AudioFileMetadataReader.hpp" #include "AudioFiles.hpp" #include "util.hpp" #include "HtmlHelper.hpp" #include "WebServerMod.hpp" #define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" #define OLD_BANK_EXTENSION ".piBank" #define BANK_EXTENSION ".piBank" #define PLUGIN_PRESETS_EXTENSION ".piPluginPresets" static const std::filesystem::path WEB_TEMP_DIR{ "/var/pipedal/web_temp" }; static const std::string PLUGIN_PRESETS_MIME_TYPE = "application/vnd.pipedal.pluginPresets"; static const std::string PRESET_MIME_TYPE = "application/vnd.pipedal.preset"; static const std::string BANK_MIME_TYPE = "application/vnd.pipedal.bank"; static const std::string CACHE_CONTROL_INDEFINITELY = "max-age=31536000,public,immutable"; // 1 year static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes using namespace pipedal; using namespace boost::system; namespace fs = std::filesystem; class UserUploadResponse { public: std::string errorMessage_; std::string path_; DECLARE_JSON_MAP(UserUploadResponse); }; JSON_MAP_BEGIN(UserUploadResponse) JSON_MAP_REFERENCE(UserUploadResponse, errorMessage) JSON_MAP_REFERENCE(UserUploadResponse, path) JSON_MAP_END() static std::string GetMimeType(const std::filesystem::path& path) { std::string extension = path.extension(); const MimeTypes& mimeTypes = MimeTypes::instance(); auto result = mimeTypes.MimeTypeFromExtension(extension); return result; } static bool IsSafeMediaPath(const fs::path path) { if (!HtmlHelper::IsSafeFileName(path)) { return false; } if (!(path.string().starts_with("/var/pipedal/audio_uploads/"))) { return false; } return true; } static bool IsSafeThumbnailPath(const fs::path path) { if (!HtmlHelper::IsSafeFileName(path)) { return false; } if ((!path.string().starts_with("/var/pipedal/tone3000_thumbnails/")) && (!path.string().starts_with("/var/pipedal/audio_uploads/tone3000_thumbnails/")) ) { return false; } auto extension = path.extension(); if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" && extension != ".webm") { return false; } return true; } int32_t ConvertThumbnailSize(const std::string& param) { if (param.empty()) { return 0; } return static_cast(std::stoi(param)); } static bool IsZipFile(const std::filesystem::path& path) { std::ifstream f(path); if (!f.is_open()) return false; char c[4]; memset(c, 0, sizeof(c)); f >> c[0] >> c[1] >> c[2] >> c[3]; // official file header according to PKware documetnation. return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04; } class ExtensionChecker { public: ExtensionChecker(const std::string& extensionList) : extensions(split(extensionList, ',')) { } bool IsValidExtension(const std::string& extension) { if (extensions.size() == 0) return true; for (const auto& ext : extensions) { if (ext == extension) return true; } return false; } private: std::vector extensions; }; static fs::path GetTone3000ThumbnailFile(const fs::path& thumbnailDirectory, const std::string& id) { std::string stem = id; // find a file in thumbnailDirectory which has a filename of {stem}.*. if (!fs::exists(thumbnailDirectory) || !fs::is_directory(thumbnailDirectory)) { return {}; } for (const auto& entry : fs::directory_iterator(thumbnailDirectory)) { if (!entry.is_regular_file()) { continue; } const auto& path = entry.path(); if (path.stem() == stem) { return path; } } return {}; } namespace pipedal::implementation { void UpgradeBank(PiPedalModel& model, const std::filesystem::path& existingBankFilePath, const std::filesystem::path& piBankFilePath, const std::vector& presetsToUpgrade ) { BankFile sourceBankFile; if (piBankFilePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(piBankFilePath)) { auto presetReader = PresetBundleReader::LoadPresetsFile(model, piBankFilePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPresetJson()); json_reader reader(ss); reader.read(&sourceBankFile); } else { std::ifstream f{ piBankFilePath }; if (!f.is_open()) { throw std::runtime_error(SS("Unable to open file " << piBankFilePath)); } // legacy json format, no zip, no media files. json_reader reader(f); reader.read(&sourceBankFile); } BankFile existingBankFile; { std::ifstream ifs{ existingBankFilePath }; if (!ifs.is_open()) { return; } json_reader reader{ ifs }; reader.read(&existingBankFile); } for (const std::string& presetToUpgrade : presetsToUpgrade) { BankFileEntry* existingEntry = existingBankFile.getPresetByName(presetToUpgrade); if (existingEntry) { BankFileEntry* newEntry = sourceBankFile.getPresetByName(presetToUpgrade); if (newEntry) { existingEntry->preset(newEntry->preset()); } } } { std::ofstream ofs{ existingBankFilePath }; json_writer writer{ ofs }; writer.write(existingBankFile); } } int64_t ImportBankFile(PiPedalModel& model, const std::filesystem::path& filePath, uint64_t uploadAfter = -1) { BankFile bankFile; if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) { auto presetReader = PresetBundleReader::LoadPresetsFile(model, filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPresetJson()); json_reader reader(ss); reader.read(&bankFile); } else { std::ifstream f{ filePath }; if (!f.is_open()) { throw std::runtime_error(SS("Unable to open file " << filePath)); } // legacy json format, no zip, no media files. json_reader reader(f); reader.read(&bankFile); } uint64_t instanceId = model.UploadBank(bankFile, uploadAfter); return instanceId; } } using namespace pipedal::implementation; class DownloadIntercept : public RequestHandler { PiPedalModel* model; public: DownloadIntercept(PiPedalModel* model) : RequestHandler("/var"), model(model) { } virtual bool wants(const std::string& method, const uri& request_uri) const { if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var") { return false; } std::string segment = request_uri.segment(1); if (segment == "t3k_uploadAsset") { return true; } if (segment == "t3k_response.html") { return true; } if (segment == "tone3000_thumbnail") { return true; } if (segment == "downloadMediaFile") { return true; } if (segment == "displayMediaFile") { return true; } if (segment == "uploadPluginPresets") { return true; } if (segment == "downloadPluginPresets") { return true; } if (segment == "downloadPreset") { std::string strInstanceId = request_uri.query("id"); if (strInstanceId != "") return true; } else if (segment == "uploadPreset") { return true; } if (segment == "downloadBank") { std::string strInstanceId = request_uri.query("id"); if (strInstanceId != "") return true; } else if (segment == "uploadBank") { return true; } else if (segment == "uploadUserFile") { return true; } else if (segment == "AudioMetadata") { return true; } else if (segment == "Thumbnail") { return true; } else if (segment == "PluginPresets") { return true; } else if (segment == "PluginPreset") { return true; } else if (segment == "PluginBanks") { return true; } else if (segment == "PluginBank") { return true; } else if (segment == "GetPluginInfo") { return true; } else if (segment == "GetPluginPresets") { return true; } else if (segment == "GetPreset") { return true; } else if (segment == "GetBank") { return true; } else if (segment == "NextAudioFile") { return true; } else if (segment == "PreviousAudioFile") { return true; } return false; } static std::string GetContentDispositionHeader(const std::string& name, const std::string& extension) { std::string fileName = name.substr(0, 64) + extension; std::stringstream s; s << "attachment; filename*=" << HtmlHelper::Rfc5987EncodeFileName(fileName) << "; filename=\"" << HtmlHelper::SafeFileName(fileName) << "\""; std::string result = s.str(); return result; } void GetPluginPresets(const uri& request_uri, std::string* pName, std::string* pContent) { std::string pluginUri = request_uri.query("id"); auto plugin = model->GetPluginHost().GetPluginInfo(pluginUri); *pName = plugin->name(); PluginPresets pluginPresets = model->GetPluginPresets(pluginUri); std::stringstream s; json_writer writer(s, false); writer.write(pluginPresets); *pContent = s.str(); } void GetPreset(const uri& request_uri, std::string* pName, std::string* pContent) { std::string strInstanceId = request_uri.query("id"); int64_t instanceId = std::stol(strInstanceId); auto pedalboard = model->GetPreset(instanceId); // a certain elegance to using same file format for banks and presets. BankFile file; file.name(pedalboard.name()); int64_t newInstanceId = file.addPreset(pedalboard); file.selectedPreset(newInstanceId); std::stringstream s; json_writer writer(s, false); writer.write(file); *pContent = s.str(); *pName = pedalboard.name(); } void GetBank(const uri& request_uri, std::string* pName, std::string* pContent) { std::string strInstanceId = request_uri.query("id"); int64_t instanceId = std::stol(strInstanceId); BankFile bank; model->GetBank(instanceId, &bank); std::stringstream s; json_writer writer(s, true); // do what we can to reduce the file size. writer.write(bank); *pContent = s.str(); *pName = bank.name(); } virtual void head_response( const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { try { std::string segment = request_uri.segment(1); if (segment == "t3k_uploadAsset") { res.set(HttpField::content_length, "0"); return; } if (segment == "t3k_response.html") { std::string mimeType = GetMimeType("x.html"); auto text = T3kResponse(); res.set(HttpField::content_type, mimeType); res.set(HttpField::content_length, SS(text.size())); return; } else if (segment == "tone3000_thumbnail") { std::string id = request_uri.query("id"); if (id == "") { throw PiPedalException("Invalid request."); } fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); if (!fs::exists(path)) { path = GetTone3000ThumbnailFile(this->model->OldTone3000ThumbnailDirectory(), id); if (!fs::exists(path)) { throw PiPedalException("File not found."); } } auto mimeType = GetMimeType(path); if (mimeType.empty()) { throw PiPedalException("Can't download files of this type."); } if (!mimeType.starts_with("image/")) { throw PiPedalException("Can't download files of this type."); } res.set(HttpField::content_type, mimeType); size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); return; } if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto mimeType = GetMimeType(path); if (mimeType.empty()) { if (isInfoFile(path)) { mimeType = "text/plain"; } else { throw PiPedalException("Can't download files of this type."); } } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); if (segment == "downloadMediaFile") { std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); res.set(HttpField::content_disposition, disposition); } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); return; } if (segment == "downloadPluginPresets") { std::string name; std::string content; GetPluginPresets(request_uri, &name, &content); TemporaryFile tmpFile{ WEB_TEMP_DIR }; PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(content.length()); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION)); return; } if (segment == "downloadPreset") { std::string name; std::string content; GetPreset(request_uri, &name, &content); TemporaryFile tmpFile{ WEB_TEMP_DIR }; PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); return; } if (segment == "downloadBank") { std::string name; std::string content; GetBank(request_uri, &name, &content); TemporaryFile tmpFile{ WEB_TEMP_DIR }; PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); res.setContentLength(contentLength); return; } throw PiPedalException("Not found."); } catch (const std::exception& e) { if (strcmp(e.what(), "Not found") == 0) { ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory); } else { ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument); } } } static void setLastModifiedFromFile(HttpResponse& res, const std::filesystem::path& path) { auto lastModified = std::filesystem::last_write_time(path); res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified)); } AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path& path) { return AudioDirectoryInfo::Create(path, GetShadowIndexDirectory( this->model->GetPluginUploadDirectory(), path)); } bool isInfoFile(const fs::path& path) { auto extension = path.extension(); if (extension == ".pdf") { return true; } auto filename = path.filename(); if (filename == "LICENSE.txt" || filename == "README.txt") { return true; } if (filename == "LICENSE.md" || filename == "README.md") { return true; } if (filename == "license.txt" || filename == "readme.txt") { return true; } return false; } std::string T3kResponse() { return "\n" "\n" "\n" "\n" "\n" "\n" "\n"; } void DownloadTone3000Tone(const uri& requestUri) { } virtual void get_response( const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { try { std::string segment = request_uri.segment(1); if (segment == "t3k_response.html") { throw std::runtime_error("Not implemented."); } if (segment == "tone3000_thumbnail") { std::string id = request_uri.query("id"); fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); if (!fs::exists(path)) { path = GetTone3000ThumbnailFile(this->model->OldTone3000ThumbnailDirectory(), id); if (!fs::exists(path)) { throw PiPedalException("File not found."); } } auto mimeType = GetMimeType(path); if (mimeType.empty()) { throw PiPedalException("Can't download files of this type."); } res.set(HttpField::content_type, mimeType); size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); res.setBodyFile(path, false); return; } else if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto mimeType = GetMimeType(path); if (mimeType.empty()) { if (isInfoFile(path)) { mimeType = "text/plain"; } else { throw PiPedalException("Can't download files of this type."); } } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); if (segment == "downloadMediaFile") { std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); res.set(HttpField::content_disposition, disposition); } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); res.setBodyFile(path, false); return; } else if (segment == "downloadPluginPresets") { std::string name; std::string content; GetPluginPresets(request_uri, &name, &content); std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION)); res.setBodyFile(tmpFile); } else if (segment == "downloadPreset") { std::string name; std::string content; GetPreset(request_uri, &name, &content); std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); res.setBodyFile(tmpFile); } else if (segment == "downloadBank") { std::string name; std::string content; GetBank(request_uri, &name, &content); std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); res.setBodyFile(tmpFile); } else if (segment == "AudioMetadata") { res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); fs::path path = model->GetStorage().FromAbstractPathString(request_uri.query("path")); if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path()); try { auto files = audioDirectory->GetFiles(); for (const auto& file : files) { std::string fileNameOnly = path.filename(); if (file.fileName() == fileNameOnly) { std::stringstream ss; json_writer writer(ss); writer.write(file); res.setBody(ss.str()); return; } } } catch (const std::exception& e) { Lv2Log::error("Error getting audio directory info: %s - (%s)", path.c_str(), e.what()); throw e; } // If we get here, the file was not found in the directory. throw PiPedalException("File not found in directory."); } else if (segment == "Thumbnail") { ThumbnailTemporaryFile thumbnailTemporaryFile; try { std::shared_ptr thumbnail; try { fs::path path = request_uri.query("path"); if (path.empty() || !fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { std::shared_ptr thumbnail; // path for folder thumbnails. path = request_uri.query("ffile"); if (!path.empty() && fs::exists(path) && this->model->IsInUploadsDirectory(path) && !HasDotDot(path)) { // path is a folder. thumbnail = std::make_shared(); thumbnail->SetNonDeletedPath(path); } else { auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile(); thumbnail = std::make_shared(); thumbnail->SetNonDeletedPath(t.Path()); } res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension())); res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified. setLastModifiedFromFile(res, thumbnail->Path()); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path()))); res.setBodyFile(thumbnail); return; } int32_t width = ConvertThumbnailSize(request_uri.query("w")); int32_t height = ConvertThumbnailSize(request_uri.query("h")); AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo( path.parent_path()); audioDirectory->GetFiles(); // ensure that the .index file is up to date. thumbnailTemporaryFile = audioDirectory->GetThumbnail(path.filename(), width, height); } catch (const std::exception& e) { fs::path defaultThumbnail = model->GetWebRoot() / "img/missing_thumbnail.jpg"; thumbnailTemporaryFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg"); } res.set(HttpField::content_type, thumbnailTemporaryFile.GetMimeType()); res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp. setLastModifiedFromFile(res, thumbnailTemporaryFile.Path()); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path()))); std::filesystem::path t = thumbnailTemporaryFile.Path(); res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile()); thumbnailTemporaryFile.Detach(); } catch (const std::exception& e) { Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what()); throw e; } } else if (segment == "NextAudioFile") { res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); fs::path path = request_uri.query("path"); try { if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto directoryPath = path.parent_path(); auto directoryInfo = CreateDirectoryInfo(directoryPath); std::string result = directoryInfo->GetNextAudioFile(path.filename()); if (!result.empty()) { result = (directoryPath / result).string(); } // json-encode the result. std::stringstream ss; json_writer writer(ss); writer.write(result); res.setBody(ss.str()); } catch (const std::exception& e) { res.setBody("\"\""); } } else if (segment == "PreviousAudioFile") { res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); fs::path path = request_uri.query("path"); try { if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { throw PiPedalException("File not found."); } auto directoryPath = path.parent_path(); auto directoryInfo = CreateDirectoryInfo(directoryPath); std::string result = directoryInfo->GetPreviousAudioFile(path.filename()); if (!result.empty()) { result = (directoryPath / result).string(); } // json-encode the result. std::stringstream ss; json_writer writer(ss); writer.write(result); res.setBody(ss.str()); } catch (const std::exception& e) { res.setBody("\"\""); } } else { throw PiPedalException("Not found"); } } catch (const std::exception& e) { if (strcmp(e.what(), "Not found") == 0) { ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory); } else { ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument); } } } static std::string GetFirstFolderOrFile(const std::vector& fileNames) { for (const auto& fileName : fileNames) { size_t nPos = fileName.find('/'); if (nPos != std::string::npos) { return fileName.substr(0, nPos); } } if (fileNames.size() == 0) { return 0; } else { return fileNames[0]; } } static bool HasSingleRootDirectory(ZipFileReader& zipFile) { bool hasDirectory = false; std::string previousDirectory; for (auto& file : zipFile.GetFiles()) { auto pos = file.find('/'); if (pos == std::string::npos) { // a file in the root. return false; } else { std::string currentRootDirectory = file.substr(0, pos); if (hasDirectory) { if (currentRootDirectory != previousDirectory) { return false; } } else { hasDirectory = true; previousDirectory = currentRootDirectory; } } } return true; } void UploadEmbeddedZipFile( PiPedalModel* model, pipedal::zip_file_input_stream& si, const std::string& inputFileName, fs::path directory, ExtensionChecker& extensionChecker, int64_t instanceId, const std::string& patchProperty) { TemporaryFile tempFile{ WEB_TEMP_DIR }; // Extract the zip_file_input_stream to the temporary file. { std::ofstream out(tempFile.Path(), std::ios::binary); if (!out) { throw std::runtime_error("Failed to open temporary file for writing."); } constexpr std::size_t bufferSize = 16384; char buffer[bufferSize]; while (si) { si.read(buffer, bufferSize); std::streamsize bytesRead = si.gcount(); if (bytesRead > 0) { out.write(buffer, bytesRead); } } out.close(); } // determine toplevel directory path. auto zipFile = ZipFileReader::Create(tempFile.Path()); std::vector files = zipFile->GetFiles(); bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); directory = directory / fs::path(inputFileName).parent_path(); if (!hasSingleRootDirectory) { directory = (fs::path(directory) / fs::path(inputFileName).filename().stem()).string(); } for (const auto& inputFile : files) { if (!inputFile.ends_with("/")) // don't process directory entries. { fs::path inputPath{ inputFile }; std::string extension = inputPath.extension(); if (extensionChecker.IsValidExtension(extension) || isInfoFile(inputFile)) { auto si = zipFile->GetFileInputStream(inputFile); std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } else if (extension == ".zip") { // recursively included zip file. :-/ auto si = zipFile->GetFileInputStream(inputFile); UploadEmbeddedZipFile( this->model, si, inputPath, directory, extensionChecker, instanceId, patchProperty); } } } } virtual void post_response( const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { try { std::string segment = request_uri.segment(1); if (segment == "t3k_uploadAsset") { std::string responseText; try { fs::path targetPath = request_uri.query("path"); if (targetPath.empty()) { throw std::runtime_error("Invalid path"); } if (!IsSafeMediaPath(targetPath) && !IsSafeThumbnailPath(targetPath)) { throw std::runtime_error("Unsafe path."); } fs::path filePath = req.get_body_temporary_file(); if (filePath.empty()) { throw std::runtime_error("Unexpected."); } try { fs::create_directories(targetPath.parent_path()); // try moving into place. if (fs::exists(targetPath)) { fs::remove(targetPath); } fs::rename(req.get_body_temporary_file(), targetPath); req.detach_body_temporary_file(); } catch (const std::exception& e) { // ok Copy into place instead. fs::copy(req.get_body_temporary_file(), targetPath); } // set target permissions to "pipedal_d:pipedald -rw-rw-r-- if we can. try { fs::permissions(targetPath, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write | fs::perms::others_read, fs::perm_options::replace); } catch (const std::exception&) { // ignore. } responseText = "{\"ok\": true}"; } catch (const std::exception& e) { std::string jsonString = json_writer::encode_string(e.what()); std::ostringstream os; os << "{\"ok\": false, \"error\": " << jsonString << "}"; responseText = os.str(); } res.set(HttpField::content_length, SS(responseText.size())); res.set(HttpField::content_type, "text/json"); res.setBody(responseText); return; } if (segment == "uploadPluginPresets") { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) { auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model), filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPluginPresetsJson()); json_reader reader(ss); reader.read(&presets); } else { json_reader reader(req.get_body_input_stream()); reader.read(&presets); } model->UploadPluginPresets(presets); res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; sResult << -1; std::string result = sResult.str(); res.setContentLength(result.length()); res.setBody(result); } else if (segment == "uploadPreset") { BankFile bankFile; fs::path filePath = req.get_body_temporary_file(); if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) { auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPresetJson()); json_reader reader(ss); reader.read(&bankFile); } else { // legacy json format, no zip, no media files. json_reader reader(req.get_body_input_stream()); reader.read(&bankFile); } uint64_t uploadAfter = -1; std::string strUploadAfter = request_uri.query("uploadAfter"); if (strUploadAfter.length() != 0) { uploadAfter = std::stol(strUploadAfter); } uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter); res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; sResult << instanceId; std::string result = sResult.str(); res.setContentLength(result.length()); res.setBody(result); } else if (segment == "uploadBank") { fs::path filePath = req.get_body_temporary_file(); uint64_t uploadAfter = -1; std::string strUploadAfter = request_uri.query("uploadAfter"); if (strUploadAfter.length() != 0) { uploadAfter = std::stol(strUploadAfter); } auto instanceId = ImportBankFile(*(this->model), filePath, uploadAfter); res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; sResult << instanceId; std::string result = sResult.str(); res.setContentLength(result.length()); res.setBody(result); } else if (segment == "uploadUserFile") { UserUploadResponse result; try { res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::string instanceIdString = request_uri.query("id"); std::string directory = request_uri.query("directory"); std::string filename = request_uri.query("filename"); std::string patchProperty = request_uri.query("property"); int64_t instanceId; { std::istringstream ss{ instanceIdString }; ss >> instanceId; if (!ss) { throw PiPedalException("Invalid instanceID"); } } if (patchProperty.length() == 0 && directory.length() == 0) { throw PiPedalException("Malformed request."); } res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); fs::path outputFileName = std::filesystem::path(directory) / filename; if (filename.ends_with(".zip")) { ExtensionChecker extensionChecker{ request_uri.query("ext") }; namespace fs = std::filesystem; try { auto zipFile = ZipFileReader::Create(req.get_body_temporary_file()); std::vector files = zipFile->GetFiles(); bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); if (!hasSingleRootDirectory) { directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string(); } for (const auto& inputFile : files) { if (!inputFile.ends_with("/")) // don't process directory entries. { fs::path inputPath{ inputFile }; std::string extension = inputPath.extension(); if (extensionChecker.IsValidExtension(extension) || isInfoFile(inputFile)) { auto si = zipFile->GetFileInputStream(inputFile); std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } else if (extension == ".zip") { // recursively included zip file. :-/ auto si = zipFile->GetFileInputStream(inputFile); UploadEmbeddedZipFile( this->model, si, inputPath, directory, extensionChecker, instanceId, patchProperty); } } } // set outputPath to the file or folder we would like focus to go to. // almost always a single folder in the root. std::string returnPath = GetFirstFolderOrFile(files); outputFileName = fs::path(directory) / returnPath; } catch (const std::exception& e) { Lv2Log::error(SS("Unzip failed. " << e.what())); throw; } FileSystemSync(); } else { outputFileName = this->model->UploadUserFile(directory, instanceId, patchProperty, filename, req.get_body_input_stream(), req.content_length()); } if (outputFileName.is_relative()) { outputFileName = this->model->GetPluginUploadDirectory() / outputFileName; } result.path_ = outputFileName.string(); } catch (const std::exception& e) { result.errorMessage_ = e.what(); } std::stringstream ss; json_writer writer(ss); writer.write(result); std::string response = ss.str(); res.setContentLength(response.length()); res.setBody(response); } else { throw PiPedalException("Not found"); } } catch (const std::exception& e) { Lv2Log::error(SS("Error uploading file: " << e.what())); if (strcmp(e.what(), "Not found") == 0) { ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory); } else { ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument); } } } }; static std::string StripPortNumber(const std::string& fromAddress) { std::string address = fromAddress; if (address.size() == 0) return fromAddress; char lastChar = address[address.size() - 1]; size_t pos = address.find_last_of(':'); // if ipv6, make sure we found an actual port address. size_t posBracket = address.find_last_of(']'); if (posBracket != std::string::npos && pos != std::string::npos) { if (posBracket > pos) { pos = std::string::npos; } } if (pos != std::string::npos) { address = address.substr(0, pos); } return address; } /* When hosting a react app, replace /var/config.json with data that will connect the react app with our socket server. */ class InterceptConfig : public RequestHandler { private: PiPedalModel& model; uint64_t maxUploadSize; int portNumber; public: InterceptConfig(PiPedalModel& model, int portNumber, uint64_t maxUploadSize) : RequestHandler("/var/config.json"), model(model), maxUploadSize(maxUploadSize), portNumber(portNumber) { } std::string GetConfig(const std::string& fromAddress) { std::string webSocketAddress = GetNonLinkLocalAddress(StripPortNumber(fromAddress)); Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber)); std::stringstream s; s << "{ \"socket_server_port\": " << portNumber << ", \"socket_server_address\": \"" << webSocketAddress << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false") << ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false") << ", \"tone3000_A2_models\": " << (model.Configuration().GetTone3000A2Models() ? " true" : "false") << " }"; return s.str(); } virtual ~InterceptConfig() {} virtual void head_response( const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { // intercepted. See the other overload. } virtual void head_response( const std::string& fromAddress, const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { std::string response = GetConfig(fromAddress); res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(response.length()); return; } virtual void get_response( const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { // intercepted. see the other overload. } virtual void get_response( const std::string& fromAddress, const uri& request_uri, HttpRequest& req, HttpResponse& res, std::error_code& ec) override { std::string response = GetConfig(fromAddress); res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(response.length()); res.setBody(response); } }; void pipedal::ConfigureWebServer( WebServer& server, PiPedalModel& model, int port, size_t maxUploadSize) { std::shared_ptr interceptConfig{ new InterceptConfig(model,port, maxUploadSize) }; server.AddRequestHandler(interceptConfig); std::shared_ptr downloadIntercept = std::make_shared(&model); server.AddRequestHandler(downloadIntercept); std::shared_ptr modWebIntercept = ModWebIntercept::Create(&model); server.AddRequestHandler(modWebIntercept); }