Update loanID generation code

Should fix #31. Apparently I implemented the loanID code
wrong, that sometimes caused book returns to fail (or even
worse, return the wrong book) if you had multiple active
loans from the same distributor.
Also adds a test case to catch this bug should it ever
occur again.
This commit is contained in:
Florian Bach 2022-09-04 11:13:53 +02:00
parent 396f0cfad0
commit c6b9e5c59b
4 changed files with 136 additions and 20 deletions

View File

@ -40,7 +40,7 @@ See the "LICENSE" file for a full copy of the GNU GPL v3.
## Known bugs ## Known bugs
- Returning an eBook to a library running Adobe Content Server 6 (ACS6) or newer will fail. The plugin will claim the book return was successful, but the book won't be marked as returned on the libraries' servers. This will hopefully be fixed with the next version of the plugin - I already dumped a bunch of logs from ADE so I know what my plugin is missing, now I just need to implement the fix and see if it works. It might be a good idea to stop returning books with the current version of the plugin, as that'd be a difference that the libraries could detect as doing something weird, if they're using ACS6. - Versions 0.0.16 and below did sometimes return the wrong eBook (or none at all) when trying to return a book to the library through the "Loaned books" list, if you had multiple active loans from the same distributor / library. This will be fixed with 0.0.17.
## Setup ## Setup

View File

@ -41,6 +41,9 @@
# fix broken URLs with missing protocol, fix loan data for loans without device ID, # fix broken URLs with missing protocol, fix loan data for loans without device ID,
# fix nonce calculation yet again, merge #26 to make importing a WINE auth more reliable, # fix nonce calculation yet again, merge #26 to make importing a WINE auth more reliable,
# update python-oscrypto to unofficial fork to fix OpenSSL 3 support. # update python-oscrypto to unofficial fork to fix OpenSSL 3 support.
# In Progress:
# Fix bug that would sometimes return the wrong book (or none at all) if you had
# multiple active loans from the same distributor.
PLUGIN_NAME = "DeACSM" PLUGIN_NAME = "DeACSM"

View File

@ -482,32 +482,28 @@ def fulfill(acsm_file, do_notify = False):
def updateLoanReturnData(fulfillmentResultToken): def updateLoanReturnData(fulfillmentResultToken, forceTestBehaviour=False):
NSMAP = { "adept" : "http://ns.adobe.com/adept" } NSMAP = { "adept" : "http://ns.adobe.com/adept" }
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag) adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
dcNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag) dcNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
try: try:
loanToken = fulfillmentResultToken.find("./%s" % (adNS("loanToken"))) fulfillment_id = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("fulfillment"))).text
if (loanToken is None): if (fulfillment_id is None):
print("Loan token not found") print("Fulfillment ID not found, can't generate loan token")
return False return False
except: except:
print("Loan token error") print("Loan token error")
return False return False
try: try:
operatorURL = loanToken.find("./%s" % (adNS("operatorURL"))).text operatorURL = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("operatorURL"))).text
except: except:
print("OperatorURL missing") print("OperatorURL missing")
return False return False
try:
loanID = None
loanID = loanToken.findall("./%s" % (adNS("loan")))[0].text
except:
pass
book_name = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata"), dcNS("title"))).text book_name = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata"), dcNS("title"))).text
@ -539,6 +535,18 @@ def updateLoanReturnData(fulfillmentResultToken):
# "loanID" is the loan ID # "loanID" is the loan ID
# "validUntil" is how long it's valid # "validUntil" is how long it's valid
new_loan_record = {
"book_name": book_name,
"user": userUUID,
"device": deviceUUID,
"loanID": fulfillment_id,
"operatorURL": operatorURL,
"validUntil": dsp_until
}
if forceTestBehaviour:
return new_loan_record
try: try:
import calibre_plugins.deacsm.prefs as prefs # type: ignore import calibre_plugins.deacsm.prefs as prefs # type: ignore
deacsmprefs = prefs.DeACSM_Prefs() deacsmprefs = prefs.DeACSM_Prefs()
@ -552,14 +560,7 @@ def updateLoanReturnData(fulfillmentResultToken):
# books, and can then return them. # books, and can then return them.
# Also, the config widget is responsible for cleaning up that list. # Also, the config widget is responsible for cleaning up that list.
deacsmprefs["list_of_rented_books"].append({ deacsmprefs["list_of_rented_books"].append(new_loan_record)
"book_name": book_name,
"user": userUUID,
"device": deviceUUID,
"loanID": loanID,
"operatorURL": operatorURL,
"validUntil": dsp_until
})
deacsmprefs.writeprefs() deacsmprefs.writeprefs()
@ -762,7 +763,9 @@ def performFulfillmentNotification(fulfillmentResultToken, forceOptional = False
doc_send = "<?xml version=\"1.0\"?>\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8") doc_send = "<?xml version=\"1.0\"?>\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
# Debug: Print notify request # Debug: Print notify request
#print(doc_send) if (verbose_logging):
print("Notify payload XML:")
print(doc_send)
try: try:
code, msg = sendRequestDocuRC(doc_send, url) code, msg = sendRequestDocuRC(doc_send, url)

View File

@ -172,6 +172,37 @@ class TestAdobe(unittest.TestCase):
self.assertEqual(sha_hash, "3452e3d11cdd70eb90323f291c06afafe10e098a", "Invalid SHA hash for node signing") self.assertEqual(sha_hash, "3452e3d11cdd70eb90323f291c06afafe10e098a", "Invalid SHA hash for node signing")
def test_hash_node_returnbugfix(self):
'''Check if the XML hash is correct when returning a book ...'''
# I don't think there's ever a case where the hashing algorithm is different,
# but I needed this test during debugging and thought, hey, why not leave it in.
mock_xml_str = """
<adept:notification xmlns:adept="http://ns.adobe.com/adept">
<adept:user>urn:uuid:6e5393e0-ff13-4ae8-8f6c-6654182ac7d5</adept:user>
<adept:device>urn:uuid:51abfbaf-f0e8-474d-b031-626c5224f90f</adept:device>
<adept:nonce>eVr2pi26AAAAAAAA</adept:nonce>
<adept:expiration>2022-08-03T09:16:22Z</adept:expiration>
<body xmlns="http://ns.adobe.com/adept">
<fulfillment>6ccfbc7a-349b-40ad-82d8-d7a4c717ca13-00000271</fulfillment>
<transaction>237493726-1749302749327354-Wed Aug 03 09:16:22 UTC 2022</transaction>
<user>urn:uuid:6e5393e0-ff13-4ae8-8f6c-6654182ac7d5</user>
<fulfilled>true</fulfilled>
<returned>true</returned>
<hmac>CB3Ql1FAJD957t5n749q5ZO8IzU=</hmac>
</body>
</adept:notification>
"""
mock_xml_obj = etree.fromstring(mock_xml_str)
sha_hash = libadobe.hash_node(mock_xml_obj).hexdigest().lower()
self.assertEqual(sha_hash, "8b0a24ba37c4333d93650c6ce52f8ee779f21533", "Invalid SHA hash for node signing")
def test_sign_node_old(self): def test_sign_node_old(self):
'''Check if the external RSA library (unused) signs correctly''' '''Check if the external RSA library (unused) signs correctly'''
@ -324,6 +355,85 @@ class TestAdobe(unittest.TestCase):
self.assertEqual(binascii.hexlify(msg), binascii.hexlify(expected_msg), "devkey encryption returned invalid result") self.assertEqual(binascii.hexlify(msg), binascii.hexlify(expected_msg), "devkey encryption returned invalid result")
class TestPluginInterface(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def forcefail(self):
self.assertEqual(1, 2, "force fail")
def test_loanReturnFulfillmentID(self):
'''Check if proper ID is used for the loan token'''
# Previous versions of the plugin had a bug where sometimes the wrong loan token
# was used, which caused wrong (or no) books to be returned to a library.
# Adding a test case so this never happens again ...
mock_data = """
<envelope xmlns="http://ns.adobe.com/adept">
<fulfillmentResult>
<fulfillment>34659b20-92c8-4004-9fd8-c5174e7eed47-00010214</fulfillment>
<returnable>true</returnable>
<initial>false</initial>
<resourceItemInfo>
<resource>urn:uuid:b7c6ccb8-1012-44a9-9c8b-0388d0c685f7</resource>
<resourceItem>0</resourceItem>
<metadata>
<dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">Book title for test</dc:title>
</metadata>
<licenseToken>
<user>urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa</user>
<resource>urn:uuid:b7c6ccb8-1012-44a9-9c8b-0388d0c685f7</resource>
<deviceType>standalone</deviceType>
<device>urn:uuid:83681cbb-b6df-44a3-a423-c2b37ba66e84</device>
<operatorURL>https://acs.example.com/fulfillment</operatorURL>
<fulfillment>34659b20-92c8-4004-9fd8-c5174e7eed47-00010214</fulfillment>
<distributor>urn:uuid:1f5c2437-58f2-4a24-9495-3e99155e6f98</distributor>
<permissions>
<display>
<loan>34659b20-92c8-4004-9fd8-c5174e7eed47-00010214</loan>
<until>2022-07-03T01:14:42Z</until>
</display>
</permissions>
</licenseToken>
</resourceItemInfo>
</fulfillmentResult>
<loanToken>
<time>2022-07-01T03:17:22+00:00</time>
<user>urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa</user>
<operatorURL>https://acs.example.com/fulfillment</operatorURL>
<licenseURL>https://nasigningservice.adobe.com/licensesign</licenseURL>
<loan>6d2dc249-2bc0-43e1-a130-3866f85020d9-00003487</loan>
<loan>6d2dc249-2bc0-43e1-a130-3866f85020d9-00003467</loan>
<loan>34659b20-92c8-4004-9fd8-c5174e7eed47-00010214</loan>
<loan>11197eb9-3543-4b41-9c6e-03ffeaf277c0-00024754</loan>
</loanToken>
</envelope>
"""
extracted_token = libadobeFulfill.updateLoanReturnData(etree.fromstring(mock_data), forceTestBehaviour=True)
expected_token = {
"book_name": "Book title for test",
"device": 'urn:uuid:83681cbb-b6df-44a3-a423-c2b37ba66e84',
"user": 'urn:uuid:2bd57a81-6192-4a1b-8eb2-64e2d197f9fa',
"operatorURL": 'https://acs.example.com/fulfillment',
"loanID": '34659b20-92c8-4004-9fd8-c5174e7eed47-00010214',
"validUntil": '2022-07-03T01:14:42Z'
}
self.assertEqual(extracted_token, expected_token, "Loan record generator broken")
class TestOther(unittest.TestCase): class TestOther(unittest.TestCase):