diff --git a/src/lib/Bcfg2/Client/Proxy.py b/src/lib/Bcfg2/Client/Proxy.py index 679b4c52b5..879bcaf1bb 100644 --- a/src/lib/Bcfg2/Client/Proxy.py +++ b/src/lib/Bcfg2/Client/Proxy.py @@ -242,6 +242,7 @@ def __init__(self, key=None, cert=None, ca=None, self.scns = scns self.timeout = timeout self.protocol = protocol + self.cookie = None def make_connection(self, host): host, self._extra_headers = self.get_host_info(host)[0:2] @@ -258,6 +259,8 @@ def request(self, host, handler, request_body, verbose=0): try: conn = self.send_request(host, handler, request_body, False) response = conn.getresponse() + if not self.cookie: + self.cookie = response.getheader('Set-Cookie') errcode = response.status errmsg = response.reason headers = response.msg @@ -285,6 +288,8 @@ def send_request(self, host, handler, request_body, debug): xmlrpclib.Transport.send_request(self, conn, handler, request_body) self.send_host(conn, host) self.send_user_agent(conn) + if self.cookie: + conn.putheader('Cookie', self.cookie) self.send_content(conn, request_body) return conn # pylint: enable=E1101 diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 892f2832a3..4c37ab655b 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -1393,7 +1393,11 @@ class NetworkCore(Core): Bcfg2.Options.Option( cf=('server', 'group'), default=0, dest='daemon_gid', type=Bcfg2.Options.Types.groupname, - help="Group to run the server daemon as")] + help="Group to run the server daemon as"), + Bcfg2.Options.Option( + cf=('server', 'allow_proxying_from'), default=[], + dest='daemon_allowed_proxies', type=Bcfg2.Options.Types.comma_list, + help="Hosts or CIDR ranges that are allowed as reverse proxies")] def __init__(self): Core.__init__(self) diff --git a/src/lib/Bcfg2/Server/Plugins/ACL.py b/src/lib/Bcfg2/Server/Plugins/ACL.py index 37f51a2a16..44774289d4 100644 --- a/src/lib/Bcfg2/Server/Plugins/ACL.py +++ b/src/lib/Bcfg2/Server/Plugins/ACL.py @@ -1,9 +1,8 @@ """ Support for client ACLs based on IP address and client metadata """ import os -import struct -import socket import Bcfg2.Server.Plugin +from Bcfg2.Utils import ip_matches def rmi_names_equal(first, second): @@ -35,31 +34,6 @@ def rmi_names_equal(first, second): return True -def ip2int(ip): - """ convert a dotted-quad IP address into an integer - representation of the same """ - return struct.unpack('>L', socket.inet_pton(socket.AF_INET, ip))[0] - - -def ip_matches(ip, entry): - """ Return True if the given IP matches the IP or IP and netmask - in the given ACL entry; False otherwise """ - if entry.get("netmask"): - try: - mask = int("1" * int(entry.get("netmask")) + - "0" * (32 - int(entry.get("netmask"))), 2) - except ValueError: - mask = ip2int(entry.get("netmask")) - return ip2int(ip) & mask == ip2int(entry.get("address")) & mask - elif entry.get("address") is None: - # no address, no netmask -- match all - return True - elif ip == entry.get("address"): - # just a plain ip address - return True - return False - - class IPACLFile(Bcfg2.Server.Plugin.XMLFileBacked): """ representation of ACL ip.xml, for IP-based ACLs """ __identifier__ = None diff --git a/src/lib/Bcfg2/Server/SSLServer.py b/src/lib/Bcfg2/Server/SSLServer.py index 6ad5b5635f..715eca9a4d 100644 --- a/src/lib/Bcfg2/Server/SSLServer.py +++ b/src/lib/Bcfg2/Server/SSLServer.py @@ -10,8 +10,10 @@ import ssl import threading import time +import Bcfg2.Options from Bcfg2.Compat import xmlrpclib, SimpleXMLRPCServer, SocketServer, \ b64decode +from Bcfg2.Utils import ip_matches class XMLRPCACLCheckException(Exception): @@ -237,6 +239,28 @@ def parse_request(self): return True def do_POST(self): + client_address = self.client_address + + # check for allowed reverse proxies + allowed_proxies = Bcfg2.Options.setup.daemon_allowed_proxies + if "X-Forwarded-For" in self.headers: + x_forwarded_for = self.headers["X-Forwarded-For"].split(",")[0] + + if not allowed_proxies: + msg = "X-Forwarded-For header specified but proxying disallowed" + self.logger.error(msg) + self.send_error(400, msg) + return + + for proxy in allowed_proxies: + try: + address, mask = proxy.split("/", 1) + except ValueError: + address, mask = (proxy, None) + entry = {"address": address, "netmask": mask} + if ip_matches(client_address[0], entry): + client_address = (x_forwarded_for, client_address[1]) + break try: max_chunk_size = 10 * 1024 * 1024 size_remaining = int(self.headers["content-length"]) @@ -252,8 +276,7 @@ def do_POST(self): if data is None: return # response has been sent - response = self.server._marshaled_dispatch(self.client_address, - data) + response = self.server._marshaled_dispatch(client_address, data) if sys.hexversion >= 0x03000000: response = response.encode('utf-8') except XMLRPCACLCheckException: @@ -261,7 +284,7 @@ def do_POST(self): self.end_headers() except: # pylint: disable=W0702 self.logger.error("Unexpected dispatch error for %s: %s" % - (self.client_address, sys.exc_info()[1])) + (client_address, sys.exc_info()[1])) try: self.send_response(500) self.send_header("Content-length", "0") @@ -273,7 +296,6 @@ def do_POST(self): raise else: # got a valid XML RPC response - client_address = self.request.getpeername() try: self.send_response(200) self.send_header("Content-type", "text/xml") diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 10057b63e3..0478b7913e 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -8,8 +8,10 @@ import re import select import shlex -import sys +import socket +import struct import subprocess +import sys import threading from Bcfg2.Compat import input, any # pylint: disable=W0622 @@ -330,3 +332,28 @@ def __init__(self, getter): def __get__(self, instance, owner): return self.getter(owner) + + +def ip2int(ip): + """ convert a dotted-quad IP address into an integer + representation of the same """ + return struct.unpack('>L', socket.inet_pton(socket.AF_INET, ip))[0] + + +def ip_matches(ip, entry): + """ Return True if the given IP matches the IP or IP and netmask + in the given ACL entry; False otherwise """ + if entry.get("netmask"): + try: + mask = int("1" * int(entry.get("netmask")) + + "0" * (32 - int(entry.get("netmask"))), 2) + except ValueError: + mask = ip2int(entry.get("netmask")) + return ip2int(ip) & mask == ip2int(entry.get("address")) & mask + elif entry.get("address") is None: + # no address, no netmask -- match all + return True + elif ip == entry.get("address"): + # just a plain ip address + return True + return False