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:
Kovid Goyal 2020-04-09 12:33:52 +05:30
parent 7158d21c93
commit 714bc11820
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 2489 additions and 9 deletions

2420
src/backports/ipaddress.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@ from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
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 @@ class ConfigWidget(ConfigWidgetBase):
)
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():

View File

@ -108,7 +108,7 @@ class Context(object):
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):

View File

@ -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 @@ class RequestData(object): # {{{
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 @@ class HTTPConnection(HTTPRequest):
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)

View File

@ -25,6 +25,11 @@ from calibre.utils.monotonic import monotonic
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 @@ class ReadBuffer(object): # {{{
# }}}
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 @@ class Connection(object): # {{{
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 @@ class ServerLoop(object):
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

View File

@ -155,6 +155,17 @@ raw_options = (
' 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'

View File

@ -16,7 +16,7 @@ from calibre.srv.bonjour import BonJour
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(