From 8fe8ba2808cbf5c44324bb3499bf35137a13183d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Soutad=C3=A9?= Date: Sun, 3 Apr 2022 09:36:23 +0200 Subject: [PATCH] Add adept_loan_mgt util --- utils/Makefile | 19 +- utils/acsmdownloader.cpp | 54 ++++- utils/adept_loan_mgt.cpp | 479 +++++++++++++++++++++++++++++++++++++++ utils/utils_common.h | 3 + 4 files changed, 548 insertions(+), 7 deletions(-) create mode 100644 utils/adept_loan_mgt.cpp diff --git a/utils/Makefile b/utils/Makefile index 318cf92..7d7882a 100644 --- a/utils/Makefile +++ b/utils/Makefile @@ -1,5 +1,5 @@ -TARGETS=acsmdownloader adept_activate adept_remove +TARGETS=acsmdownloader adept_activate adept_remove adept_loan_mgt CXXFLAGS=-Wall -fPIC -I$(ROOT)/include -I$(ROOT)/lib/pugixml/src/ @@ -18,17 +18,26 @@ else CXXFLAGS += -O2 endif -COMMON_DEPS = drmprocessorclientimpl.cpp utils_common.cpp $(STATIC_DEP) +COMMON_DEPS = drmprocessorclientimpl.cpp utils_common.cpp +COMMON_OBJECTS = $(COMMON_DEPS:.cpp=.o) +COMMON_LIB = utils.a all: $(TARGETS) -acsmdownloader: acsmdownloader.cpp $(COMMON_DEPS) +${COMMON_LIB}: ${COMMON_DEPS} ${STATIC_DEP} + $(CXX) $(CXXFLAGS) ${COMMON_DEPS} $(LDFLAGS) -c + $(AR) crs $@ ${COMMON_OBJECTS} $(STATIC_DEP) + +acsmdownloader: acsmdownloader.cpp ${COMMON_LIB} $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ -adept_activate: adept_activate.cpp $(COMMON_DEPS) +adept_activate: adept_activate.cpp ${COMMON_LIB} $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ -adept_remove: adept_remove.cpp $(COMMON_DEPS) +adept_remove: adept_remove.cpp ${COMMON_LIB} + $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ + +adept_loan_mgt: adept_loan_mgt.cpp ${COMMON_LIB} $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ clean: diff --git a/utils/acsmdownloader.cpp b/utils/acsmdownloader.cpp index 360cfda..dbe551f 100644 --- a/utils/acsmdownloader.cpp +++ b/utils/acsmdownloader.cpp @@ -26,12 +26,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - #include +#include +#include #include #include #include +#include + #include "drmprocessorclientimpl.h" #include "utils_common.h" @@ -54,7 +57,6 @@ public: int ret = 0; try { - DRMProcessorClientImpl client; gourou::DRMProcessor processor(&client, deviceFile, activationFile, devicekeyFile); gourou::User* user = processor.getUser(); @@ -118,6 +120,8 @@ public: filename = finalName; } std::cout << "Created " << filename << std::endl; + + serializeLoanToken(item); } } catch(std::exception& e) { @@ -127,6 +131,52 @@ public: return ret; } + + void serializeLoanToken(gourou::FulfillmentItem* item) + { + gourou::LoanToken* token = item->getLoanToken(); + + // No loan token available + if (!token) + return; + + pugi::xml_document doc; + + pugi::xml_node decl = doc.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node root = doc.append_child("loanToken"); + gourou::appendTextElem(root, "id", (*token)["id"]); + gourou::appendTextElem(root, "operatorURL", (*token)["operatorURL"]); + gourou::appendTextElem(root, "validity", (*token)["validity"]); + gourou::appendTextElem(root, "name", item->getMetadata("title")); + + char * activationDir = strdup(deviceFile); + activationDir = dirname(activationDir); + + gourou::StringXMLWriter xmlWriter; + doc.save(xmlWriter, " "); + std::string xmlStr = xmlWriter.getResult(); + + // Use first bytes of SHA1(id) as filename + unsigned char sha1[gourou::SHA1_LEN]; + client.digest("SHA1", (unsigned char*)(*token)["id"].c_str(), (*token)["id"].size(), sha1); + gourou::ByteArray tmp(sha1, sizeof(sha1)); + std::string filenameHex = tmp.toHex(); + std::string filename(filenameHex.c_str(), ID_HASH_SIZE); + std::string fullPath = std::string(activationDir); + fullPath += std::string ("/") + std::string(LOANS_DIR); + mkpath(fullPath.c_str()); + fullPath += filename + std::string(".xml"); + gourou::writeFile(fullPath, xmlStr); + + std::cout << "Loan token serialized into " << fullPath << std::endl; + + free(activationDir); + } + +private: + DRMProcessorClientImpl client; }; diff --git a/utils/adept_loan_mgt.cpp b/utils/adept_loan_mgt.cpp new file mode 100644 index 0000000..a8ac487 --- /dev/null +++ b/utils/adept_loan_mgt.cpp @@ -0,0 +1,479 @@ +/* + Copyright (c) 2022, Grégory Soutadé + + All rights reserved. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * 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 REGENTS 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 REGENTS AND 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. +*/ + +#include + +#include +#include + +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include + +#include +#include +#include "drmprocessorclientimpl.h" +#include "utils_common.h" + +#define MAX_SIZE_BOOK_NAME 30 + +static char* activationDir = 0; +static const char* deviceFile = "device.xml"; +static const char* activationFile = "activation.xml"; +static const char* devicekeyFile = "devicesalt"; +static bool list = false; +static const char* returnID = 0; +static const char* deleteID = 0; + +struct Loan +{ + std::string id; + std::string operatorURL; + std::string validity; + std::string bookName; + + std::string path; +}; + +class LoanMGT +{ +public: + ~LoanMGT() + { + for (const auto& kv : loanedBooks) + delete kv.second; + } + + int run() + { + int ret = 0; + try + { + DRMProcessorClientImpl client; + gourou::DRMProcessor processor(&client, deviceFile, activationFile, devicekeyFile); + + loadLoanedBooks(); + + if (list) + displayLoanList(); + else if (returnID) + returnBook(processor); + else if (deleteID) + deleteLoan(); + } catch(std::exception& e) + { + std::cout << e.what() << std::endl; + ret = 1; + } + + return ret; + } + +private: + void loadLoanedBooks() + { + DIR *dp; + struct dirent *ep; + int entryLen; + struct Loan* loan; + char * res; + + std::string loanDir = std::string(activationDir) + std::string("/") + LOANS_DIR; + + if (!fileExists(loanDir.c_str())) + return; + + dp = opendir (loanDir.c_str()); + + if(!dp) + EXCEPTION(gourou::USER_INVALID_INPUT, "Cannot read directory " << loanDir); + + while ((ep = readdir (dp))) + { + if (ep->d_type != DT_LNK && + ep->d_type != DT_REG) + continue; + + entryLen = strlen(ep->d_name); + + if (entryLen <= 4 || + ep->d_name[entryLen-4] != '.' || + ep->d_name[entryLen-3] != 'x' || + ep->d_name[entryLen-2] != 'm' || + ep->d_name[entryLen-1] != 'l') + continue; + + std::string id = std::string(ep->d_name, entryLen-4); + + loan = new Loan; + loan->path = loanDir + std::string("/") + ep->d_name; + + pugi::xml_document xmlDoc; + pugi::xml_node node; + + if (!xmlDoc.load_file(loan->path.c_str(), pugi::parse_ws_pcdata_single|pugi::parse_escapes, pugi::encoding_utf8)) + { + std::cout << "Invalid loan entry " << loan->path << std::endl; + goto error; + } + + // id + node = xmlDoc.select_node("//id").node(); + if (!node) + { + std::cout << "Invalid loan entry " << ep->d_name << ", no id element" << std::endl; + goto error; + } + loan->id = node.first_child().value(); + + // operatorURL + node = xmlDoc.select_node("//operatorURL").node(); + if (!node) + { + std::cout << "Invalid loan entry " << ep->d_name << ", no operatorURL element" << std::endl; + goto error; + } + loan->operatorURL = node.first_child().value(); + + // validity + node = xmlDoc.select_node("//validity").node(); + if (!node) + { + std::cout << "Invalid loan entry " << ep->d_name << ", no validity element" << std::endl; + goto error; + } + loan->validity = node.first_child().value(); + + // bookName + node = xmlDoc.select_node("//name").node(); + if (!node) + { + std::cout << "Invalid loan entry " << ep->d_name << ", no name element" << std::endl; + goto error; + } + loan->bookName = node.first_child().value(); + + struct tm tm; + res = strptime(loan->validity.c_str(), "%Y-%m-%dT%H:%M:%S%Z", &tm); + if (*res == 0) + { + if (mktime(&tm) <= time(NULL)) + loan->validity = " (Expired)"; + } + else + { + std::cout << "Unable to parse validity timestamp :" << loan->validity << std::endl; + loan->validity = " (Unknown)"; + } + + loanedBooks[id] = loan; + continue; + + error: + if (loan) + delete loan; + } + + closedir (dp); + } + + void displayLoanList() + { + if (!loanedBooks.size()) + { + std::cout << "Any book loaned" << std::endl; + return; + } + + struct Loan* loan; + unsigned int maxSizeBookName=0; + // Compute max size + for (const auto& kv : loanedBooks) + { + loan = kv.second; + if (loan->bookName.size() > maxSizeBookName) + maxSizeBookName = loan->bookName.size(); + } + + if (maxSizeBookName > MAX_SIZE_BOOK_NAME) + maxSizeBookName = MAX_SIZE_BOOK_NAME; + else if ((maxSizeBookName % 2)) + maxSizeBookName++; + + // std::cout << " ID Book Expiration" << std::endl; + // std::cout << "------------------------------" << std::endl; + + int fillID, fillBookName, fillExpiration=(20 - 10)/2; + + fillID = (ID_HASH_SIZE - 2) / 2; + fillBookName = (maxSizeBookName - 4) / 2; + + std::cout.width (fillID); + std::cout << ""; + std::cout << "ID" ; + std::cout.width (fillID); + std::cout << ""; + std::cout << " " ; + + std::cout.width (fillBookName); + std::cout << ""; + std::cout << "Book" ; + std::cout.width (fillBookName); + std::cout << ""; + std::cout << " " ; + + std::cout.width (fillExpiration); + std::cout << ""; + std::cout << "Exipration"; + std::cout.width (fillExpiration); + std::cout << "" << std::endl; + + std::cout.fill ('-'); + std::cout.width (ID_HASH_SIZE + 4 + maxSizeBookName + 4 + 20); + std::cout << "" << std::endl; + std::cout.fill (' '); + + std::string bookName; + + for (const auto& kv : loanedBooks) + { + loan = kv.second; + + std::cout << kv.first; + std::cout << " "; + + if (loan->bookName.size() > MAX_SIZE_BOOK_NAME) + bookName = std::string(loan->bookName.c_str(), MAX_SIZE_BOOK_NAME); + else + bookName = loan->bookName; + + std::cout << bookName; + std::cout.width (maxSizeBookName - bookName.size()); + std::cout << ""; + std::cout << " "; + + std::cout << loan->validity << std::endl; + } + + std::cout << std::endl; + } + + void returnBook(gourou::DRMProcessor& processor) + { + struct Loan* loan = loanedBooks[std::string(returnID)]; + + if (!loan) + { + std::cout << "Error : Loan " << returnID << " doesn't exists" << std::endl; + return; + } + + processor.returnLoan(loan->id, loan->operatorURL); + + deleteID = returnID; + if (deleteLoan(false)) + { + std::cout << "Loan " << returnID << " successfully returned" << std::endl; + } + } + + bool deleteLoan(bool displayResult=true) + { + struct Loan* loan = loanedBooks[std::string(deleteID)]; + + if (!loan) + { + std::cout << "Error : Loan " << deleteID << " doesn't exists" << std::endl; + return false; + } + + if (unlink(loan->path.c_str())) + { + std::cout << "Error : Cannot delete " << loan->path << std::endl; + return false; + } + else if (displayResult) + { + std::cout << "Loan " << deleteID << " deleted" << std::endl; + } + + return true; + } + + std::map loanedBooks; +}; + + +static void usage(const char* cmd) +{ + std::cout << "Manage loaned books" << std::endl; + + std::cout << "Usage: " << cmd << " [(-d|--activation-dir) dir] (-l|--list)|(-D|--delete loanID)|(-R|--delete loanID) [(-v|--verbose)] [(-h|--help)]" << std::endl << std::endl; + + std::cout << " " << "-d|--activation-dir" << "\t" << "Directory of device.xml/activation.xml and device key" << std::endl; + std::cout << " " << "-l|--list" << "\t\t" << "List all loaned books" << std::endl; + std::cout << " " << "-r|--return" << "\t\t" << "Return a loaned book" << std::endl; + std::cout << " " << "-D|--delete" << "\t\t" << "Delete a loan entry without returning it" << std::endl; + std::cout << " " << "-v|--verbose" << "\t\t" << "Increase verbosity, can be set multiple times" << std::endl; + std::cout << " " << "-V|--version" << "\t\t" << "Display libgourou version" << std::endl; + std::cout << " " << "-h|--help" << "\t\t" << "This help" << std::endl; + + std::cout << std::endl; + std::cout << "Activation directory is optional. If not set, it's looked into :" << std::endl; + std::cout << " * Current directory" << std::endl; + std::cout << " * .adept" << std::endl; + std::cout << " * adobe-digital-editions directory" << std::endl; + std::cout << " * .adobe-digital-editions directory" << std::endl; +} + +int main(int argc, char** argv) +{ + int c, ret = -1; + + const char** files[] = {&devicekeyFile, &deviceFile, &activationFile}; + int verbose = gourou::DRMProcessor::getLogLevel(); + int actions = 0; + + while (1) { + int option_index = 0; + static struct option long_options[] = { + {"activation-dir", required_argument, 0, 'd' }, + {"list", no_argument, 0, 'l' }, + {"return", no_argument, 0, 'r' }, + {"delete", no_argument, 0, 'D' }, + {"verbose", no_argument, 0, 'v' }, + {"version", no_argument, 0, 'V' }, + {"help", no_argument, 0, 'h' }, + {0, 0, 0, 0 } + }; + + c = getopt_long(argc, argv, "d:lr:D:vVh", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'd': + activationDir = optarg; + break; + case 'l': + list = true; + actions++; + break; + case 'r': + returnID = optarg; + actions++; + break; + case 'D': + deleteID = optarg; + actions++; + break; + case 'v': + verbose++; + break; + case 'V': + version(); + return 0; + case 'h': + usage(argv[0]); + return 0; + default: + usage(argv[0]); + return -1; + } + } + + gourou::DRMProcessor::setLogLevel(verbose); + + // By default, simply list books loaned + if (actions == 0) + list = true; + else if (actions != 1) + { + usage(argv[0]); + return -1; + } + + LoanMGT loanMGT; + + int i; + bool hasErrors = false; + const char* orig; + char *filename; + for (i=0; i<(int)ARRAY_SIZE(files); i++) + { + orig = *files[i]; + + if (activationDir) + { + std::string path = std::string(activationDir) + std::string("/") + orig; + filename = strdup(path.c_str()); + } + else + filename = strdup(orig); + *files[i] = findFile(filename); + free(filename); + if (!*files[i]) + { + std::cout << "Error : " << orig << " doesn't exists, did you activate your device ?" << std::endl; + hasErrors = true; + } + } + + if (hasErrors) + { + // In case of activation dir was provided by user + activationDir = 0; + goto end; + } + + if (activationDir) + activationDir = strdup(activationDir); // For below free + else + { + activationDir = strdup(deviceFile); + activationDir = dirname(activationDir); + } + + ret = loanMGT.run(); + +end: + for (i=0; i<(int)ARRAY_SIZE(files); i++) + { + if (*files[i]) + free((void*)*files[i]); + } + + if (activationDir) + free(activationDir); + + return ret; +} diff --git a/utils/utils_common.h b/utils/utils_common.h index 376a7c8..5bad2a9 100644 --- a/utils/utils_common.h +++ b/utils/utils_common.h @@ -29,6 +29,9 @@ #ifndef _UTILS_COMMON_H_ #define _UTILS_COMMON_H_ +#define LOANS_DIR "loans/" +#define ID_HASH_SIZE 16 + #define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0])) /**