Merge PR #256 from meejah/2774.status-api-only.0-part2

This commit is contained in:
Brian Warner 2016-04-12 12:31:30 -07:00
commit 359c233d26
4 changed files with 157 additions and 5 deletions

View File

@ -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
@ -332,6 +333,9 @@ class Client(node.Node, pollmixin.PollMixin):
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()
@ -341,6 +345,28 @@ class Client(node.Node, pollmixin.PollMixin):
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)

View File

@ -235,7 +235,7 @@ class Node(service.MultiService):
"""
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

View File

@ -2,17 +2,19 @@ import os.path, re, urllib, time, cgi
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
@ -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)

View File

@ -5,7 +5,7 @@ import simplejson
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
@ -15,6 +15,7 @@ from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
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
@ -363,9 +364,11 @@ class MyExceptionHandler(appserver.DefaultExceptionHandler):
traceback = f.getTraceback()
return self.simple(ctx, traceback, http.INTERNAL_SERVER_ERROR)
class NeedOperationHandleError(WebError):
pass
class RenderMixin:
def renderHTTP(self, ctx):
@ -379,6 +382,47 @@ class RenderMixin:
# 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)