|
|
@ -75,8 +75,26 @@ CONNECTION_SPECIFIC_HEADERS = [
|
|
|
|
def get_kobo_activated():
|
|
|
|
def get_kobo_activated():
|
|
|
|
return config.config_kobo_sync
|
|
|
|
return config.config_kobo_sync
|
|
|
|
|
|
|
|
|
|
|
|
def redirect_or_proxy_request(proxy=False):
|
|
|
|
|
|
|
|
if config.config_kobo_proxy or proxy == True:
|
|
|
|
def make_request_to_kobo_store(sync_token=None):
|
|
|
|
|
|
|
|
outgoing_headers = Headers(request.headers)
|
|
|
|
|
|
|
|
outgoing_headers.remove("Host")
|
|
|
|
|
|
|
|
if sync_token:
|
|
|
|
|
|
|
|
sync_token.set_kobo_store_header(outgoing_headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
store_response = requests.request(
|
|
|
|
|
|
|
|
method=request.method,
|
|
|
|
|
|
|
|
url=get_store_url_for_current_request(),
|
|
|
|
|
|
|
|
headers=outgoing_headers,
|
|
|
|
|
|
|
|
data=request.get_data(),
|
|
|
|
|
|
|
|
allow_redirects=False,
|
|
|
|
|
|
|
|
timeout=(2, 10)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return store_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def redirect_or_proxy_request():
|
|
|
|
|
|
|
|
if config.config_kobo_proxy:
|
|
|
|
if request.method == "GET":
|
|
|
|
if request.method == "GET":
|
|
|
|
return redirect(get_store_url_for_current_request(), 307)
|
|
|
|
return redirect(get_store_url_for_current_request(), 307)
|
|
|
|
if request.method == "DELETE":
|
|
|
|
if request.method == "DELETE":
|
|
|
@ -84,15 +102,7 @@ def redirect_or_proxy_request(proxy=False):
|
|
|
|
return make_response(jsonify({}))
|
|
|
|
return make_response(jsonify({}))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
|
|
|
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
|
|
|
outgoing_headers = Headers(request.headers)
|
|
|
|
store_response = make_request_to_kobo_store()
|
|
|
|
outgoing_headers.remove("Host")
|
|
|
|
|
|
|
|
store_response = requests.request(
|
|
|
|
|
|
|
|
method=request.method,
|
|
|
|
|
|
|
|
url=get_store_url_for_current_request(),
|
|
|
|
|
|
|
|
headers=outgoing_headers,
|
|
|
|
|
|
|
|
data=request.get_data(),
|
|
|
|
|
|
|
|
allow_redirects=False,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response_headers = store_response.headers
|
|
|
|
response_headers = store_response.headers
|
|
|
|
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
|
|
|
for header_key in CONNECTION_SPECIFIC_HEADERS:
|
|
|
@ -104,6 +114,7 @@ def redirect_or_proxy_request(proxy=False):
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
return make_response(jsonify({}))
|
|
|
|
return make_response(jsonify({}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.route("/v1/library/sync")
|
|
|
|
@kobo.route("/v1/library/sync")
|
|
|
|
@requires_kobo_auth
|
|
|
|
@requires_kobo_auth
|
|
|
|
@download_required
|
|
|
|
@download_required
|
|
|
@ -162,35 +173,24 @@ def HandleSyncRequest():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_sync_response(request, sync_token, entitlements):
|
|
|
|
def generate_sync_response(request, sync_token, entitlements):
|
|
|
|
# We first merge in sync results from the official Kobo store.
|
|
|
|
extra_headers = {}
|
|
|
|
outgoing_headers = Headers(request.headers)
|
|
|
|
if config.config_kobo_proxy:
|
|
|
|
outgoing_headers.remove("Host")
|
|
|
|
# Merge in sync results from the official Kobo store.
|
|
|
|
sync_token.set_kobo_store_header(outgoing_headers)
|
|
|
|
try:
|
|
|
|
store_response = requests.request(
|
|
|
|
store_response = make_request_to_kobo_store(sync_token)
|
|
|
|
method=request.method,
|
|
|
|
|
|
|
|
url=get_store_url_for_current_request(),
|
|
|
|
store_entitlements = store_response.json()
|
|
|
|
headers=outgoing_headers,
|
|
|
|
entitlements += store_entitlements
|
|
|
|
data=request.get_data(),
|
|
|
|
sync_token.merge_from_store_response(store_response)
|
|
|
|
)
|
|
|
|
extra_headers["x-kobo-sync"] = store_response.headers.get("x-kobo-sync")
|
|
|
|
|
|
|
|
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
|
|
|
|
|
|
|
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
|
|
|
|
|
|
|
sync_token.to_headers(extra_headers)
|
|
|
|
|
|
|
|
|
|
|
|
store_entitlements = store_response.json()
|
|
|
|
response = make_response(jsonify(entitlements), extra_headers)
|
|
|
|
entitlements += store_entitlements
|
|
|
|
|
|
|
|
sync_token.merge_from_store_response(store_response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response = make_response(jsonify(entitlements))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sync_token.to_headers(response.headers)
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
# These headers could probably use some more investigation.
|
|
|
|
|
|
|
|
response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"]
|
|
|
|
|
|
|
|
response.headers["x-kobo-sync-mode"] = store_response.headers[
|
|
|
|
|
|
|
|
"x-kobo-sync-mode"
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
response.headers["x-kobo-recent-reads"] = store_response.headers[
|
|
|
|
|
|
|
|
"x-kobo-recent-reads"
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
@ -379,6 +379,7 @@ def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_
|
|
|
|
log.debug("Alternative Request received:")
|
|
|
|
log.debug("Alternative Request received:")
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: Implement the following routes
|
|
|
|
# TODO: Implement the following routes
|
|
|
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
|
|
@ -386,9 +387,21 @@ def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_
|
|
|
|
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
|
|
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
|
|
|
def HandleUserRequest(dummy=None):
|
|
|
|
def HandleUserRequest(dummy=None):
|
|
|
|
log.debug("Unimplemented Request received: %s", request.base_url)
|
|
|
|
log.debug("Unimplemented User Request received: %s", request.base_url)
|
|
|
|
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
|
|
|
|
|
|
|
def HandleProductsRequest(dummy=None):
|
|
|
|
|
|
|
|
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.app_errorhandler(404)
|
|
|
|
@kobo.app_errorhandler(404)
|
|
|
|
def handle_404(err):
|
|
|
|
def handle_404(err):
|
|
|
|
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
|
|
|
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
|
|
@ -397,11 +410,9 @@ def handle_404(err):
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
|
|
|
def make_calibre_web_auth_response():
|
|
|
|
@requires_kobo_auth
|
|
|
|
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
|
|
|
def HandleAuthRequest():
|
|
|
|
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
|
|
|
# Missing feature: Authentication :)
|
|
|
|
|
|
|
|
log.debug('Kobo Auth request')
|
|
|
|
|
|
|
|
content = request.get_json()
|
|
|
|
content = request.get_json()
|
|
|
|
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
|
|
|
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
|
|
|
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
|
|
|
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
|
|
@ -419,12 +430,32 @@ def HandleAuthRequest():
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
|
|
|
|
|
|
|
@requires_kobo_auth
|
|
|
|
|
|
|
|
def HandleAuthRequest():
|
|
|
|
|
|
|
|
log.debug('Kobo Auth request')
|
|
|
|
|
|
|
|
if config.config_kobo_proxy:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
return redirect_or_proxy_request()
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
|
|
|
|
|
|
|
return make_calibre_web_auth_response()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_calibre_web_init_response(calibre_web_url):
|
|
|
|
|
|
|
|
resources = NATIVE_KOBO_RESOURCES(calibre_web_url)
|
|
|
|
|
|
|
|
response = make_response(jsonify({"Resources": resources}))
|
|
|
|
|
|
|
|
response.headers["x-kobo-apitoken"] = "e30="
|
|
|
|
return response
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@kobo.route("/v1/initialization")
|
|
|
|
@kobo.route("/v1/initialization")
|
|
|
|
@requires_kobo_auth
|
|
|
|
@requires_kobo_auth
|
|
|
|
def HandleInitRequest():
|
|
|
|
def HandleInitRequest():
|
|
|
|
|
|
|
|
log.info('Init')
|
|
|
|
|
|
|
|
|
|
|
|
if not current_app.wsgi_app.is_proxied:
|
|
|
|
if not current_app.wsgi_app.is_proxied:
|
|
|
|
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
|
|
|
log.debug('Kobo: Received unproxied request, changed request port to server port')
|
|
|
|
calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
|
|
|
|
calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
|
|
|
@ -434,34 +465,29 @@ def HandleInitRequest():
|
|
|
|
)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
calibre_web_url = url_for("web.index", _external=True).strip("/")
|
|
|
|
calibre_web_url = url_for("web.index", _external=True).strip("/")
|
|
|
|
if config.config_kobo_proxy:
|
|
|
|
|
|
|
|
outgoing_headers = Headers(request.headers)
|
|
|
|
|
|
|
|
outgoing_headers.remove("Host")
|
|
|
|
|
|
|
|
store_response = requests.request(
|
|
|
|
|
|
|
|
method=request.method,
|
|
|
|
|
|
|
|
url=get_store_url_for_current_request(),
|
|
|
|
|
|
|
|
headers=outgoing_headers,
|
|
|
|
|
|
|
|
data=request.get_data(),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
store_response_json = store_response.json()
|
|
|
|
if config.config_kobo_proxy:
|
|
|
|
if "Resources" in store_response_json:
|
|
|
|
try:
|
|
|
|
kobo_resources = store_response_json["Resources"]
|
|
|
|
store_response = make_request_to_kobo_store()
|
|
|
|
# calibre_web_url = url_for("web.index", _external=True).strip("/")
|
|
|
|
|
|
|
|
kobo_resources["image_host"] = calibre_web_url
|
|
|
|
store_response_json = store_response.json()
|
|
|
|
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
|
|
|
if "Resources" in store_response_json:
|
|
|
|
auth_token = kobo_auth.get_auth_token(),
|
|
|
|
kobo_resources = store_response_json["Resources"]
|
|
|
|
book_uuid="{ImageId}"))
|
|
|
|
# calibre_web_url = url_for("web.index", _external=True).strip("/")
|
|
|
|
kobo_resources["image_url_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
|
|
|
kobo_resources["image_host"] = calibre_web_url
|
|
|
|
auth_token = kobo_auth.get_auth_token(),
|
|
|
|
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
|
|
|
book_uuid="{ImageId}"))
|
|
|
|
auth_token = kobo_auth.get_auth_token(),
|
|
|
|
|
|
|
|
book_uuid="{ImageId}"))
|
|
|
|
|
|
|
|
kobo_resources["image_url_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
|
|
|
|
|
|
|
|
auth_token = kobo_auth.get_auth_token(),
|
|
|
|
|
|
|
|
book_uuid="{ImageId}"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return make_response(store_response_json, store_response.status_code)
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return make_calibre_web_init_response(calibre_web_url)
|
|
|
|
|
|
|
|
|
|
|
|
return make_response(store_response_json, store_response.status_code)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
resources = NATIVE_KOBO_RESOURCES(calibre_web_url)
|
|
|
|
|
|
|
|
response = make_response(jsonify({"Resources": resources}))
|
|
|
|
|
|
|
|
response.headers["x-kobo-apitoken"] = "e30="
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def NATIVE_KOBO_RESOURCES(calibre_web_url):
|
|
|
|
def NATIVE_KOBO_RESOURCES(calibre_web_url):
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|