mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-18 18:56:28 +00:00
Merge PR #256 from meejah/2774.status-api-only.0-part2
This commit is contained in:
commit
359c233d26
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user