From 1ea7e16f3e697c666c23939ba9df69c07d4a2174 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Tue, 4 Oct 2022 09:33:53 -0700 Subject: [PATCH] Cloud storage: add Dropbox short-lived tokens (#9496) --- frontend/apps/cloudstorage/cloudstorage.lua | 84 ++++++++++++++------ frontend/apps/cloudstorage/dropbox.lua | 86 +++++++++------------ frontend/apps/cloudstorage/dropboxapi.lua | 52 ++++++++++--- 3 files changed, 137 insertions(+), 85 deletions(-) diff --git a/frontend/apps/cloudstorage/cloudstorage.lua b/frontend/apps/cloudstorage/cloudstorage.lua index 62b5c38de..11cc576c7 100644 --- a/frontend/apps/cloudstorage/cloudstorage.lua +++ b/frontend/apps/cloudstorage/cloudstorage.lua @@ -11,6 +11,7 @@ local InfoMessage = require("ui/widget/infomessage") local InputDialog = require("ui/widget/inputdialog") local LuaSettings = require("luasettings") local Menu = require("ui/widget/menu") +local NetworkMgr = require("ui/network/manager") local PathChooser = require("ui/widget/pathchooser") local UIManager = require("ui/uimanager") local WebDav = require("apps/cloudstorage/webdav") @@ -139,14 +140,30 @@ function CloudStorage:selectCloudType() return true end +function CloudStorage:generateDropBoxAccessToken() + if self.username or self.address == nil or self.address == "" then + -- short-lived token has been generated already in this session + -- or we have long-lived token in self.password + return true + else + local token = DropBox:getAccessToken(self.password, self.address) + if token then + self.password = token -- short-lived token + self.username = true -- flag + return true + end + end +end + function CloudStorage:openCloudServer(url) local tbl, e - local NetworkMgr = require("ui/network/manager") if self.type == "dropbox" then if NetworkMgr:willRerunWhenOnline(function() self:openCloudServer(url) end) then return end - tbl, e = DropBox:run(url, self.password, self.choose_folder_mode) + if self:generateDropBoxAccessToken() then + tbl, e = DropBox:run(url, self.password, self.choose_folder_mode) + end elseif self.type == "ftp" then if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then return @@ -423,31 +440,38 @@ function CloudStorage:onMenuHold(item) end function CloudStorage:synchronizeCloud(item) + if NetworkMgr:willRerunWhenOnline(function() self:synchronizeCloud(item) end) then + return + end + self.password = item.password + self.address = item.address local Trapper = require("ui/trapper") Trapper:wrap(function() Trapper:setPausedText("Download paused.\nDo you want to continue or abort downloading files?") - local ok, downloaded_files, failed_files = pcall(self.downloadListFiles, self, item) - if ok and downloaded_files then - if not failed_files then failed_files = 0 end - local text - if downloaded_files == 0 and failed_files == 0 then - text = _("No files to download from Dropbox.") - else - text = T(N_("Successfully downloaded 1 file from Dropbox to local storage.", "Successfully downloaded %1 files from Dropbox to local storage.", downloaded_files), downloaded_files) - if failed_files > 0 then - text = text .. "\n" .. T(N_("Failed to download 1 file.", "Failed to download %1 files.", failed_files), failed_files) + if self:generateDropBoxAccessToken() then + local ok, downloaded_files, failed_files = pcall(self.downloadListFiles, self, item) + if ok and downloaded_files then + if not failed_files then failed_files = 0 end + local text + if downloaded_files == 0 and failed_files == 0 then + text = _("No files to download from Dropbox.") + else + text = T(N_("Successfully downloaded 1 file from Dropbox to local storage.", "Successfully downloaded %1 files from Dropbox to local storage.", downloaded_files), downloaded_files) + if failed_files > 0 then + text = text .. "\n" .. T(N_("Failed to download 1 file.", "Failed to download %1 files.", failed_files), failed_files) + end end + UIManager:show(InfoMessage:new{ + text = text, + timeout = 3, + }) + else + Trapper:reset() -- close any last widget not cleaned if error + UIManager:show(InfoMessage:new{ + text = _("No files to download from Dropbox.\nPlease check your configuration and connection."), + timeout = 3, + }) end - UIManager:show(InfoMessage:new{ - text = text, - timeout = 3, - }) - else - Trapper:reset() -- close any last widget not cleaned if error - UIManager:show(InfoMessage:new{ - text = _("No files to download from Dropbox.\nPlease check your configuration and connection."), - timeout = 3, - }) end end) end @@ -468,7 +492,7 @@ function CloudStorage:downloadListFiles(item) end end end - local remote_files = DropBox:showFiles(item.sync_source_folder, item.password) + local remote_files = DropBox:showFiles(item.sync_source_folder, self.password) if #remote_files == 0 then UI:clear() return false @@ -499,7 +523,7 @@ function CloudStorage:downloadListFiles(item) if not go_on then break end - response = DropBox:downloadFileNoUI(file.url, item.password, item.sync_dest_folder .. "/" .. file.text) + response = DropBox:downloadFileNoUI(file.url, self.password, item.sync_dest_folder .. "/" .. file.text) if response then success_files = success_files + 1 else @@ -686,6 +710,7 @@ function CloudStorage:configCloud(type) table.insert(cs_servers,{ name = fields[1], password = fields[2], + address = fields[3], type = "dropbox", url = "/" }) @@ -732,6 +757,7 @@ function CloudStorage:editCloudServer(item) if server.name == updated_config.text and server.password == updated_config.password then server.name = fields[1] server.password = fields[2] + server.address = fields[3] cs_servers[i] = server break end @@ -790,7 +816,15 @@ end function CloudStorage:infoServer(item) if item.type == "dropbox" then - DropBox:info(item.password) + if NetworkMgr:willRerunWhenOnline(function() self:infoServer(item) end) then + return + end + self.password = item.password + self.address = item.address + if self:generateDropBoxAccessToken() then + DropBox:info(self.password) + self.username = nil + end elseif item.type == "ftp" then Ftp:info(item) elseif item.type == "webdav" then diff --git a/frontend/apps/cloudstorage/dropbox.lua b/frontend/apps/cloudstorage/dropbox.lua index e2bb25716..74132e698 100644 --- a/frontend/apps/cloudstorage/dropbox.lua +++ b/frontend/apps/cloudstorage/dropbox.lua @@ -12,6 +12,10 @@ local _ = require("gettext") local DropBox = {} +function DropBox:getAccessToken(refresh_token, app_key_colon_secret) + return DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret) +end + function DropBox:run(url, password, choose_folder_mode) return DropBoxApi:listFolder(url, password, choose_folder_mode) end @@ -92,39 +96,31 @@ function DropBox:createFolder(url, password, folder_name, callback_close) end function DropBox:config(item, callback) - local text_info = "How to generate Access Token:\n".. - "1. Open the following URL in your Browser, and log in using your account: https://www.dropbox.com/developers/apps.\n".. - "2. Click on >>Create App<<, then select >>Dropbox API app<<.\n".. - "3. Now go on with the configuration, choosing the app permissions and access restrictions to your DropBox folder.\n".. - "4. Enter the >>App Name<< that you prefer (e.g. KOReader).\n".. - "5. Now, click on the >>Create App<< button.\n" .. - "6. When your new App is successfully created, please click on the Generate button.\n".. - "7. Under the 'Generated access token' section, then enter code in Dropbox token field." - local hint_top = _("Your Dropbox name") - local text_top = "" - local hint_bottom = _("Dropbox token\n\n\n\n") - local text_bottom = "" - local title - local text_button_right = _("Add") + local text_info = _([[ +Dropbox access tokens are short-lived (4 hours). +To generate new access token please use Dropbox refresh token and : Base64 encoded string. + +Some of the previously generated long-lived tokens are still valid.]]) + local text_name, text_token, text_appkey if item then - title = _("Edit Dropbox account") - text_button_right = _("Apply") - text_top = item.text - text_bottom = item.password - else - title = _("Add Dropbox account") + text_name = item.text + text_token = item.password + text_appkey = item.address end self.settings_dialog = MultiInputDialog:new { - title = title, + title = _("Dropbox cloud storage"), fields = { { - text = text_top, - hint = hint_top , + text = text_name, + hint = _("Cloud storage displayed name"), + }, + { + text = text_token, + hint = _("Dropbox refresh token\nor long-lived token (deprecated)"), }, { - text = text_bottom, - hint = hint_bottom, - scroll = false, + text = text_appkey, + hint = _("Dropbox :\n(leave blank for long-lived token)"), }, }, buttons = { @@ -144,29 +140,20 @@ function DropBox:config(item, callback) end }, { - text = text_button_right, + text = _("Save"), callback = function() local fields = MultiInputDialog:getFields() - if fields[1] ~= "" and fields[2] ~= "" then - if item then - --edit - callback(item, fields) - else - -- add new - callback(fields) - end - self.settings_dialog:onClose() - UIManager:close(self.settings_dialog) + if item then + callback(item, fields) else - UIManager:show(InfoMessage:new{ - text = _("Please fill in all fields.") - }) + callback(fields) end + self.settings_dialog:onClose() + UIManager:close(self.settings_dialog) end }, }, }, - input_type = "text", } UIManager:show(self.settings_dialog) self.settings_dialog:onShowKeyboard() @@ -174,14 +161,17 @@ end function DropBox:info(token) local info = DropBoxApi:fetchInfo(token) - local info_text - if info and info.name then - info_text = T(_"Type: %1\nName: %2\nEmail: %3\nCountry: %4", - "Dropbox",info.name.display_name, info.email, info.country) - else - info_text = _("No information available") + local space_usage = DropBoxApi:fetchInfo(token, true) + if info and space_usage then + local account_type = info.account_type and info.account_type[".tag"] + local name = info.name and info.name.display_name + local space_total = space_usage.allocation and space_usage.allocation.allocated + UIManager:show(InfoMessage:new{ + text = T(_"Type: %1\nName: %2\nEmail: %3\nCountry: %4\nSpace total: %5\nSpace used: %6", + account_type, name, info.email, info.country, + util.getFriendlySize(space_total), util.getFriendlySize(space_usage.used)), + }) end - UIManager:show(InfoMessage:new{text = info_text}) end return DropBox diff --git a/frontend/apps/cloudstorage/dropboxapi.lua b/frontend/apps/cloudstorage/dropboxapi.lua index e387a348f..745d1b135 100644 --- a/frontend/apps/cloudstorage/dropboxapi.lua +++ b/frontend/apps/cloudstorage/dropboxapi.lua @@ -13,36 +13,64 @@ local _ = require("gettext") local DropBoxApi = { } +local API_TOKEN = "https://api.dropbox.com/oauth2/token" local API_URL_INFO = "https://api.dropboxapi.com/2/users/get_current_account" +local API_GET_SPACE_USAGE = "https://api.dropboxapi.com/2/users/get_space_usage" local API_LIST_FOLDER = "https://api.dropboxapi.com/2/files/list_folder" local API_DOWNLOAD_FILE = "https://content.dropboxapi.com/2/files/download" local API_UPLOAD_FILE = "https://content.dropboxapi.com/2/files/upload" local API_CREATE_FOLDER = "https://api.dropboxapi.com/2/files/create_folder_v2" local API_LIST_ADD_FOLDER = "https://api.dropboxapi.com/2/files/list_folder/continue" -function DropBoxApi:fetchInfo(token) +function DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret) local sink = {} + local data = "grant_type=refresh_token&refresh_token=" .. refresh_token + local request = { + url = API_TOKEN, + method = "POST", + headers = { + ["Authorization"] = "Basic " .. require("ffi/sha2").bin_to_base64(app_key_colon_secret), + ["Content-Type"] = "application/x-www-form-urlencoded", + ["Content-Length"] = string.len(data), + }, + source = ltn12.source.string(data), + sink = ltn12.sink.table(sink), + } socketutil:set_timeout() + local code = socket.skip(1, http.request(request)) + socketutil:reset_timeout() + if code == 200 then + local headers = table.concat(sink) + if headers ~= "" then + local _, result = pcall(JSON.decode, headers) + return result["access_token"] + end + end + logger.info("Dropbox: cannot get access token") +end + +function DropBoxApi:fetchInfo(token, space_usage) + local url = space_usage and API_GET_SPACE_USAGE or API_URL_INFO + local sink = {} local request = { - url = API_URL_INFO, + url = url, method = "POST", headers = { ["Authorization"] = "Bearer " .. token, }, sink = ltn12.sink.table(sink), } - local headers_request = socket.skip(1, http.request(request)) + socketutil:set_timeout() + local code = socket.skip(1, http.request(request)) socketutil:reset_timeout() - local result_response = table.concat(sink) - if headers_request == nil then - return nil - end - if result_response ~= "" then - local _, result = pcall(JSON.decode, result_response) - return result - else - return nil + if code == 200 then + local headers = table.concat(sink) + if headers ~= "" then + local _, result = pcall(JSON.decode, headers) + return result + end end + logger.info("Dropbox: cannot get account info") end function DropBoxApi:fetchListFolders(path, token)