mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
391 lines
12 KiB
Python
391 lines
12 KiB
Python
"""The HTTPS Santiago listener and sender.
|
|
|
|
FIXME: add real authentication.
|
|
FIXME: sanitize or properly escape user input (XSS, attacks on the client).
|
|
FIXME: make sure we never try to execute user input (injection, attacks on the
|
|
server).
|
|
FIXME: all the Blammos. They're terrible, unacceptable failures.
|
|
|
|
"""
|
|
|
|
|
|
import santiago
|
|
|
|
from Cheetah.Template import Template
|
|
import cherrypy
|
|
import httplib, urllib, urlparse
|
|
import sys
|
|
import logging
|
|
|
|
def allow_ips(ips = None):
|
|
"""Refuse connections from non-whitelisted IPs.
|
|
|
|
Defaults to the localhost.
|
|
|
|
Hook documentation is available in:
|
|
|
|
http://docs.cherrypy.org/dev/progguide/extending/customtools.html
|
|
|
|
"""
|
|
if ips == None:
|
|
ips = [ "127.0.0.1" ]
|
|
|
|
if cherrypy.request.remote.ip not in ips:
|
|
santiago.debug_log("Request from non-local IP. Forbidden.")
|
|
raise cherrypy.HTTPError(403)
|
|
|
|
cherrypy.tools.ip_filter = cherrypy.Tool('before_handler', allow_ips)
|
|
|
|
def start(*args, **kwargs):
|
|
"""Module-level start function, called after listener and sender started.
|
|
|
|
"""
|
|
cherrypy.engine.start()
|
|
|
|
def stop(*args, **kwargs):
|
|
"""Module-level stop function, called after listener and sender stopped.
|
|
|
|
"""
|
|
cherrypy.engine.stop()
|
|
cherrypy.engine.exit()
|
|
|
|
|
|
class Listener(santiago.SantiagoListener):
|
|
|
|
def __init__(self, my_santiago, socket_port=0,
|
|
ssl_certificate="", ssl_private_key=""):
|
|
|
|
santiago.debug_log("Creating Listener.")
|
|
|
|
super(santiago.SantiagoListener, self).__init__(my_santiago)
|
|
|
|
cherrypy.server.socket_port = socket_port
|
|
cherrypy.server.ssl_certificate = ssl_certificate
|
|
cherrypy.server.ssl_private_key = ssl_private_key
|
|
|
|
d = cherrypy.dispatch.RoutesDispatcher()
|
|
d.connect("index", "/", self.index)
|
|
d.connect("learn", "/learn/:host/:service", self.learn)
|
|
d.connect("where", "/where/:host/:service", self.where)
|
|
d.connect("provide", "/provide/:client/:service/:location", self.provide)
|
|
|
|
cherrypy.tree.mount(cherrypy.Application(self), "",
|
|
{"/": {"request.dispatch": d}})
|
|
|
|
santiago.debug_log("Listener Created.")
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def index(self, **kwargs):
|
|
"""Receive an incoming Santiago request from another Santiago client."""
|
|
|
|
santiago.debug_log("Received request {0}".format(str(kwargs)))
|
|
|
|
# FIXME Blammo!
|
|
# make sure there's some verification of the incoming connection here.
|
|
|
|
try:
|
|
self.incoming_request(kwargs["request"])
|
|
except Exception as e:
|
|
logging.exception(e)
|
|
|
|
raise cherrypy.HTTPRedirect("/freedombuddy")
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def learn(self, host, service, **kwargs):
|
|
"""Request a resource from another Santiago client.
|
|
|
|
TODO: add request whitelisting.
|
|
|
|
"""
|
|
# TODO enforce restfulness, POST, and build a request form.
|
|
# if not cherrypy.request.method == "POST":
|
|
# return
|
|
|
|
return super(Listener, self).learn(host, service)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def where(self, host, service, **kwargs):
|
|
"""Show where a host is providing me services.
|
|
|
|
TODO: make the output format a parameter.
|
|
|
|
"""
|
|
return list(super(Listener, self).where(host, service))
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def provide(self, client, service, location, **kwargs):
|
|
"""Provide a service for the client at the location."""
|
|
|
|
return super(Listener, self).provide(client, service, location)
|
|
|
|
class Sender(santiago.SantiagoSender):
|
|
|
|
def __init__(self, my_santiago, proxy_host, proxy_port):
|
|
|
|
super(santiago.SantiagoSender, self).__init__(my_santiago)
|
|
self.proxy_host = proxy_host
|
|
self.proxy_port = proxy_port
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def outgoing_request(self, request, destination):
|
|
"""Send an HTTPS request to each Santiago client.
|
|
|
|
Don't queue, just immediately send the reply to each location we know.
|
|
|
|
It's both simple and as reliable as possible.
|
|
|
|
``request`` is literally the request's text. It needs to be wrapped for
|
|
transport across the protocol.
|
|
|
|
"""
|
|
santiago.debug_log("request {0}".format(str(request)))
|
|
to_send = { "request": request }
|
|
|
|
params = urllib.urlencode(to_send)
|
|
santiago.debug_log("params {0}".format(str(params)))
|
|
|
|
# TODO: Does HTTPSConnection require the cert and key?
|
|
# Is the fact that the server has it sufficient? I think so.
|
|
# FIXME Blammo!
|
|
connection = httplib.HTTPSConnection(destination.split("//")[1])
|
|
|
|
# proxying required and available only in Python 2.7 or later.
|
|
# TODO: fail if Python version < 2.7.
|
|
# FIXME Blammo!
|
|
if sys.version_info >= (2, 7):
|
|
connection.set_tunnel(self.proxy_host, self.proxy_port)
|
|
|
|
# FIXME Blammo!
|
|
connection.request("GET", "/?%s" % params)
|
|
connection.close()
|
|
|
|
class Monitor(santiago.SantiagoMonitor):
|
|
|
|
def __init__(self, aSantiago):
|
|
santiago.debug_log("Creating Monitor.")
|
|
|
|
super(Monitor, self).__init__(aSantiago)
|
|
|
|
try:
|
|
d = cherrypy.tree.apps[""].config["/"]["request.dispatch"]
|
|
except KeyError:
|
|
d = cherrypy.dispatch.RoutesDispatcher()
|
|
|
|
root = Root(self.santiago)
|
|
|
|
routing_pairs = (
|
|
('/hosting/:client/:service', HostedService(self.santiago)),
|
|
('/hosting/:client', HostedClient(self.santiago)),
|
|
('/hosting', Hosting(self.santiago)),
|
|
('/consuming/:host/:service', ConsumedService(self.santiago)),
|
|
('/consuming/:host', ConsumedHost(self.santiago)),
|
|
('/consuming', Consuming(self.santiago)),
|
|
("/stop", Stop(self.santiago)),
|
|
("/freedombuddy", root),
|
|
)
|
|
|
|
for location, handler in routing_pairs:
|
|
Monitor.rest_connect(d, location, handler)
|
|
|
|
cherrypy.tree.mount(root, "", {"/": {"request.dispatch": d}})
|
|
|
|
santiago.debug_log("Monitor Created.")
|
|
|
|
@classmethod
|
|
def rest_connect(cls, dispatcher, location, controller, trailing_slash=True):
|
|
"""Simple REST connector for object/location mapping."""
|
|
|
|
if trailing_slash:
|
|
location = location.rstrip("/")
|
|
location = [location, location + "/"]
|
|
else:
|
|
location = [location]
|
|
|
|
for place in location:
|
|
for a_method in ("PUT", "GET", "POST", "DELETE"):
|
|
dispatcher.connect(controller.__class__.__name__ + a_method,
|
|
place, controller=controller, action=a_method,
|
|
conditions={ "method": [a_method] })
|
|
|
|
return dispatcher
|
|
|
|
class RestMonitor(santiago.RestController):
|
|
|
|
def __init__(self, aSantiago):
|
|
super(RestMonitor, self).__init__()
|
|
self.santiago = aSantiago
|
|
self.relative_path = "protocols/https/templates/"
|
|
|
|
def respond(self, template, values):
|
|
return [str(Template(
|
|
file=self.relative_path + template,
|
|
searchList = [dict(values)]))]
|
|
|
|
class HostedService(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self, client, service):
|
|
return self.respond("hostedService-get.tmpl", {
|
|
"service": service,
|
|
"client": client,
|
|
"locations": self.santiago.hosting[client][service] })
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, client="", service="", put="", delete=""):
|
|
if put:
|
|
self.PUT(client, service, put)
|
|
elif delete:
|
|
self.DELETE(client, service, delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/hosting/{0}/{1}/".format(client, service))
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, client, service, location):
|
|
self.santiago.create_hosting_location(client, service, [location])
|
|
|
|
# Have to remove instead of delete for locations as $service is a list
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, client, service, location):
|
|
if location in self.santiago.hosting[client][service]:
|
|
self.santiago.hosting[client][service].remove(location)
|
|
|
|
class HostedClient(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self, client):
|
|
return self.respond("hostedClient-get.tmpl",
|
|
{ "client": client,
|
|
"services": self.santiago.hosting[client] })
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, client="", put="", delete=""):
|
|
if put:
|
|
self.PUT(client, put)
|
|
elif delete:
|
|
self.DELETE(client, delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/hosting/" + client)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, client, service):
|
|
self.santiago.create_hosting_service(client, service)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, client, service):
|
|
if service in self.santiago.hosting[client]:
|
|
del self.santiago.hosting[client][service]
|
|
|
|
class Hosting(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self):
|
|
return self.respond("hosting-get.tmpl",
|
|
{"clients": [x for x in self.santiago.hosting]})
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, put="", delete=""):
|
|
if put:
|
|
self.PUT(put)
|
|
elif delete:
|
|
self.DELETE(delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/hosting")
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, client):
|
|
self.santiago.create_hosting_client(client)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, client):
|
|
if client in self.santiago.hosting:
|
|
del self.santiago.hosting[client]
|
|
|
|
class ConsumedService(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self, host, service):
|
|
return self.respond("consumedService-get.tmpl",
|
|
{ "service": service,
|
|
"host": host,
|
|
"locations":
|
|
self.santiago.consuming[host][service] })
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, host="", service="", put="", delete=""):
|
|
if put:
|
|
self.PUT(host, service, put)
|
|
elif delete:
|
|
self.DELETE(host, service, delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/consuming/{0}/{1}/".format(host, service))
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, host, service, location):
|
|
self.santiago.create_consuming_location(host, service, [location])
|
|
|
|
# Have to remove instead of delete for locations as $service is a list
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, host, service, location):
|
|
if location in self.santiago.consuming[host][service]:
|
|
self.santiago.consuming[host][service].remove(location)
|
|
|
|
class ConsumedHost(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self, host):
|
|
return self.respond("consumedHost-get.tmpl",
|
|
{ "services": self.santiago.consuming[host],
|
|
"host": host })
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, host="", put="", delete=""):
|
|
if put:
|
|
self.PUT(host, put)
|
|
elif delete:
|
|
self.DELETE(host, delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/consuming/" + host)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, host, service):
|
|
self.santiago.create_consuming_service(host, service)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, host, service):
|
|
if service in self.santiago.consuming[host]:
|
|
del self.santiago.consuming[host][service]
|
|
|
|
class Consuming(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self):
|
|
return self.respond("consuming-get.tmpl",
|
|
{ "hosts": [x for x in self.santiago.consuming]})
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self, put="", delete=""):
|
|
if put:
|
|
self.PUT(put)
|
|
elif delete:
|
|
self.DELETE(delete)
|
|
|
|
raise cherrypy.HTTPRedirect("/consuming")
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def PUT(self, host):
|
|
self.santiago.create_consuming_host(host)
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def DELETE(self, host):
|
|
if host in self.santiago.consuming:
|
|
del self.santiago.consuming[host]
|
|
|
|
class Root(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self):
|
|
return self.respond("root-get.tmpl", {})
|
|
|
|
class Stop(RestMonitor):
|
|
@cherrypy.tools.ip_filter()
|
|
def POST(self):
|
|
self.santiago.live = 0
|
|
raise cherrypy.HTTPRedirect("/")
|
|
|
|
@cherrypy.tools.ip_filter()
|
|
def GET(self):
|
|
self.POST() # FIXME cause it's late and I'm tired.
|