diff --git a/pyogp/lib/base/agent.py b/pyogp/lib/base/agent.py index 7c8c797..f250f96 100644 --- a/pyogp/lib/base/agent.py +++ b/pyogp/lib/base/agent.py @@ -16,5 +16,3 @@ class Agent(object): - - \ No newline at end of file diff --git a/pyogp/lib/base/agentdomain.py b/pyogp/lib/base/agentdomain.py index de03db2..51f47bc 100644 --- a/pyogp/lib/base/agentdomain.py +++ b/pyogp/lib/base/agentdomain.py @@ -2,6 +2,8 @@ from agent import Agent from interfaces import ISerialization from caps import SeedCapability +USE_REDIRECT=True + import urllib2 from zope.interface import implements @@ -10,24 +12,29 @@ import grokcore.component as grok from indra.base import llsd from interfaces import ICredentialSerializer, IPlaceAvatar, IAgentDomain +from network import IRESTClient, HTTPError +from zope.component import queryUtility, adapts, getUtility from agent import Agent from avatar import Avatar from caps import SeedCapability + # URL Opener for the agent domain login -# -class RedirectHandler(urllib2.HTTPRedirectHandler): +# - def http_error_302(self, req, fp, code, msg, headers): - #ignore the redirect, grabbing the seed cap url from the headers - # TODO: add logging and error handling - print "huhu" - return headers['location'] +if USE_REDIRECT: + # REMOVE THIS WHEN THE REDIRECT IS NOT NEEDED ANYMORE FOR LINDEN LAB! + class RedirectHandler(urllib2.HTTPRedirectHandler): + + def http_error_302(self, req, fp, code, msg, headers): + #ignore the redirect, grabbing the seed cap url from the headers + # TODO: add logging and error handling + return headers['location'] -# post to auth.cgi, ignoring the built in redirect -AgentDomainLoginOpener = urllib2.build_opener(RedirectHandler()) + # post to auth.cgi, ignoring the built in redirect + AgentDomainLoginOpener = urllib2.build_opener(RedirectHandler()) class AgentDomain(object): @@ -49,19 +56,30 @@ class AgentDomain(object): # now create the request. We assume for now that self.uri is the login uri # TODO: make this pluggable so we can use other transports like eventlet in the future # TODO: add logging and error handling - # - request = urllib2.Request(self.uri,payload,headers) - try: - res = AgentDomainLoginOpener.open(request) - except urllib2.HTTPError,e: - print e.read() - raise - if type(res)!=type(""): - seed_cap_url_data = res.read() # it might be an addinfourl object - seed_cap_url = llsd.parse(seed_cap_url_data)['agent_seed_capability'] + if USE_REDIRECT: + request = urllib2.Request(self.uri,payload,headers) + try: + res = AgentDomainLoginOpener.open(request) + except urllib2.HTTPError,e: + print e.read() + raise + if type(res)!=type(""): + seed_cap_url_data = res.read() # it might be an addinfourl object + seed_cap_url = llsd.parse(seed_cap_url_data)['agent_seed_capability'] + else: + # this only happens in the Linden Lab Agent Domain with their redirect + seed_cap_url = res else: - # this only happens in the Linden Lab Agent Domain with their redirect - seed_cap_url = res + restclient = getUtility(IRESTClient) + try: + response = restclient.POST(self.uri, payload, headers=headers) + except HTTPError, error: + print "error", error.code, error.msg + print error.fp.read() + raise + + seed_cap_url_data = response.body + seed_cap_url = llsd.parse(seed_cap_url_data)['agent_seed_capability'] self.seed_cap = SeedCapability('seed_cap', seed_cap_url) return Agent(self) @@ -83,7 +101,7 @@ class PlaceAvatar(grok.Adapter): """initiate the placing process""" region_uri = region.uri payload = {'region_url' : region_uri } - result = self.place_avatar_cap(payload) + result = self.place_avatar_cap.POST(payload) region.details = result avatar = Avatar(region) diff --git a/pyogp/lib/base/api.py b/pyogp/lib/base/api.py index b1778ed..e78ae9d 100644 --- a/pyogp/lib/base/api.py +++ b/pyogp/lib/base/api.py @@ -15,7 +15,7 @@ def login_with_plainpassword(agentdomain_url, firstname, lastname, password): takes firstname, lastname and plain password and returns an agent object Using it is simple: - >>> agent = login_with_plainpassword("http://localhost:12345","Firstname","Lastname","secret") + >>> agent = login_with_plainpassword("http://localhost:12345/","Firstname","Lastname","secret") Now this agent should contain an agentdomain object >>> agent.agentdomain @@ -40,7 +40,7 @@ def place_avatar(agent, region_url): Placing an avatar is simple. We just need an agent object and a region url. We get an agent object via the login: - >>> agent = login_with_plainpassword("http://localhost:12345","Firstname","Lastname","secret") + >>> agent = login_with_plainpassword("http://localhost:12345/","Firstname","Lastname","secret") And now we can call it: >>> avatar = place_avatar(agent, "http://localhost:12345/region") @@ -64,7 +64,7 @@ def run_loop(avatar): """run the UDP loop for the avatar First we create one as seen above: - >>> agent = login_with_plainpassword("http://localhost:12345","Firstname","Lastname","secret") + >>> agent = login_with_plainpassword("http://localhost:12345/","Firstname","Lastname","secret") >>> avatar = place_avatar(agent, "http://localhost:12345/region") And now we can run the loop: diff --git a/pyogp/lib/base/caps.py b/pyogp/lib/base/caps.py index a848dc9..87bd063 100644 --- a/pyogp/lib/base/caps.py +++ b/pyogp/lib/base/caps.py @@ -1,11 +1,13 @@ -from zope.interface import implements -from zope.component import queryUtility, adapts - import urllib2 + +from zope.interface import implements +from zope.component import queryUtility, adapts, getUtility +import grokcore.component as grok from indra.base import llsd from interfaces import ICapability, ISeedCapability from interfaces import ISerialization, IDeserialization +from network import IRESTClient, HTTPError class Capability(object): @@ -19,7 +21,7 @@ class Capability(object): self.name = name self.public_url = public_url - def __call__(self,payload,custom_headers={}): + def POST(self,payload,custom_headers={}): """call this capability, return the parsed result""" # serialize the data @@ -32,25 +34,23 @@ class Capability(object): # TODO: better errorhandling with own exceptions try: - request = urllib2.Request(self.public_url, serialized_payload, headers) - result = urllib2.urlopen(request) - except urllib2.HTTPError, e: + restclient = getUtility(IRESTClient) + response = restclient.POST(self.public_url, serialized_payload, headers=headers) + except HTTPError, e: print "** failure while calling cap:", print e.read() raise # now deserialize the data again, we ask for a utility with the content type # as the name - content_type_charset = result.headers['Content-Type'] + content_type_charset = response.headers['Content-Type'] content_type = content_type_charset.split(";")[0] # remove the charset part deserializer = queryUtility(IDeserialization,name=content_type) if deserializer is None: # TODO: do better error handling here - print "RESULT", result.read() - print result.headers raise "deserialization for %s not supported" %(content_type) - return deserializer.deserialize_string(result.read()) + return deserializer.deserialize_string(response.body) def __repr__(self): return "" %self.public_url @@ -63,7 +63,7 @@ class SeedCapability(Capability): def get(self, names=[]): """if this is a seed cap we can retrieve other caps here""" payload = {'caps':names} - parsed_result = self(payload)['caps'] + parsed_result = self.POST(payload)['caps'] caps = {} for name in names: @@ -82,7 +82,7 @@ class SeedCapability(Capability): #### -class DictLLSDSerializer(object): +class DictLLSDSerializer(grok.Adapter): """adapter for serializing a dictionary to LLSD An example: @@ -94,8 +94,8 @@ class DictLLSDSerializer(object): 'application/llsd+xml' """ - implements(ISerialization) - adapts(dict) + grok.implements(ISerialization) + grok.context(dict) def __init__(self, context): self.context = context @@ -109,7 +109,7 @@ class DictLLSDSerializer(object): """return the content type of this serializer""" return "application/llsd+xml" -class LLSDDeserializer(object): +class LLSDDeserializer(grok.GlobalUtility): """utility for deserializing LLSD data The deserialization component is defined as a utility because the input @@ -135,7 +135,8 @@ class LLSDDeserializer(object): {'test': 1234, 'foo': 'bar'} """ - implements(IDeserialization) + grok.implements(IDeserialization) + grok.name('application/llsd+xml') def deserialize_string(self, data): """deserialize a string""" @@ -146,14 +147,6 @@ class LLSDDeserializer(object): data = fp.read() return self.deserialize_string(data) - -# register everything -# now we register this adapter so it can be used later: -from zope.component import provideAdapter, provideUtility - -# register adapters for the HTML node -provideAdapter(DictLLSDSerializer) -provideUtility(LLSDDeserializer(), IDeserialization, name="application/llsd+xml") diff --git a/pyogp/lib/base/credentials.py b/pyogp/lib/base/credentials.py index c226334..dba43ae 100644 --- a/pyogp/lib/base/credentials.py +++ b/pyogp/lib/base/credentials.py @@ -1,6 +1,8 @@ from zope.interface import implements from zope.component import adapts +import md5 + from indra.base import llsd import grokcore.component as grok @@ -44,9 +46,10 @@ class PlainPasswordLLSDSerializer(grok.Adapter): """return the credential as a string""" loginparams={ - 'password' : self.context.password, - 'lastname' : self.context.lastname, - 'firstname' : self.context.firstname +# 'password' : "$1$"+md5.new(self.context.password).hexdigest(), + 'password' : self.context.password, + 'lastname' : self.context.lastname, + 'firstname' : self.context.firstname } llsdlist = llsd.format_xml(loginparams) diff --git a/pyogp/lib/base/network/__init__.py b/pyogp/lib/base/network/__init__.py new file mode 100644 index 0000000..0985d01 --- /dev/null +++ b/pyogp/lib/base/network/__init__.py @@ -0,0 +1,10 @@ + +from stdlib_client import StdLibClient +from mockup_client import MockupClient + +from interfaces import IRESTClient + +from exc import HTTPError + +from zope.component import provideUtility +provideUtility(StdLibClient(), IRESTClient) diff --git a/pyogp/lib/base/network/exc.py b/pyogp/lib/base/network/exc.py new file mode 100644 index 0000000..206503f --- /dev/null +++ b/pyogp/lib/base/network/exc.py @@ -0,0 +1,15 @@ + +class HTTPError(Exception): + """an HTTP error""" + + def __init__(self, code, msg, fp, details=""): + """initialize this exception""" + self.code = code + self.msg = msg + self.details = details + self.fp = fp + + def __str__(self): + """return a string representation""" + return "%s %s" %(self.code, self.msg) + diff --git a/pyogp/lib/base/network/interfaces.py b/pyogp/lib/base/network/interfaces.py new file mode 100644 index 0000000..836bebf --- /dev/null +++ b/pyogp/lib/base/network/interfaces.py @@ -0,0 +1,43 @@ +from zope.interface import Interface, Attribute + +class IRESTClient(Interface): + """a RESTful client""" + + def GET(url, headers={}): + """send a GET request to the resource identified by url + + optionally you can pass headers in which get added to the header list + (or overwritten if they are already defined) + + returns a webob.Response object + + """ + + def POST(url, data, headers={}): + """POST data to a resource identified by url. + + optionally you can pass headers in which get added to the header list + (or overwritten if they are already defined) + + returns a webob.Response object + """ + + def PUT(url, data, headers={}): + """PUT data to a resource identified by url. + + optionally you can pass headers in which get added to the header list + (or overwritten if they are already defined) + + returns a webob.Response object + """ + + def DELETE(url, headers={}): + """DELETE the resource identified by url. + + optionally you can pass headers in which get added to the header list + (or overwritten if they are already defined) + + returns a webob.Response object + """ + + \ No newline at end of file diff --git a/pyogp/lib/base/network/mockup_client.py b/pyogp/lib/base/network/mockup_client.py new file mode 100644 index 0000000..6971668 --- /dev/null +++ b/pyogp/lib/base/network/mockup_client.py @@ -0,0 +1,49 @@ +from zope.interface import implements +import urlparse + +from pyogp.lib.base.network.interfaces import IRESTClient +from exc import HTTPError + +from webob import Request, Response +from webob.exc import HTTPException, HTTPExceptionMiddleware + +from cStringIO import StringIO + +class MockupClient(object): + """implement a REST client on top of urllib2""" + + def __init__(self, wsgi_app): + self.app=wsgi_app + + def strip_url(self, url): + """remove server/host from the URL""" + o = urlparse.urlparse(url) + p=o[2] + if o[4]: + p=p+"?"+o[4] + if o[5]: + p=p+"#"+o[5] + return url + + def GET(self, url, headers={}): + """GET a resource""" + request = Request.blank(self.strip_url(url)) + request.method="GET" + response = request.get_response(self.app) + if not response.status.startswith("2"): + parts = response.status.split(" ") + msg = " ".join(parts[1:]) + raise HTTPError(response.status_int, msg, StringIO(response.body)) + return response + + def POST(self, url, data, headers={}): + """POST data to a resource""" + request = Request.blank(self.strip_url(url)) + request.body = data + request.method="POST" + response = request.get_response(self.app) + if not response.status.startswith("2"): + parts = response.status.split(" ") + msg = " ".join(parts[1:]) + raise HTTPError(response.status_int, msg, StringIO(response.body)) + return response diff --git a/pyogp/lib/base/network/stdlib_client.py b/pyogp/lib/base/network/stdlib_client.py new file mode 100644 index 0000000..49ea776 --- /dev/null +++ b/pyogp/lib/base/network/stdlib_client.py @@ -0,0 +1,47 @@ +from zope.interface import implements +import urllib2 + +from pyogp.lib.base.network.interfaces import IRESTClient +from exc import HTTPError + +from webob import Request, Response + +class StdLibClient(object): + """implement a REST client on top of urllib2""" + + def GET(self, url, headers={}): + """GET a resource""" + request = urllib2.Request(url, headers=headers) + try: + result = urllib2.urlopen(request) + except urllib2.HTTPError, error: + raise HTTPError(error.code,error.msg,error.fp) + + # convert back to webob + headerlist = result.headers.items() + status = "%s %s" %(result.code, result.msg) + response = Response(body = result.read(), status = status, headerlist = headerlist) + return response + + def POST(self, url, data, headers={}): + """POST data to a resource""" + request = urllib2.Request(url, data, headers=headers) + try: + result = urllib2.urlopen(request) + except urllib2.HTTPError, error: + raise HTTPError(error.code,error.msg,error.fp) + + # convert back to webob + headerlist = result.headers.items() + status = "%s %s" %(result.code, result.msg) + response = Response(body = result.read(), status = status, headerlist = headerlist) + return response + + + + + + + + + \ No newline at end of file diff --git a/pyogp/lib/base/network/tests/__init__.py b/pyogp/lib/base/network/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyogp/lib/base/network/tests/basics.txt b/pyogp/lib/base/network/tests/basics.txt new file mode 100644 index 0000000..0035f00 --- /dev/null +++ b/pyogp/lib/base/network/tests/basics.txt @@ -0,0 +1,49 @@ +Networking Basics +================= + +The networking layer is basically a replaceable REST client. It is defined as a utility +with the interface IRESTClient provinding methods such as GET, POST etc. + +Let's retrieve the standard utility (this is overridden here in this test to use a mockup network library): + +>>> from pyogp.lib.base.network import IRESTClient, HTTPError +>>> from zope.component import getUtility +>>> client = getUtility(IRESTClient) + +Now we can use it. Let's post something to our test server: + +>>> response = client.GET('http://localhost:12345/network/get') +>>> response.body +'Hello, World' + +Let's try a 404: +>>> client.GET('http://localhost:12345/foobar') +Traceback (most recent call last): + ... +HTTPError: 404 Not Found + +The error object also has some more information in it: +>>> try: +... client.GET('http://localhost:12345/foobar') +... except HTTPError, error: +... pass + +Let's check what's available +>>> error.code +404 +>>> error.msg +'Not Found' +>>> error.fp.read() +'resource not found.' + + +POSTing something +================= + +>>> response = client.POST('http://localhost:12345/network/post','test me') +>>> response.body +'returned: test me' + + + + diff --git a/pyogp/lib/base/network/tests/network_test.txt b/pyogp/lib/base/network/tests/network_test.txt new file mode 100644 index 0000000..e69de29 diff --git a/pyogp/lib/base/network/tests/testDocTests.py b/pyogp/lib/base/network/tests/testDocTests.py new file mode 100644 index 0000000..f22fe0b --- /dev/null +++ b/pyogp/lib/base/network/tests/testDocTests.py @@ -0,0 +1,27 @@ +import unittest +import doctest + +optionflags = doctest.REPORT_ONLY_FIRST_FAILURE | doctest.ELLIPSIS + +# setup functions + +def setUp(self): + # override the default + from pyogp.lib.base.network import IRESTClient, MockupClient + from zope.component import provideUtility + from pyogp.lib.base.tests.base import AgentDomain + provideUtility(MockupClient(AgentDomain()), IRESTClient) + +def tearDown(self): + print "down" + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest( + doctest.DocFileSuite("basics.txt", + package="pyogp.lib.base.network.tests", + setUp = setUp, + tearDown = tearDown, + ) + ) + return suite diff --git a/pyogp/lib/base/tests/base.py b/pyogp/lib/base/tests/base.py index c0e8a88..40c1d00 100644 --- a/pyogp/lib/base/tests/base.py +++ b/pyogp/lib/base/tests/base.py @@ -2,6 +2,11 @@ import BaseHTTPServer from indra.base import llsd from webob import Request, Response +import md5 + +#PW = "$1$"+md5.new("secret").hexdigest() +PW="secret" + class AgentDomain(object): @@ -12,7 +17,10 @@ class AgentDomain(object): self.response = Response() - l = int(self.request.headers.get('Content-Length','0')) + l = self.request.headers.get('Content-Length','0') + if l=='': + l='0' + l = int(l) data = self.request.body # we assume it's LLSD for now and try to parse it @@ -24,16 +32,27 @@ class AgentDomain(object): return self.response(self.environ, self.start) if self.request.path=="/": return self.handle_login(data) + elif self.request.path=="/network/get" and self.request.method=="GET": + self.response.status=200 + self.response.body="Hello, World" + return self.response(self.environ, self.start) + elif self.request.path=="/network/post" and self.request.method=="POST": + data = self.request.body + self.response.status=200 + self.response.body="returned: %s" %data + return self.response(self.environ, self.start) elif self.request.path=="/seed_cap": return self.handle_seedcap(data) + elif self.request.path=="/seed_cap_wrong_content_type": + return self.handle_seedcap(data,content_type="text/html") elif self.request.path=="/cap/place_avatar": return self.place_avatar(data) elif self.request.path=="/cap/some_capability": return self.some_capability(data) else: - return self.send_response(404) + return self.send_response(404, 'resource not found.') - def handle_seedcap(self, data): + def handle_seedcap(self, data, content_type="application/llsd+xml"): """return some other caps""" caps = data.get("caps",[]) d = {'lastname': 'lastname', 'firstname': 'firstname'} @@ -44,7 +63,7 @@ class AgentDomain(object): d['caps'] = return_caps data = llsd.format_xml(d) self.response.status=200 - self.response.content_type='application/llsd+xml' + self.response.content_type=content_type self.response.body = data return self.response(self.environ, self.start) @@ -74,8 +93,8 @@ class AgentDomain(object): """handle the login string""" # TODO: test for all the correct fields in the data password = data.get('password') - if password!='secret': - self.send_response(400) + if password!=PW: + self.send_response(403) return data={'agent_seed_capability':"http://127.0.0.1:12345/seed_cap"} @@ -86,8 +105,9 @@ class AgentDomain(object): self.response.body=data return self.response(self.environ, self.start) - def send_response(self, status): + def send_response(self, status, body=''): self.response.status = status + self.response.body = body return self.response(self.environ, self.start) diff --git a/pyogp/lib/base/tests/caps.txt b/pyogp/lib/base/tests/caps.txt index bc7e713..50565af 100644 --- a/pyogp/lib/base/tests/caps.txt +++ b/pyogp/lib/base/tests/caps.txt @@ -23,7 +23,7 @@ Let's store the some_capability cap in a variable: The capability now can be simply called with a payload and returns some data itself. First we call it: ->>> data = some_cap({'a':'b'}) +>>> data = some_cap.POST({'a':'b'}) And now we can check the data: >>> data['something'] @@ -49,6 +49,19 @@ As we can see, it's not a secret URL in this mockup case but in production it wi +Testing errors +============== + +Now we can test what happens to our code when the server returns a wrong content type. +In this case it should not find a deserializer and say so: +>>> seed = SeedCapability('seed', 'http://127.0.0.1:12345/seed_cap_wrong_content_type') +>>> cap = seed.get(['some_capability']) +Traceback (most recent call last): +... +deserialization for text/html not supported + + + diff --git a/pyogp/lib/base/tests/login.txt b/pyogp/lib/base/tests/login.txt index d91c956..625bae5 100644 --- a/pyogp/lib/base/tests/login.txt +++ b/pyogp/lib/base/tests/login.txt @@ -11,7 +11,7 @@ First we create some credentials: >>> credentials = PlainPasswordCredential('Firstname', 'Lastname', 'secret') Then we need some agent domain to connect to. This might automatically retrieve some XRDS file to get the actual login endpoint: ->>> agentdomain = AgentDomain('http://localhost:12345') +>>> agentdomain = AgentDomain('http://localhost:12345/') Now we can use both to get an agent object (which transparently handles capabilities etc.): >>> agent = agentdomain.login(credentials) diff --git a/pyogp/lib/base/tests/testDocTests.py b/pyogp/lib/base/tests/testDocTests.py index 9c60790..c3ca036 100644 --- a/pyogp/lib/base/tests/testDocTests.py +++ b/pyogp/lib/base/tests/testDocTests.py @@ -8,6 +8,15 @@ optionflags = doctest.REPORT_ONLY_FIRST_FAILURE | doctest.ELLIPSIS def setUp(self): from pyogp.lib.base.registration import init init() + import pyogp.lib.base.agentdomain + pyogp.lib.base.agentdomain.USE_REDIRECT=False + + + # override the default + from pyogp.lib.base.network import IRESTClient, MockupClient + from zope.component import provideUtility + from pyogp.lib.base.tests.base import AgentDomain + provideUtility(MockupClient(AgentDomain()), IRESTClient) def tearDown(self): pass diff --git a/setup.py b/setup.py index 57ca1e6..5fa32aa 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup(name='pyogp.lib.base', # -*- Extra requirements: -*- 'zope.interface', 'zope.component [zcml]', - 'webob', + 'WebOb', 'wsgiref', 'grokcore.component',