mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04: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
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.preferences import AbortCommit, ConfigWidgetBase, test_widget
|
|||||||
from calibre.gui2.widgets import HistoryLineEdit
|
from calibre.gui2.widgets import HistoryLineEdit
|
||||||
from calibre.srv.code import custom_list_template as default_custom_list_template
|
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.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.library_broker import load_gui_libraries
|
||||||
from calibre.srv.opts import change_settings, options, server_config
|
from calibre.srv.opts import change_settings, options, server_config
|
||||||
from calibre.srv.users import (
|
from calibre.srv.users import (
|
||||||
@ -1380,6 +1381,14 @@ class ConfigWidget(ConfigWidgetBase):
|
|||||||
)
|
)
|
||||||
self.tabs_widget.setCurrentWidget(self.users_tab)
|
self.tabs_widget.setCurrentWidget(self.users_tab)
|
||||||
return False
|
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():
|
if not self.custom_list_tab.commit():
|
||||||
return False
|
return False
|
||||||
if not self.search_net_tab.commit():
|
if not self.search_net_tab.commit():
|
||||||
|
@ -108,7 +108,7 @@ class Context(object):
|
|||||||
|
|
||||||
def check_for_write_access(self, request_data):
|
def check_for_write_access(self, request_data):
|
||||||
if not request_data.username:
|
if not request_data.username:
|
||||||
if request_data.is_local_connection and self.opts.local_write:
|
if request_data.is_trusted_ip:
|
||||||
return
|
return
|
||||||
raise HTTPForbidden('Anonymous users are not allowed to make changes')
|
raise HTTPForbidden('Anonymous users are not allowed to make changes')
|
||||||
if self.user_manager.is_readonly(request_data.username):
|
if self.user_manager.is_readonly(request_data.username):
|
||||||
|
@ -219,7 +219,7 @@ class RequestData(object): # {{{
|
|||||||
username = None
|
username = None
|
||||||
|
|
||||||
def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol,
|
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):
|
tdir, forwarded_for, request_original_uri=None):
|
||||||
|
|
||||||
(self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders,
|
(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
|
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.forwarded_for = forwarded_for
|
||||||
self.request_original_uri = request_original_uri
|
self.request_original_uri = request_original_uri
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
@ -446,7 +446,7 @@ class HTTPConnection(HTTPRequest):
|
|||||||
data = RequestData(
|
data = RequestData(
|
||||||
self.method, self.path, self.query, inheaders, request_body_file,
|
self.method, self.path, self.query, inheaders, request_body_file,
|
||||||
outheaders, self.response_protocol, self.static_cache, self.opts,
|
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.translator_cache, self.tdir, self.forwarded_for, self.request_original_uri
|
||||||
)
|
)
|
||||||
self.queue_job(self.run_request_handler, data)
|
self.queue_job(self.run_request_handler, data)
|
||||||
|
@ -25,6 +25,11 @@ from calibre.utils.monotonic import monotonic
|
|||||||
from calibre.utils.mdns import get_external_ip
|
from calibre.utils.mdns import get_external_ip
|
||||||
from polyglot.builtins import iteritems, unicode_type
|
from polyglot.builtins import iteritems, unicode_type
|
||||||
from polyglot.queue import Empty, Full
|
from polyglot.queue import Empty, Full
|
||||||
|
try:
|
||||||
|
import ipaddress
|
||||||
|
except ImportError:
|
||||||
|
from backports import ipaddress
|
||||||
|
|
||||||
|
|
||||||
READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT'
|
READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT'
|
||||||
WAKEUP, JOB_DONE = b'\0', b'\x01'
|
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): # {{{
|
class Connection(object): # {{{
|
||||||
|
|
||||||
def __init__(self, socket, opts, ssl_context, tdir, addr, pool, log, access_log, wakeup):
|
def __init__(self, socket, opts, ssl_context, tdir, addr, pool, log, access_log, wakeup):
|
||||||
@ -126,10 +158,13 @@ class Connection(object): # {{{
|
|||||||
try:
|
try:
|
||||||
self.remote_addr = addr[0]
|
self.remote_addr = addr[0]
|
||||||
self.remote_port = addr[1]
|
self.remote_port = addr[1]
|
||||||
|
self.parsed_remote_addr = ipaddress.ip_address(as_unicode(self.remote_addr))
|
||||||
except Exception:
|
except Exception:
|
||||||
# In case addr is None, which can occassionally happen
|
# In case addr is None, which can occasionally happen
|
||||||
self.remote_addr = self.remote_port = None
|
self.remote_addr = self.remote_port = self.parsed_remote_addr = None
|
||||||
self.is_local_connection = self.remote_addr in ('127.0.0.1', '::1')
|
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.orig_send_bufsize = self.send_bufsize = 4096
|
||||||
self.tdir = tdir
|
self.tdir = tdir
|
||||||
self.wait_for = READ
|
self.wait_for = READ
|
||||||
@ -347,6 +382,8 @@ class ServerLoop(object):
|
|||||||
self.ready = False
|
self.ready = False
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.opts = opts or Options()
|
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.log = log or ThreadSafeLog(level=ThreadSafeLog.DEBUG)
|
||||||
self.jobs_manager = JobsManager(self.opts, self.log)
|
self.jobs_manager = JobsManager(self.opts, self.log)
|
||||||
self.access_log = access_log
|
self.access_log = access_log
|
||||||
|
@ -155,6 +155,17 @@ raw_options = (
|
|||||||
' turning on this option means any program running on the computer'
|
' turning on this option means any program running on the computer'
|
||||||
' can make changes to your calibre libraries.'),
|
' 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'),
|
_('Path to user database'),
|
||||||
'userdb', None,
|
'userdb', None,
|
||||||
_('Path to a file in which to store the user and password information. Normally a'
|
_('Path to a file in which to store the user and password information. Normally a'
|
||||||
|
@ -16,7 +16,7 @@ from calibre.srv.bonjour import BonJour
|
|||||||
from calibre.srv.handler import Handler
|
from calibre.srv.handler import Handler
|
||||||
from calibre.srv.http_response import create_http_handler
|
from calibre.srv.http_response import create_http_handler
|
||||||
from calibre.srv.library_broker import load_gui_libraries
|
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.manage_users_cli import manage_users_cli
|
||||||
from calibre.srv.opts import opts_to_parser
|
from calibre.srv.opts import opts_to_parser
|
||||||
from calibre.srv.users import connect
|
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')
|
raise SystemExit('The --log option must point to a file, not a directory')
|
||||||
if opts.access_log and os.path.isdir(opts.access_log):
|
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')
|
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 getattr(opts, 'daemonize', False):
|
||||||
if not opts.log and not iswindows:
|
if not opts.log and not iswindows:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user