1 | /*␊ |
2 | Copyright (c) 2022, Grégory Soutadé␊ |
3 | ␊ |
4 | All rights reserved.␊ |
5 | Redistribution and use in source and binary forms, with or without␊ |
6 | modification, are permitted provided that the following conditions are met:␊ |
7 | ␊ |
8 | * Redistributions of source code must retain the above copyright␊ |
9 | notice, this list of conditions and the following disclaimer.␊ |
10 | * Redistributions in binary form must reproduce the above copyright␊ |
11 | notice, this list of conditions and the following disclaimer in the␊ |
12 | documentation and/or other materials provided with the distribution.␊ |
13 | * Neither the name of the copyright holder nor the␊ |
14 | names of its contributors may be used to endorse or promote products␊ |
15 | derived from this software without specific prior written permission.␊ |
16 | ␊ |
17 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY␊ |
18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED␊ |
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE␊ |
20 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY␊ |
21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES␊ |
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;␊ |
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND␊ |
24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT␊ |
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS␊ |
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.␊ |
27 | */␊ |
28 | ␊ |
29 | #include <getopt.h>␊ |
30 | ␊ |
31 | #include <iostream>␊ |
32 | #include <algorithm>␊ |
33 | ␊ |
34 | #define _XOPEN_SOURCE 700␊ |
35 | #include <stdio.h>␊ |
36 | #include <sys/types.h>␊ |
37 | #include <dirent.h>␊ |
38 | #include <libgen.h>␊ |
39 | #include <time.h>␊ |
40 | ␊ |
41 | #include <libgourou.h>␊ |
42 | #include <libgourou_common.h>␊ |
43 | #include "drmprocessorclientimpl.h"␊ |
44 | #include "utils_common.h"␊ |
45 | ␊ |
46 | #define MAX_SIZE_BOOK_NAME 30␊ |
47 | ␊ |
48 | static char* adeptDir = 0;␊ |
49 | static const char* deviceFile = "device.xml";␊ |
50 | static const char* activationFile = "activation.xml";␊ |
51 | static const char* devicekeyFile = "devicesalt";␊ |
52 | static bool list = false;␊ |
53 | static const char* returnID = 0;␊ |
54 | static const char* deleteID = 0;␊ |
55 | static bool notify = true;␊ |
56 | ␊ |
57 | struct Loan␊ |
58 | {␊ |
59 | std::string id;␊ |
60 | std::string operatorURL;␊ |
61 | std::string validity;␊ |
62 | std::string bookName;␊ |
63 | ␊ |
64 | std::string path;␊ |
65 | };␊ |
66 | ␊ |
67 | class LoanMGT␊ |
68 | {␊ |
69 | public:␊ |
70 | ~LoanMGT()␊ |
71 | {␊ |
72 | ␉for (const auto& kv : loanedBooks)␊ |
73 | ␉ delete kv.second;␊ |
74 | }␊ |
75 | ␊ |
76 | int run()␊ |
77 | {␊ |
78 | ␉int ret = 0;␊ |
79 | ␉try␊ |
80 | ␉{␊ |
81 | ␉ DRMProcessorClientImpl client;␊ |
82 | ␉ gourou::DRMProcessor processor(&client, deviceFile, activationFile, devicekeyFile);␊ |
83 | ␊ |
84 | ␉ loadLoanedBooks();␊ |
85 | ␊ |
86 | ␉ if (list)␊ |
87 | ␉␉displayLoanList();␊ |
88 | ␉ else if (returnID)␊ |
89 | ␉␉returnBook(processor);␊ |
90 | ␉ else if (deleteID)␊ |
91 | ␉␉deleteLoan();␊ |
92 | ␉} catch(std::exception& e)␊ |
93 | ␉{␊ |
94 | ␉ std::cout << e.what() << std::endl;␊ |
95 | ␉ ret = 1;␊ |
96 | ␉}␊ |
97 | ␊ |
98 | ␉return ret;␊ |
99 | }␊ |
100 | ␊ |
101 | private:␊ |
102 | void loadLoanedBooks()␊ |
103 | {␊ |
104 | ␉DIR *dp;␊ |
105 | ␉struct dirent *ep;␊ |
106 | ␉int entryLen;␊ |
107 | ␉struct Loan* loan;␊ |
108 | ␉char * res;␊ |
109 | ␉␊ |
110 | ␉std::string loanDir = std::string(adeptDir) + std::string("/") + LOANS_DIR;␊ |
111 | ␊ |
112 | ␉if (!fileExists(loanDir.c_str()))␊ |
113 | ␉ return;␊ |
114 | ␉␊ |
115 | ␉dp = opendir (loanDir.c_str());␊ |
116 | ␊ |
117 | ␉if(!dp)␊ |
118 | ␉ EXCEPTION(gourou::USER_INVALID_INPUT, "Cannot read directory " << loanDir);␊ |
119 | ␊ |
120 | ␉while ((ep = readdir (dp)))␊ |
121 | ␉{␊ |
122 | ␉ if (ep->d_type != DT_LNK &&␊ |
123 | ␉␉ep->d_type != DT_REG)␊ |
124 | ␉␉continue;␊ |
125 | ␊ |
126 | ␉ entryLen = strlen(ep->d_name);␊ |
127 | ␊ |
128 | ␉ if (entryLen <= 4 ||␊ |
129 | ␉␉ep->d_name[entryLen-4] != '.' ||␊ |
130 | ␉␉ep->d_name[entryLen-3] != 'x' ||␊ |
131 | ␉␉ep->d_name[entryLen-2] != 'm' ||␊ |
132 | ␉␉ep->d_name[entryLen-1] != 'l')␊ |
133 | ␉␉continue;␊ |
134 | ␊ |
135 | ␉ std::string id = std::string(ep->d_name, entryLen-4);␊ |
136 | ␉ ␊ |
137 | ␉ loan = new Loan;␊ |
138 | ␉ loan->path = loanDir + std::string("/") + ep->d_name;␊ |
139 | ␊ |
140 | ␉ pugi::xml_document xmlDoc;␊ |
141 | ␉ pugi::xml_node node;␊ |
142 | ␊ |
143 | ␉ if (!xmlDoc.load_file(loan->path.c_str(), pugi::parse_ws_pcdata_single|pugi::parse_escapes, pugi::encoding_utf8))␊ |
144 | ␉ {␊ |
145 | ␉␉std::cout << "Invalid loan entry " << loan->path << std::endl;␊ |
146 | ␉␉goto error;␊ |
147 | ␉ }␊ |
148 | ␊ |
149 | ␉ // id␊ |
150 | ␉ node = xmlDoc.select_node("//id").node();␊ |
151 | ␉ if (!node)␊ |
152 | ␉ {␊ |
153 | ␉␉std::cout << "Invalid loan entry " << ep->d_name << ", no id element" << std::endl;␊ |
154 | ␉␉goto error;␊ |
155 | ␉ }␊ |
156 | ␉ loan->id = node.first_child().value();␊ |
157 | ␊ |
158 | ␉ // operatorURL␊ |
159 | ␉ node = xmlDoc.select_node("//operatorURL").node();␊ |
160 | ␉ if (!node)␊ |
161 | ␉ {␊ |
162 | ␉␉std::cout << "Invalid loan entry " << ep->d_name << ", no operatorURL element" << std::endl;␊ |
163 | ␉␉goto error;␊ |
164 | ␉ }␊ |
165 | ␉ loan->operatorURL = node.first_child().value();␊ |
166 | ␊ |
167 | ␉ // validity␊ |
168 | ␉ node = xmlDoc.select_node("//validity").node();␊ |
169 | ␉ if (!node)␊ |
170 | ␉ {␊ |
171 | ␉␉std::cout << "Invalid loan entry " << ep->d_name << ", no validity element" << std::endl;␊ |
172 | ␉␉goto error;␊ |
173 | ␉ }␊ |
174 | ␉ loan->validity = node.first_child().value();␊ |
175 | ␊ |
176 | ␉ // bookName␊ |
177 | ␉ node = xmlDoc.select_node("//name").node();␊ |
178 | ␉ if (!node)␊ |
179 | ␉ {␊ |
180 | ␉␉std::cout << "Invalid loan entry " << ep->d_name << ", no name element" << std::endl;␊ |
181 | ␉␉goto error;␊ |
182 | ␉ }␊ |
183 | ␉ loan->bookName = node.first_child().value();␊ |
184 | ␊ |
185 | ␉ struct tm tm;␊ |
186 | ␉ res = strptime(loan->validity.c_str(), "%Y-%m-%dT%H:%M:%S%Z", &tm);␊ |
187 | ␉ if (*res == 0)␊ |
188 | ␉ {␊ |
189 | ␉␉if (mktime(&tm) <= time(NULL))␊ |
190 | ␉␉ loan->validity = " (Expired)";␊ |
191 | ␉ }␊ |
192 | ␉ else␊ |
193 | ␉ {␊ |
194 | ␉␉std::cout << "Unable to parse validity timestamp :" << loan->validity << std::endl;␊ |
195 | ␉␉loan->validity = " (Unknown)";␊ |
196 | ␉ }␊ |
197 | ␉ ␊ |
198 | ␉ loanedBooks[id] = loan;␊ |
199 | ␉ continue;␊ |
200 | ␊ |
201 | ␉error:␊ |
202 | ␉ if (loan)␊ |
203 | ␉␉delete loan;␊ |
204 | ␉}␊ |
205 | ␊ |
206 | ␉closedir (dp);␊ |
207 | }␊ |
208 | ␊ |
209 | void displayLoanList()␊ |
210 | {␊ |
211 | ␉if (!loanedBooks.size())␊ |
212 | ␉{␊ |
213 | ␉ std::cout << "No books loaned" << std::endl;␊ |
214 | ␉ return;␊ |
215 | ␉}␊ |
216 | ␊ |
217 | ␉struct Loan* loan;␊ |
218 | ␉unsigned int maxSizeBookName=0;␊ |
219 | ␉// Compute max size␊ |
220 | ␉for (const auto& kv : loanedBooks)␊ |
221 | ␉{␊ |
222 | ␉ loan = kv.second;␊ |
223 | ␉ if (loan->bookName.size() > maxSizeBookName)␊ |
224 | ␉␉maxSizeBookName = loan->bookName.size();␊ |
225 | ␉}␊ |
226 | ␊ |
227 | ␉if (maxSizeBookName > MAX_SIZE_BOOK_NAME)␊ |
228 | ␉ maxSizeBookName = MAX_SIZE_BOOK_NAME;␊ |
229 | ␉else if ((maxSizeBookName % 2))␊ |
230 | ␉ maxSizeBookName++;␊ |
231 | ␊ |
232 | ␉// std::cout << " ID Book Expiration" << std::endl;␊ |
233 | ␉// std::cout << "------------------------------" << std::endl;␊ |
234 | ␊ |
235 | ␉int fillID, fillBookName, fillExpiration=(20 - 10)/2;␊ |
236 | ␉␊ |
237 | ␉fillID = (ID_HASH_SIZE - 2) / 2;␊ |
238 | ␉fillBookName = (maxSizeBookName - 4) / 2;␊ |
239 | ␊ |
240 | ␉std::cout.width (fillID);␊ |
241 | ␉std::cout << "";␊ |
242 | ␉std::cout << "ID" ;␊ |
243 | ␉std::cout.width (fillID);␊ |
244 | ␉std::cout << "";␊ |
245 | ␉std::cout << " " ;␊ |
246 | ␉␊ |
247 | ␉std::cout.width (fillBookName);␊ |
248 | ␉std::cout << "";␊ |
249 | ␉std::cout << "Book" ;␊ |
250 | ␉std::cout.width (fillBookName);␊ |
251 | ␉std::cout << "";␊ |
252 | ␉std::cout << " " ;␊ |
253 | ␊ |
254 | ␉std::cout.width (fillExpiration);␊ |
255 | ␉std::cout << "";␊ |
256 | ␉std::cout << "Expiration";␊ |
257 | ␉std::cout.width (fillExpiration);␊ |
258 | ␉std::cout << "" << std::endl;␊ |
259 | ␊ |
260 | ␉std::cout.fill ('-');␊ |
261 | ␉std::cout.width (ID_HASH_SIZE + 4 + maxSizeBookName + 4 + 20);␊ |
262 | ␉std::cout << "" << std::endl;␊ |
263 | ␉std::cout.fill (' ');␊ |
264 | ␊ |
265 | ␉std::string bookName;␊ |
266 | ␊ |
267 | ␉for (const auto& kv : loanedBooks)␊ |
268 | ␉{␊ |
269 | ␉ loan = kv.second;␊ |
270 | ␊ |
271 | ␉ std::cout << kv.first;␊ |
272 | ␉ std::cout << " ";␊ |
273 | ␊ |
274 | ␉ if (loan->bookName.size() > MAX_SIZE_BOOK_NAME)␊ |
275 | ␉␉bookName = std::string(loan->bookName.c_str(), MAX_SIZE_BOOK_NAME);␊ |
276 | ␉ else␊ |
277 | ␉␉bookName = loan->bookName;␊ |
278 | ␊ |
279 | ␉ std::cout << bookName;␊ |
280 | ␉ std::cout.width (maxSizeBookName - bookName.size());␊ |
281 | ␉ std::cout << "";␊ |
282 | ␉ std::cout << " ";␊ |
283 | ␉ ␊ |
284 | ␉ std::cout << loan->validity << std::endl;␊ |
285 | ␉}␊ |
286 | ␊ |
287 | ␉std::cout << std::endl;␊ |
288 | }␊ |
289 | ␊ |
290 | void returnBook(gourou::DRMProcessor& processor)␊ |
291 | {␊ |
292 | ␉struct Loan* loan = loanedBooks[std::string(returnID)];␊ |
293 | ␊ |
294 | ␉if (!loan)␊ |
295 | ␉{␊ |
296 | ␉ std::cout << "Error : Loan " << returnID << " doesn't exists" << std::endl;␊ |
297 | ␉ return;␊ |
298 | ␉}␊ |
299 | ␊ |
300 | ␉processor.returnLoan(loan->id, loan->operatorURL, notify);␊ |
301 | ␉␊ |
302 | ␉deleteID = returnID;␊ |
303 | ␉if (deleteLoan(false))␊ |
304 | ␉{␊ |
305 | ␉ std::cout << "Loan " << returnID << " successfully returned" << std::endl;␊ |
306 | ␉}␊ |
307 | }␊ |
308 | ␊ |
309 | bool deleteLoan(bool displayResult=true)␊ |
310 | {␊ |
311 | ␉struct Loan* loan = loanedBooks[std::string(deleteID)];␊ |
312 | ␊ |
313 | ␉if (!loan)␊ |
314 | ␉{␊ |
315 | ␉ std::cout << "Error : Loan " << deleteID << " doesn't exists" << std::endl;␊ |
316 | ␉ return false;␊ |
317 | ␉}␊ |
318 | ␊ |
319 | ␉if (unlink(loan->path.c_str()))␊ |
320 | ␉{␊ |
321 | ␉ std::cout << "Error : Cannot delete " << loan->path << std::endl;␊ |
322 | ␉ return false;␊ |
323 | ␉}␊ |
324 | ␉else if (displayResult)␊ |
325 | ␉{␊ |
326 | ␉ std::cout << "Loan " << deleteID << " deleted" << std::endl;␊ |
327 | ␉}␊ |
328 | ␉␊ |
329 | ␉return true;␊ |
330 | }␊ |
331 | ␊ |
332 | std::map<std::string, struct Loan*> loanedBooks;␊ |
333 | };␉ ␊ |
334 | ␊ |
335 | ␊ |
336 | static void usage(const char* cmd)␊ |
337 | {␊ |
338 | std::cout << basename((char*)cmd) << " manage loaned books" << std::endl << std::endl;␊ |
339 | ␊ |
340 | std::cout << "Usage: " << basename((char*)cmd) << " [OPTIONS]" << std::endl << std::endl;␊ |
341 | ␊ |
342 | std::cout << "Global Options:" << std::endl;␊ |
343 | std::cout << " " << "-l|--list" << "\t\t" << "List all loaned books" << std::endl;␊ |
344 | std::cout << " " << "-r|--return" << "\t\t" << "Return a loaned book" << std::endl;␊ |
345 | std::cout << " " << "-d|--delete" << "\t\t" << "Delete a loan entry without returning it" << std::endl;␊ |
346 | std::cout << " " << "-N|--no-notify" << "\t\t" << "Don't notify server, even if requested" << std::endl;␊ |
347 | std::cout << " " << "-v|--verbose" << "\t\t" << "Increase verbosity, can be set multiple times" << std::endl;␊ |
348 | std::cout << " " << "-V|--version" << "\t\t" << "Display libgourou version" << std::endl;␊ |
349 | std::cout << " " << "-h|--help" << "\t\t" << "This help" << std::endl;␊ |
350 | ␊ |
351 | std::cout << "ADEPT Options:" << std::endl;␊ |
352 | std::cout << " " << "-D|--adept-directory" << "\t" << ".adept directory that must contains device.xml, activation.xml and devicesalt" << std::endl;␊ |
353 | std::cout << std::endl;␊ |
354 | ␊ |
355 | std::cout << "Environment:" << std::endl;␊ |
356 | std::cout << "ADEPT directory is optional. If not set, it's looked into :" << std::endl;␊ |
357 | std::cout << " * $ADEPT_DIR environment variable" << std::endl;␊ |
358 | std::cout << " * /home/<user>/.config/adept" << std::endl;␊ |
359 | std::cout << " * Current directory" << std::endl;␊ |
360 | std::cout << " * .adept" << std::endl;␊ |
361 | std::cout << " * adobe-digital-editions directory" << std::endl;␊ |
362 | std::cout << " * .adobe-digital-editions directory" << std::endl;␊ |
363 | }␊ |
364 | ␊ |
365 | int main(int argc, char** argv)␊ |
366 | {␊ |
367 | int c, ret = -1;␊ |
368 | ␊ |
369 | const char** files[] = {&devicekeyFile, &deviceFile, &activationFile};␊ |
370 | int verbose = gourou::DRMProcessor::getLogLevel();␊ |
371 | int actions = 0;␊ |
372 | ␊ |
373 | while (1) {␊ |
374 | ␉int option_index = 0;␊ |
375 | ␉static struct option long_options[] = {␊ |
376 | ␉ {"adept-directory", required_argument, 0, 'D' },␊ |
377 | ␉ {"list", no_argument, 0, 'l' },␊ |
378 | ␉ {"return", no_argument, 0, 'r' },␊ |
379 | ␉ {"delete", no_argument, 0, 'd' },␊ |
380 | ␉ {"no-notify", no_argument, 0, 'N' },␊ |
381 | ␉ {"verbose", no_argument, 0, 'v' },␊ |
382 | ␉ {"version", no_argument, 0, 'V' },␊ |
383 | ␉ {"help", no_argument, 0, 'h' },␊ |
384 | ␉ {0, 0, 0, 0 }␊ |
385 | ␉};␊ |
386 | ␊ |
387 | ␉c = getopt_long(argc, argv, "d:lr:D:vVh",␊ |
388 | long_options, &option_index);␊ |
389 | ␉if (c == -1)␊ |
390 | ␉ break;␊ |
391 | ␊ |
392 | ␉switch (c) {␊ |
393 | ␉case 'D':␊ |
394 | ␉ adeptDir = optarg;␊ |
395 | ␉ break;␊ |
396 | ␉case 'l':␊ |
397 | ␉ list = true;␊ |
398 | ␉ actions++;␊ |
399 | ␉ break;␊ |
400 | ␉case 'r':␊ |
401 | ␉ returnID = optarg;␊ |
402 | ␉ actions++;␊ |
403 | ␉ break;␊ |
404 | ␉case 'd':␊ |
405 | ␉ deleteID = optarg;␊ |
406 | ␉ actions++;␊ |
407 | ␉ break;␊ |
408 | ␉case 'N':␊ |
409 | ␉ notify = false;␊ |
410 | ␉ break;␊ |
411 | ␉case 'v':␊ |
412 | ␉ verbose++;␊ |
413 | ␉ break;␊ |
414 | ␉case 'V':␊ |
415 | ␉ version();␊ |
416 | ␉ return 0;␊ |
417 | ␉case 'h':␊ |
418 | ␉ usage(argv[0]);␊ |
419 | ␉ return 0;␊ |
420 | ␉default:␊ |
421 | ␉ usage(argv[0]);␊ |
422 | ␉ return -1;␊ |
423 | ␉}␊ |
424 | }␊ |
425 | ␊ |
426 | gourou::DRMProcessor::setLogLevel(verbose);␊ |
427 | ␊ |
428 | // By default, simply list books loaned␊ |
429 | if (actions == 0)␊ |
430 | ␉list = true;␊ |
431 | else if (actions != 1)␊ |
432 | {␊ |
433 | ␉usage(argv[0]);␊ |
434 | ␉return -1;␊ |
435 | }␊ |
436 | ␊ |
437 | LoanMGT loanMGT;␊ |
438 | ␊ |
439 | int i;␊ |
440 | bool hasErrors = false;␊ |
441 | const char* orig;␊ |
442 | char *filename;␊ |
443 | for (i=0; i<(int)ARRAY_SIZE(files); i++)␊ |
444 | {␊ |
445 | ␉orig = *files[i];␊ |
446 | ␉␊ |
447 | ␉if (adeptDir)␊ |
448 | ␉{␊ |
449 | ␉ std::string path = std::string(adeptDir) + std::string("/") + orig;␊ |
450 | ␉ filename = strdup(path.c_str());␊ |
451 | ␉}␊ |
452 | ␉else␊ |
453 | ␉ filename = strdup(orig);␊ |
454 | ␉*files[i] = findFile(filename);␊ |
455 | ␉free(filename);␊ |
456 | ␉if (!*files[i])␊ |
457 | ␉{␊ |
458 | ␉ std::cout << "Error : " << orig << " doesn't exists, did you activate your device ?" << std::endl;␊ |
459 | ␉ hasErrors = true;␊ |
460 | ␉}␊ |
461 | }␊ |
462 | ␊ |
463 | if (hasErrors)␊ |
464 | {␊ |
465 | ␉// In case of adept dir was provided by user␊ |
466 | ␉adeptDir = 0;␊ |
467 | ␉goto end;␊ |
468 | }␊ |
469 | ␊ |
470 | if (adeptDir)␊ |
471 | ␉adeptDir = strdup(adeptDir); // For below free␊ |
472 | else␊ |
473 | {␊ |
474 | ␉adeptDir = strdup(deviceFile);␊ |
475 | ␉adeptDir = dirname(adeptDir);␊ |
476 | }␊ |
477 | ␊ |
478 | ret = loanMGT.run();␊ |
479 | ␊ |
480 | end:␊ |
481 | for (i=0; i<(int)ARRAY_SIZE(files); i++)␊ |
482 | {␊ |
483 | ␉if (*files[i])␊ |
484 | ␉ free((void*)*files[i]);␊ |
485 | }␊ |
486 | ␊ |
487 | if (adeptDir)␊ |
488 | ␉free(adeptDir);␊ |
489 | ␊ |
490 | return ret;␊ |
491 | }␊ |