Skip to content

Commit

Permalink
Merge PR #256 from meejah/2774.status-api-only.0-part2
Browse files Browse the repository at this point in the history
  • Loading branch information
warner committed Apr 12, 2016
2 parents 47b9218 + 308bb8c commit 359c233
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 5 deletions.
26 changes: 26 additions & 0 deletions src/allmydata/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os, stat, time, weakref
from allmydata import node
from base64 import urlsafe_b64encode

from zope.interface import implements
from twisted.internet import reactor, defer
Expand Down Expand Up @@ -332,6 +333,9 @@ def init_client(self):
DEP["n"] = int(self.get_config("client", "shares.total", DEP["n"]))
DEP["happy"] = int(self.get_config("client", "shares.happy", DEP["happy"]))

# for the CLI to authenticate to local JSON endpoints
self._create_auth_token()

self.init_client_storage_broker()
self.history = History(self.stats_provider)
self.terminator = Terminator()
Expand All @@ -341,6 +345,28 @@ def init_client(self):
self.init_blacklist()
self.init_nodemaker()

def get_auth_token(self):
"""
This returns a local authentication token, which is just some
random data in "api_auth_token" which must be echoed to API
calls.
Currently only the URI '/magic' for magic-folder status; other
endpoints are invited to include this as well, as appropriate.
"""
return self.get_private_config('api_auth_token')

def _create_auth_token(self):
"""
Creates new auth-token data written to 'private/api_auth_token'.
This is intentionally re-created every time the node starts.
"""
self.write_private_config(
'api_auth_token',
urlsafe_b64encode(os.urandom(32)) + '\n',
)

def init_client_storage_broker(self):
# create a StorageFarmBroker object, for use by Uploader/Downloader
# (and everybody else who wants to use storage servers)
Expand Down
2 changes: 1 addition & 1 deletion src/allmydata/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def get_private_config(self, name, default=_None):
"""
privname = os.path.join(self.basedir, "private", name)
try:
return fileutil.read(privname)
return fileutil.read(privname).strip()
except EnvironmentError:
if os.path.exists(privname):
raise
Expand Down
84 changes: 83 additions & 1 deletion src/allmydata/test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
import simplejson
from StringIO import StringIO

from zope.interface import implementer
from twisted.application import service
from twisted.trial import unittest
from twisted.internet import defer, reactor
from twisted.internet.task import Clock
from twisted.web import client, error, http
from twisted.web import client, error, http, server
from twisted.python import failure, log

from foolscap.api import fireEventually, flushEventualQueue

from nevow.util import escapeToXML
from nevow import rend
from nevow.inevow import IRequest

from allmydata import interfaces, uri, webish, dirnode
from allmydata.storage.shares import get_share_file
Expand Down Expand Up @@ -5901,3 +5903,83 @@ class CompletelyUnhandledError(Exception):
class ErrorBoom(rend.Page):
def beforeRender(self, ctx):
raise CompletelyUnhandledError("whoops")


@implementer(IRequest)
class FakeRequest(object):
def __init__(self):
self.method = "POST"
self.args = dict()
self.fields = []


class FakeClientWithToken(object):
token = 'a' * 32

def get_auth_token(self):
return self.token


class TestTokenOnlyApi(unittest.TestCase):

def setUp(self):
self.client = FakeClientWithToken()
self.page = common.TokenOnlyWebApi(self.client)

def test_not_post(self):
req = FakeRequest()
req.method = "GET"

self.assertRaises(
server.UnsupportedMethod,
self.page.renderHTTP, req,
)

def test_missing_token(self):
req = FakeRequest()

exc = self.assertRaises(
common.WebError,
self.page.renderHTTP, req,
)
self.assertEquals(exc.text, "Missing token")
self.assertEquals(exc.code, 401)

def test_invalid_token(self):
wrong_token = 'b' * 32
req = FakeRequest()
req.args['token'] = [wrong_token]

exc = self.assertRaises(
common.WebError,
self.page.renderHTTP, req,
)
self.assertEquals(exc.text, "Invalid token")
self.assertEquals(exc.code, 401)

def test_valid_token_no_t_arg(self):
req = FakeRequest()
req.args['token'] = [self.client.token]

with self.assertRaises(common.WebError) as exc:
self.page.renderHTTP(req)
self.assertEquals(exc.exception.text, "Must provide 't=' argument")
self.assertEquals(exc.exception.code, 400)

def test_valid_token_invalid_t_arg(self):
req = FakeRequest()
req.args['token'] = [self.client.token]
req.args['t'] = 'not at all json'

with self.assertRaises(common.WebError) as exc:
self.page.renderHTTP(req)
self.assertTrue("invalid type" in exc.exception.text)
self.assertEquals(exc.exception.code, 400)

def test_valid(self):
req = FakeRequest()
req.args['token'] = [self.client.token]
req.args['t'] = ['json']

result = self.page.renderHTTP(req)
self.assertTrue(result == NotImplemented)
50 changes: 47 additions & 3 deletions src/allmydata/web/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from twisted.web import http, server
from twisted.python import log
from zope.interface import Interface
from nevow import loaders, appserver
from nevow import loaders, appserver, rend
from nevow.inevow import IRequest
from nevow.util import resource_filename
from allmydata import blacklist
Expand All @@ -15,6 +15,7 @@
MustBeReadonlyError, MustNotBeUnknownRWError, SDMF_VERSION, MDMF_VERSION
from allmydata.mutable.common import UnrecoverableFileError
from allmydata.util import abbreviate
from allmydata.util.hashutil import timing_safe_compare
from allmydata.util.time_format import format_time, format_delta
from allmydata.util.encodingutil import to_str, quote_output

Expand Down Expand Up @@ -363,9 +364,11 @@ def renderHTTP_exception(self, ctx, f):
traceback = f.getTraceback()
return self.simple(ctx, traceback, http.INTERNAL_SERVER_ERROR)


class NeedOperationHandleError(WebError):
pass


class RenderMixin:

def renderHTTP(self, ctx):
Expand All @@ -379,6 +382,47 @@ def renderHTTP(self, ctx):
# do the same thing.
m = getattr(self, 'render_' + request.method, None)
if not m:
from twisted.web.server import UnsupportedMethod
raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
raise server.UnsupportedMethod(getattr(self, 'allowedMethods', ()))
return m(ctx)


class TokenOnlyWebApi(rend.Page):
"""
I provide a rend.Page implementation that only accepts POST calls,
and only if they have a 'token=' arg with the correct
authentication token (see
:meth:`allmydata.client.Client.get_auth_token`). Callers must also
provide the "t=" argument to indicate the return-value (the only
valid value for this is "json")
Subclasses should override '_render_json' which should process the
API call and return a valid JSON object. This will only be called
if the correct token is present and valid (during renderHTTP
processing).
"""

def __init__(self, client):
super(TokenOnlyWebApi, self).__init__()
self.client = client

def post_json(self, req):
return NotImplemented

def renderHTTP(self, ctx):
req = IRequest(ctx)
if req.method != 'POST':
raise server.UnsupportedMethod(('POST',))

token = get_arg(req, "token", None)
if not token:
raise WebError("Missing token", http.UNAUTHORIZED)
if not timing_safe_compare(token, self.client.get_auth_token()):
raise WebError("Invalid token", http.UNAUTHORIZED)

t = get_arg(req, "t", "").strip()
if not t:
raise WebError("Must provide 't=' argument")
if t == u'json':
return self.post_json(req)
else:
raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST)

0 comments on commit 359c233

Please sign in to comment.