Added 2 new kobo settings: Enable Kobo Sync (currently not working) and proxy Requests to Kobo

Added fix for kobo reader generating requests without right port number, causing url_for not working correct
pull/1179/head
Ozzieisaacs 4 years ago
parent a986faea56
commit 0411d4a8c9

@ -532,6 +532,9 @@ def _configuration_update_helper():
_config_checkbox_int("config_uploading") _config_checkbox_int("config_uploading")
_config_checkbox_int("config_anonbrowse") _config_checkbox_int("config_anonbrowse")
_config_checkbox_int("config_public_reg") _config_checkbox_int("config_public_reg")
_config_checkbox_int("config_kobo_sync")
_config_checkbox_int("config_kobo_proxy")
_config_int("config_ebookconverter") _config_int("config_ebookconverter")
_config_string("config_calibre") _config_string("config_calibre")

@ -68,6 +68,7 @@ class _Settings(_Base):
config_anonbrowse = Column(SmallInteger, default=0) config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0)
config_remote_login = Column(Boolean, default=False) config_remote_login = Column(Boolean, default=False)
config_kobo_sync = Column(Boolean, default=False)
config_default_role = Column(SmallInteger, default=0) config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=6143) config_default_show = Column(SmallInteger, default=6143)
@ -89,7 +90,8 @@ class _Settings(_Base):
config_login_type = Column(Integer, default=0) config_login_type = Column(Integer, default=0)
# config_oauth_provider = Column(Integer) config_kobo_proxy = Column(Boolean, default=False)
config_ldap_provider_url = Column(String, default='localhost') config_ldap_provider_url = Column(String, default='localhost')
config_ldap_port = Column(SmallInteger, default=389) config_ldap_port = Column(SmallInteger, default=389)

@ -19,7 +19,6 @@
import sys import sys
import uuid import uuid
from datetime import datetime
from time import gmtime, strftime from time import gmtime, strftime
try: try:
from urllib import unquote from urllib import unquote
@ -34,6 +33,7 @@ from flask import (
current_app, current_app,
url_for, url_for,
redirect, redirect,
abort
) )
from flask_login import login_required from flask_login import login_required
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
@ -44,7 +44,7 @@ from . import config, logger, kobo_auth, db, helper
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
from .web import download_required from .web import download_required
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["KEPUB"]} KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
KOBO_STOREAPI_URL = "https://storeapi.kobo.com" KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>") kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
@ -71,31 +71,33 @@ CONNECTION_SPECIFIC_HEADERS = [
def redirect_or_proxy_request(): def redirect_or_proxy_request():
if request.method == "GET": if config.config_kobo_proxy:
return redirect(get_store_url_for_current_request(), 307) if request.method == "GET":
if request.method == "DELETE": return redirect(get_store_url_for_current_request(), 307)
log.info('Delete Book') if request.method == "DELETE":
return make_response(jsonify({})) log.info('Delete Book')
else: return make_response(jsonify({}))
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves. else:
outgoing_headers = Headers(request.headers) # The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
outgoing_headers.remove("Host") outgoing_headers = Headers(request.headers)
store_response = requests.request( outgoing_headers.remove("Host")
method=request.method, store_response = requests.request(
url=get_store_url_for_current_request(), method=request.method,
headers=outgoing_headers, url=get_store_url_for_current_request(),
data=request.get_data(), headers=outgoing_headers,
allow_redirects=False, data=request.get_data(),
) allow_redirects=False,
)
response_headers = store_response.headers
for header_key in CONNECTION_SPECIFIC_HEADERS:
response_headers.pop(header_key, default=None)
return make_response( response_headers = store_response.headers
store_response.content, store_response.status_code, response_headers.items() for header_key in CONNECTION_SPECIFIC_HEADERS:
) response_headers.pop(header_key, default=None)
return make_response(
store_response.content, store_response.status_code, response_headers.items()
)
else:
return make_response(jsonify({}))
@kobo.route("/v1/library/sync") @kobo.route("/v1/library/sync")
@login_required @login_required
@ -103,6 +105,8 @@ def redirect_or_proxy_request():
def HandleSyncRequest(): def HandleSyncRequest():
sync_token = SyncToken.SyncToken.from_headers(request.headers) sync_token = SyncToken.SyncToken.from_headers(request.headers)
log.info("Kobo library sync request received.") log.info("Kobo library sync request received.")
if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to server port')
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header # TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
# instead so that the device triggers another sync. # instead so that the device triggers another sync.
@ -145,30 +149,33 @@ def HandleSyncRequest():
sync_token.books_last_created = new_books_last_created sync_token.books_last_created = new_books_last_created
sync_token.books_last_modified = new_books_last_modified sync_token.books_last_modified = new_books_last_modified
if config.config_kobo_proxy:
return generate_sync_response(request, sync_token, entitlements)
return make_response(jsonify(entitlements))
# Missing feature: Detect server-side book deletions. # Missing feature: Detect server-side book deletions.
return generate_sync_response(request, sync_token, entitlements)
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. # We first merge in sync results from the official Kobo store.
#outgoing_headers = Headers(request.headers) outgoing_headers = Headers(request.headers)
#outgoing_headers.remove("Host") outgoing_headers.remove("Host")
#sync_token.set_kobo_store_header(outgoing_headers) sync_token.set_kobo_store_header(outgoing_headers)
#store_response = requests.request( store_response = requests.request(
# method=request.method, method=request.method,
# url=get_store_url_for_current_request(), url=get_store_url_for_current_request(),
# headers=outgoing_headers, headers=outgoing_headers,
# data=request.get_data(), data=request.get_data(),
#) )
#store_entitlements = store_response.json() store_entitlements = store_response.json()
#entitlements += store_entitlements entitlements += store_entitlements
#sync_token.merge_from_store_response(store_response) sync_token.merge_from_store_response(store_response)
response = make_response(jsonify(entitlements)) response = make_response(jsonify(entitlements))
# sync_token.to_headers(request.headers)
# sync_token.to_headers(response.headers) sync_token.to_headers(response.headers)
'''try: try:
# These headers could probably use some more investigation. # These headers could probably use some more investigation.
response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"] response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"]
response.headers["x-kobo-sync-mode"] = store_response.headers[ response.headers["x-kobo-sync-mode"] = store_response.headers[
@ -178,7 +185,7 @@ def generate_sync_response(request, sync_token, entitlements):
"x-kobo-recent-reads" "x-kobo-recent-reads"
] ]
except KeyError: except KeyError:
pass''' pass
return response return response
@ -187,6 +194,8 @@ def generate_sync_response(request, sync_token, entitlements):
@login_required @login_required
@download_required @download_required
def HandleMetadataRequest(book_uuid): def HandleMetadataRequest(book_uuid):
if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to server port')
log.info("Kobo library metadata request received for book %s" % book_uuid) log.info("Kobo library metadata request received for book %s" % book_uuid)
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
if not book or not book.data: if not book or not book.data:
@ -199,7 +208,6 @@ def HandleMetadataRequest(book_uuid):
def get_download_url_for_book(book, book_format): def get_download_url_for_book(book, book_format):
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Received unproxied request, changed request port to server port')
return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format( return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format(
url_scheme=request.environ['wsgi.url_scheme'], url_scheme=request.environ['wsgi.url_scheme'],
url_base=request.environ['SERVER_NAME'], url_base=request.environ['SERVER_NAME'],
@ -344,7 +352,10 @@ def HandleCoverImageRequest(book_uuid):
book_uuid, use_generic_cover_on_failure=False book_uuid, use_generic_cover_on_failure=False
) )
if not book_cover: if not book_cover:
return redirect(get_store_url_for_current_request(), 307) if config.config_kobo_proxy:
return redirect(get_store_url_for_current_request(), 307)
else:
abort(404)
return book_cover return book_cover
@ -371,8 +382,7 @@ def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_
@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 Request received: %s", request.base_url)
return make_response(jsonify({})) 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):
@ -382,42 +392,46 @@ def handle_404(err):
return redirect_or_proxy_request() return redirect_or_proxy_request()
'''@kobo.route("/v1/initialization") @kobo.route("/v1/initialization")
@login_required @login_required
def HandleInitRequest(): def HandleInitRequest():
outgoing_headers = Headers(request.headers) if not current_app.wsgi_app.is_proxied:
outgoing_headers.remove("Host") log.debug('Kobo: Received unproxied request, changed request port to server port')
store_response = requests.request( calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
method=request.method, url_scheme=request.environ['wsgi.url_scheme'],
url=get_store_url_for_current_request(), url_base=request.environ['SERVER_NAME'],
headers=outgoing_headers, url_port=config.config_port
data=request.get_data(), )
) else:
store_response_json = store_response.json()
if "Resources" in store_response_json:
kobo_resources = store_response_json["Resources"]
calibre_web_url = url_for("web.index", _external=True).strip("/") calibre_web_url = url_for("web.index", _external=True).strip("/")
kobo_resources["image_host"] = calibre_web_url if config.config_kobo_proxy:
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, outgoing_headers = Headers(request.headers)
auth_token = kobo_auth.get_auth_token(), outgoing_headers.remove("Host")
book_uuid="{ImageId}")) store_response = requests.request(
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, method=request.method,
auth_token = kobo_auth.get_auth_token(), url=get_store_url_for_current_request(),
book_uuid="{ImageId}")) headers=outgoing_headers,
data=request.get_data(),
return make_response(store_response_json, store_response.status_code) )
'''
@kobo.route("/v1/initialization")
def HandleInitRequest():
resources = NATIVE_KOBO_RESOURCES(
calibre_web_url=url_for("web.index", _external=True).strip("/")
)
response = make_response(jsonify({"Resources": resources}))
response.headers["x-kobo-apitoken"] = "e30="
return response
store_response_json = store_response.json()
if "Resources" in store_response_json:
kobo_resources = store_response_json["Resources"]
# calibre_web_url = url_for("web.index", _external=True).strip("/")
kobo_resources["image_host"] = calibre_web_url
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
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)
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 {
@ -471,10 +485,12 @@ def NATIVE_KOBO_RESOURCES(calibre_web_url):
"giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem", "giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem",
"help_page": "http://www.kobo.com/help", "help_page": "http://www.kobo.com/help",
"image_host": calibre_web_url, "image_host": calibre_web_url,
"image_url_quality_template": calibre_web_url "image_url_quality_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
+ "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg", auth_token = kobo_auth.get_auth_token(),
"image_url_template": calibre_web_url book_uuid="{ImageId}")),
+ "/{ImageId}/{Width}/{Height}/false/image.jpg", "image_url_template": unquote(calibre_web_url + url_for("kobo.HandleCoverImageRequest",
auth_token = kobo_auth.get_auth_token(),
book_uuid="{ImageId}")),
"kobo_audiobooks_enabled": "False", "kobo_audiobooks_enabled": "False",
"kobo_audiobooks_orange_deal_enabled": "False", "kobo_audiobooks_orange_deal_enabled": "False",
"kobo_audiobooks_subscriptions_enabled": "False", "kobo_audiobooks_subscriptions_enabled": "False",

@ -62,7 +62,7 @@ from datetime import datetime
from os import urandom from os import urandom
from flask import g, Blueprint, url_for from flask import g, Blueprint, url_for
from flask_login import login_user, current_user, login_required from flask_login import login_user, login_required
from flask_babel import gettext as _ from flask_babel import gettext as _
from . import logger, ub, lm from . import logger, ub, lm
@ -102,8 +102,7 @@ def load_user_from_kobo_request(request):
login_user(user) login_user(user)
return user return user
log.info("Received Kobo request without a recognizable auth token.") log.info("Received Kobo request without a recognizable auth token.")
return None return
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth") kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")

@ -1,6 +1,6 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div class="discover"> <div class="discover" xmlns:text-indent="http://www.w3.org/1999/xhtml">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off"> <form role="form" method="POST" autocomplete="off">
<div class="panel-group"> <div class="panel-group">
@ -175,7 +175,7 @@
<label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label> <label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label>
</div> </div>
<div data-related="kobo-settings"> <div data-related="kobo-settings">
<div class="form-group"> <div class="form-group" style="text-indent:10px;">
<input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}> <input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}>
<label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label> <label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label>
</div> </div>

Loading…
Cancel
Save