First release, remove not used lines from gitignore

This commit is contained in:
HelloZeroNet 2015-01-12 02:03:45 +01:00
parent c0bfb3b062
commit d28e1cb4a6
85 changed files with 7205 additions and 50 deletions

View file

@ -0,0 +1,466 @@
# By: HurlSly
# Source: https://github.com/HurlSly/Python/blob/master/BitcoinECC.py
# Modified: random number generator in def GeneratePrivateKey(self):
import random
import hashlib
import os
class GaussInt:
#A class for the Gauss integers of the form a + b sqrt(n) where a,b are integers.
#n can be positive or negative.
def __init__(self,x,y,n,p=0):
if p:
self.x=x%p
self.y=y%p
self.n=n%p
else:
self.x=x
self.y=y
self.n=n
self.p=p
def __add__(self,b):
return GaussInt(self.x+b.x,self.y+b.y,self.n,self.p)
def __sub__(self,b):
return GaussInt(self.x-b.x,self.y-b.y,self.n,self.p)
def __mul__(self,b):
return GaussInt(self.x*b.x+self.n*self.y*b.y,self.x*b.y+self.y*b.x,self.n,self.p)
def __div__(self,b):
return GaussInt((self.x*b.x-self.n*self.y*b.y)/(b.x*b.x-self.n*b.y*b.y),(-self.x*b.y+self.y*b.x)/(b.x*b.x-self.n*b.y*b.y),self.n,self.p)
def __eq__(self,b):
return self.x==b.x and self.y==b.y
def __repr__(self):
if self.p:
return "%s+%s (%d,%d)"%(self.x,self.y,self.n,self.p)
else:
return "%s+%s (%d)"%(self.x,self.y,self.n)
def __pow__(self,n):
b=Base(n,2)
t=GaussInt(1,0,self.n)
while b:
t=t*t
if b.pop():
t=self*t
return t
def Inv(self):
return GaussInt(self.x/(self.x*self.x-self.n*self.y*self.y),-self.y/(self.x*self.x-self.n*self.y*self.y),self.n,self.p)
def Cipolla(a,p):
#Find a square root of a modulo p using the algorithm of Cipolla
b=0
while pow((b*b-a)%p,(p-1)/2,p)==1:
b+=1
return (GaussInt(b,1,b**2-a,p)**((p+1)/2)).x
def Base(n,b):
#Decompose n in base b
l=[]
while n:
l.append(n%b)
n/=b
return l
def InvMod(a,n):
#Find the inverse mod n of a.
#Use the Extended Euclides Algorithm.
m=[]
s=n
while n:
m.append(a/n)
(a,n)=(n,a%n)
u=1
v=0
while m:
(u,v)=(v,u-m.pop()*v)
return u%s
def b58encode(v):
#Encode a byte string to the Base58
digit="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
base=len(digit)
val=0
for c in v:
val*=256
val+=ord(c)
result=""
while val:
(val,mod)=divmod(val,base)
result=digit[mod]+result
pad=0
for c in v:
if c=="\0":
pad+=1
else:
break
return (digit[0]*pad)+result
def b58decode(v):
#Decode a Base58 string to byte string
digit="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
base=len(digit)
val=0
for c in v:
val*=base
val+=digit.find(c)
result=""
while val:
(val,mod)=divmod(val,256)
result=chr(mod)+result
pad=0
for c in v:
if c==digit[0]:
pad+=1
else:
break
result="\0"*pad+result
return result
def Byte2Hex(b):
#Convert a byte string to hex number
out=""
for x in b:
y=hex(ord(x))[2:]
if len(y)==1:
y="0"+y
out+="%2s"%y
return out
def Int2Byte(n,b):
#Convert a integer to a byte string of length b
out=""
for i in range(b):
(n,m)=divmod(n,256)
out=chr(m)+out
return out
class EllipticCurvePoint:
#Main class
#It is an point on an Elliptic Curve
def __init__(self,x,a,b,p,n=0):
#We store the coordinate in x and the elliptic curbe parameter.
#x is of length 3. This is the 3 projective coordinates of the point.
self.x=x[:]
self.a=a
self.b=b
self.p=p
self.n=n
def EqualProj(self,y):
#Does y equals self ?
#It computes self cross product with y and check if the result is 0.
return self.x[0]*y.x[1]==self.x[1]*y.x[0] and self.x[1]*y.x[2]==self.x[2]*y.x[1] and self.x[2]*y.x[0]==self.x[0]*y.x[2]
def __add__(self,y):
#The main function to add self and y
#It uses the formulas I derived in projective coordinates.
#Projectives coordinates are more performant than the usual (x,y) coordinates
#because it we don't need to compute inverse mod p, which is faster.
z=EllipticCurvePoint([0,0,0],self.a,self.b,self.p)
if self.EqualProj(y):
d=(2*self.x[1]*self.x[2])%self.p
d3=pow(d,3,self.p)
n=(3*pow(self.x[0],2,self.p)+self.a*pow(self.x[2],2,self.p))%self.p
z.x[0]=(pow(n,2,self.p)*d*self.x[2]-2*d3*self.x[0])%self.p
z.x[1]=(3*self.x[0]*n*pow(d,2,self.p)-pow(n,3,self.p)*self.x[2]-self.x[1]*d3)%self.p
z.x[2]=(self.x[2]*d3)%self.p
else:
d=(y.x[0]*self.x[2]-y.x[2]*self.x[0])%self.p
d3=pow(d,3,self.p)
n=(y.x[1]*self.x[2]-self.x[1]*y.x[2])%self.p
z.x[0]=(y.x[2]*self.x[2]*pow(n,2,self.p)*d-d3*(y.x[2]*self.x[0]+y.x[0]*self.x[2]))%self.p
z.x[1]=(pow(d,2,self.p)*n*(2*self.x[0]*y.x[2]+y.x[0]*self.x[2])-pow(n,3,self.p)*self.x[2]*y.x[2]-self.x[1]*d3*y.x[2])%self.p
z.x[2]=(self.x[2]*d3*y.x[2])%self.p
return z
def __mul__(self,n):
#The fast multiplication of point n times by itself.
b=Base(n,2)
t=EllipticCurvePoint(self.x,self.a,self.b,self.p)
b.pop()
while b:
t+=t
if b.pop():
t+=self
return t
def __repr__(self):
#print a point in (x,y) coordinate.
return "x=%d\ny=%d\n"%((self.x[0]*InvMod(self.x[2],self.p))%self.p,(self.x[1]*InvMod(self.x[2],self.p))%self.p)
def __eq__(self,x):
#Does self==x ?
return self.x==x.x and self.a==x.a and self.b==x.b and self.p==x.p
def __ne__(self,x):
#Does self!=x ?
return self.x!=x.x or self.a!=x.a or self.b!=x.b or self.p!=x.p
def Check(self):
#Is self on the curve ?
return (self.x[0]**3+self.a*self.x[0]*self.x[2]**2+self.b*self.x[2]**3-self.x[1]**2*self.x[2])%self.p==0
def GeneratePrivateKey(self):
#Generate a private key. It's just a random number between 1 and n-1.
#Of course, this function isn't cryptographically secure.
#Don't use it to generate your key. Use a cryptographically secure source of randomness instead.
#self.d = random.randint(1,self.n-1)
self.d = int(os.urandom(32).encode("hex"), 16) # Better random fix
def SignECDSA(self,m):
#Sign a message. The private key is self.d .
h=hashlib.new("SHA256")
h.update(m)
z=int(h.hexdigest(),16)
r=0
s=0
while not r or not s:
k=random.randint(1,self.n-1)
R=self*k
R.Normalize()
r=R.x[0]%self.n
s=(InvMod(k,self.n)*(z+r*self.d))%self.n
return (r,s)
def CheckECDSA(self,sig,m):
#Check a signature (r,s) of the message m using the public key self.Q
# and the generator which is self.
#This is not the one used by Bitcoin because the public key isn't known;
# only a hash of the public key is known. See the next function.
(r,s)=sig
h=hashlib.new("SHA256")
h.update(m)
z=int(h.hexdigest(),16)
if self.Q.x[2]==0:
return False
if not self.Q.Check():
return False
if (self.Q*self.n).x[2]!=0:
return False
if r<1 or r>self.n-1 or s<1 or s>self.n-1:
return False
w=InvMod(s,self.n)
u1=(z*w)%self.n
u2=(r*w)%self.n
R=self*u1+self.Q*u2
R.Normalize()
return (R.x[0]-r)%self.n==0
def VerifyMessageFromBitcoinAddress(self,adresse,m,sig):
#Check a signature (r,s) for the message m signed by the Bitcoin
# address "addresse".
h=hashlib.new("SHA256")
h.update(m)
z=int(h.hexdigest(),16)
(r,s)=sig
x=r
y2=(pow(x,3,self.p)+self.a*x+self.b)%self.p
y=Cipolla(y2,self.p)
for i in range(2):
kG=EllipticCurvePoint([x,y,1],self.a,self.b,self.p,self.n)
mzG=self*((-z)%self.n)
self.Q=(kG*s+mzG)*InvMod(r,self.n)
adr=self.BitcoinAddresFromPublicKey()
if adr==adresse:
break
y=(-y)%self.p
if adr!=adresse:
return False
return True
def BitcoinAddressFromPrivate(self,pri=None):
#Transform a private key in base58 encoding to a bitcoin address.
#normal means "uncompressed".
if not pri:
print "Private Key :",
pri=raw_input()
normal=(len(pri)==51)
pri=b58decode(pri)
if normal:
pri=pri[1:-4]
else:
pri=pri[1:-5]
self.d=int(Byte2Hex(pri),16)
return self.BitcoinAddress(normal)
def PrivateEncoding(self,normal=True):
#Encode a private key self.d to base58 encoding.
p=Int2Byte(self.d,32)
p="\80"+p
if not normal:
p+=chr(1)
h=hashlib.new("SHA256")
h.update(p)
s=h.digest()
h=hashlib.new("SHA256")
h.update(s)
s=h.digest()
cs=s[:4]
p+=cs
p=b58encode(p)
return p
def BitcoinAddresFromPublicKey(self,normal=True):
#Find the bitcoin address from the public key self.Q
#We do normalization to go from the projective coordinates to the usual
# (x,y) coordinates.
self.Q.Normalize()
if normal:
pk=chr(4)+Int2Byte(self.Q.x[0],32)+Int2Byte((self.Q.x[1])%self.p,32)
else:
if self.Q.x[1]%2==0:
pk=chr(2)+Int2Byte(self.Q.x[0],32)
else:
pk=chr(3)+Int2Byte(self.Q.x[0],32)
version=chr(0)
h=hashlib.new("SHA256")
h.update(pk)
s=h.digest()
h=hashlib.new("RIPEMD160")
h.update(s)
kh=version+h.digest()
h=hashlib.new("SHA256")
h.update(kh)
cs=h.digest()
h=hashlib.new("SHA256")
h.update(cs)
cs=h.digest()[:4]
adr=b58encode(kh+cs)
return adr
def BitcoinAddress(self,normal=True):
#Computes a bitcoin address given the private key self.d.
self.Q=self*self.d
return self.BitcoinAddresFromPublicKey(normal)
def BitcoinAddressGenerator(self,k,filename):
#Generate Bitcoin address and write them in the filename in the multibit format.
#Change the date as you like.
f=open(filename,"w")
for i in range(k):
self.GeneratePrivateKey()
adr=self.BitcoinAddress()
p=self.PrivateEncoding()
f.write("#%s\n%s 2014-01-30T12:00:00Z\n"%(adr,p))
#print hex(self.d)
print adr,p
f.close()
def TestSign(self):
#Test signature
self.GeneratePrivateKey()
self.Q=self*self.d
m="Hello World"
adresse=self.BitcoinAddresFromPublicKey()
(r,s)=self.SignECDSA(m)
m="Hello World"
print self.VerifyMessageFromBitcoinAddress(adresse,m,r,s)
def Normalize(self):
#Transform projective coordinates of self to the usual (x,y) coordinates.
if self.x[2]:
self.x[0]=(self.x[0]*InvMod(self.x[2],self.p))%self.p
self.x[1]=(self.x[1]*InvMod(self.x[2],self.p))%self.p
self.x[2]=1
elif self.x[1]:
self.x[0]=(self.x[0]*InvMod(self.x[1],self.p))%self.p
self.x[1]=1
elif self.x[0]:
self.x[0]=1
else:
raise Exception
def Bitcoin():
#Create the Bitcoin elliptiv curve
a=0
b=7
p=2**256-2**32-2**9-2**8-2**7-2**6-2**4-1
#Create the generator G of the Bitcoin elliptic curve, with is order n.
Gx=int("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",16)
Gy=int("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8",16)
n =int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",16)
#Create the generator
return EllipticCurvePoint([Gx,Gy,1],a,b,p,n)
if __name__ == "__main__":
bitcoin=Bitcoin()
#Generate the public key from the private one
print bitcoin.BitcoinAddressFromPrivate("23DKRBLkeDbcSaddsMYLAHXhanPmGwkWAhSPVGbspAkc72Hw9BdrDF")
print bitcoin.BitcoinAddress()
#Print the bitcoin address of the public key generated at the previous line
adr=bitcoin.BitcoinAddresFromPublicKey()
print adr
#Sign a message with the current address
m="Hello World"
sig=bitcoin.SignECDSA("Hello World")
#Verify the message using only the bitcoin adress, the signature and the message.
#Not using the public key as it is not needed.
print bitcoin.VerifyMessageFromBitcoinAddress(adr,m,sig)

View file

0
src/lib/__init__.py Normal file
View file

View file

View file

@ -0,0 +1,29 @@
import re
def prefix(content):
content = re.sub("@keyframes (.*? {.*?[^ ]})", "@keyframes \\1\n@-webkit-keyframes \\1\n@-moz-keyframes \\1\n", content, flags=re.DOTALL)
content = re.sub('([^-\*])(border-radius|box-shadow|transition|animation|box-sizing|transform|filter|perspective|animation-[a-z-]+): (.*?)([;}])', '\\1-webkit-\\2: \\3; -moz-\\2: \\3; -o-\\2: \\3; -ms-\\2: \\3; \\2: \\3 \\4', content)
content = re.sub('(?<=[^a-zA-Z0-9-])([a-zA-Z0-9-]+): {0,1}(linear-gradient)\((.*?)(\)[;\n])',
'\\1: -webkit-\\2(\\3);'+
'\\1: -moz-\\2(\\3);'+
'\\1: -o-\\2(\\3);'+
'\\1: -ms-\\2(\\3);'+
'\\1: \\2(\\3);', content)
return content
if __name__ == "__main__":
print prefix("""
.test {
border-radius: 5px;
background: linear-gradient(red, blue);
}
@keyframes flip {
0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); }
50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) }
100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); }
}
""")

View file

@ -0,0 +1,21 @@
VERSION = (0, 9, 3, 'final', 0)
__all__ = [
'WebSocketApplication',
'Resource',
'WebSocketServer',
'WebSocketError',
'get_version'
]
def get_version(*args, **kwargs):
from .utils import get_version
return get_version(*args, **kwargs)
try:
from .resource import WebSocketApplication, Resource
from .server import WebSocketServer
from .exceptions import WebSocketError
except ImportError:
pass

View file

@ -0,0 +1,19 @@
from socket import error as socket_error
class WebSocketError(socket_error):
"""
Base class for all websocket errors.
"""
class ProtocolError(WebSocketError):
"""
Raised if an error occurs when de/encoding the websocket protocol.
"""
class FrameTooLargeException(ProtocolError):
"""
Raised if a frame is received that is too large.
"""

View file

@ -0,0 +1,6 @@
from geventwebsocket.handler import WebSocketHandler
from gunicorn.workers.ggevent import GeventPyWSGIWorker
class GeventWebSocketWorker(GeventPyWSGIWorker):
wsgi_handler = WebSocketHandler

View file

@ -0,0 +1,283 @@
# Modified: Werkzeug Debugger workaround in run_websocket(self):
import base64
import hashlib
import warnings
from gevent.pywsgi import WSGIHandler
from .websocket import WebSocket, Stream
from .logging import create_logger
class Client(object):
def __init__(self, address, ws):
self.address = address
self.ws = ws
class WebSocketHandler(WSGIHandler):
"""
Automatically upgrades the connection to a websocket.
To prevent the WebSocketHandler to call the underlying WSGI application,
but only setup the WebSocket negotiations, do:
mywebsockethandler.prevent_wsgi_call = True
before calling run_application(). This is useful if you want to do more
things before calling the app, and want to off-load the WebSocket
negotiations to this library. Socket.IO needs this for example, to send
the 'ack' before yielding the control to your WSGI app.
"""
SUPPORTED_VERSIONS = ('13', '8', '7')
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def run_websocket(self):
"""
Called when a websocket has been created successfully.
"""
if getattr(self, 'prevent_wsgi_call', False):
return
# In case WebSocketServer is not used
if not hasattr(self.server, 'clients'):
self.server.clients = {}
# Since we're now a websocket connection, we don't care what the
# application actually responds with for the http response
try:
self.server.clients[self.client_address] = Client(
self.client_address, self.websocket)
if self.application.__class__.__name__ == "DebuggedApplication": # Modified: Werkzeug Debugger workaround (https://bitbucket.org/Jeffrey/gevent-websocket/issue/53/if-the-application-returns-a-generator-we)
list(self.application(self.environ, lambda s, h: []))
else:
self.application(self.environ, lambda s, h: [])
finally:
del self.server.clients[self.client_address]
if not self.websocket.closed:
self.websocket.close()
self.environ.update({
'wsgi.websocket': None
})
self.websocket = None
def run_application(self):
if (hasattr(self.server, 'pre_start_hook')
and self.server.pre_start_hook):
self.logger.debug("Calling pre-start hook")
if self.server.pre_start_hook(self):
return super(WebSocketHandler, self).run_application()
self.logger.debug("Initializing WebSocket")
self.result = self.upgrade_websocket()
if hasattr(self, 'websocket'):
if self.status and not self.headers_sent:
self.write('')
self.run_websocket()
else:
if self.status:
# A status was set, likely an error so just send the response
if not self.result:
self.result = []
self.process_result()
return
# This handler did not handle the request, so defer it to the
# underlying application object
return super(WebSocketHandler, self).run_application()
def upgrade_websocket(self):
"""
Attempt to upgrade the current environ into a websocket enabled
connection. If successful, the environ dict with be updated with two
new entries, `wsgi.websocket` and `wsgi.websocket_version`.
:returns: Whether the upgrade was successful.
"""
# Some basic sanity checks first
self.logger.debug("Validating WebSocket request")
if self.environ.get('REQUEST_METHOD', '') != 'GET':
# This is not a websocket request, so we must not handle it
self.logger.debug('Can only upgrade connection if using GET method.')
return
upgrade = self.environ.get('HTTP_UPGRADE', '').lower()
if upgrade == 'websocket':
connection = self.environ.get('HTTP_CONNECTION', '').lower()
if 'upgrade' not in connection:
# This is not a websocket request, so we must not handle it
self.logger.warning("Client didn't ask for a connection "
"upgrade")
return
else:
# This is not a websocket request, so we must not handle it
return
if self.request_version != 'HTTP/1.1':
self.start_response('402 Bad Request', [])
self.logger.warning("Bad server protocol in headers")
return ['Bad protocol version']
if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'):
return self.upgrade_connection()
else:
self.logger.warning("No protocol defined")
self.start_response('426 Upgrade Required', [
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))])
return ['No Websocket protocol version defined']
def upgrade_connection(self):
"""
Validate and 'upgrade' the HTTP request to a WebSocket request.
If an upgrade succeeded then then handler will have `start_response`
with a status of `101`, the environ will also be updated with
`wsgi.websocket` and `wsgi.websocket_version` keys.
:param environ: The WSGI environ dict.
:param start_response: The callable used to start the response.
:param stream: File like object that will be read from/written to by
the underlying WebSocket object, if created.
:return: The WSGI response iterator is something went awry.
"""
self.logger.debug("Attempting to upgrade connection")
version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION")
if version not in self.SUPPORTED_VERSIONS:
msg = "Unsupported WebSocket Version: {0}".format(version)
self.logger.warning(msg)
self.start_response('400 Bad Request', [
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))
])
return [msg]
key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip()
if not key:
# 5.2.1 (3)
msg = "Sec-WebSocket-Key header is missing/empty"
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
try:
key_len = len(base64.b64decode(key))
except TypeError:
msg = "Invalid key: {0}".format(key)
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
if key_len != 16:
# 5.2.1 (3)
msg = "Invalid key: {0}".format(key)
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
# Check for WebSocket Protocols
requested_protocols = self.environ.get(
'HTTP_SEC_WEBSOCKET_PROTOCOL', '')
protocol = None
if hasattr(self.application, 'app_protocol'):
allowed_protocol = self.application.app_protocol(
self.environ['PATH_INFO'])
if allowed_protocol and allowed_protocol in requested_protocols:
protocol = allowed_protocol
self.logger.debug("Protocol allowed: {0}".format(protocol))
self.websocket = WebSocket(self.environ, Stream(self), self)
self.environ.update({
'wsgi.websocket_version': version,
'wsgi.websocket': self.websocket
})
headers = [
("Upgrade", "websocket"),
("Connection", "Upgrade"),
("Sec-WebSocket-Accept", base64.b64encode(
hashlib.sha1(key + self.GUID).digest())),
]
if protocol:
headers.append(("Sec-WebSocket-Protocol", protocol))
self.logger.debug("WebSocket request accepted, switching protocols")
self.start_response("101 Switching Protocols", headers)
@property
def logger(self):
if not hasattr(self.server, 'logger'):
self.server.logger = create_logger(__name__)
return self.server.logger
def log_request(self):
if '101' not in self.status:
self.logger.info(self.format_request())
@property
def active_client(self):
return self.server.clients[self.client_address]
def start_response(self, status, headers, exc_info=None):
"""
Called when the handler is ready to send a response back to the remote
endpoint. A websocket connection may have not been created.
"""
writer = super(WebSocketHandler, self).start_response(
status, headers, exc_info=exc_info)
self._prepare_response()
return writer
def _prepare_response(self):
"""
Sets up the ``pywsgi.Handler`` to work with a websocket response.
This is used by other projects that need to support WebSocket
connections as part of a larger effort.
"""
assert not self.headers_sent
if not self.environ.get('wsgi.websocket'):
# a WebSocket connection is not established, do nothing
return
# So that `finalize_headers` doesn't write a Content-Length header
self.provided_content_length = False
# The websocket is now controlling the response
self.response_use_chunked = False
# Once the request is over, the connection must be closed
self.close_connection = True
# Prevents the Date header from being written
self.provided_date = True

View file

@ -0,0 +1,31 @@
from __future__ import absolute_import
from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG
def create_logger(name, debug=False, format=None):
Logger = getLoggerClass()
class DebugLogger(Logger):
def getEffectiveLevel(x):
if x.level == 0 and debug:
return DEBUG
else:
return Logger.getEffectiveLevel(x)
class DebugHandler(StreamHandler):
def emit(x, record):
StreamHandler.emit(x, record) if debug else None
handler = DebugHandler()
handler.setLevel(DEBUG)
if format:
handler.setFormatter(Formatter(format))
logger = getLogger(name)
del logger.handlers[:]
logger.__class__ = DebugLogger
logger.addHandler(handler)
return logger

View file

@ -0,0 +1,35 @@
class BaseProtocol(object):
PROTOCOL_NAME = ''
def __init__(self, app):
self._app = app
def on_open(self):
self.app.on_open()
def on_message(self, message):
self.app.on_message(message)
def on_close(self, reason=None):
self.app.on_close(reason)
@property
def app(self):
if self._app:
return self._app
else:
raise Exception("No application coupled")
@property
def server(self):
if not hasattr(self.app, 'ws'):
return None
return self.app.ws.handler.server
@property
def handler(self):
if not hasattr(self.app, 'ws'):
return None
return self.app.ws.handler

View file

@ -0,0 +1,234 @@
import inspect
import random
import string
import types
try:
import ujson as json
except ImportError:
try:
import simplejson as json
except ImportError:
import json
from ..exceptions import WebSocketError
from .base import BaseProtocol
def export_rpc(arg=None):
if isinstance(arg, types.FunctionType):
arg._rpc = arg.__name__
return arg
def serialize(data):
return json.dumps(data)
class Prefixes(object):
def __init__(self):
self.prefixes = {}
def add(self, prefix, uri):
self.prefixes[prefix] = uri
def resolve(self, curie_or_uri):
if "http://" in curie_or_uri:
return curie_or_uri
elif ':' in curie_or_uri:
prefix, proc = curie_or_uri.split(':', 1)
return self.prefixes[prefix] + proc
else:
raise Exception(curie_or_uri)
class RemoteProcedures(object):
def __init__(self):
self.calls = {}
def register_procedure(self, uri, proc):
self.calls[uri] = proc
def register_object(self, uri, obj):
for k in inspect.getmembers(obj, inspect.ismethod):
if '_rpc' in k[1].__dict__:
proc_uri = uri + k[1]._rpc
self.calls[proc_uri] = (obj, k[1])
def call(self, uri, args):
if uri in self.calls:
proc = self.calls[uri]
# Do the correct call whether it's a function or instance method.
if isinstance(proc, tuple):
if proc[1].__self__ is None:
# Create instance of object and call method
return proc[1](proc[0](), *args)
else:
# Call bound method on instance
return proc[1](*args)
else:
return self.calls[uri](*args)
else:
raise Exception("no such uri '{}'".format(uri))
class Channels(object):
def __init__(self):
self.channels = {}
def create(self, uri, prefix_matching=False):
if uri not in self.channels:
self.channels[uri] = []
# TODO: implement prefix matching
def subscribe(self, uri, client):
if uri in self.channels:
self.channels[uri].append(client)
def unsubscribe(self, uri, client):
if uri not in self.channels:
return
client_index = self.channels[uri].index(client)
self.channels[uri].pop(client_index)
if len(self.channels[uri]) == 0:
del self.channels[uri]
def publish(self, uri, event, exclude=None, eligible=None):
if uri not in self.channels:
return
# TODO: exclude & eligible
msg = [WampProtocol.MSG_EVENT, uri, event]
for client in self.channels[uri]:
try:
client.ws.send(serialize(msg))
except WebSocketError:
# Seems someone didn't unsubscribe before disconnecting
self.channels[uri].remove(client)
class WampProtocol(BaseProtocol):
MSG_WELCOME = 0
MSG_PREFIX = 1
MSG_CALL = 2
MSG_CALL_RESULT = 3
MSG_CALL_ERROR = 4
MSG_SUBSCRIBE = 5
MSG_UNSUBSCRIBE = 6
MSG_PUBLISH = 7
MSG_EVENT = 8
PROTOCOL_NAME = "wamp"
def __init__(self, *args, **kwargs):
self.procedures = RemoteProcedures()
self.prefixes = Prefixes()
self.session_id = ''.join(
[random.choice(string.digits + string.letters)
for i in xrange(16)])
super(WampProtocol, self).__init__(*args, **kwargs)
def register_procedure(self, *args, **kwargs):
self.procedures.register_procedure(*args, **kwargs)
def register_object(self, *args, **kwargs):
self.procedures.register_object(*args, **kwargs)
def register_pubsub(self, *args, **kwargs):
if not hasattr(self.server, 'channels'):
self.server.channels = Channels()
self.server.channels.create(*args, **kwargs)
def do_handshake(self):
from geventwebsocket import get_version
welcome = [
self.MSG_WELCOME,
self.session_id,
1,
'gevent-websocket/' + get_version()
]
self.app.ws.send(serialize(welcome))
def _get_exception_info(self, e):
uri = 'http://TODO#generic'
desc = str(type(e))
details = str(e)
return [uri, desc, details]
def rpc_call(self, data):
call_id, curie_or_uri = data[1:3]
args = data[3:]
if not isinstance(call_id, (str, unicode)):
raise Exception()
if not isinstance(curie_or_uri, (str, unicode)):
raise Exception()
uri = self.prefixes.resolve(curie_or_uri)
try:
result = self.procedures.call(uri, args)
result_msg = [self.MSG_CALL_RESULT, call_id, result]
except Exception, e:
result_msg = [self.MSG_CALL_ERROR,
call_id] + self._get_exception_info(e)
self.app.on_message(serialize(result_msg))
def pubsub_action(self, data):
action = data[0]
curie_or_uri = data[1]
if not isinstance(action, int):
raise Exception()
if not isinstance(curie_or_uri, (str, unicode)):
raise Exception()
uri = self.prefixes.resolve(curie_or_uri)
if action == self.MSG_SUBSCRIBE and len(data) == 2:
self.server.channels.subscribe(data[1], self.handler.active_client)
elif action == self.MSG_UNSUBSCRIBE and len(data) == 2:
self.server.channels.unsubscribe(
data[1], self.handler.active_client)
elif action == self.MSG_PUBLISH and len(data) >= 3:
payload = data[2] if len(data) >= 3 else None
exclude = data[3] if len(data) >= 4 else None
eligible = data[4] if len(data) >= 5 else None
self.server.channels.publish(uri, payload, exclude, eligible)
def on_open(self):
self.app.on_open()
self.do_handshake()
def on_message(self, message):
data = json.loads(message)
if not isinstance(data, list):
raise Exception('incoming data is no list')
if data[0] == self.MSG_PREFIX and len(data) == 3:
prefix, uri = data[1:3]
self.prefixes.add(prefix, uri)
elif data[0] == self.MSG_CALL and len(data) >= 3:
return self.rpc_call(data)
elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE,
self.MSG_PUBLISH):
return self.pubsub_action(data)
else:
raise Exception("Unknown call")

View file

@ -0,0 +1,74 @@
import re
from .protocols.base import BaseProtocol
from .exceptions import WebSocketError
class WebSocketApplication(object):
protocol_class = BaseProtocol
def __init__(self, ws):
self.protocol = self.protocol_class(self)
self.ws = ws
def handle(self):
self.protocol.on_open()
while True:
try:
message = self.ws.receive()
except WebSocketError:
self.protocol.on_close()
break
self.protocol.on_message(message)
def on_open(self, *args, **kwargs):
pass
def on_close(self, *args, **kwargs):
pass
def on_message(self, message, *args, **kwargs):
self.ws.send(message, **kwargs)
@classmethod
def protocol_name(cls):
return cls.protocol_class.PROTOCOL_NAME
class Resource(object):
def __init__(self, apps=None):
self.apps = apps if apps else []
def _app_by_path(self, environ_path):
# Which app matched the current path?
for path, app in self.apps.iteritems():
if re.match(path, environ_path):
return app
def app_protocol(self, path):
app = self._app_by_path(path)
if hasattr(app, 'protocol_name'):
return app.protocol_name()
else:
return ''
def __call__(self, environ, start_response):
environ = environ
current_app = self._app_by_path(environ['PATH_INFO'])
if current_app is None:
raise Exception("No apps defined")
if 'wsgi.websocket' in environ:
ws = environ['wsgi.websocket']
current_app = current_app(ws)
current_app.ws = ws # TODO: needed?
current_app.handle()
return None
else:
return current_app(environ, start_response)

View file

@ -0,0 +1,34 @@
from gevent.pywsgi import WSGIServer
from .handler import WebSocketHandler
from .logging import create_logger
class WebSocketServer(WSGIServer):
debug_log_format = (
'-' * 80 + '\n' +
'%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' +
'%(message)s\n' +
'-' * 80
)
def __init__(self, *args, **kwargs):
self.debug = kwargs.pop('debug', False)
self.pre_start_hook = kwargs.pop('pre_start_hook', None)
self._logger = None
self.clients = {}
kwargs['handler_class'] = WebSocketHandler
super(WebSocketServer, self).__init__(*args, **kwargs)
def handle(self, socket, address):
handler = self.handler_class(socket, address, self)
handler.handle()
@property
def logger(self):
if not self._logger:
self._logger = create_logger(
__name__, self.debug, self.debug_log_format)
return self._logger

View file

@ -0,0 +1,128 @@
###############################################################################
##
## Copyright 2011-2013 Tavendo GmbH
##
## Note:
##
## This code is a Python implementation of the algorithm
##
## "Flexible and Economical UTF-8 Decoder"
##
## by Bjoern Hoehrmann
##
## bjoern@hoehrmann.de
## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##
###############################################################################
## use Cython implementation of UTF8 validator if available
##
try:
from wsaccel.utf8validator import Utf8Validator
except:
## fallback to pure Python implementation
class Utf8Validator:
"""
Incremental UTF-8 validator with constant memory consumption (minimal
state).
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
"""
## DFA transitions
UTF8VALIDATOR_DFA = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
]
UTF8_ACCEPT = 0
UTF8_REJECT = 1
def __init__(self):
self.reset()
def decode(self, b):
"""
Eat one UTF-8 octet, and validate on the fly.
Returns UTF8_ACCEPT when enough octets have been consumed, in which case
self.codepoint contains the decoded Unicode code point.
Returns UTF8_REJECT when invalid UTF-8 was encountered.
Returns some other positive integer when more octets need to be eaten.
"""
type = Utf8Validator.UTF8VALIDATOR_DFA[b]
if self.state != Utf8Validator.UTF8_ACCEPT:
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
else:
self.codepoint = (0xff >> type) & b
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type]
return self.state
def reset(self):
"""
Reset validator to start new incremental UTF-8 decode/validation.
"""
self.state = Utf8Validator.UTF8_ACCEPT
self.codepoint = 0
self.i = 0
def validate(self, ba):
"""
Incrementally validate a chunk of bytes provided as string.
Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex).
As soon as an octet is encountered which renders the octet sequence
invalid, a quad with valid? == False is returned. currentIndex returns
the index within the currently consumed chunk, and totalIndex the
index within the total consumed sequence that was the point of bail out.
When valid? == True, currentIndex will be len(ba) and totalIndex the
total amount of consumed bytes.
"""
l = len(ba)
for i in xrange(l):
## optimized version of decode(), since we are not interested in actual code points
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]]
if self.state == Utf8Validator.UTF8_REJECT:
self.i += i
return False, False, i, self.i
self.i += l
return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i

View file

@ -0,0 +1,45 @@
import subprocess
def get_version(version=None):
"Returns a PEP 386-compliant version number from VERSION."
if version is None:
from geventwebsocket import VERSION as version
else:
assert len(version) == 5
assert version[3] in ('alpha', 'beta', 'rc', 'final')
# Now build the two parts of the version number:
# main = X.Y[.Z]
# sub = .devN - for pre-alpha releases
# | {a|b|c}N - for alpha, beta and rc releases
parts = 2 if version[2] == 0 else 3
main = '.'.join(str(x) for x in version[:parts])
sub = ''
if version[3] == 'alpha' and version[4] == 0:
hg_changeset = get_hg_changeset()
if hg_changeset:
sub = '.dev{0}'.format(hg_changeset)
elif version[3] != 'final':
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
sub = mapping[version[3]] + str(version[4])
return str(main + sub)
def get_hg_changeset():
rev, err = subprocess.Popen(
'hg id -i',
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
if err:
return None
else:
return rev.strip().replace('+', '')

View file

@ -0,0 +1,543 @@
import struct
from socket import error
from .exceptions import ProtocolError
from .exceptions import WebSocketError
from .exceptions import FrameTooLargeException
from .utf8validator import Utf8Validator
MSG_SOCKET_DEAD = "Socket is dead"
MSG_ALREADY_CLOSED = "Connection is already closed"
MSG_CLOSED = "Connection closed"
class WebSocket(object):
"""
Base class for supporting websocket operations.
:ivar environ: The http environment referenced by this connection.
:ivar closed: Whether this connection is closed/closing.
:ivar stream: The underlying file like object that will be read from /
written to by this WebSocket object.
"""
__slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed',
'stream', 'raw_write', 'raw_read', 'handler')
OPCODE_CONTINUATION = 0x00
OPCODE_TEXT = 0x01
OPCODE_BINARY = 0x02
OPCODE_CLOSE = 0x08
OPCODE_PING = 0x09
OPCODE_PONG = 0x0a
def __init__(self, environ, stream, handler):
self.environ = environ
self.closed = False
self.stream = stream
self.raw_write = stream.write
self.raw_read = stream.read
self.utf8validator = Utf8Validator()
self.handler = handler
def __del__(self):
try:
self.close()
except:
# close() may fail if __init__ didn't complete
pass
def _decode_bytes(self, bytestring):
"""
Internal method used to convert the utf-8 encoded bytestring into
unicode.
If the conversion fails, the socket will be closed.
"""
if not bytestring:
return u''
try:
return bytestring.decode('utf-8')
except UnicodeDecodeError:
self.close(1007)
raise
def _encode_bytes(self, text):
"""
:returns: The utf-8 byte string equivalent of `text`.
"""
if isinstance(text, str):
return text
if not isinstance(text, unicode):
text = unicode(text or '')
return text.encode('utf-8')
def _is_valid_close_code(self, code):
"""
:returns: Whether the returned close code is a valid hybi return code.
"""
if code < 1000:
return False
if 1004 <= code <= 1006:
return False
if 1012 <= code <= 1016:
return False
if code == 1100:
# not sure about this one but the autobahn fuzzer requires it.
return False
if 2000 <= code <= 2999:
return False
return True
@property
def current_app(self):
if hasattr(self.handler.server.application, 'current_app'):
return self.handler.server.application.current_app
else:
# For backwards compatibility reasons
class MockApp():
def on_close(self, *args):
pass
return MockApp()
@property
def origin(self):
if not self.environ:
return
return self.environ.get('HTTP_ORIGIN')
@property
def protocol(self):
if not self.environ:
return
return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
@property
def version(self):
if not self.environ:
return
return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION')
@property
def path(self):
if not self.environ:
return
return self.environ.get('PATH_INFO')
@property
def logger(self):
return self.handler.logger
def handle_close(self, header, payload):
"""
Called when a close frame has been decoded from the stream.
:param header: The decoded `Header`.
:param payload: The bytestring payload associated with the close frame.
"""
if not payload:
self.close(1000, None)
return
if len(payload) < 2:
raise ProtocolError('Invalid close frame: {0} {1}'.format(
header, payload))
code = struct.unpack('!H', str(payload[:2]))[0]
payload = payload[2:]
if payload:
validator = Utf8Validator()
val = validator.validate(payload)
if not val[0]:
raise UnicodeError
if not self._is_valid_close_code(code):
raise ProtocolError('Invalid close code {0}'.format(code))
self.close(code, payload)
def handle_ping(self, header, payload):
self.send_frame(payload, self.OPCODE_PONG)
def handle_pong(self, header, payload):
pass
def read_frame(self):
"""
Block until a full frame has been read from the socket.
This is an internal method as calling this will not cleanup correctly
if an exception is called. Use `receive` instead.
:return: The header and payload as a tuple.
"""
header = Header.decode_header(self.stream)
if header.flags:
raise ProtocolError
if not header.length:
return header, ''
try:
payload = self.raw_read(header.length)
except error:
payload = ''
except Exception:
# TODO log out this exception
payload = ''
if len(payload) != header.length:
raise WebSocketError('Unexpected EOF reading frame payload')
if header.mask:
payload = header.unmask_payload(payload)
return header, payload
def validate_utf8(self, payload):
# Make sure the frames are decodable independently
self.utf8validate_last = self.utf8validator.validate(payload)
if not self.utf8validate_last[0]:
raise UnicodeError("Encountered invalid UTF-8 while processing "
"text message at payload octet index "
"{0:d}".format(self.utf8validate_last[3]))
def read_message(self):
"""
Return the next text or binary message from the socket.
This is an internal method as calling this will not cleanup correctly
if an exception is called. Use `receive` instead.
"""
opcode = None
message = ""
while True:
header, payload = self.read_frame()
f_opcode = header.opcode
if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
# a new frame
if opcode:
raise ProtocolError("The opcode in non-fin frame is "
"expected to be zero, got "
"{0!r}".format(f_opcode))
# Start reading a new message, reset the validator
self.utf8validator.reset()
self.utf8validate_last = (True, True, 0, 0)
opcode = f_opcode
elif f_opcode == self.OPCODE_CONTINUATION:
if not opcode:
raise ProtocolError("Unexpected frame with opcode=0")
elif f_opcode == self.OPCODE_PING:
self.handle_ping(header, payload)
continue
elif f_opcode == self.OPCODE_PONG:
self.handle_pong(header, payload)
continue
elif f_opcode == self.OPCODE_CLOSE:
self.handle_close(header, payload)
return
else:
raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode))
if opcode == self.OPCODE_TEXT:
self.validate_utf8(payload)
message += payload
if header.fin:
break
if opcode == self.OPCODE_TEXT:
self.validate_utf8(message)
return message
else:
return bytearray(message)
def receive(self):
"""
Read and return a message from the stream. If `None` is returned, then
the socket is considered closed/errored.
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
raise WebSocketError(MSG_ALREADY_CLOSED)
try:
return self.read_message()
except UnicodeError:
self.close(1007)
except ProtocolError:
self.close(1002)
except error:
self.close()
self.current_app.on_close(MSG_CLOSED)
return None
def send_frame(self, message, opcode):
"""
Send a frame over the websocket with message as its payload
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
raise WebSocketError(MSG_ALREADY_CLOSED)
if opcode == self.OPCODE_TEXT:
message = self._encode_bytes(message)
elif opcode == self.OPCODE_BINARY:
message = str(message)
header = Header.encode_header(True, opcode, '', len(message), 0)
try:
self.raw_write(header + message)
except error:
raise WebSocketError(MSG_SOCKET_DEAD)
def send(self, message, binary=None):
"""
Send a frame over the websocket with message as its payload
"""
if binary is None:
binary = not isinstance(message, (str, unicode))
opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT
try:
self.send_frame(message, opcode)
except WebSocketError:
self.current_app.on_close(MSG_SOCKET_DEAD)
raise WebSocketError(MSG_SOCKET_DEAD)
def close(self, code=1000, message=''):
"""
Close the websocket and connection, sending the specified code and
message. The underlying socket object is _not_ closed, that is the
responsibility of the initiator.
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
try:
message = self._encode_bytes(message)
self.send_frame(
struct.pack('!H%ds' % len(message), code, message),
opcode=self.OPCODE_CLOSE)
except WebSocketError:
# Failed to write the closing frame but it's ok because we're
# closing the socket anyway.
self.logger.debug("Failed to write closing frame -> closing socket")
finally:
self.logger.debug("Closed WebSocket")
self.closed = True
self.stream = None
self.raw_write = None
self.raw_read = None
self.environ = None
#self.current_app.on_close(MSG_ALREADY_CLOSED)
class Stream(object):
"""
Wraps the handler's socket/rfile attributes and makes it in to a file like
object that can be read from/written to by the lower level websocket api.
"""
__slots__ = ('handler', 'read', 'write')
def __init__(self, handler):
self.handler = handler
self.read = handler.rfile.read
self.write = handler.socket.sendall
class Header(object):
__slots__ = ('fin', 'mask', 'opcode', 'flags', 'length')
FIN_MASK = 0x80
OPCODE_MASK = 0x0f
MASK_MASK = 0x80
LENGTH_MASK = 0x7f
RSV0_MASK = 0x40
RSV1_MASK = 0x20
RSV2_MASK = 0x10
# bitwise mask that will determine the reserved bits for a frame header
HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK
def __init__(self, fin=0, opcode=0, flags=0, length=0):
self.mask = ''
self.fin = fin
self.opcode = opcode
self.flags = flags
self.length = length
def mask_payload(self, payload):
payload = bytearray(payload)
mask = bytearray(self.mask)
for i in xrange(self.length):
payload[i] ^= mask[i % 4]
return str(payload)
# it's the same operation
unmask_payload = mask_payload
def __repr__(self):
return ("<Header fin={0} opcode={1} length={2} flags={3} at "
"0x{4:x}>").format(self.fin, self.opcode, self.length,
self.flags, id(self))
@classmethod
def decode_header(cls, stream):
"""
Decode a WebSocket header.
:param stream: A file like object that can be 'read' from.
:returns: A `Header` instance.
"""
read = stream.read
data = read(2)
if len(data) != 2:
raise WebSocketError("Unexpected EOF while decoding header")
first_byte, second_byte = struct.unpack('!BB', data)
header = cls(
fin=first_byte & cls.FIN_MASK == cls.FIN_MASK,
opcode=first_byte & cls.OPCODE_MASK,
flags=first_byte & cls.HEADER_FLAG_MASK,
length=second_byte & cls.LENGTH_MASK)
has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK
if header.opcode > 0x07:
if not header.fin:
raise ProtocolError(
"Received fragmented control frame: {0!r}".format(data))
# Control frames MUST have a payload length of 125 bytes or less
if header.length > 125:
raise FrameTooLargeException(
"Control frame cannot be larger than 125 bytes: "
"{0!r}".format(data))
if header.length == 126:
# 16 bit length
data = read(2)
if len(data) != 2:
raise WebSocketError('Unexpected EOF while decoding header')
header.length = struct.unpack('!H', data)[0]
elif header.length == 127:
# 64 bit length
data = read(8)
if len(data) != 8:
raise WebSocketError('Unexpected EOF while decoding header')
header.length = struct.unpack('!Q', data)[0]
if has_mask:
mask = read(4)
if len(mask) != 4:
raise WebSocketError('Unexpected EOF while decoding header')
header.mask = mask
return header
@classmethod
def encode_header(cls, fin, opcode, mask, length, flags):
"""
Encodes a WebSocket header.
:param fin: Whether this is the final frame for this opcode.
:param opcode: The opcode of the payload, see `OPCODE_*`
:param mask: Whether the payload is masked.
:param length: The length of the frame.
:param flags: The RSV* flags.
:return: A bytestring encoded header.
"""
first_byte = opcode
second_byte = 0
extra = ''
if fin:
first_byte |= cls.FIN_MASK
if flags & cls.RSV0_MASK:
first_byte |= cls.RSV0_MASK
if flags & cls.RSV1_MASK:
first_byte |= cls.RSV1_MASK
if flags & cls.RSV2_MASK:
first_byte |= cls.RSV2_MASK
# now deal with length complexities
if length < 126:
second_byte += length
elif length <= 0xffff:
second_byte += 126
extra = struct.pack('!H', length)
elif length <= 0xffffffffffffffff:
second_byte += 127
extra = struct.pack('!Q', length)
else:
raise FrameTooLargeException
if mask:
second_byte |= cls.MASK_MASK
extra += mask
return chr(first_byte) + chr(second_byte) + extra

23
src/lib/subtl/LICENCE Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2012, Packetloop. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Packetloop nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

28
src/lib/subtl/README.md Normal file
View file

@ -0,0 +1,28 @@
# subtl
## Overview
SUBTL is a **s**imple **U**DP **B**itTorrent **t**racker **l**ibrary for Python, licenced under the modified BSD license.
## Example
This short example will list a few IP Addresses from a certain hash:
from subtl import UdpTrackerClient
utc = UdpTrackerClient('tracker.openbittorrent.com', 80)
utc.connect()
if not utc.poll_once():
raise Exception('Could not connect')
print('Success!')
utc.announce(info_hash='089184ED52AA37F71801391C451C5D5ADD0D9501')
data = utc.poll_once()
if not data:
raise Exception('Could not announce')
for a in data['response']['peers']:
print(a)
## Caveats
* There is no automatic retrying of sending packets yet.
* This library won't download torrent files--it is simply a tracker client.

View file

220
src/lib/subtl/subtl.py Normal file
View file

@ -0,0 +1,220 @@
'''
Based on the specification at http://bittorrent.org/beps/bep_0015.html
'''
import random
import struct
import time
import socket
from collections import defaultdict
__version__ = '0.0.1'
CONNECT = 0
ANNOUNCE = 1
SCRAPE = 2
ERROR = 3
def norm_info_hash(info_hash):
if len(info_hash) == 40:
info_hash = info_hash.decode('hex')
if len(info_hash) != 20:
raise UdpTrackerClientException(
'info_hash length is not 20: {}'.format(len(info_hash)))
return info_hash
def info_hash_to_str(info_hash):
return binascii.hexlify(info_hash)
class UdpTrackerClientException(Exception):
pass
class UdpTrackerClient:
def __init__(self, host, port):
self.host = host
self.port = port
self.peer_port = 6881
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.conn_id = 0x41727101980
self.transactions = {}
self.peer_id = self._generate_peer_id()
self.timeout = 2
def connect(self):
return self._send(CONNECT)
def announce(self, **kwargs):
if not kwargs:
raise UdpTrackerClientException('arguments missing')
args = {
'peer_id': self.peer_id,
'downloaded': 0,
'left': 0,
'uploaded': 0,
'event': 0,
'key': 0,
'num_want': 10,
'ip_address': 0,
'port': self.peer_port,
}
args.update(kwargs)
fields = 'info_hash peer_id downloaded left uploaded event ' \
'ip_address key num_want port'
# Check and raise if missing fields
self._check_fields(args, fields)
# Humans tend to use hex representations of the hash. Wasteful humans.
args['info_hash'] = norm_info_hash(args['info_hash'])
values = [args[a] for a in fields.split()]
payload = struct.pack('!20s20sQQQLLLLH', *values)
return self._send(ANNOUNCE, payload)
def scrape(self, info_hash_list):
if len(info_hash_list) > 74:
raise UdpTrackerClientException('Max info_hashes is 74')
payload = ''
for info_hash in info_hash_list:
info_hash = norm_info_hash(info_hash)
payload += info_hash
trans = self._send(SCRAPE, payload)
trans['sent_hashes'] = info_hash_list
return trans
def poll_once(self):
self.sock.settimeout(self.timeout)
try:
response = self.sock.recv(10240)
except socket.timeout:
return
header = response[:8]
payload = response[8:]
action, trans_id = struct.unpack('!LL', header)
try:
trans = self.transactions[trans_id]
except KeyError:
self.error('transaction_id not found')
return
trans['response'] = self._process_response(action, payload, trans)
trans['completed'] = True
del self.transactions[trans_id]
return trans
def error(self, message):
print('error: {}'.format(message))
def _send(self, action, payload=None):
if not payload:
payload = ''
trans_id, header = self._request_header(action)
self.transactions[trans_id] = trans = {
'action': action,
'time': time.time(),
'payload': payload,
'completed': False,
}
self.sock.sendto(header + payload, (self.host, self.port))
return trans
def _request_header(self, action):
trans_id = random.randint(0, (1 << 32) - 1)
return trans_id, struct.pack('!QLL', self.conn_id, action, trans_id)
def _process_response(self, action, payload, trans):
if action == CONNECT:
return self._process_connect(payload, trans)
elif action == ANNOUNCE:
return self._process_announce(payload, trans)
elif action == SCRAPE:
return self._process_scrape(payload, trans)
elif action == ERROR:
return self._proecss_error(payload, trans)
else:
raise UdpTrackerClientException(
'Unknown action response: {}'.format(action))
def _process_connect(self, payload, trans):
self.conn_id = struct.unpack('!Q', payload)[0]
return self.conn_id
def _process_announce(self, payload, trans):
response = {}
info_struct = '!LLL'
info_size = struct.calcsize(info_struct)
info = payload[:info_size]
interval, leechers, seeders = struct.unpack(info_struct, info)
peer_data = payload[info_size:]
peer_struct = '!LH'
peer_size = struct.calcsize(peer_struct)
peer_count = len(peer_data) / peer_size
peers = []
for peer_offset in xrange(peer_count):
off = peer_size * peer_offset
peer = peer_data[off:off + peer_size]
addr, port = struct.unpack(peer_struct, peer)
peers.append({
'addr': socket.inet_ntoa(struct.pack('!L', addr)),
'port': port,
})
return {
'interval': interval,
'leechers': leechers,
'seeders': seeders,
'peers': peers,
}
def _process_scrape(self, payload, trans):
info_struct = '!LLL'
info_size = struct.calcsize(info_struct)
info_count = len(payload) / info_size
hashes = trans['sent_hashes']
response = {}
for info_offset in xrange(info_count):
off = info_size * info_offset
info = payload[off:off + info_size]
seeders, completed, leechers = struct.unpack(info_struct, info)
response[hashes[info_offset]] = {
'seeders': seeders,
'completed': completed,
'leechers': leechers,
}
return response
def _process_error(self, payload, trans):
'''
I haven't seen this action type be sent from a tracker, but I've left
it here for the possibility.
'''
self.error(payload)
return payload
def _generate_peer_id(self):
'''http://www.bittorrent.org/beps/bep_0020.html'''
peer_id = '-PU' + __version__.replace('.', '-') + '-'
remaining = 20 - len(peer_id)
numbers = [str(random.randint(0, 9)) for _ in xrange(remaining)]
peer_id += ''.join(numbers)
assert(len(peer_id) == 20)
return peer_id
def _check_fields(self, args, fields):
for f in fields:
try:
args.get(f)
except KeyError:
raise UdpTrackerClientException('field missing: {}'.format(f))