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