mirror of
git://github.com/kovidgoyal/calibre.git
synced 2026-04-26 03:33:09 +02:00
Content server: Add a new setting to allow un-authenticated users from specific IP addresses to make changes to the calibre library
This commit is contained in:
parent
7158d21c93
commit
714bc11820
7 changed files with 2489 additions and 9 deletions
2420
src/backports/ipaddress.py
Normal file
2420
src/backports/ipaddress.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,7 @@
|
|||
from calibre.gui2.widgets import HistoryLineEdit
|
||||
from calibre.srv.code import custom_list_template as default_custom_list_template
|
||||
from calibre.srv.embedded import custom_list_template, search_the_net_urls
|
||||
from calibre.srv.loop import parse_trusted_ips
|
||||
from calibre.srv.library_broker import load_gui_libraries
|
||||
from calibre.srv.opts import change_settings, options, server_config
|
||||
from calibre.srv.users import (
|
||||
|
|
@ -1380,6 +1381,14 @@ def save_changes(self):
|
|||
)
|
||||
self.tabs_widget.setCurrentWidget(self.users_tab)
|
||||
return False
|
||||
if settings['trusted_ips']:
|
||||
try:
|
||||
tuple(parse_trusted_ips(settings['trusted_ips']))
|
||||
except Exception as e:
|
||||
error_dialog(
|
||||
self, _('Invalid trusted IPs'), str(e), show=True)
|
||||
return False
|
||||
|
||||
if not self.custom_list_tab.commit():
|
||||
return False
|
||||
if not self.search_net_tab.commit():
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ def allowed_book_ids(self, request_data, db):
|
|||
|
||||
def check_for_write_access(self, request_data):
|
||||
if not request_data.username:
|
||||
if request_data.is_local_connection and self.opts.local_write:
|
||||
if request_data.is_trusted_ip:
|
||||
return
|
||||
raise HTTPForbidden('Anonymous users are not allowed to make changes')
|
||||
if self.user_manager.is_readonly(request_data.username):
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ class RequestData(object): # {{{
|
|||
username = None
|
||||
|
||||
def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol,
|
||||
static_cache, opts, remote_addr, remote_port, is_local_connection, translator_cache,
|
||||
static_cache, opts, remote_addr, remote_port, is_trusted_ip, translator_cache,
|
||||
tdir, forwarded_for, request_original_uri=None):
|
||||
|
||||
(self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders,
|
||||
|
|
@ -228,7 +228,7 @@ def __init__(self, method, path, query, inheaders, request_body_file, outheaders
|
|||
response_protocol, static_cache, translator_cache
|
||||
)
|
||||
|
||||
self.remote_addr, self.remote_port, self.is_local_connection = remote_addr, remote_port, is_local_connection
|
||||
self.remote_addr, self.remote_port, self.is_trusted_ip = remote_addr, remote_port, is_trusted_ip
|
||||
self.forwarded_for = forwarded_for
|
||||
self.request_original_uri = request_original_uri
|
||||
self.opts = opts
|
||||
|
|
@ -446,7 +446,7 @@ def prepare_response(self, inheaders, request_body_file):
|
|||
data = RequestData(
|
||||
self.method, self.path, self.query, inheaders, request_body_file,
|
||||
outheaders, self.response_protocol, self.static_cache, self.opts,
|
||||
self.remote_addr, self.remote_port, self.is_local_connection,
|
||||
self.remote_addr, self.remote_port, self.is_trusted_ip,
|
||||
self.translator_cache, self.tdir, self.forwarded_for, self.request_original_uri
|
||||
)
|
||||
self.queue_job(self.run_request_handler, data)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
from calibre.utils.mdns import get_external_ip
|
||||
from polyglot.builtins import iteritems, unicode_type
|
||||
from polyglot.queue import Empty, Full
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
from backports import ipaddress
|
||||
|
||||
|
||||
READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT'
|
||||
WAKEUP, JOB_DONE = b'\0', b'\x01'
|
||||
|
|
@ -119,6 +124,33 @@ def readline(self):
|
|||
# }}}
|
||||
|
||||
|
||||
class BadIPSpec(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_trusted_ips(spec):
|
||||
for part in as_unicode(spec).split(','):
|
||||
part = part.strip()
|
||||
try:
|
||||
if '/' in part:
|
||||
yield ipaddress.ip_network(part)
|
||||
else:
|
||||
yield ipaddress.ip_address(part)
|
||||
except Exception as e:
|
||||
raise BadIPSpec(_('{0} is not a valid IP address/network, with error: {1}').format(part, e))
|
||||
|
||||
|
||||
def is_ip_trusted(remote_addr, trusted_ips):
|
||||
for tip in trusted_ips:
|
||||
if hasattr(tip, 'hosts'):
|
||||
if remote_addr in tip:
|
||||
return True
|
||||
else:
|
||||
if tip == remote_addr:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Connection(object): # {{{
|
||||
|
||||
def __init__(self, socket, opts, ssl_context, tdir, addr, pool, log, access_log, wakeup):
|
||||
|
|
@ -126,10 +158,13 @@ def __init__(self, socket, opts, ssl_context, tdir, addr, pool, log, access_log,
|
|||
try:
|
||||
self.remote_addr = addr[0]
|
||||
self.remote_port = addr[1]
|
||||
self.parsed_remote_addr = ipaddress.ip_address(as_unicode(self.remote_addr))
|
||||
except Exception:
|
||||
# In case addr is None, which can occassionally happen
|
||||
self.remote_addr = self.remote_port = None
|
||||
self.is_local_connection = self.remote_addr in ('127.0.0.1', '::1')
|
||||
# In case addr is None, which can occasionally happen
|
||||
self.remote_addr = self.remote_port = self.parsed_remote_addr = None
|
||||
self.is_trusted_ip = bool(self.opts.local_write and getattr(self.parsed_remote_addr, 'is_loopback', False))
|
||||
if not self.is_trusted_ip and self.opts.trusted_ips and self.parsed_remote_addr is not None:
|
||||
self.is_trusted_ip = is_ip_trusted(self.parsed_remote_addr, self.opts.trusted_ips)
|
||||
self.orig_send_bufsize = self.send_bufsize = 4096
|
||||
self.tdir = tdir
|
||||
self.wait_for = READ
|
||||
|
|
@ -347,6 +382,8 @@ def __init__(
|
|||
self.ready = False
|
||||
self.handler = handler
|
||||
self.opts = opts or Options()
|
||||
if self.opts.trusted_ips:
|
||||
self.opts.trusted_ips = tuple(parse_trusted_ips(self.opts.trusted_ips))
|
||||
self.log = log or ThreadSafeLog(level=ThreadSafeLog.DEBUG)
|
||||
self.jobs_manager = JobsManager(self.opts, self.log)
|
||||
self.access_log = access_log
|
||||
|
|
|
|||
|
|
@ -155,6 +155,17 @@ def __new__(cls, *args):
|
|||
' turning on this option means any program running on the computer'
|
||||
' can make changes to your calibre libraries.'),
|
||||
|
||||
_('Allow un-authenticated connections from specific IP addresses to make changes'),
|
||||
'trusted_ips', None,
|
||||
_('Normally, if you do not turn on authentication, the server operates in'
|
||||
' read-only mode, so as to not allow anonymous users to make changes to your'
|
||||
' calibre libraries. This option allows anybody connecting from the specified'
|
||||
' IP addresses to make changes. Must be a comma separated list of address or network specifications.'
|
||||
' This is useful if you want to run the server without authentication but still'
|
||||
' use calibredb to make changes to your calibre libraries. Note that'
|
||||
' turning on this option means anyone connecting from the specified IP addresses'
|
||||
' can make changes to your calibre libraries.'),
|
||||
|
||||
_('Path to user database'),
|
||||
'userdb', None,
|
||||
_('Path to a file in which to store the user and password information. Normally a'
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
from calibre.srv.handler import Handler
|
||||
from calibre.srv.http_response import create_http_handler
|
||||
from calibre.srv.library_broker import load_gui_libraries
|
||||
from calibre.srv.loop import ServerLoop
|
||||
from calibre.srv.loop import BadIPSpec, ServerLoop
|
||||
from calibre.srv.manage_users_cli import manage_users_cli
|
||||
from calibre.srv.opts import opts_to_parser
|
||||
from calibre.srv.users import connect
|
||||
|
|
@ -222,7 +222,10 @@ def main(args=sys.argv):
|
|||
raise SystemExit('The --log option must point to a file, not a directory')
|
||||
if opts.access_log and os.path.isdir(opts.access_log):
|
||||
raise SystemExit('The --access-log option must point to a file, not a directory')
|
||||
server = Server(libraries, opts)
|
||||
try:
|
||||
server = Server(libraries, opts)
|
||||
except BadIPSpec as e:
|
||||
raise SystemExit('{}'.format(e))
|
||||
if getattr(opts, 'daemonize', False):
|
||||
if not opts.log and not iswindows:
|
||||
raise SystemExit(
|
||||
|
|
|
|||
Loading…
Reference in a new issue