1 | /*␊ |
2 | Copyright (c) 2021, 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 | #include <libgen.h>␊ |
31 | ␊ |
32 | #include <iostream>␊ |
33 | #include <algorithm>␊ |
34 | ␊ |
35 | #include <libgourou.h>␊ |
36 | #include <libgourou_common.h>␊ |
37 | ␊ |
38 | #include "drmprocessorclientimpl.h"␊ |
39 | #include "utils_common.h"␊ |
40 | ␊ |
41 | static const char* deviceFile = "device.xml";␊ |
42 | static const char* activationFile = "activation.xml";␊ |
43 | static const char* devicekeyFile = "devicesalt";␊ |
44 | static const char* acsmFile = 0;␊ |
45 | static bool exportPrivateKey = false;␊ |
46 | static const char* outputFile = 0;␊ |
47 | static const char* outputDir = 0;␊ |
48 | static bool resume = false;␊ |
49 | static bool notify = true;␊ |
50 | ␊ |
51 | ␊ |
52 | class ACSMDownloader␊ |
53 | {␊ |
54 | public:␊ |
55 | ␊ |
56 | int run()␊ |
57 | {␊ |
58 | ␉int ret = 0;␊ |
59 | ␉try␊ |
60 | ␉{␊ |
61 | ␉ gourou::DRMProcessor processor(&client, deviceFile, activationFile, devicekeyFile);␊ |
62 | ␉ gourou::User* user = processor.getUser();␊ |
63 | ␉ ␊ |
64 | ␉ if (exportPrivateKey)␊ |
65 | ␉ {␊ |
66 | ␉␉std::string filename;␊ |
67 | ␉␉if (!outputFile)␊ |
68 | ␉␉ filename = std::string("Adobe_PrivateLicenseKey--") + user->getUsername() + ".der";␊ |
69 | ␉␉else␊ |
70 | ␉␉ filename = outputFile;␊ |
71 | ␉ ␊ |
72 | ␉␉if (outputDir)␊ |
73 | ␉␉{␊ |
74 | ␉␉ if (!fileExists(outputDir))␊ |
75 | ␉␉␉mkpath(outputDir);␊ |
76 | ␉␉ ␊ |
77 | ␉␉ filename = std::string(outputDir) + "/" + filename;␊ |
78 | ␉␉}␊ |
79 | ␊ |
80 | ␉␉processor.exportPrivateLicenseKey(filename);␊ |
81 | ␊ |
82 | ␉␉std::cout << "Private license key exported to " << filename << std::endl;␊ |
83 | ␉ }␊ |
84 | ␉ else␊ |
85 | ␉ {␊ |
86 | ␉␉gourou::FulfillmentItem* item = processor.fulfill(acsmFile, notify);␊ |
87 | ␊ |
88 | ␉␉std::string filename;␊ |
89 | ␉␉if (!outputFile)␊ |
90 | ␉␉{␊ |
91 | ␉␉ filename = item->getMetadata("title");␊ |
92 | ␉␉ if (filename == "")␊ |
93 | ␉␉␉filename = "output";␊ |
94 | ␉␉ else␊ |
95 | ␉␉ {␊ |
96 | ␉␉␉// Remove invalid characters␊ |
97 | ␉␉␉std::replace(filename.begin(), filename.end(), '/', '_');␊ |
98 | ␉␉ }␊ |
99 | ␉␉}␊ |
100 | ␉␉else␊ |
101 | ␉␉ filename = outputFile;␊ |
102 | ␉ ␊ |
103 | ␉␉if (outputDir)␊ |
104 | ␉␉{␊ |
105 | ␉␉ if (!fileExists(outputDir))␊ |
106 | ␉␉␉mkpath(outputDir);␊ |
107 | ␊ |
108 | ␉␉ filename = std::string(outputDir) + "/" + filename;␊ |
109 | ␉␉}␊ |
110 | ␉ ␊ |
111 | ␉␉gourou::DRMProcessor::ITEM_TYPE type = processor.download(item, filename, resume);␊ |
112 | ␊ |
113 | ␉␉if (!outputFile)␊ |
114 | ␉␉{␊ |
115 | ␉␉ std::string finalName = filename;␊ |
116 | ␉␉ if (type == gourou::DRMProcessor::ITEM_TYPE::PDF)␊ |
117 | ␉␉␉finalName += ".pdf";␊ |
118 | ␉␉ else␊ |
119 | ␉␉␉finalName += ".epub";␊ |
120 | ␉␉ rename(filename.c_str(), finalName.c_str());␊ |
121 | ␉␉ filename = finalName;␊ |
122 | ␉␉}␊ |
123 | ␉␉std::cout << "Created " << filename << std::endl;␊ |
124 | ␊ |
125 | ␉␉serializeLoanToken(item);␊ |
126 | ␉ }␊ |
127 | ␉} catch(std::exception& e)␊ |
128 | ␉{␊ |
129 | ␉ std::cout << e.what() << std::endl;␊ |
130 | ␉ ret = 1;␊ |
131 | ␉}␊ |
132 | ␊ |
133 | ␉return ret;␊ |
134 | }␊ |
135 | ␊ |
136 | void serializeLoanToken(gourou::FulfillmentItem* item)␊ |
137 | {␊ |
138 | ␉gourou::LoanToken* token = item->getLoanToken();␊ |
139 | ␊ |
140 | ␉// No loan token available␊ |
141 | ␉if (!token)␊ |
142 | ␉ return;␊ |
143 | ␊ |
144 | ␉pugi::xml_document doc;␊ |
145 | ␊ |
146 | ␉pugi::xml_node decl = doc.append_child(pugi::node_declaration);␊ |
147 | ␉decl.append_attribute("version") = "1.0";␊ |
148 | ␊ |
149 | ␉pugi::xml_node root = doc.append_child("loanToken");␊ |
150 | ␉gourou::appendTextElem(root, "id", (*token)["id"]);␊ |
151 | ␉gourou::appendTextElem(root, "operatorURL", (*token)["operatorURL"]);␊ |
152 | ␉gourou::appendTextElem(root, "validity", (*token)["validity"]);␊ |
153 | ␉gourou::appendTextElem(root, "name", item->getMetadata("title"));␊ |
154 | ␊ |
155 | ␉char * activationDir = strdup(deviceFile);␊ |
156 | ␉activationDir = dirname(activationDir);␊ |
157 | ␉␉␊ |
158 | ␉gourou::StringXMLWriter xmlWriter;␊ |
159 | ␉doc.save(xmlWriter, " ");␊ |
160 | ␉std::string xmlStr = xmlWriter.getResult();␊ |
161 | ␊ |
162 | ␉// Use first bytes of SHA1(id) as filename␊ |
163 | ␉unsigned char sha1[gourou::SHA1_LEN];␊ |
164 | ␉client.digest("SHA1", (unsigned char*)(*token)["id"].c_str(), (*token)["id"].size(), sha1);␊ |
165 | ␉gourou::ByteArray tmp(sha1, sizeof(sha1));␊ |
166 | ␉std::string filenameHex = tmp.toHex();␊ |
167 | ␉std::string filename(filenameHex.c_str(), ID_HASH_SIZE);␊ |
168 | ␉std::string fullPath = std::string(activationDir);␊ |
169 | ␉fullPath += std::string ("/") + std::string(LOANS_DIR);␊ |
170 | ␉mkpath(fullPath.c_str());␊ |
171 | ␉fullPath += filename + std::string(".xml");␊ |
172 | ␉gourou::writeFile(fullPath, xmlStr);␊ |
173 | ␊ |
174 | ␉std::cout << "Loan token serialized into " << fullPath << std::endl;␊ |
175 | ␊ |
176 | ␉free(activationDir);␊ |
177 | }␊ |
178 | ␊ |
179 | private:␊ |
180 | DRMProcessorClientImpl client;␊ |
181 | };␉ ␊ |
182 | ␊ |
183 | ␊ |
184 | static void usage(const char* cmd)␊ |
185 | { ␊ |
186 | std::cout << basename((char*)cmd) << " download EPUB file from ACSM request file" << std::endl << std::endl;␊ |
187 | std::cout << "Usage: " << basename((char*)cmd) << " [OPTIONS] file.acsm" << std::endl << std::endl;␊ |
188 | std::cout << "Global Options:" << std::endl;␊ |
189 | std::cout << " " << "-O|--output-dir" << "\t" << "Optional output directory were to put result (default ./)" << std::endl;␊ |
190 | std::cout << " " << "-o|--output-file" << "\t" << "Optional output filename (default <title.(epub|pdf|der)>)" << std::endl;␊ |
191 | std::cout << " " << "-f|--acsm-file" << "\t" << "Backward compatibility: ACSM request file for epub download" << std::endl;␊ |
192 | std::cout << " " << "-e|--export-private-key"<< "\t" << "Export private key in DER format" << std::endl;␊ |
193 | std::cout << " " << "-r|--resume" << "\t\t" << "Try to resume download (in case of previous failure)" << std::endl;␊ |
194 | std::cout << " " << "-N|--no-notify" << "\t\t" << "Don't notify server, even if requested" << std::endl;␊ |
195 | std::cout << " " << "-v|--verbose" << "\t\t" << "Increase verbosity, can be set multiple times" << std::endl;␊ |
196 | std::cout << " " << "-V|--version" << "\t\t" << "Display libgourou version" << std::endl;␊ |
197 | std::cout << " " << "-h|--help" << "\t\t" << "This help" << std::endl;␊ |
198 | ␊ |
199 | std::cout << "ADEPT Options:" << std::endl;␊ |
200 | std::cout << " " << "-D|--adept-directory" << "\t" << ".adept directory that must contains device.xml, activation.xml and devicesalt" << std::endl;␊ |
201 | std::cout << " " << "-d|--device-file" << "\t" << "device.xml file from eReader" << std::endl;␊ |
202 | std::cout << " " << "-a|--activation-file" << "\t" << "activation.xml file from eReader" << std::endl;␊ |
203 | std::cout << " " << "-k|--device-key-file" << "\t" << "private device key file (eg devicesalt/devkey.bin) from eReader" << std::endl;␊ |
204 | ␊ |
205 | std::cout << std::endl;␊ |
206 | ␊ |
207 | std::cout << "Environment:" << std::endl;␊ |
208 | std::cout << "Device file, activation file and device key file are optionals. If not set, they are looked into :" << std::endl << std::endl;␊ |
209 | std::cout << " * $ADEPT_DIR environment variable" << std::endl;␊ |
210 | std::cout << " * /home/<user>/.config/adept" << std::endl;␊ |
211 | std::cout << " * Current directory" << std::endl;␊ |
212 | std::cout << " * .adept" << std::endl;␊ |
213 | std::cout << " * adobe-digital-editions directory" << std::endl;␊ |
214 | std::cout << " * .adobe-digital-editions directory" << std::endl;␊ |
215 | }␊ |
216 | ␊ |
217 | int main(int argc, char** argv)␊ |
218 | {␊ |
219 | int c, ret = -1;␊ |
220 | std::string _deviceFile, _activationFile, _devicekeyFile;␊ |
221 | ␊ |
222 | const char** files[] = {&devicekeyFile, &deviceFile, &activationFile};␊ |
223 | int verbose = gourou::DRMProcessor::getLogLevel();␊ |
224 | ␊ |
225 | while (1) {␊ |
226 | ␉int option_index = 0;␊ |
227 | ␉static struct option long_options[] = {␊ |
228 | ␉ {"adept-directory", required_argument, 0, 'D' },␊ |
229 | ␉ {"device-file", required_argument, 0, 'd' },␊ |
230 | ␉ {"activation-file", required_argument, 0, 'a' },␊ |
231 | ␉ {"device-key-file", required_argument, 0, 'k' },␊ |
232 | ␉ {"output-dir", required_argument, 0, 'O' },␊ |
233 | ␉ {"output-file", required_argument, 0, 'o' },␊ |
234 | ␉ {"acsm-file", required_argument, 0, 'f' },␊ |
235 | ␉ {"export-private-key",no_argument, 0, 'e' },␊ |
236 | ␉ {"resume", no_argument, 0, 'r' },␊ |
237 | ␉ {"no-notify", no_argument, 0, 'N' },␊ |
238 | ␉ {"verbose", no_argument, 0, 'v' },␊ |
239 | ␉ {"version", no_argument, 0, 'V' },␊ |
240 | ␉ {"help", no_argument, 0, 'h' },␊ |
241 | ␉ {0, 0, 0, 0 }␊ |
242 | ␉};␊ |
243 | ␊ |
244 | ␉c = getopt_long(argc, argv, "D:d:a:k:O:o:f:erNvVh",␊ |
245 | long_options, &option_index);␊ |
246 | ␉if (c == -1)␊ |
247 | ␉ break;␊ |
248 | ␊ |
249 | ␉switch (c) {␊ |
250 | ␉case 'D':␊ |
251 | ␉ _deviceFile = std::string(optarg) + "/device.xml";␊ |
252 | ␉ _activationFile = std::string(optarg) + "/activation.xml";␊ |
253 | ␉ _devicekeyFile = std::string(optarg) + "/devicesalt";␊ |
254 | ␉ deviceFile = _deviceFile.c_str();␊ |
255 | ␉ activationFile = _activationFile.c_str();␊ |
256 | ␉ devicekeyFile = _devicekeyFile.c_str();␊ |
257 | ␉ break;␊ |
258 | ␉case 'd':␊ |
259 | ␉ deviceFile = optarg;␊ |
260 | ␉ break;␊ |
261 | ␉case 'a':␊ |
262 | ␉ activationFile = optarg;␊ |
263 | ␉ break;␊ |
264 | ␉case 'k':␊ |
265 | ␉ devicekeyFile = optarg;␊ |
266 | ␉ break;␊ |
267 | ␉case 'f':␊ |
268 | ␉ acsmFile = optarg;␊ |
269 | ␉ break;␊ |
270 | ␉case 'O':␊ |
271 | ␉ outputDir = optarg;␊ |
272 | ␉ break;␊ |
273 | ␉case 'o':␊ |
274 | ␉ outputFile = optarg;␊ |
275 | ␉ break;␊ |
276 | ␉case 'e':␊ |
277 | ␉ exportPrivateKey = true;␊ |
278 | ␉ break;␊ |
279 | ␉case 'r':␊ |
280 | ␉ resume = true;␊ |
281 | ␉ break;␊ |
282 | ␉case 'N':␊ |
283 | ␉ notify = false;␊ |
284 | ␉ break;␊ |
285 | ␉case 'v':␊ |
286 | ␉ verbose++;␊ |
287 | ␉ break;␊ |
288 | ␉case 'V':␊ |
289 | ␉ version();␊ |
290 | ␉ return 0;␊ |
291 | ␉case 'h':␊ |
292 | ␉ usage(argv[0]);␊ |
293 | ␉ return 0;␊ |
294 | ␉default:␊ |
295 | ␉ usage(argv[0]);␊ |
296 | ␉ return -1;␊ |
297 | ␉}␊ |
298 | }␊ |
299 | ␊ |
300 | gourou::DRMProcessor::setLogLevel(verbose);␊ |
301 | ␊ |
302 | if (optind == argc-1)␊ |
303 | ␉acsmFile = argv[optind];␊ |
304 | ␊ |
305 | if ((!acsmFile && !exportPrivateKey) || (outputDir && !outputDir[0]) ||␊ |
306 | ␉(outputFile && !outputFile[0]))␊ |
307 | {␊ |
308 | ␉usage(argv[0]);␊ |
309 | ␉return -1;␊ |
310 | }␊ |
311 | ␊ |
312 | ACSMDownloader downloader;␊ |
313 | ␊ |
314 | int i;␊ |
315 | bool hasErrors = false;␊ |
316 | const char* orig;␊ |
317 | for (i=0; i<(int)ARRAY_SIZE(files); i++)␊ |
318 | {␊ |
319 | ␉orig = *files[i];␊ |
320 | ␉*files[i] = findFile(*files[i]);␊ |
321 | ␉if (!*files[i])␊ |
322 | ␉{␊ |
323 | ␉ std::cout << "Error : " << orig << " doesn't exists, did you activate your device ?" << std::endl;␊ |
324 | ␉ ret = -1;␊ |
325 | ␉ hasErrors = true;␊ |
326 | ␉}␊ |
327 | }␊ |
328 | ␊ |
329 | if (hasErrors)␊ |
330 | ␉goto end;␊ |
331 | ␊ |
332 | if (exportPrivateKey)␊ |
333 | {␊ |
334 | ␉if (acsmFile)␊ |
335 | ␉{␊ |
336 | ␉ usage(argv[0]);␊ |
337 | ␉ return -1;␊ |
338 | ␉}␊ |
339 | }␊ |
340 | else␊ |
341 | {␊ |
342 | ␉if (!fileExists(acsmFile))␊ |
343 | ␉{␊ |
344 | ␉ std::cout << "Error : " << acsmFile << " doesn't exists" << std::endl;␊ |
345 | ␉ ret = -1;␊ |
346 | ␉ goto end;␊ |
347 | ␉}␊ |
348 | }␊ |
349 | ␊ |
350 | ret = downloader.run();␊ |
351 | ␊ |
352 | end:␊ |
353 | for (i=0; i<(int)ARRAY_SIZE(files); i++)␊ |
354 | {␊ |
355 | ␉if (*files[i])␊ |
356 | ␉ free((void*)*files[i]);␊ |
357 | }␊ |
358 | ␊ |
359 | return ret;␊ |
360 | }␊ |