+++ /dev/null
-#!/usr/bin/python
-
-import datetime
-import ipaddr
-import logging
-import pymongo
-import re
-import simplejson
-import tornado.database
-import tornado.httpserver
-import tornado.ioloop
-import tornado.options
-import tornado.web
-
-import webapp.backend as backend
-
-DATABASE_HOST = ["irma.ipfire.org", "madeye.ipfire.org"]
-DATABASE_NAME = "stasy"
-
-DEFAULT_HOST = "www.ipfire.org"
-
-MIN_PROFILE_VERSION = 0
-MAX_PROFILE_VERSION = 0
-
-class Profile(dict):
- def __getattr__(self, key):
- try:
- return self[key]
- except KeyError:
- raise AttributeError, key
-
- def __setattr__(self, key, val):
- self[key] = val
-
-
-class Stasy(tornado.web.Application):
- def __init__(self, **kwargs):
- settings = dict(
- debug = False,
- default_host = DEFAULT_HOST,
- gzip = True,
- )
- settings.update(kwargs)
-
- tornado.web.Application.__init__(self, **settings)
-
- # Establish database connection
- self.connection = pymongo.Connection(DATABASE_HOST)
- self.db = self.connection[DATABASE_NAME]
- logging.info("Successfully connected to database: %s:%s" % \
- (self.connection.host, self.connection.port))
-
- self.add_handlers(r"(fireinfo|stasy).ipfire.org", [
- (r"/", tornado.web.RedirectHandler, { "url" : "http://www.ipfire.org/" }),
- (r"/send/([a-z0-9]+)", ProfileSendHandler),
- (r"/debug", DebugHandler),
- ])
-
- # ipfire.org
- # this should not be neccessary (see default_host) but some versions
- # of tornado have a bug.
- self.add_handlers(r".*", [
- (r".*", tornado.web.RedirectHandler, { "url" : "http://" + DEFAULT_HOST + "/" })
- ])
-
- def __del__(self):
- logging.debug("Disconnecting from database")
- self.connection.disconnect()
-
- @property
- def ioloop(self):
- return tornado.ioloop.IOLoop.instance()
-
- def start(self, port=9001):
- logging.info("Starting application")
-
- http_server = tornado.httpserver.HTTPServer(self, xheaders=True)
- http_server.listen(port)
-
- # Register automatic cleanup for old profiles, etc.
- automatic_cleanup = tornado.ioloop.PeriodicCallback(
- self.automatic_cleanup, 60*60*1000)
- automatic_cleanup.start()
-
- self.ioloop.start()
-
- def stop(self):
- logging.info("Stopping application")
- self.ioloop.stop()
-
- def db_get_collection(self, name):
- return pymongo.collection.Collection(self.db, name)
-
- @property
- def profiles(self):
- return self.db_get_collection("profiles")
-
- @property
- def archives(self):
- return self.db_get_collection("archives")
-
- def automatic_cleanup(self):
- logging.info("Starting automatic cleanup...")
-
- # Remove all profiles that were not updated since 4 weeks.
- not_updated_since = datetime.datetime.utcnow() - \
- datetime.timedelta(weeks=4)
-
- self.move_profiles({ "updated" : { "$lt" : not_updated_since }})
-
- def move_profiles(self, find):
- """
- Move all profiles by the "find" criteria.
- """
- for p in self.profiles.find(find):
- self.archives.save(p)
- self.profiles.remove(find)
-
-
-class BaseHandler(tornado.web.RequestHandler):
- @property
- def geoip(self):
- return backend.GeoIP()
-
- @property
- def db(self):
- return self.application.db
-
- def db_get_collection(self, name):
- return self.application.db_get_collection(name)
-
- @property
- def db_collections(self):
- return [self.db_get_collection(c) for c in self.db.collection_names()]
-
-
-DEBUG_STR = """
-Database information:
- Host: %(db_host)s:%(db_port)s
-
- All nodes: %(db_nodes)s
-
- %(collections)s
-
-"""
-
-DEBUG_COLLECTION_STR = """
- Collection: %(name)s
- Total documents: %(count)d
-"""
-
-class DebugHandler(BaseHandler):
- def get(self):
- # This handler is only available in debugging mode.
- if not self.application.settings["debug"]:
- return tornado.web.HTTPError(404)
-
- self.set_header("Content-type", "text/plain")
-
- conn, db = (self.application.connection, self.db)
-
- debug_info = dict(
- db_host = conn.host,
- db_port = conn.port,
- db_nodes = list(conn.nodes),
- )
-
- collections = []
- for collection in self.db_collections:
- collections.append(DEBUG_COLLECTION_STR % {
- "name" : collection.name, "count" : collection.count(),
- })
- debug_info["collections"] = "".join(collections)
-
- self.write(DEBUG_STR % debug_info)
- self.finish()
-
-
-class ProfileSendHandler(BaseHandler):
- @property
- def archives(self):
- return self.application.archives
-
- @property
- def profiles(self):
- return self.application.profiles
-
- def prepare(self):
- # Create an empty profile.
- self.profile = Profile()
-
- def __check_attributes(self, profile):
- """
- Check for attributes that must be provided,
- """
-
- attributes = (
- "private_id",
- "profile_version",
- "public_id",
- "updated",
- )
- for attr in attributes:
- if not profile.has_key(attr):
- raise tornado.web.HTTPError(400, "Profile lacks '%s' attribute: %s" % (attr, profile))
-
- def __check_valid_ids(self, profile):
- """
- Check if IDs contain valid data.
- """
-
- for id in ("public_id", "private_id"):
- if re.match(r"^([a-f0-9]{40})$", "%s" % profile[id]) is None:
- raise tornado.web.HTTPError(400, "ID '%s' has wrong format: %s" % (id, profile))
-
- def __check_equal_ids(self, profile):
- """
- Check if public_id and private_id are equal.
- """
-
- if profile.public_id == profile.private_id:
- raise tornado.web.HTTPError(400, "Public and private IDs are equal: %s" % profile)
-
- def __check_matching_ids(self, profile):
- """
- Check if a profile with the given public_id is already in the
- database. If so we need to check if the private_id matches.
- """
- p = self.profiles.find_one({ "public_id" : profile["public_id"]})
- if not p:
- return
-
- p = Profile(p)
- if p.private_id != profile.private_id:
- raise tornado.web.HTTPError(400, "Mismatch of private_id: %s" % profile)
-
- def __check_profile_version(self, profile):
- """
- Check if this version of the server software does support the
- received profile.
- """
- version = profile.profile_version
-
- if version < MIN_PROFILE_VERSION or version > MAX_PROFILE_VERSION:
- raise tornado.web.HTTPError(400,
- "Profile version is not supported: %s" % version)
-
- def check_profile(self):
- """
- This method checks if the blob is sane.
- """
-
- checks = (
- self.__check_attributes,
- self.__check_valid_ids,
- self.__check_equal_ids,
- self.__check_profile_version,
- # These checks require at least one database query and should be done
- # at last.
- self.__check_matching_ids,
- )
-
- for check in checks:
- check(self.profile)
-
- # If we got here, everything is okay and we can go on...
-
- def move_profiles(self, find):
- self.application.move_profiles(find)
-
- # The GET method is only allowed in debugging mode.
- def get(self, public_id):
- if not self.application.settings["debug"]:
- return tornado.web.HTTPError(405)
-
- return self.post(public_id)
-
- def post(self, public_id):
- profile = self.get_argument("profile", None)
-
- # Send "400 bad request" if no profile was provided
- if not profile:
- raise tornado.web.HTTPError(400, "No profile received.")
-
- # Try to decode the profile.
- try:
- self.profile.update(simplejson.loads(profile))
- except simplejson.decoder.JSONDecodeError, e:
- raise tornado.web.HTTPError(400, "Profile could not be decoded: %s" % e)
-
- # Create a shortcut and overwrite public_id from query string
- profile = self.profile
- profile.public_id = public_id
-
- # Add timestamp to the profile
- profile.updated = datetime.datetime.utcnow()
-
- # Check if profile contains proper data.
- self.check_profile()
-
- # Get GeoIP information if address is not defined in rfc1918
- addr = ipaddr.IPAddress(self.request.remote_ip)
- if not addr.is_private:
- profile.geoip = self.geoip.get_all(self.request.remote_ip)
-
- # Move previous profiles to archive and keep only the latest one
- # in profiles. This will make full table lookups faster.
- self.move_profiles({ "public_id" : profile.public_id })
-
- # Write profile to database
- id = self.profiles.save(profile)
-
- self.write("Your profile was successfully saved to the database.")
- self.finish()
-
- logging.debug("Saved profile: %s" % profile)
-
-
-if __name__ == "__main__":
- app = Stasy()
-
- app.start()