# run with: twistd -ny mp3.py

__author__ = "Yoan Blanc <yoan at dosimple dot ch>"
__revision__ = "20070903"

import os
import re
import time
import urllib

from sha import sha
from random import random

from twisted.web2 import server, http, resource, channel
from twisted.web2 import static, stream, http_headers, responsecode

class AtomAuthentication(object):
	"""
	Borg! that handle all the Atom Authentication over the objects
	"""
	
	qop = "atom-auth"
	algorithm = "SHA"
	
	__shared_state = {}
	def __init__(self):
		self.__dict__ = self.__shared_state
		
		self.__dict__["realm"] = "Atomized MP3 Server"
		self.__dict__["username"] = "atom"
		self.__dict__["password"] = "mota"
		self.__dict__["db"] = {}
	
	def __getattr__(self, name):
		if self.__dict__.has_key(name):
			return self.__dict__[name]
		else:
			raise NameError("%s not found" % name)
		
	def __checkNonce(self, nonce):
		return self.db.has_key(nonce)
	
	def __generateNonce(self):
		return "%x" % int(0xFFFF/random())
	
	def getPassword(self, username):
		"""
		Return the password of the given username
		"""
		#Need a user db somewhere
		if self.username != username:
			raise NameError("User not found")
		else:
			return self.password
	
	def getCounter(self, nonce):
		if not self.__checkNonce(nonce):
			raise NameError("nonce not existing")
		return self.db[nonce]
	
	def incCounter(self, nonce):
		counter = self.getCounter(nonce)
		counter += 1
		
		self.db[nonce] = counter
		return self.getCounter(nonce)
	
	def getNonce(self):
		"""
		Return a new nonce, ready to use
		"""
		nonce = self.__generateNonce()
		# Check that nonce does exist
		while self.__checkNonce(nonce):
			nonce = self.__generateNonce()
		# put Nonce in DB
		self.db[nonce] = 0
		return nonce
	
	def calcResponse(self, method, resource, username, nonce, cnonce, counter):
		
		internal_counter = self.incCounter(nonce)
		if counter != internal_counter:
			raise NameError("Counters don't match")
		
		a1 = "%s:%s:%s" % (username, self.realm, self.getPassword(username))
		a2 = "%s:%s" % (method, resource)
		
		response = sha("%s:%s:%08d:%s:%s:%s" % ( \
			sha(a1).hexdigest(), \
			nonce, \
			counter, \
			cnonce, \
			self.qop, \
			sha(a2).hexdigest())).hexdigest()
		
		return response

class AtomAuthenticateResource(resource.Resource):
	"""
	A resource for data that need to be authenticated
	"""
	
	def __init__(self):
		self.atom = AtomAuthentication()
		self.nonce = self.atom.getNonce()
	
	def locateChild(self, req, segments):
		raise NotImplementedError, "locateChild not implemented"
	
	def renderHTTP(self, req):
		"""
		Returns a page that tell the visitor to get a ticket
		"""
		
		header = http_headers.Headers()
		
		header.setHeader('content-type', \
			http_headers.MimeType('text', 'xml', \
				[('charset', 'utf-8')] \
			) \
		)
		
		header.addRawHeader('WWW-Authenticate', \
			http_headers.generateWWWAuthenticate([
				("Atom", \
					{ \
						"realm": self.atom.realm, \
						"qop": self.atom.qop, \
						"algorithm": self.atom.algorithm, \
						"nonce": self.nonce \
					} \
				) \
			])[0] \
		)
		
		# Flash doesn't like headers
		return http.Response( \
			200, header, 
			"""<xml>
	<rsp stat="fail">
		<error message="Unauthorized page" />
		<challenge realm="%s" qop="%s" algorithm="%s" nonce="%s" />
	</rsp>
</xml>
""" % (self.atom.realm, self.atom.qop, self.atom.algorithm, self.nonce))
	
class NeverCachedFile(static.File):
	def render(self, req):
		response = static.File.render(self, req)
		
		response.headers.setHeader("Cache-Control", {"no-store":None, "no-cache":None, "must-revalidate":None, "post-check":0, "pre-check":0})
		response.headers.addRawHeader("Pragma", "no-cache")
		response.headers.setHeader("Expires", 1.)
		response.headers.setHeader("ETag", None)
		
		return response

class SimpleFiles(resource.Resource):
	addSlash = True
	whitelist = ["player.swf", "feed.xml"]
	
	def __init__(self):
		resource.Resource.__init__(self)
		self.atom = AtomAuthentication()
	
	def childFactory(self, request, name):		
		if request.method == "GET":
			if name and name in self.whitelist:
				return static.File(name)
			
			# Auth
			values = request.__dict__["args"]
			
			if values.has_key("cnonce") and values.has_key("response") and \
				values.has_key("nonce") and values.has_key("user") and \
				values.has_key("nc"):
				
				try:
					# get the response
					response = self.atom.calcResponse( \
						request.method, \
						urllib.unquote(request.path), \
						values["user"][0], \
						values["nonce"][0], \
						values["cnonce"][0], \
						int(values["nc"][0]) \
					)
					
					if response == values["response"][0]:
						if(os.path.exists(name)):
							return NeverCachedFile(name)
						else:
							return None # 404
				except NameError, error:
					pass
		# same for everyone!
		return AtomAuthenticateResource()
	
	def renderHTTP(self, req):
		return http.Response( \
			200,  \
			{'content-type': http_headers.MimeType('text', 'html', [('charset', 'utf-8')])}, \
			"""<html>
	<head>
		<title>Auth mp3 player</title>
		<style type="text/css">html,body{margin:0;padding:0;}</style>
	</head>
	<body>
		<object type="application/x-shockwave-flash"
			data="player.swf" 
			width="100%" height="100%">
			<param name="movie" value="player.swf" />
				No Flash
		</object>
	</body>
	</html>""")

site = server.Site(SimpleFiles())

# Standard twisted application Boilerplate
from twisted.application import service, strports
application = service.Application("authserver")
s = strports.service('tcp:8080', channel.HTTPFactory(site))
s.setServiceParent(application)


