Skip to content

Commit

Permalink
added koa.common.basic_auth, which also required KoaContext.throw()
Browse files Browse the repository at this point in the history
  • Loading branch information
KjellSchubert committed Oct 29, 2014
1 parent bf87522 commit 151f06e
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 4 deletions.
31 changes: 30 additions & 1 deletion koa/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import os.path
import urllib
import base64

# koa.js-style middleware for logging request handling times.
# Similar to https://www.npmjs.org/package/koa-logger and https://www.npmjs.org/package/koa-response-time
Expand Down Expand Up @@ -155,6 +156,7 @@ def mount(parent_path, middleware):
# apps or middleware that will function correctly regardless of which path segment(s)
# they should operate on.'
assert parent_path.startswith('/'), 'mount path must begin with "/"'
assert asyncio.iscoroutinefunction(middleware), "mount argument is supposed to be middleware, so a coroutine function"

if parent_path.endswith('/'):
parent_path = parent_path[0:-1] # strip trailing slash to normalize prefix (if parent_path was '/' to begin with then it's an empty string now)
Expand Down Expand Up @@ -248,6 +250,7 @@ class KoaRoute:
# param path like "/config"
# param handler is koajs middleware (so a coroutine taking KoaContext and next)
def __init__(self, method, path, handler):
assert asyncio.iscoroutinefunction(handler), "route handler is supposed to be middleware, so a coroutine function"
self.method = method
self.path = ExpressJsStyleRoute(path)
self.handler = handler
Expand Down Expand Up @@ -302,4 +305,30 @@ def inner(context, next):
yield from next
return inner

return KoaRouter()
return KoaRouter()

def basic_auth(credential_validator):
"""
:param credential_validator: is a coroutine that takes (username, password) strings and is supposed to
return True if the user is authenticated.
"""

@asyncio.coroutine
def basic_auth_middleware(koa_context, next):

# deal with koa_context.request.headers.get('AUTHORIZATION') == 'Basic ...'
is_authenticated = False
auth_header = koa_context.request.headers.get('AUTHORIZATION')
if auth_header != None:
(auth_type, auth_payload) = auth_header.split(' ')
if auth_type.lower() == 'basic':
decodedPayload = base64.b64decode(auth_payload).decode('utf8')
(user, password) = decodedPayload.split(':')
is_authenticated = yield from credential_validator(user, password)

if is_authenticated:
yield from next
else:
koa_context.throw("unauthorized", 401) # if we'd just set response.status = 401 then 'yield from next' would still kick in due to ensure_we_yield_to_next()

return basic_auth_middleware
35 changes: 33 additions & 2 deletions koa/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,22 @@ def send_http_response(writer, message, content_type, response_bytes, status_cod
)
response.add_header('Content-Type', content_type)
response.add_header('Content-Length', str(len(response_bytes))) # len encoded bytes
if status_code == 401:
response.add_header('WWW-Authenticate', 'Basic realm="Authorization Required"')
response.send_headers()
response.write(response_bytes)
#print("response.write wrote {} bytes".format(len(response_bytes)))
yield from response.write_eof()

@asyncio.coroutine
def send_http_response_status_only(writer, message, status_code):
response = aiohttp.Response(
writer, status_code, http_version=message.version
)
response.send_headers()
yield from response.write_eof()


class KoaRequest:
# param message is the message passed to aiohttp.server.ServerHttpProtocol.handle_request()
def __init__(self, message):
Expand All @@ -57,12 +68,21 @@ def __init__(self):
self.status = None # 200, 404, ...
self.body = None # {}, string, ...

class KoaException(Exception):
def __init__(self, message, status):
self.message = message
self.status = status

class KoaContext:
# param message is the message passed to aiohttp.server.ServerHttpProtocol.handle_request()
def __init__(self, message):
self.request = KoaRequest(message)
self.response = KoaResponse() # to be filled out by the middleware handlers

# like ctx.throw() at http://koajs.com/
def throw(self, message, status):
raise KoaException(message, status)

@asyncio.coroutine
def koa_write_response(koa_context):
body = koa_context.response.body
Expand All @@ -77,6 +97,8 @@ def koa_write_response(koa_context):
yield from send_http_response(writer, message, 'application/octet-stream', koa_context.response.body, status_code)
elif koa_context.response.body != None:
yield from send_http_response_text(writer, message, "unknown response type: {}".format(koa_context.response.body.__class__.__name__), status_code = 500)
elif koa_context.response.body == None and koa_context.response.status != None:
yield from send_http_response_status_only(writer, message, koa_context.response.status)
else:
yield from send_http_response_text(writer, message, "no response for method={} path={}".format(koa_context.request.method, koa_context.request.path.path), status_code = 404)

Expand Down Expand Up @@ -108,7 +130,16 @@ def handle_request(self, message, payload):
# This is the same mechanism koa.js uses for chaining & nesting middleware, I wonder
# there's a more straightforward way to achieve the same.
next = koa_write_response(context) # final one to execute, the only one that doesn't take a 'next' param
yield from self.middleware(context, next)
try:
yield from self.middleware(context, next)
except KoaException as ex:
# this here deals explicitly with exceptions thrown via KoaContext.throw(), e.g. thrown
# by koa.common.basic_auth() to send a 401 without executing any remaining middleware.
# Other kinda of exceptions are OK to bubble out of here, they should yield a 500
context.response.status = ex.status
context.response.body = ex.message
yield from next # calls koa_write_response(context)


# This stores the chain of middleware your app is composed of, executing this
# chain for each incoming HTTP request
Expand All @@ -120,7 +151,7 @@ def __init__(self):
# wires up koa.js-style middleware
# param middleware is a coroutine that will receive params (request, next)
def use(self, middleware):
assert callable(middleware), "middleware is supposed to be a (coroutine) function"
assert asyncio.iscoroutinefunction(middleware), "middleware is supposed to be a coroutine function"
#assert len(inspect.getargspec(middleware).args) == 2, "middleware is supposed to be a coroutine function taking 2 args KoaContext and next"
# TODO: assert that the func takes 2 params: koa_context and next
self.middlewares += [middleware]
Expand Down
38 changes: 37 additions & 1 deletion tests/test_koa.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test():

def test_exception_in_middleware_yields_response_500(self):

#@asyncio.coroutine
@asyncio.coroutine
def middleware(koa_context, next):
raise Exception('something something')

Expand Down Expand Up @@ -357,3 +357,39 @@ def test():

test_session = KoaTestSession(app)
test_session.run_async_test(test())

def test_koa_auth(self):
app = koa.core.app()

# this is not a middleware, it's just a regular coroutine returning a bool
@asyncio.coroutine
def my_credential_validator(user, password):
return user == 'foo' and password == 'secret'

# this is middleware that is supposed to be basic-auth-protected
@asyncio.coroutine
def my_middleware(koa_context, next):
koa_context.response.body = "hi"

app.use(koa.common.basic_auth(my_credential_validator))
app.use(my_middleware)

@asyncio.coroutine
def test():

# test successful auth
response = yield from test_session.request('get', '/foo', auth = aiohttp.helpers.BasicAuth('foo', 'secret', 'utf-8'))
responseText = yield from response.text()
self.assertEqual(response.status, 200)
self.assertEqual(responseText, "hi")

# test wrong pass
response = yield from test_session.request('get', '/foo', auth = aiohttp.helpers.BasicAuth('foo', 'wrong pass', 'utf-8'))
self.assertEqual(response.status, 401)

# test missing auth
response = yield from test_session.request('get', '/foo')
self.assertEqual(response.status, 401)

test_session = KoaTestSession(app)
test_session.run_async_test(test())

0 comments on commit 151f06e

Please sign in to comment.