PK»YjGK;fúFFvxsandbox/utils.py# -*- test-case-name: vxsandbox.tests.test_utils -*- import os from twisted.trial.unittest import SkipTest class SandboxError(Exception): """Raised when an error occurs inside the sandbox.""" def find_nodejs_or_skip_test(worker_class): """ Find the node.js executable by checking the ``VUMI_TEST_NODE_PATH`` envvar and falling back to the provided worker's own detection method. If no executable is found, :class:`SkipTest` is raised. """ path = os.environ.get('VUMI_TEST_NODE_PATH') if path is not None: if os.path.isfile(path): return path raise RuntimeError( "VUMI_TEST_NODE_PATH specified, but does not exist: %s" % (path,)) path = worker_class.find_nodejs() if path is None: raise SkipTest("No node.js executable found.") return path PKŠEºH6eånã ã vxsandbox/rlimiter.py# -*- test-case-name: vxsandbox.tests.test_sandbox_rlimiter -*- """ A helper for applying RLIMITS to a sandboxed process. """ import resource class SandboxRlimiter(object): """ Spawn a process that applies rlimits and then execs another program. It's necessary because Twisted's spawnProcess has no equivalent of the `preexec_fn` argument to :class:`subprocess.POpen`. See http://twistedmatrix.com/trac/ticket/4159. """ # From the bash manual, regarding ulimit: # Values are in 1024-byte increments, except for -t, which is in seconds; # -p, which is in units of 512-byte blocks; and -T, -b, -n and -u, which # are unscaled values. ULIMIT_PARAMS = { resource.RLIMIT_CORE: ("c", 1024), resource.RLIMIT_CPU: ("t", 1), resource.RLIMIT_FSIZE: ("f", 1024), resource.RLIMIT_DATA: ("d", 1024), resource.RLIMIT_STACK: ("s", 1024), resource.RLIMIT_RSS: ("m", 1024), resource.RLIMIT_NOFILE: ("n", 1), resource.RLIMIT_MEMLOCK: ("l", 1024), resource.RLIMIT_AS: ("v", 1024), } def __init__(self, rlimits, args, **kwargs): self._args = args self._rlimits = rlimits self._kwargs = kwargs def execute(self, reactor, protocol): reactor.spawnProcess( protocol, '/bin/bash', args=self.build_args(), **self._kwargs) def build_args(self): return ['bash', '-e', '-c', self.build_script(), '--'] + self._args def build_script(self): script_lines = ["#!/bin/bash", ""] script_lines.extend(self._build_rlimit_commands()) script_lines.extend(["", 'exec "$@"']) return "\n".join(script_lines) def _build_rlimit_commands(self): yield "# Set resource limits." for rlimit, (soft, hard) in sorted(self._rlimits.items()): param, scale = self.ULIMIT_PARAMS[rlimit] rsoft, rhard = resource.getrlimit(int(rlimit)) yield "ulimit -S%s %s" % (param, ulimit_value(soft, rsoft, scale)) yield "ulimit -H%s %s" % (param, ulimit_value(hard, rhard, scale)) @classmethod def spawn(cls, reactor, protocol, rlimits, args, **kwargs): self = cls(rlimits, args, **kwargs) self.execute(reactor, protocol) def ulimit_value(new, current, scale): if current >= 0 and (new < 0 or new > current): # The current limit is lower than the new one, so use that instead. return current / scale return new / scale if new >= 0 else "unlimited" PKŠEºHgaó僃vxsandbox/protocol.py# -*- test-case-name: vxsandbox.tests.test_protocol -*- """A protocol for managing sandboxed processes.""" import logging from twisted.internet import reactor from twisted.internet.defer import Deferred, DeferredList from twisted.internet.protocol import ProcessProtocol from twisted.internet.error import ProcessDone from twisted.python.failure import Failure from vumi import log from .rlimiter import SandboxRlimiter from .utils import SandboxError from .resources import SandboxCommand class MultiDeferred(object): """A callable that returns new deferreds each time and then fires them all together. """ NOT_FIRED = object() def __init__(self): self._result = self.NOT_FIRED self._deferreds = [] def callback(self, result): self._result = result for d in self._deferreds: d.callback(result) self._deferreds = [] def get(self): d = Deferred() if self.fired(): d.callback(self._result) else: self._deferreds.append(d) return d def fired(self): return self._result is not self.NOT_FIRED class SandboxProtocol(ProcessProtocol): """A protocol for communicating over stdin and stdout with a sandboxed process. The sandbox process is created by calling :meth:`spawn`. This spawns a child process that applies the supplied rlimits and then `exec`s the given executable and its args. Once a spawned process starts, the parent process communicates with it over `stdin`, `stdout` and `stderr` reading and writing a stream of newline separated JSON commands that are parsed and formatted by :class:`SandboxCommand`. Incoming commands are dispatched to :class:`SandboxResource` instances via the supplied :class:`SandboxApi`. """ def __init__(self, sandbox_id, api, executable, args, spawn_kwargs, rlimits, timeout, recv_limit): self.sandbox_id = sandbox_id self.api = api self.executable = executable self.args = args self.spawn_kwargs = spawn_kwargs self.rlimits = rlimits self._started = MultiDeferred() self._done = MultiDeferred() self._pending_requests = [] self.exit_reason = None self.timeout_task = reactor.callLater(timeout, self.kill) self.recv_limit = recv_limit self.recv_bytes = 0 self.chunk = '' self.error_chunk = '' self.error_lines = [] api.set_sandbox(self) def spawn(self): args = [self.executable] + self.args SandboxRlimiter.spawn( reactor, self, self.rlimits, args, **self.spawn_kwargs) def done(self): """Returns a deferred that will be called when the process ends.""" return self._done.get() def started(self): """Returns a deferred that will be called once the process starts.""" return self._started.get() def kill(self): """Kills the underlying process.""" if self.transport.pid is not None: self.transport.signalProcess('KILL') def send(self, command): """Writes the command to the processes' stdin.""" self.transport.write(command.to_json()) self.transport.write("\n") def check_recv(self, nbytes): self.recv_bytes += nbytes if self.recv_bytes <= self.recv_limit: return True else: self.kill() self.api.log("Sandbox %r killed for producing too much data on" " stderr and stdout." % (self.sandbox_id), level=logging.ERROR) return False def connectionMade(self): self._started.callback(self) def _process_data(self, chunk, data): if not self.check_recv(len(data)): return [''] # skip the data if it's too big line_parts = data.split("\n") line_parts[0] = chunk + line_parts[0] return line_parts def _parse_command(self, line): try: return SandboxCommand.from_json(line) except Exception, e: return SandboxCommand(cmd="unknown", line=line, exception=e) def outReceived(self, data): lines = self._process_data(self.chunk, data) for i in range(len(lines) - 1): d = self.api.dispatch_request(self._parse_command(lines[i])) self._pending_requests.append(d) self.chunk = lines[-1] def outConnectionLost(self): if self.chunk: line, self.chunk = self.chunk, "" d = self.api.dispatch_request(self._parse_command(line)) self._pending_requests.append(d) def errReceived(self, data): lines = self._process_data(self.error_chunk, data) for i in range(len(lines) - 1): self.error_lines.append(lines[i]) self.error_chunk = lines[-1] def errConnectionLost(self): if self.error_chunk: self.error_lines.append(self.error_chunk) self.error_chunk = "" def _process_request_results(self, results): for success, result in results: if not success: # errors here are bugs in Vumi and thus should always # be logged via Twisted too. log.error(result) # we log them again in a simplified form via the sandbox # api so that the sandbox owner gets to see them too self.api.log(result.getErrorMessage(), logging.ERROR) def processEnded(self, reason): if self.timeout_task.active(): self.timeout_task.cancel() if isinstance(reason.value, ProcessDone): result = reason.value.status else: result = reason if not self._started.fired(): self._started.callback(Failure( SandboxError("Process failed to start."))) if self.error_lines: self.api.log("\n".join(self.error_lines), logging.ERROR) self.error_lines = [] requests_done = DeferredList(self._pending_requests) requests_done.addCallback(self._process_request_results) requests_done.addCallback(lambda _r: self._done.callback(result)) PKLÉHÌð±ììvxsandbox/__init__.py""" Sandbox application worker for Vumi. """ from .worker import Sandbox, JsSandbox, JsFileSandbox from .utils import SandboxError from .resources import ( SandboxResource, LoggingResource, HttpClientResource, MetricsResource, OutboundResource, RedisResource) __version__ = "0.6.1" __all__ = [ "Sandbox", "JsSandbox", "JsFileSandbox", "SandboxError", "SandboxResource", "LoggingResource", "HttpClientResource", "MetricsResource", "OutboundResource", "RedisResource", ] PK‹š¿HftZÆž?ž?vxsandbox/worker.py# -*- test-case-name: vxsandbox.tests.test_worker -*- """An application for sandboxing message processing.""" import resource import os import pkg_resources import logging from twisted.internet.defer import inlineCallbacks, returnValue, succeed from vumi.config import ConfigText, ConfigInt, ConfigList, ConfigDict from vumi.application.base import ApplicationWorker from vumi.errors import ConfigError from vumi import log from .utils import SandboxError from .protocol import SandboxProtocol from .resources import ( SandboxResources, SandboxResource, SandboxCommand, LoggingResource) class JsSandboxResource(SandboxResource): """ Resource that initializes a Javascript sandbox. Typically used alongside vumi/applicaiton/sandboxer.js which is a simple node.js based Javascript sandbox. Requires the worker to have a `javascript_for_api` method. """ def sandbox_init(self, api): javascript = self.app_worker.javascript_for_api(api) app_context = self.app_worker.app_context_for_api(api) api.sandbox_send(SandboxCommand(cmd="initialize", javascript=javascript, app_context=app_context)) class SandboxApi(object): """A sandbox API instance for a particular sandbox run.""" def __init__(self, resources, config): self._sandbox = None self._inbound_messages = {} self.resources = resources self.fallback_resource = SandboxResource("fallback", None, {}) potential_logger = None if config.logging_resource: potential_logger = self.resources.resources.get( config.logging_resource) if potential_logger is None: log.warning("Failed to find logging resource %r." " Falling back to Twisted logging." % (config.logging_resource,)) elif not hasattr(potential_logger, 'log'): log.warning("Logging resource %r has no attribute 'log'." " Falling abck to Twisted logging." % (config.logging_resource,)) potential_logger = None self.logging_resource = potential_logger self.config = config @property def sandbox_id(self): return self._sandbox.sandbox_id def set_sandbox(self, sandbox): if self._sandbox is not None: raise SandboxError("Sandbox already set (" "existing id: %r, new id: %r)." % (self.sandbox_id, sandbox.sandbox_id)) self._sandbox = sandbox def sandbox_init(self): for sandbox_resource in self.resources.resources.values(): sandbox_resource.sandbox_init(self) def sandbox_inbound_message(self, msg): self._inbound_messages[msg['message_id']] = msg self.sandbox_send(SandboxCommand(cmd="inbound-message", msg=msg.payload)) def sandbox_inbound_event(self, event): self.sandbox_send(SandboxCommand(cmd="inbound-event", msg=event.payload)) def sandbox_send(self, msg): self._sandbox.send(msg) def sandbox_kill(self): self._sandbox.kill() def get_inbound_message(self, message_id): return self._inbound_messages.get(message_id) def log(self, msg, level): if self.logging_resource is None: # fallback to vumi.log logging if we don't # have a logging resource. return succeed(log.msg(msg, logLevel=level)) else: return self.logging_resource.log(self, msg, level=level) @inlineCallbacks def dispatch_request(self, command): resource_name, sep, rest = command['cmd'].partition('.') if not sep: resource_name, rest = '', resource_name command['cmd'] = rest resource = self.resources.resources.get(resource_name, self.fallback_resource) try: reply = yield resource.dispatch_request(self, command) except Exception, e: # errors here are bugs in Vumi so we always log them # via Twisted. However, we reply to the sandbox with # a failure and log via the sandbox api so that the # sandbox owner can be notified. log.error() self.log(str(e), level=logging.ERROR) reply = SandboxCommand( reply=True, cmd_id=command['cmd_id'], success=False, reason=unicode(e)) if reply is not None: reply['cmd'] = '%s%s%s' % (resource_name, sep, rest) self.sandbox_send(reply) class SandboxConfig(ApplicationWorker.CONFIG_CLASS): sandbox = ConfigDict( "Dictionary of resources to provide to the sandbox." " Keys are the names of resources (as seen inside the sandbox)." " Values are dictionaries which must contain a `cls` key that" " gives the full name of the class that provides the resource." " Other keys are additional configuration for that resource.", default={}, static=True) executable = ConfigText( "Full path to the executable to run in the sandbox.") args = ConfigList( "List of arguments to pass to the executable (not including" " the path of the executable itself).", default=[]) path = ConfigText("Current working directory to run the executable in.") env = ConfigDict( "Custom environment variables for the sandboxed process.", default={}) timeout = ConfigInt( "Length of time the subprocess is given to process a message.", default=60) recv_limit = ConfigInt( "Maximum number of bytes that will be read from a sandboxed" " process' stdout and stderr combined.", default=1024 * 1024) rlimits = ConfigDict( "Dictionary of resource limits to be applied to sandboxed" " processes. Defaults are fairly restricted. Keys maybe" " names or values of the RLIMIT constants in" " Python `resource` module. Values should be appropriate integers.", default={}) logging_resource = ConfigText( "Name of the logging resource to use to report errors detected" " in sandboxed code (e.g. lines written to stderr, unexpected" " process termination). Set to null to disable and report" " these directly using Twisted logging instead.", default=None) sandbox_id = ConfigText("This is set based on individual messages.") class Sandbox(ApplicationWorker): """Sandbox application worker.""" CONFIG_CLASS = SandboxConfig KB, MB = 1024, 1024 * 1024 DEFAULT_RLIMITS = { resource.RLIMIT_CORE: (1 * MB, 1 * MB), resource.RLIMIT_CPU: (60, 60), resource.RLIMIT_FSIZE: (1 * MB, 1 * MB), resource.RLIMIT_DATA: (64 * MB, 64 * MB), resource.RLIMIT_STACK: (1 * MB, 1 * MB), resource.RLIMIT_RSS: (10 * MB, 10 * MB), resource.RLIMIT_NOFILE: (15, 15), resource.RLIMIT_MEMLOCK: (64 * KB, 64 * KB), resource.RLIMIT_AS: (196 * MB, 196 * MB), } def validate_config(self): config = self.get_static_config() self.resources = self.create_sandbox_resources(config.sandbox) self.resources.validate_config() def get_config(self, msg): config = self.config.copy() config['sandbox_id'] = self.sandbox_id_for_message(msg) return succeed(self.CONFIG_CLASS(config)) def _convert_rlimits(self, rlimits_config): rlimits = dict((getattr(resource, key, key), value) for key, value in rlimits_config.iteritems()) for key in rlimits.iterkeys(): if not isinstance(key, (int, long)): raise ConfigError("Unknown resource limit key %r" % (key,)) return rlimits def setup_application(self): return self.resources.setup_resources() def teardown_application(self): return self.resources.teardown_resources() def setup_connectors(self): # Set the default event handler so we can handle events from any # endpoint. d = super(Sandbox, self).setup_connectors() def cb(connector): connector.set_default_event_handler(self.dispatch_event) return connector return d.addCallback(cb) def create_sandbox_resources(self, config): return SandboxResources(self, config) def get_executable_and_args(self, config): return config.executable, config.args def get_rlimits(self, config): rlimits = self.DEFAULT_RLIMITS.copy() rlimits.update(self._convert_rlimits(config.rlimits)) return rlimits def create_sandbox_protocol(self, api): rlimits = self.get_rlimits(api.config) spawn_kwargs = dict(env=api.config.env, path=api.config.path) executable, args = self.get_executable_and_args(api.config) return SandboxProtocol( api.config.sandbox_id, api, executable, args, spawn_kwargs, rlimits, api.config.timeout, api.config.recv_limit) def create_sandbox_api(self, resources, config): return SandboxApi(resources, config) def sandbox_id_for_message(self, msg_or_event): """Return a sandbox id for a message or event. This implementation simply returns ``msg_or_event['sandbox_id']``. Sub-classes may override this to retrieve a more appropriate id. """ return msg_or_event['sandbox_id'] def sandbox_protocol_for_message(self, msg_or_event, config): """Return a sandbox protocol for a message or event. This implementation ignores ``msg_or_event`` and returns a sandbox protocol based on the given ``config``. Sub-classes may override this to retrieve a custom protocol if needed. """ api = self.create_sandbox_api(self.resources, config) protocol = self.create_sandbox_protocol(api) return protocol def _process_in_sandbox(self, sandbox_protocol, api_callback): sandbox_protocol.spawn() def on_start(_result): sandbox_protocol.api.sandbox_init() api_callback() d = sandbox_protocol.done() d.addErrback(log.error) return d d = sandbox_protocol.started() d.addCallbacks(on_start, log.error) return d @inlineCallbacks def process_message_in_sandbox(self, msg): config = yield self.get_config(msg) sandbox_protocol = yield self.sandbox_protocol_for_message(msg, config) def sandbox_init(): sandbox_protocol.api.sandbox_inbound_message(msg) status = yield self._process_in_sandbox(sandbox_protocol, sandbox_init) returnValue(status) @inlineCallbacks def process_event_in_sandbox(self, event): config = yield self.get_config(event) sandbox_protocol = yield self.sandbox_protocol_for_message( event, config) def sandbox_init(): sandbox_protocol.api.sandbox_inbound_event(event) status = yield self._process_in_sandbox(sandbox_protocol, sandbox_init) returnValue(status) def consume_user_message(self, msg): return self.process_message_in_sandbox(msg) def close_session(self, msg): return self.process_message_in_sandbox(msg) def consume_ack(self, event): return self.process_event_in_sandbox(event) def consume_nack(self, event): return self.process_event_in_sandbox(event) def consume_delivery_report(self, event): return self.process_event_in_sandbox(event) class JsSandboxConfig(SandboxConfig): "JavaScript sandbox configuration." javascript = ConfigText("JavaScript code to run.", required=True) app_context = ConfigText("Custom context to execute JS with.") logging_resource = ConfigText( "Name of the logging resource to use to report errors detected" " in sandboxed code (e.g. lines written to stderr, unexpected" " process termination). Set to null to disable and report" " these directly using Twisted logging instead.", default='log') class JsSandbox(Sandbox): """ Configuration options: As for :class:`Sandbox` except: * `executable` defaults to searching for a `node.js` binary. * `args` defaults to the JS sandbox script in the `vumi.application` module. * An instance of :class:`JsSandboxResource` is added to the sandbox resources under the name `js` if no `js` resource exists. * An instance of :class:`LoggingResource` is added to the sandbox resources under the name `log` if no `log` resource exists. * `logging_resource` is set to `log` if it is not set. * An extra 'javascript' parameter specifies the javascript to execute. * An extra optional 'app_context' parameter specifying a custom context for the 'javascript' application to execute with. Example 'javascript' that logs information via the sandbox API (provided as 'this' to 'on_inbound_message') and checks that logging was successful:: api.on_inbound_message = function(command) { this.log_info("From command: inbound-message", function (reply) { this.log_info("Log successful: " + reply.success); this.done(); }); } Example 'app_context' that makes the Node.js 'path' module available under the name 'path' in the context that the sandboxed javascript executes in:: {path: require('path')} """ CONFIG_CLASS = JsSandboxConfig POSSIBLE_NODEJS_EXECUTABLES = [ '/usr/local/bin/node', '/usr/local/bin/nodejs', '/usr/bin/node', '/usr/bin/nodejs', ] @classmethod def find_nodejs(cls): for path in cls.POSSIBLE_NODEJS_EXECUTABLES: if os.path.isfile(path): return path return None @classmethod def find_sandbox_js(cls): return pkg_resources.resource_filename( 'vumi.application.sandbox', 'sandboxer.js') def get_js_resource(self): return JsSandboxResource('js', self, {}) def get_log_resource(self): return LoggingResource('log', self, {}) def javascript_for_api(self, api): """Called by JsSandboxResource. :returns: String containing Javascript for the app to run. """ return api.config.javascript def app_context_for_api(self, api): """Called by JsSandboxResource :returns: String containing Javascript expression that returns addition context for the namespace the app is being run in. This Javascript is expected to be trusted code. """ return api.config.app_context def get_executable_and_args(self, config): executable = config.executable if executable is None: executable = self.find_nodejs() args = config.args or [self.find_sandbox_js()] return executable, args def validate_config(self): super(JsSandbox, self).validate_config() if 'js' not in self.resources.resources: self.resources.add_resource('js', self.get_js_resource()) if 'log' not in self.resources.resources: self.resources.add_resource('log', self.get_log_resource()) class JsFileSandbox(JsSandbox): class CONFIG_CLASS(SandboxConfig): javascript_file = ConfigText( "The file containting the Javascript to run", required=True) app_context = ConfigText("Custom context to execute JS with.") def javascript_for_api(self, api): return file(api.config.javascript_file).read() class StandaloneJsFileSandbox(JsFileSandbox): def sandbox_id_for_message(self, msg_or_event): """Return a sandbox id for a message or event. This implementation simply returns the sandbox_id from the app config. """ return self.config['sandbox_id'] PKLÉH”Äz‡ Y Yvxsandbox/tests/test_worker.py"""Tests for vxsandbox.worker.""" import os import sys import json import resource import pkg_resources import logging from datetime import datetime from twisted.internet.defer import inlineCallbacks, DeferredQueue from twisted.internet.error import ProcessTerminated from vumi.application.tests.helpers import ApplicationHelper from vumi.tests.utils import LogCatcher from vumi.tests.helpers import VumiTestCase from vxsandbox.worker import ( Sandbox, SandboxApi, SandboxCommand, SandboxResources, JsSandboxResource, JsSandbox, JsFileSandbox, StandaloneJsFileSandbox) from vxsandbox import SandboxResource, LoggingResource from vxsandbox.tests.utils import DummyAppWorker from vxsandbox.resources.tests.utils import ResourceTestCaseBase from vxsandbox.utils import find_nodejs_or_skip_test class MockResource(SandboxResource): def __init__(self, name, app_worker, **handlers): super(MockResource, self).__init__(name, app_worker, {}) for name, handler in handlers.iteritems(): setattr(self, "handle_%s" % name, handler) class ListLoggingResource(LoggingResource): def __init__(self, name, app_worker, config): super(ListLoggingResource, self).__init__(name, app_worker, config) self.msgs = [] def log(self, api, msg, level): self.msgs.append((level, msg)) class SandboxTestCaseBase(VumiTestCase): application_class = Sandbox def setUp(self): self.app_helper = self.add_helper( ApplicationHelper(self.application_class)) def setup_app(self, executable=None, args=None, extra_config=None): tmp_path = self.mktemp() os.mkdir(tmp_path) config = { 'path': tmp_path, 'timeout': '10', } if executable is not None: config['executable'] = executable if args is not None: config['args'] = args if extra_config is not None: config.update(extra_config) return self.app_helper.get_application(config) class TestSandbox(SandboxTestCaseBase): def setup_app(self, python_code, extra_config=None): return super(TestSandbox, self).setup_app( sys.executable, ['-c', python_code], extra_config=extra_config) @inlineCallbacks def test_bad_command_from_sandbox(self): app = yield self.setup_app( "import sys, time\n" "sys.stdout.write('{}\\n')\n" "sys.stdout.flush()\n" "time.sleep(5)\n" ) with LogCatcher(log_level=logging.ERROR) as lc: status = yield app.process_event_in_sandbox( self.app_helper.make_ack(sandbox_id='sandbox1')) [msg] = lc.messages() self.assertTrue(msg.startswith( "Resource fallback received unknown command 'unknown'" " from sandbox 'sandbox1'. Killing sandbox." " [Full command: = 0 else "unlimited" cpu_hard = uvalue(resource.getrlimit(resource.RLIMIT_CPU)[1]) nofile_soft = uvalue(resource.getrlimit(resource.RLIMIT_NOFILE)[0]) rlimiter = SandboxRlimiter({ resource.RLIMIT_CPU: (40, -1), resource.RLIMIT_NOFILE: (-1, 15), }, []) self.assertEqual(rlimiter.build_script(), "\n".join([ "#!/bin/bash", "", "# Set resource limits.", "ulimit -St 40", "ulimit -Ht %s" % (uvalue(cpu_hard),), "ulimit -Sn %s" % (uvalue(nofile_soft),), "ulimit -Hn 15", "", 'exec "$@"', ])) def test_build_args(self): """ The args contain a bash command to run the script we built. """ rlimiter = SandboxRlimiter({ resource.RLIMIT_CPU: (40, 60), resource.RLIMIT_NOFILE: (15, 15), }, ['/bin/echo', 'hello', 'world']) script = rlimiter.build_script() self.assertEqual(rlimiter.build_args(), [ 'bash', '-e', '-c', script, '--', '/bin/echo', 'hello', 'world']) def test_ulimit_value(self): """ The ulimit value is the minimum of the new and current limits (if either exists) or "unlimited". """ self.assertEqual(ulimit_value(-1, -1, 1), "unlimited") self.assertEqual(ulimit_value(0, -1, 1), 0) self.assertEqual(ulimit_value(-1, 0, 1), 0) self.assertEqual(ulimit_value(1, -1, 1), 1) self.assertEqual(ulimit_value(-1, 1, 1), 1) self.assertEqual(ulimit_value(0, 1, 1), 0) self.assertEqual(ulimit_value(1, 0, 1), 0) self.assertEqual(ulimit_value(3, 5, 1), 3) self.assertEqual(ulimit_value(5, 3, 1), 3) def test_ulimit_value_scaling(self): """ The ulimit value is (if it exists) is scaled by the given factor. """ self.assertEqual(ulimit_value(-1, -1, 4), "unlimited") self.assertEqual(ulimit_value(-1, 20, 4), 5) self.assertEqual(ulimit_value(20, -1, 4), 5) self.assertEqual(ulimit_value(-1, 21, 2), 10) self.assertEqual(ulimit_value(21, -1, 2), 10) self.assertEqual(ulimit_value(20, 21, 2), 10) self.assertEqual(ulimit_value(10, 25, 3), 3) self.assertEqual(ulimit_value(25, 10, 3), 3) PK»YjG˰j[ [ vxsandbox/tests/test_utils.py"""Tests for vxsandbox.utils.""" import contextlib import os from twisted.trial.unittest import SkipTest from vumi.tests.helpers import VumiTestCase from vxsandbox.utils import SandboxError, find_nodejs_or_skip_test class WorkerWithNodejs(object): @classmethod def find_nodejs(cls): return "/tmp/nodejs.worker.dummy" class WorkerWithoutNodejs(object): @classmethod def find_nodejs(cls): return None class TestSandboxError(VumiTestCase): def test_type(self): err = SandboxError("Eep") self.assertTrue(isinstance(err, Exception)) def test_str(self): err = SandboxError("Eep") self.assertEqual(str(err), "Eep") class TestFindNodejsOrSkipTest(VumiTestCase): def patch_vumi_test_node_path(self, path): def patched_get(name, default=None): if name == "VUMI_TEST_NODE_PATH": return path if path is not None else default return orig_env_get(name, default) orig_env_get = os.environ.get self.patch(os.environ, 'get', patched_get) def patch_os_path_isfile(self, path, value): def patched_isfile(filename): if filename == path: return value return orig_isfile(filename) orig_isfile = os.path.isfile self.patch(os.path, 'isfile', patched_isfile) @contextlib.contextmanager def fail_on_skip_test(self): try: yield except SkipTest: self.fail("SkipTest raised when node.js path expected") def test_valid_vumi_test_node_path(self): self.patch_vumi_test_node_path("/tmp/nodejs.env.dummy") self.patch_os_path_isfile("/tmp/nodejs.env.dummy", True) with self.fail_on_skip_test(): self.assertEqual( find_nodejs_or_skip_test(WorkerWithoutNodejs), "/tmp/nodejs.env.dummy") def test_invalid_vumi_test_node_path(self): self.patch_vumi_test_node_path("/tmp/nodejs.env.dummy") self.patch_os_path_isfile("/tmp/nodejs.env.dummy", False) with self.fail_on_skip_test(): err = self.failUnlessRaises( RuntimeError, find_nodejs_or_skip_test, WorkerWithoutNodejs) self.assertEqual(str(err), ( "VUMI_TEST_NODE_PATH specified, but does not exist:" " /tmp/nodejs.env.dummy")) def test_worker_finds_nodejs(self): self.patch_vumi_test_node_path(None) with self.fail_on_skip_test(): self.assertEqual( find_nodejs_or_skip_test(WorkerWithNodejs), "/tmp/nodejs.worker.dummy") def test_worker_doesnt_find_nodejs(self): self.patch_vumi_test_node_path(None) err = self.failUnlessRaises( SkipTest, find_nodejs_or_skip_test, WorkerWithoutNodejs) self.assertEqual(str(err), "No node.js executable found.") PK»YjG’¹®bú ú vxsandbox/resources/utils.py# -*- test-case-name: vxsandbox.resources.tests.test_utils -*- """Utilities for building sandbox resources.""" from __future__ import absolute_import import json import logging from uuid import uuid4 from twisted.internet.defer import inlineCallbacks, maybeDeferred from vumi.utils import load_class_by_string, to_kwargs from vumi.message import Message class SandboxCommand(Message): @staticmethod def generate_id(): return uuid4().get_hex() def process_fields(self, fields): fields = super(SandboxCommand, self).process_fields(fields) fields.setdefault('cmd', 'unknown') fields.setdefault('cmd_id', self.generate_id()) fields.setdefault('reply', False) return fields def validate_fields(self): super(SandboxCommand, self).validate_fields() self.assert_field_present( 'cmd', 'cmd_id', 'reply', ) @classmethod def from_json(cls, json_string): # We override this to avoid the datetime conversions. return cls(_process_fields=False, **to_kwargs(json.loads(json_string))) class SandboxResources(object): """Class for holding resources common to a set of sandboxes.""" def __init__(self, app_worker, config): self.app_worker = app_worker self.config = config self.resources = {} def add_resource(self, resource_name, resource): """Add additional resources -- should only be called before calling :meth:`setup_resources`.""" self.resources[resource_name] = resource def validate_config(self): # FIXME: The name of this method is a vicious lie. # It does not validate configs. It constructs resources objects. # Fixing that is beyond the scope of this commit, however. for name, config in self.config.iteritems(): cls = load_class_by_string(config.pop('cls')) self.resources[name] = cls(name, self.app_worker, config) @inlineCallbacks def setup_resources(self): for resource in self.resources.itervalues(): yield resource.setup() @inlineCallbacks def teardown_resources(self): for resource in self.resources.itervalues(): yield resource.teardown() class SandboxResource(object): """Base class for sandbox resources.""" # TODO: SandboxResources should probably have their own config definitions. # Is that overkill? def __init__(self, name, app_worker, config): self.name = name self.app_worker = app_worker self.config = config def setup(self): pass def teardown(self): pass def sandbox_init(self, api): pass def reply(self, command, **kwargs): return SandboxCommand(cmd=command['cmd'], reply=True, cmd_id=command['cmd_id'], **kwargs) def reply_error(self, command, reason): return self.reply(command, success=False, reason=reason) def dispatch_request(self, api, command): handler_name = 'handle_%s' % (command['cmd'],) handler = getattr(self, handler_name, self.unknown_request) return maybeDeferred(handler, api, command) def unknown_request(self, api, command): api.log("Resource %s received unknown command %r from" " sandbox %r. Killing sandbox. [Full command: %r]" % (self.name, command['cmd'], api.sandbox_id, command), logging.ERROR) api.sandbox_kill() # it's a harsh world PKLÉHtÙ¾t)t)vxsandbox/resources/http.py# -*- test-case-name: vxsandbox.resources.tests.test_http -*- """An HTTP client resource for Vumi's application sandbox.""" import base64 import operator from StringIO import StringIO from twisted.internet import reactor from twisted.internet.defer import succeed, maybeDeferred from twisted.web.client import WebClientContextFactory, Agent from OpenSSL.SSL import ( VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_CLIENT_ONCE, VERIFY_NONE, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD) from treq.client import HTTPClient from vumi.utils import HttpDataLimitError from .utils import SandboxResource try: from twisted.web.client import BrowserLikePolicyForHTTPS from twisted.internet.ssl import optionsForClientTLS class HttpClientPolicyForHTTPS(BrowserLikePolicyForHTTPS): """ This client policy is used if we have Twisted 14.0.0 or newer and are not explicitly disabling host verification. """ def __init__(self, ssl_method=None): super(HttpClientPolicyForHTTPS, self).__init__() self.ssl_method = ssl_method def creatorForNetloc(self, hostname, port): options = {} if self.ssl_method is not None: options['method'] = self.ssl_method return optionsForClientTLS( hostname.decode("ascii"), extraCertificateOptions=options) except ImportError: HttpClientPolicyForHTTPS = None class HttpClientContextFactory(object): """ This context factory is used if we have a Twisted version older than 14.0.0 or if we are explicitly disabling host verification. """ def __init__(self, verify_options=None, ssl_method=None): self.verify_options = verify_options self.ssl_method = ssl_method def getContext(self, hostname, port): context = self._get_noverify_context() if self.verify_options in (None, VERIFY_NONE): # We don't want to do anything with verification here. return context if self.verify_options is not None: def verify_callback(conn, cert, errno, errdepth, ok): return ok context.set_verify(self.verify_options, verify_callback) return context def _get_noverify_context(self): """ Use ClientContextFactory directly and set the method if necessary. This will perform no host verification at all. """ from twisted.internet.ssl import ClientContextFactory context_factory = ClientContextFactory() if self.ssl_method is not None: context_factory.method = self.ssl_method return context_factory.getContext() def make_context_factory(ssl_method=None, verify_options=None): if HttpClientPolicyForHTTPS is None or verify_options == VERIFY_NONE: return HttpClientContextFactory( verify_options=verify_options, ssl_method=ssl_method) else: return HttpClientPolicyForHTTPS(ssl_method=ssl_method) class HttpClientResource(SandboxResource): """ Resource that allows making HTTP calls to outside services. All command on this resource share a common set of command and response fields: Command fields: - ``url``: The URL to request - ``verify_options``: A list of options to verify when doing an HTTPS request. Possible string values are ``VERIFY_NONE``, ``VERIFY_PEER``, ``VERIFY_CLIENT_ONCE`` and ``VERIFY_FAIL_IF_NO_PEER_CERT``. Specifying multiple values results in passing along a reduced ``OR`` value (e.g. VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT) - ``headers``: A dictionary of keys for the header name and a list of values to provide as header values. - ``data``: The payload to submit as part of the request. - ``files``: A dictionary, submitted as multipart/form-data in the request: .. code-block:: javascript [{ "field name": { "file_name": "the file name", "content_type": "content-type", "data": "data to submit, encoded as base64", } }, ...] The ``data`` field in the dictionary will be base64 decoded before the HTTP request is made. Success reply fields: - ``success``: Set to ``true`` - ``body``: The response body - ``code``: The HTTP response code Failure reply fields: - ``success``: set to ``false`` - ``reason``: Reason for the failure Example: .. code-block:: javascript api.request( 'http.get', {url: 'http://foo/'}, function(reply) { api.log_info(reply.body); }); """ DEFAULT_TIMEOUT = 30 # seconds DEFAULT_DATA_LIMIT = 128 * 1024 # 128 KB agent_class = Agent http_client_class = HTTPClient def setup(self): self.timeout = self.config.get('timeout', self.DEFAULT_TIMEOUT) self.data_limit = self.config.get('data_limit', self.DEFAULT_DATA_LIMIT) def _make_request_from_command(self, method, command): url = command.get('url', None) if not isinstance(url, basestring): return succeed(self.reply(command, success=False, reason="No URL given")) url = url.encode("utf-8") verify_map = { 'VERIFY_NONE': VERIFY_NONE, 'VERIFY_PEER': VERIFY_PEER, 'VERIFY_CLIENT_ONCE': VERIFY_CLIENT_ONCE, 'VERIFY_FAIL_IF_NO_PEER_CERT': VERIFY_FAIL_IF_NO_PEER_CERT, } method_map = { 'SSLv3': SSLv3_METHOD, 'SSLv23': SSLv23_METHOD, 'TLSv1': TLSv1_METHOD, } if 'verify_options' in command: verify_options = [verify_map[key] for key in command.get('verify_options', [])] verify_options = reduce(operator.or_, verify_options) else: verify_options = None if 'ssl_method' in command: # TODO: Fail better with unknown method. ssl_method = method_map[command['ssl_method']] else: ssl_method = None context_factory = make_context_factory( verify_options=verify_options, ssl_method=ssl_method) headers = command.get('headers', None) data = command.get('data', None) files = command.get('files', None) d = self._make_request(method, url, headers=headers, data=data, files=files, timeout=self.timeout, context_factory=context_factory, data_limit=self.data_limit) d.addCallback(self._make_success_reply, command) d.addErrback(self._make_failure_reply, command) return d def _make_request(self, method, url, headers=None, data=None, files=None, timeout=None, context_factory=None, data_limit=None): context_factory = (context_factory if context_factory is not None else WebClientContextFactory()) if headers is not None: headers = dict((k.encode("utf-8"), [x.encode("utf-8") for x in v]) for k, v in headers.items()) if data is not None: data = data.encode("utf-8") if files is not None: files = dict([ (key, (value['file_name'], value['content_type'], StringIO(base64.b64decode(value['data'])))) for key, value in files.iteritems()]) agent = self.agent_class(reactor, contextFactory=context_factory) http_client = self.http_client_class(agent) d = http_client.request(method, url, headers=headers, data=data, files=files, timeout=timeout) d.addCallback(self._ensure_data_limit, method, data_limit) return d def _ensure_data_limit(self, response, method, data_limit): header = response.headers.getRawHeaders('Content-Length') def data_limit_check(response, length): if data_limit is not None and length > data_limit: raise HttpDataLimitError( "Received %d bytes, maximum of %d bytes allowed." % (length, data_limit,)) return response if header is None or method.upper() == 'HEAD': d = response.content() d.addCallback(lambda body: data_limit_check(response, len(body))) return d content_length = header[0] return maybeDeferred(data_limit_check, response, int(content_length)) def _make_success_reply(self, response, command): d = response.content() d.addCallback( lambda body: self.reply(command, success=True, body=body, code=response.code)) return d def _make_failure_reply(self, failure, command): return self.reply(command, success=False, reason=failure.getErrorMessage()) def handle_get(self, api, command): """ Make an HTTP GET request. See :class:`HttpResource` for details. """ return self._make_request_from_command('GET', command) def handle_put(self, api, command): """ Make an HTTP PUT request. See :class:`HttpResource` for details. """ return self._make_request_from_command('PUT', command) def handle_delete(self, api, command): """ Make an HTTP DELETE request. See :class:`HttpResource` for details. """ return self._make_request_from_command('DELETE', command) def handle_head(self, api, command): """ Make an HTTP HEAD request. See :class:`HttpResource` for details. """ return self._make_request_from_command('HEAD', command) def handle_post(self, api, command): """ Make an HTTP POST request. See :class:`HttpResource` for details. """ return self._make_request_from_command('POST', command) def handle_patch(self, api, command): """ Make an HTTP PATCH request. See :class:`HttpResource` for details. """ return self._make_request_from_command('PATCH', command) PKLÉHИ–¹''vxsandbox/resources/metrics.py# -*- test-case-name: go.apps.jsbox.tests.test_metrics -*- # -*- coding: utf-8 -*- """Metrics for JS Box sandboxes""" import re from twisted.internet.defer import inlineCallbacks from vumi.errors import ConfigError from vumi.blinkenlights.metrics import ( SUM, AVG, MIN, MAX, LAST, MetricPublisher, Metric, MetricManager) from .utils import SandboxResource class MetricEventError(Exception): """Raised when a command cannot be converted to a metric event.""" class MetricEvent(object): AGGREGATORS = { 'sum': SUM, 'avg': AVG, 'min': MIN, 'max': MAX, 'last': LAST } NAME_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9._-]{,100}$") def __init__(self, store, metric, value, agg): self.store = store self.metric = metric self.value = value self.agg = agg def __eq__(self, other): if not isinstance(other, self.__class__): return False return all((self.store == other.store, self.metric == other.metric, self.value == other.value, self.agg is other.agg)) @classmethod def _parse_name(cls, name, kind): if name is None: raise MetricEventError("Missing %s name." % (kind,)) if not isinstance(name, basestring): raise MetricEventError("Invalid type for %s name: %r" % (kind, name)) if not cls.NAME_REGEX.match(name): raise MetricEventError("Invalid %s name: %r." % (kind, name)) return name @classmethod def _parse_value(cls, value): try: value = float(value) except (ValueError, TypeError): raise MetricEventError("Invalid metric value %r." % (value,)) return value @classmethod def _parse_agg(cls, agg): if not isinstance(agg, basestring): raise MetricEventError("Invalid metric aggregator %r" % (agg,)) if agg not in cls.AGGREGATORS: raise MetricEventError("Invalid metric aggregator %r." % (agg,)) return cls.AGGREGATORS[agg] @classmethod def from_command(cls, command): store = cls._parse_name(command.get('store', 'default'), 'store') metric = cls._parse_name(command.get('metric'), 'metric') value = cls._parse_value(command.get('value')) agg = cls._parse_agg(command.get('agg')) return cls(store, metric, value, agg) class MetricsResource(SandboxResource): """Resource that provides metric storing. :param string metrics_prefix: Prefix for metric names. Metric names will be structured as `.stores..`. """ @inlineCallbacks def setup(self): prefix = self.config.get('metrics_prefix', None) if prefix is None: raise ConfigError("metrics_prefix config parameter not supplied") self.metric_publisher = yield self.app_worker.start_publisher( MetricPublisher) def _metric_manager_prefix(self, store_name): prefix = self.config['metrics_prefix'] return "%s.stores.%s." % (prefix, store_name) def _publish_event(self, api, ev): """Publish a metric event.""" if ev.agg is not None: agg = [ev.agg] metric = Metric(ev.metric, agg) prefix = self._metric_manager_prefix(ev.store) manager = MetricManager(prefix, publisher=self.metric_publisher) manager.oneshot(metric, ev.value) manager.publish_metrics() def handle_fire(self, api, command): """Fire a metric value.""" try: ev = MetricEvent.from_command(command) except MetricEventError, e: return self.reply(command, success=False, reason=unicode(e)) self._publish_event(api, ev) return self.reply(command, success=True) PK»YjG93,2± ± vxsandbox/resources/logging.py# -*- test-case-name: vxsandbox.resources.tests.test_logging -*- """A logging resource for Vumi's application sandbox.""" from __future__ import absolute_import import logging from twisted.internet.defer import succeed, inlineCallbacks, returnValue from vumi import log from .utils import SandboxResource class LoggingResource(SandboxResource): """ Resource that allows a sandbox to log messages via Twisted's logging framework. """ def log(self, api, msg, level): """Logs a message via vumi.log (i.e. Twisted logging). Sub-class should override this if they wish to log messages elsewhere. The `api` parameter is provided for use by such sub-classes. The `log` method should always return a deferred. """ return succeed(log.msg(msg, logLevel=level)) @inlineCallbacks def handle_log(self, api, command, level=None): """ Log a message at the specified severity level. The other log commands are identical except that ``level`` need not be specified. Using the log-level specific commands is preferred. Command fields: - ``level``: The severity level to log at. Must be an integer log level. Default severity is the ``INFO`` log level. - ``msg``: The message to log. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'log.log', {level: 20, msg: 'Abandon ship!'}, function(reply) { api.log_info('New value: ' + reply.value); } ); """ level = command.get('level', level) if level is None: level = logging.INFO msg = command.get('msg') if msg is None: returnValue(self.reply(command, success=False, reason="Value expected for msg")) if not isinstance(msg, basestring): msg = str(msg) elif isinstance(msg, unicode): msg = msg.encode('utf-8') yield self.log(api, msg, level) returnValue(self.reply(command, success=True)) def handle_debug(self, api, command): """ Logs a message at the ``DEBUG`` log level. See :func:`handle_log` for details. """ return self.handle_log(api, command, level=logging.DEBUG) def handle_info(self, api, command): """ Logs a message at the ``INFO`` log level. See :func:`handle_log` for details. """ return self.handle_log(api, command, level=logging.INFO) def handle_warning(self, api, command): """ Logs a message at the ``WARNING`` log level. See :func:`handle_log` for details. """ return self.handle_log(api, command, level=logging.WARNING) def handle_error(self, api, command): """ Logs a message at the ``ERROR`` log level. See :func:`handle_log` for details. """ return self.handle_log(api, command, level=logging.ERROR) def handle_critical(self, api, command): """ Logs a message at the ``CRITICAL`` log level. See :func:`handle_log` for details. """ return self.handle_log(api, command, level=logging.CRITICAL) PKLÉHîƒálÊÊvxsandbox/resources/__init__.py""" Sandbox resources. """ from .utils import SandboxResource, SandboxCommand, SandboxResources from .logging import LoggingResource from .http import HttpClientResource from .kv import RedisResource from .metrics import MetricsResource from .outbound import OutboundResource __all__ = [ "SandboxResource", "SandboxCommand", "SandboxResources", "LoggingResource", "HttpClientResource", "MetricsResource", "OutboundResource", "RedisResource", ] PK»YjG«ñÝÞÞvxsandbox/resources/kv.py# -*- test-case-name: vxsandbox.resources.tests.test_kv -*- """A Redis key-value store resource for Vumi's application sandbox.""" from __future__ import absolute_import import logging import json from twisted.internet.defer import inlineCallbacks, returnValue from vumi.persist.txredis_manager import TxRedisManager from .utils import SandboxResource class RedisResource(SandboxResource): """ Resource that provides access to a simple key-value store. Configuration options: :param dict redis_manager: Redis manager configuration options. :param int keys_per_user_soft: Maximum number of keys each user may make use of in redis before usage warnings are logged. (default: 80% of hard limit). :param int keys_per_user_hard: Maximum number of keys each user may make use of in redis (default: 100). Falls back to keys_per_user. :param int keys_per_user: Synonym for `keys_per_user_hard`. Deprecated. """ # FIXME: # - Currently we allow key expiry to be set. Keys that expire are # not decremented from the sandbox's key limit. This means that # some sandboxes might hit their key limit too soon. This is # better than not allowing expiry of keys and filling up Redis # though. @inlineCallbacks def setup(self): self.r_config = self.config.get('redis_manager', {}) self.keys_per_user_hard = self.config.get( 'keys_per_user_hard', self.config.get('keys_per_user', 100)) self.keys_per_user_soft = self.config.get( 'keys_per_user_soft', int(0.8 * self.keys_per_user_hard)) self.redis = yield TxRedisManager.from_config(self.r_config) def teardown(self): return self.redis.close_manager() def _count_key(self, sandbox_id): return "#".join(["count", sandbox_id]) def _sandboxed_key(self, sandbox_id, key): return "#".join(["sandboxes", sandbox_id, key]) def _too_many_keys(self, command): return self.reply(command, success=False, reason="Too many keys") @inlineCallbacks def check_keys(self, api, key): if (yield self.redis.exists(key)): returnValue(True) count_key = self._count_key(api.sandbox_id) key_count = yield self.redis.incr(count_key, 1) if key_count > self.keys_per_user_soft: if key_count < self.keys_per_user_hard: api.log('Redis soft limit of %s keys reached for sandbox %s. ' 'Once the hard limit of %s is reached no more keys ' 'can be written.' % ( self.keys_per_user_soft, api.sandbox_id, self.keys_per_user_hard), logging.WARNING) else: api.log('Redis hard limit of %s keys reached for sandbox %s. ' 'No more keys can be written.' % ( self.keys_per_user_hard, api.sandbox_id), logging.ERROR) yield self.redis.incr(count_key, -1) returnValue(False) returnValue(True) @inlineCallbacks def handle_set(self, api, command): """ Set the value of a key. Command fields: - ``key``: The key whose value should be set. - ``value``: The value to store. May be any JSON serializable object. - ``seconds``: Lifetime of the key in seconds. The default ``null`` indicates that the key should not expire. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'kv.set', {key: 'foo', value: {x: '42'}}, function(reply) { api.log_info('Value store: ' + reply.success); }); """ key = self._sandboxed_key(api.sandbox_id, command.get('key')) seconds = command.get('seconds') if not (seconds is None or isinstance(seconds, (int, long))): returnValue(self.reply_error( command, "seconds must be a number or null")) if not (yield self.check_keys(api, key)): returnValue(self._too_many_keys(command)) json_value = json.dumps(command.get('value')) if seconds is None: yield self.redis.set(key, json_value) else: yield self.redis.setex(key, seconds, json_value) returnValue(self.reply(command, success=True)) @inlineCallbacks def handle_get(self, api, command): """ Retrieve the value of a key. Command fields: - ``key``: The key whose value should be retrieved. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. - ``value``: The value retrieved. Example: .. code-block:: javascript api.request( 'kv.get', {key: 'foo'}, function(reply) { api.log_info( 'Value retrieved: ' + JSON.stringify(reply.value)); } ); """ key = self._sandboxed_key(api.sandbox_id, command.get('key')) raw_value = yield self.redis.get(key) value = json.loads(raw_value) if raw_value is not None else None returnValue(self.reply(command, success=True, value=value)) @inlineCallbacks def handle_delete(self, api, command): """ Delete a key. Command fields: - ``key``: The key to delete. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'kv.delete', {key: 'foo'}, function(reply) { api.log_info('Value deleted: ' + reply.success); } ); """ key = self._sandboxed_key(api.sandbox_id, command.get('key')) existed = bool((yield self.redis.delete(key))) if existed: count_key = self._count_key(api.sandbox_id) yield self.redis.incr(count_key, -1) returnValue(self.reply(command, success=True, existed=existed)) @inlineCallbacks def handle_incr(self, api, command): """ Atomically increment the value of an integer key. The current value of the key must be an integer. If the key does not exist, it is set to zero. Command fields: - ``key``: The key to delete. - ``amount``: The integer amount to increment the key by. Defaults to 1. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. - ``value``: The new value of the key. Example: .. code-block:: javascript api.request( 'kv.incr', {key: 'foo', amount: 3}, function(reply) { api.log_info('New value: ' + reply.value); } ); """ key = self._sandboxed_key(api.sandbox_id, command.get('key')) if not (yield self.check_keys(api, key)): returnValue(self._too_many_keys(command)) amount = command.get('amount', 1) try: value = yield self.redis.incr(key, amount=amount) except Exception, e: returnValue(self.reply(command, success=False, reason=unicode(e))) returnValue(self.reply(command, value=int(value), success=True)) PKLÉH)y˜:à(à(vxsandbox/resources/outbound.py# -*- test-case-name: vxsandbox.resources.tests.test_outbound -*- """An outbound message sending for Vumi's application sandbox.""" from twisted.internet.defer import succeed from vumi.errors import InvalidEndpoint from .utils import SandboxResource class InvalidOutboundCommand(Exception): """ Internal exception raised when a sandboxed application sends and invalid outbound command. """ class OutboundResource(SandboxResource): """ Resource that provides the ability to send outbound messages. Includes support for replying to the sender of the current message, replying to the group the current message was from and sending messages that aren't replies. """ def setup(self): self._allowed_helper_metadata = set( self.config.get('allowed_helper_metadata', [])) def _mkfail(self, command, reason): return self.reply(command, success=False, reason=reason) def _mkfaild(self, command, reason): return succeed(self._mkfail(command, reason)) def _reply_callbacks(self, command): callback = lambda r: self.reply(command, success=True) errback = lambda f: self._mkfail(command, unicode(f.getErrorMessage())) return (callback, errback) def _get_cmd_params(self, api, command, params): return [ getattr(self, '_param_%s' % name)(api, command) for name in params ] def _param_content(self, api, command): if 'content' not in command: raise InvalidOutboundCommand(u"'content' must be given.") content = command['content'] if not isinstance(content, (unicode, type(None))): raise InvalidOutboundCommand("'content' must be unicode or null.") return content def _param_continue_session(self, api, command): continue_session = command.get('continue_session', True) if continue_session not in (True, False): raise InvalidOutboundCommand( u"'continue_session' must be either true or false if given") return continue_session def _param_in_reply_to(self, api, command): in_reply_to = command.get('in_reply_to') if not isinstance(in_reply_to, unicode): raise InvalidOutboundCommand(u"'in_reply_to' must be given.") orig_msg = api.get_inbound_message(in_reply_to) if orig_msg is None: raise InvalidOutboundCommand( u"Could not find original message with id: %r" % in_reply_to) return orig_msg def _param_helper_metadata(self, api, command): helper_metadata = command.get('helper_metadata') if helper_metadata in [None, {}]: # No helper metadata, so return an empty dict. return {} if not self._allowed_helper_metadata: raise InvalidOutboundCommand("'helper_metadata' is not allowed") if not isinstance(helper_metadata, dict): raise InvalidOutboundCommand( "'helper_metadata' must be object or null.") if any(key not in self._allowed_helper_metadata for key in helper_metadata.iterkeys()): raise InvalidOutboundCommand( "'helper_metadata' may only contain the following keys: %s" % ', '.join(sorted(self._allowed_helper_metadata))) # Anything we have left is valid. return helper_metadata def _param_endpoint(self, api, command): endpoint = command.get('endpoint') if not isinstance(endpoint, unicode): raise InvalidOutboundCommand( u"'endpoint' must be given in sends.") try: self.app_worker.check_endpoint( self.app_worker.ALLOWED_ENDPOINTS, endpoint) except InvalidEndpoint: raise InvalidOutboundCommand( u"Endpoint %r not configured" % (endpoint,)) return endpoint def _param_to_addr(self, api, command): to_addr = command.get('to_addr') if not isinstance(to_addr, unicode): raise InvalidOutboundCommand(u"'to_addr' must be given in sends.") return to_addr def handle_reply_to(self, api, command): """ Sends a reply to the individual who sent a received message. Command fields: - ``content``: The body of the reply message. - ``in_reply_to``: The ``message id`` of the message being replied to. - ``continue_session``: Whether to continue the session (if any). Defaults to ``true``. - ``helper_metadata``: An object of additional helper metadata fields to include in the reply. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.reply_to', {content: 'Welcome!', in_reply_to: '06233d4eede945a3803bf9f3b78069ec'}, function(reply) { api.log_info('Reply sent: ' + reply.success); }); """ try: content, orig_msg, continue_session, helper_metadata = ( self._get_cmd_params(api, command, [ 'content', 'in_reply_to', 'continue_session', 'helper_metadata'])) except InvalidOutboundCommand, err: return self._mkfaild(command, reason=unicode(err)) d = self.app_worker.reply_to( orig_msg, content, continue_session=continue_session, helper_metadata=helper_metadata) return d.addCallbacks(*self._reply_callbacks(command)) def handle_reply_to_group(self, api, command): """ Sends a reply to the group from which a received message was sent. Command fields: - ``content``: The body of the reply message. - ``in_reply_to``: The ``message id`` of the message being replied to. - ``continue_session``: Whether to continue the session (if any). Defaults to ``true``. - ``helper_metadata``: An object of additional helper metadata fields to include in the reply. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.reply_to_group', {content: 'Welcome!', in_reply_to: '06233d4eede945a3803bf9f3b78069ec'}, function(reply) { api.log_info('Reply to group sent: ' + reply.success); }); """ try: content, orig_msg, continue_session, helper_metadata = ( self._get_cmd_params(api, command, [ 'content', 'in_reply_to', 'continue_session', 'helper_metadata'])) except InvalidOutboundCommand, err: return self._mkfaild(command, reason=unicode(err)) d = self.app_worker.reply_to_group( orig_msg, content, continue_session=continue_session, helper_metadata=helper_metadata) return d.addCallbacks(*self._reply_callbacks(command)) def handle_send_to(self, api, command): """ Sends a message to a specified address. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``endpoint``: The name of the endpoint to send the message via. Optional (default is ``"default"``). - ``helper_metadata``: An object of additional helper metadata fields to include in the message being sent. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to', {content: 'Welcome!', to_addr: '+27831234567', endpoint: 'default'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ if 'endpoint' not in command: command['endpoint'] = u'default' try: content, to_addr, endpoint, helper_metadata = ( self._get_cmd_params(api, command, [ 'content', 'to_addr', 'endpoint', 'helper_metadata'])) except InvalidOutboundCommand, err: return self._mkfaild(command, reason=unicode(err)) d = self.app_worker.send_to( to_addr, content, endpoint=endpoint, helper_metadata=helper_metadata) return d.addCallbacks(*self._reply_callbacks(command)) def handle_send_to_endpoint(self, api, command): """ Sends a message to a specified endpoint. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``endpoint``: The name of the endpoint to send the message via. - ``helper_metadata``: An object of additional helper metadata fields to include in the message being sent. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to_endpoint', {content: 'Welcome!', to_addr: '+27831234567', endpoint: 'sms'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ try: content, to_addr, endpoint, helper_metadata = ( self._get_cmd_params(api, command, [ 'content', 'to_addr', 'endpoint', 'helper_metadata'])) except InvalidOutboundCommand, err: return self._mkfaild(command, reason=unicode(err)) d = self.app_worker.send_to( to_addr, content, endpoint=endpoint, helper_metadata=helper_metadata) return d.addCallbacks(*self._reply_callbacks(command)) PK»YjG †®kvxsandbox/resources/config.py# -*- test-case-name: vxsandbox.resources.tests.test_config -*- """A file-based configuration resource for Vumi's application sandbox.""" from .utils import SandboxResource class FileConfigResource(SandboxResource): """ Resource that provides access to a file-based configuration resource Configuration options: :param dict keys: A mapping between configuration keys and filenames """ def handle_get(self, api, command): """ Retrieve the value of a configuration specified by a key. Command fields: - ``key``: The key whose configuration should be retrieved. Reply: - The contents of the file specified by the configuration mapping for the given key. """ key = command.get('key') filename = self.config.get('keys').get(key) if filename is None: return self.reply_error( command, reason='Configuration key %r not found' % (key,)) try: with open(filename, 'r') as f: value = f.read() return self.reply(command, value=value, success=True) except EnvironmentError: return self.reply_error( command, reason='Cannot read file %r' % (filename,)) PKLÉHç{ØDD$vxsandbox/resources/tests/test_kv.pyimport json import logging from twisted.internet.defer import inlineCallbacks from vumi.tests.helpers import PersistenceHelper from vxsandbox.resources.kv import RedisResource from vxsandbox.resources.tests.utils import ResourceTestCaseBase class TestRedisResource(ResourceTestCaseBase): resource_cls = RedisResource @inlineCallbacks def setUp(self): yield super(TestRedisResource, self).setUp() self.persistence_helper = self.add_helper(PersistenceHelper()) self.r_server = yield self.persistence_helper.get_redis_manager() yield self.create_resource({}) def create_resource(self, config): config.setdefault('redis_manager', { 'FAKE_REDIS': self.r_server, 'key_prefix': self.r_server._key_prefix, }) return super(TestRedisResource, self).create_resource(config) @inlineCallbacks def create_metric(self, metric, value, total_count=1): metric_key = 'sandboxes#test_id#' + metric count_key = 'count#test_id' yield self.r_server.set(metric_key, value) yield self.r_server.set(count_key, total_count) @inlineCallbacks def check_metric(self, metric, value, total_count, seconds=None): metric_key = 'sandboxes#test_id#' + metric count_key = 'count#test_id' self.assertEqual((yield self.r_server.get(metric_key)), value) self.assertEqual((yield self.r_server.get(count_key)), str(total_count) if total_count is not None else None) ttl = yield self.r_server.ttl(metric_key) if seconds is None: self.assertEqual(ttl, None) else: self.assertNotEqual(ttl, None) self.assertTrue(0 < ttl <= seconds) def assert_api_log(self, expected_level, expected_message): [log_entry] = self.api.logs level, message = log_entry self.assertEqual(level, expected_level) self.assertEqual(message, expected_message) @inlineCallbacks def test_handle_set(self): reply = yield self.dispatch_command('set', key='foo', value='bar') self.check_reply(reply, success=True) yield self.check_metric('foo', json.dumps('bar'), 1) @inlineCallbacks def test_handle_set_with_expiry(self): reply = yield self.dispatch_command( 'set', key='foo', value='bar', seconds=5) self.check_reply(reply, success=True) yield self.check_metric('foo', json.dumps('bar'), 1, seconds=5) @inlineCallbacks def test_handle_set_with_bad_seconds(self): reply = yield self.dispatch_command( 'set', key='foo', value='bar', seconds='foo') self.check_reply( reply, success=False, reason="seconds must be a number or null") yield self.check_metric('foo', None, None) @inlineCallbacks def test_handle_set_soft_limit_reached(self): yield self.create_metric('foo', 'a', total_count=80) reply = yield self.dispatch_command('set', key='bar', value='bar') self.check_reply(reply, success=True) self.assert_api_log( logging.WARNING, 'Redis soft limit of 80 keys reached for sandbox test_id. ' 'Once the hard limit of 100 is reached no more keys can ' 'be written.' ) @inlineCallbacks def test_handle_set_hard_limit_reached(self): yield self.create_metric('foo', 'a', total_count=100) reply = yield self.dispatch_command('set', key='bar', value='bar') self.check_reply(reply, success=False, reason='Too many keys') yield self.check_metric('bar', None, 100) self.assert_api_log( logging.ERROR, 'Redis hard limit of 100 keys reached for sandbox test_id. ' 'No more keys can be written.' ) @inlineCallbacks def test_keys_per_user_fallback_hard_limit(self): yield self.create_resource({ 'keys_per_user': 10, }) yield self.create_metric('foo', 'a', total_count=10) reply = yield self.dispatch_command('set', key='bar', value='bar') self.check_reply(reply, success=False, reason='Too many keys') self.assert_api_log( logging.ERROR, 'Redis hard limit of 10 keys reached for sandbox test_id. ' 'No more keys can be written.' ) @inlineCallbacks def test_keys_per_user_fallback_soft_limit(self): yield self.create_resource({ 'keys_per_user': 10, }) yield self.create_metric('foo', 'a', total_count=8) reply = yield self.dispatch_command('set', key='bar', value='bar') self.check_reply(reply, success=True) self.assert_api_log( logging.WARNING, 'Redis soft limit of 8 keys reached for sandbox test_id. ' 'Once the hard limit of 10 is reached no more keys can ' 'be written.' ) @inlineCallbacks def test_handle_get(self): yield self.create_metric('foo', json.dumps('bar')) reply = yield self.dispatch_command('get', key='foo') self.check_reply(reply, success=True, value='bar') @inlineCallbacks def test_handle_get_for_unknown_key(self): reply = yield self.dispatch_command('get', key='foo') self.check_reply(reply, success=True, value=None) @inlineCallbacks def test_handle_delete(self): self.create_metric('foo', json.dumps('bar')) yield self.r_server.set('count#test_id', '1') reply = yield self.dispatch_command('delete', key='foo') self.check_reply(reply, success=True, existed=True) yield self.check_metric('foo', None, 0) @inlineCallbacks def test_handle_incr_default_amount(self): reply = yield self.dispatch_command('incr', key='foo') self.check_reply(reply, success=True, value=1) yield self.check_metric('foo', '1', 1) @inlineCallbacks def test_handle_incr_create(self): reply = yield self.dispatch_command('incr', key='foo', amount=2) self.check_reply(reply, success=True, value=2) yield self.check_metric('foo', '2', 1) @inlineCallbacks def test_handle_incr_existing(self): self.create_metric('foo', '2') reply = yield self.dispatch_command('incr', key='foo', amount=2) self.check_reply(reply, success=True, value=4) yield self.check_metric('foo', '4', 1) @inlineCallbacks def test_handle_incr_existing_non_int(self): self.create_metric('foo', 'a') reply = yield self.dispatch_command('incr', key='foo', amount=2) self.check_reply(reply, success=False) self.assertTrue(reply['reason']) yield self.check_metric('foo', 'a', 1) @inlineCallbacks def test_handle_incr_soft_limit_reached(self): yield self.create_metric('foo', 'a', total_count=80) reply = yield self.dispatch_command('incr', key='bar', amount=2) self.check_reply(reply, success=True) [limit_warning] = self.api.logs level, message = limit_warning self.assertEqual(level, logging.WARNING) self.assertEqual( message, 'Redis soft limit of 80 keys reached for sandbox test_id. ' 'Once the hard limit of 100 is reached no more keys can ' 'be written.') @inlineCallbacks def test_handle_incr_hard_limit_reached(self): yield self.create_metric('foo', 'a', total_count=100) reply = yield self.dispatch_command('incr', key='bar', amount=2) self.check_reply(reply, success=False, reason='Too many keys') yield self.check_metric('bar', None, 100) [limit_error] = self.api.logs level, message = limit_error self.assertEqual(level, logging.ERROR) self.assertEqual( message, 'Redis hard limit of 100 keys reached for sandbox test_id. ' 'No more keys can be written.') PKLÉHK•øEJEJ&vxsandbox/resources/tests/test_http.pyimport base64 import json from OpenSSL.SSL import ( VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_NONE, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD) from twisted.web.http_headers import Headers from twisted.internet.defer import inlineCallbacks, fail, succeed from vxsandbox.resources.http import ( HttpClientContextFactory, HttpClientPolicyForHTTPS, make_context_factory, HttpClientResource) from vxsandbox.resources.tests.utils import ResourceTestCaseBase class DummyResponse(object): def __init__(self): self.headers = Headers({}) class DummyHTTPClient(object): def __init__(self): self._next_http_request_result = None self.http_requests = [] def set_agent(self, agent): self.agent = agent def get_context_factory(self): # We need to dig around inside our Agent to find the context factory. # Since this involves private attributes that have changed a few times # recently, we need to try various options. if hasattr(self.agent, "_contextFactory"): # For Twisted 13.x return self.agent._contextFactory elif hasattr(self.agent, "_policyForHTTPS"): # For Twisted 14.x return self.agent._policyForHTTPS elif hasattr(self.agent, "_endpointFactory"): # For Twisted 15.0.0 (and possibly newer) return self.agent._endpointFactory._policyForHTTPS else: raise NotImplementedError( "I can't find the context factory on this Agent. This seems" " to change every few versions of Twisted.") def fail_next(self, error): self._next_http_request_result = fail(error) def succeed_next(self, body, code=200, headers={}): default_headers = { 'Content-Length': len(body), } default_headers.update(headers) response = DummyResponse() response.code = code for header, value in default_headers.items(): response.headers.addRawHeader(header, value) response.content = lambda: succeed(body) self._next_http_request_result = succeed(response) def request(self, *args, **kw): self.http_requests.append((args, kw)) return self._next_http_request_result class TestHttpClientResource(ResourceTestCaseBase): resource_cls = HttpClientResource @inlineCallbacks def setUp(self): super(TestHttpClientResource, self).setUp() yield self.create_resource({}) self.dummy_client = DummyHTTPClient() self.patch(self.resource_cls, 'http_client_class', self.get_dummy_client) def get_dummy_client(self, agent): self.dummy_client.set_agent(agent) return self.dummy_client def http_request_fail(self, error): self.dummy_client.fail_next(error) def http_request_succeed(self, body, code=200, headers={}): self.dummy_client.succeed_next(body, code, headers) def assert_not_unicode(self, arg): self.assertFalse(isinstance(arg, unicode)) def get_context_factory(self): return self.dummy_client.get_context_factory() def get_context(self, context_factory=None): if context_factory is None: context_factory = self.get_context_factory() if hasattr(context_factory, 'creatorForNetloc'): # This context_factory is a new-style IPolicyForHTTPS # implementation, so we need to get a context from through its # client connection creator. The creator could either be a wrapper # around a ClientContextFactory (in which case we treat it like # one) or a ClientTLSOptions object (which means we have to grab # the context from a private attribute). creator = context_factory.creatorForNetloc('example.com', 80) if hasattr(creator, 'getContext'): return creator.getContext() else: return creator._ctx else: # This context_factory is an old-style WebClientContextFactory and # will build us a context object if we ask nicely. return context_factory.getContext('example.com', 80) def assert_http_request(self, url, method='GET', headers=None, data=None, timeout=None, files=None): timeout = (timeout if timeout is not None else self.resource.timeout) args = (method, url,) kw = dict(headers=headers, data=data, timeout=timeout, files=files) [(actual_args, actual_kw)] = self.dummy_client.http_requests # NOTE: Files are handed over to treq as file pointer-ish things # which in our case are `StringIO` instances. actual_kw_files = actual_kw.get('files') if actual_kw_files is not None: actual_kw_files = actual_kw.pop('files', None) kw_files = kw.pop('files', {}) for name, file_data in actual_kw_files.items(): kw_file_data = kw_files[name] file_name, content_type, sio = file_data self.assertEqual( (file_name, content_type, sio.getvalue()), kw_file_data) self.assertEqual((actual_args, actual_kw), (args, kw)) self.assert_not_unicode(actual_args[0]) self.assert_not_unicode(actual_kw.get('data')) headers = actual_kw.get('headers') if headers is not None: for key, values in headers.items(): self.assert_not_unicode(key) for value in values: self.assert_not_unicode(value) def test_make_context_factory_no_method_verify_none(self): context_factory = make_context_factory(verify_options=VERIFY_NONE) self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, VERIFY_NONE) self.assertEqual(context_factory.ssl_method, None) self.assertEqual( self.get_context(context_factory).get_verify_mode(), VERIFY_NONE) def test_make_context_factory_sslv3_verify_none(self): context_factory = make_context_factory( verify_options=VERIFY_NONE, ssl_method=SSLv3_METHOD) self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, VERIFY_NONE) self.assertEqual(context_factory.ssl_method, SSLv3_METHOD) self.assertEqual( self.get_context(context_factory).get_verify_mode(), VERIFY_NONE) def test_make_context_factory_no_method_verify_peer(self): # This test's behaviour depends on the version of Twisted being used. context_factory = make_context_factory(verify_options=VERIFY_PEER) context = self.get_context(context_factory) self.assertEqual(context_factory.ssl_method, None) self.assertNotEqual(context.get_verify_mode(), VERIFY_NONE) if HttpClientPolicyForHTTPS is None: # We have Twisted<14.0.0 self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, VERIFY_PEER) self.assertEqual(context.get_verify_mode(), VERIFY_PEER) else: self.assertIsInstance(context_factory, HttpClientPolicyForHTTPS) def test_make_context_factory_no_method_verify_peer_or_fail(self): # This test's behaviour depends on the version of Twisted being used. context_factory = make_context_factory( verify_options=(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT)) context = self.get_context(context_factory) self.assertEqual(context_factory.ssl_method, None) self.assertNotEqual(context.get_verify_mode(), VERIFY_NONE) if HttpClientPolicyForHTTPS is None: # We have Twisted<14.0.0 self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual( context_factory.verify_options, VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT) self.assertEqual( context.get_verify_mode(), VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT) else: self.assertIsInstance(context_factory, HttpClientPolicyForHTTPS) def test_make_context_factory_no_method_no_verify(self): # This test's behaviour depends on the version of Twisted being used. context_factory = make_context_factory() self.assertEqual(context_factory.ssl_method, None) if HttpClientPolicyForHTTPS is None: # We have Twisted<14.0.0 self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, None) else: self.assertIsInstance(context_factory, HttpClientPolicyForHTTPS) def test_make_context_factory_sslv3_no_verify(self): # This test's behaviour depends on the version of Twisted being used. context_factory = make_context_factory(ssl_method=SSLv3_METHOD) self.assertEqual(context_factory.ssl_method, SSLv3_METHOD) if HttpClientPolicyForHTTPS is None: # We have Twisted<14.0.0 self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, None) else: self.assertIsInstance(context_factory, HttpClientPolicyForHTTPS) @inlineCallbacks def test_handle_get(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('get', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='GET') @inlineCallbacks def test_handle_post(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('post', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='POST') @inlineCallbacks def test_handle_patch(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('patch', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='PATCH') @inlineCallbacks def test_handle_head(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('head', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='HEAD') @inlineCallbacks def test_handle_delete(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('delete', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='DELETE') @inlineCallbacks def test_handle_put(self): self.http_request_succeed("foo") reply = yield self.dispatch_command('put', url='http://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('http://www.example.com', method='PUT') @inlineCallbacks def test_failed_get(self): self.http_request_fail(ValueError("HTTP request failed")) reply = yield self.dispatch_command('get', url='http://www.example.com') self.assertFalse(reply['success']) self.assertEqual(reply['reason'], "HTTP request failed") self.assert_http_request('http://www.example.com', method='GET') @inlineCallbacks def test_null_url(self): reply = yield self.dispatch_command('get') self.assertFalse(reply['success']) self.assertEqual(reply['reason'], "No URL given") @inlineCallbacks def test_https_request(self): # This test's behaviour depends on the version of Twisted being used. self.http_request_succeed("foo") reply = yield self.dispatch_command('get', url='https://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context_factory = self.get_context_factory() self.assertEqual(context_factory.ssl_method, None) if HttpClientPolicyForHTTPS is None: self.assertIsInstance(context_factory, HttpClientContextFactory) self.assertEqual(context_factory.verify_options, None) else: self.assertIsInstance(context_factory, HttpClientPolicyForHTTPS) @inlineCallbacks def test_https_request_verify_none(self): self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com', verify_options=['VERIFY_NONE']) self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context = self.get_context() self.assertEqual(context.get_verify_mode(), VERIFY_NONE) @inlineCallbacks def test_https_request_verify_peer_or_fail(self): # This test's behaviour depends on the version of Twisted being used. self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com', verify_options=['VERIFY_PEER', 'VERIFY_FAIL_IF_NO_PEER_CERT']) self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context = self.get_context() # We don't control verify mode in newer Twisted. self.assertNotEqual(context.get_verify_mode(), VERIFY_NONE) if HttpClientPolicyForHTTPS is None: self.assertEqual( context.get_verify_mode(), VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT) @inlineCallbacks def test_handle_post_files(self): self.http_request_succeed('') reply = yield self.dispatch_command( 'post', url='https://www.example.com', files={ 'foo': { 'file_name': 'foo.json', 'content_type': 'application/json', 'data': base64.b64encode(json.dumps({'foo': 'bar'})), } }) self.assertTrue(reply['success']) self.assert_http_request( 'https://www.example.com', method='POST', files={ 'foo': ('foo.json', 'application/json', json.dumps({'foo': 'bar'})), }) @inlineCallbacks def test_data_limit_exceeded_using_head_method(self): self.http_request_succeed('', headers={ 'Content-Length': self.resource.DEFAULT_DATA_LIMIT + 1, }) reply = yield self.dispatch_command( 'head', url='https://www.example.com',) self.assertTrue(reply['success']) self.assertEqual(reply['body'], "") self.assert_http_request('https://www.example.com', method='HEAD') @inlineCallbacks def test_data_limit_exceeded_using_header(self): self.http_request_succeed('', headers={ 'Content-Length': self.resource.DEFAULT_DATA_LIMIT + 1, }) reply = yield self.dispatch_command( 'get', url='https://www.example.com',) self.assertFalse(reply['success']) self.assertEqual( reply['reason'], 'Received %d bytes, maximum of %s bytes allowed.' % ( self.resource.DEFAULT_DATA_LIMIT + 1, self.resource.DEFAULT_DATA_LIMIT,)) @inlineCallbacks def test_data_limit_exceeded_inferred_from_body(self): self.http_request_succeed('1' * (self.resource.DEFAULT_DATA_LIMIT + 1)) reply = yield self.dispatch_command( 'get', url='https://www.example.com',) self.assertFalse(reply['success']) self.assertEqual( reply['reason'], 'Received %d bytes, maximum of %s bytes allowed.' % ( self.resource.DEFAULT_DATA_LIMIT + 1, self.resource.DEFAULT_DATA_LIMIT,)) @inlineCallbacks def test_https_request_method_default(self): self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context_factory = self.get_context_factory() self.assertEqual(context_factory.ssl_method, None) @inlineCallbacks def test_https_request_method_SSLv3(self): self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com', ssl_method='SSLv3') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context_factory = self.get_context_factory() self.assertEqual(context_factory.ssl_method, SSLv3_METHOD) @inlineCallbacks def test_https_request_method_SSLv23(self): self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com', ssl_method='SSLv23') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context_factory = self.get_context_factory() self.assertEqual(context_factory.ssl_method, SSLv23_METHOD) @inlineCallbacks def test_https_request_method_TLSv1(self): self.http_request_succeed("foo") reply = yield self.dispatch_command( 'get', url='https://www.example.com', ssl_method='TLSv1') self.assertTrue(reply['success']) self.assertEqual(reply['body'], "foo") self.assert_http_request('https://www.example.com', method='GET') context_factory = self.get_context_factory() self.assertEqual(context_factory.ssl_method, TLSv1_METHOD) PK»YjG=13.1.0)", "vumi (>=0.5)"]}], "summary": "A sandbox application worker for Vumi.", "version": "0.6.1"}PK!LÉH”;c× 'vxsandbox-0.6.1.dist-info/top_level.txtvxsandbox PK"LÉHŒ''\\vxsandbox-0.6.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK"LÉH½»…­--"vxsandbox-0.6.1.dist-info/METADATAMetadata-Version: 2.0 Name: vxsandbox Version: 0.6.1 Summary: A sandbox application worker for Vumi. Home-page: http://github.com/praekelt/vumi-sandbox Author: Praekelt Foundation Author-email: dev@praekeltfoundation.org License: BSD Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Networking Requires-Dist: Twisted (>=13.1.0) Requires-Dist: vumi (>=0.5) Vumi Sandbox ============ A sandbox application worker for `Vumi`_. .. _Vumi: http://github.com/praekelt/vumi |sandbox-ci|_ |sandbox-cover|_ .. |sandbox-ci| image:: https://travis-ci.org/praekelt/vumi-sandbox.png?branch=develop .. _sandbox-ci: https://travis-ci.org/praekelt/vumi-sandbox .. |sandbox-cover| image:: https://coveralls.io/repos/praekelt/vumi-sandbox/badge.png?branch=develop .. _sandbox-cover: https://coveralls.io/r/praekelt/vumi-sandbox You can contact the Vumi development team in the following ways: * via *email* by joining the the `vumi-dev@googlegroups.com`_ mailing list * on *irc* in *#vumi* on the `Freenode IRC network`_ .. _vumi-dev@googlegroups.com: https://groups.google.com/forum/?fromgroups#!forum/vumi-dev .. _Freenode IRC network: https://webchat.freenode.net/?channels=#vumi Issues can be filed in the GitHub issue tracker. Please don't use the issue tracker for general support queries. PK"LÉH{K·¤¡ ¡ vxsandbox-0.6.1.dist-info/RECORDvxsandbox/__init__.py,sha256=B0o3HbKKZ2oScZkZMsP8qpGEo6sFqBW3BW2pyUrzrLY,492 vxsandbox/protocol.py,sha256=a3me2ptXB8SqtaNwJBP04YrE8CIIdlsdAnTXDx69eVg,6275 vxsandbox/rlimiter.py,sha256=ghiLHYNRnUV-xGdJG6qeQ8Uu3VQ4dn0W7uEf7l8AKbk,2531 vxsandbox/utils.py,sha256=Qp-ErqKdT1WMxRwMXT9mQiHHSkOQJi8w6zgPra6MrrI,838 vxsandbox/worker.py,sha256=pTbvvTzpbbRvzk4AueFROsWtCJCAddR-zCoIG_zGobo,16286 vxsandbox/resources/__init__.py,sha256=xqtPNw9y917pZ39LRVswnda-DUfd_XTuWVigYjKw86s,458 vxsandbox/resources/config.py,sha256=UvyMgDEyfx-UOV0WFKpbS1Dv9-bWlG-K6EJEzjdLJlU,1310 vxsandbox/resources/http.py,sha256=VUQ3kH-Hc8xXjUiVDfPAq6HHvQ_RK0vMhf7gPjGfO0Q,10612 vxsandbox/resources/kv.py,sha256=WSBXGsESvvo1SasVpq4ZVtAYho8ld4HUn9bTabmZsm8,8158 vxsandbox/resources/logging.py,sha256=fgcB_pEKSpR1IhL_TgVccEzFjEO5ju_pq1OClaaVRZk,3505 vxsandbox/resources/metrics.py,sha256=DzragShhXCAUuXX2grTJcmWm5mHMfb7A6zTBhIWg2Es,3879 vxsandbox/resources/outbound.py,sha256=l_pMzisb4REXDj5qODTeRc57zVYzQF6HbsibjvasLOw,10464 vxsandbox/resources/utils.py,sha256=Nfs9iw75CAzsQV0pbmxwNBra7lGvP_dNpHbW91NdHt4,3578 vxsandbox/resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 vxsandbox/resources/tests/test_config.py,sha256=YDlP2goIkHomZZEzg9DbyTQ8OBX1Ki6S2k0VJLUmeGk,1614 vxsandbox/resources/tests/test_http.py,sha256=92T5lSpDtOgzrNAm4XbC44Zsh1WEeYb7TfPKtvxS48Q,19013 vxsandbox/resources/tests/test_kv.py,sha256=FjDzaCvGXikIigNWlQcglvRY5Rtni-a9zjpgfhXvVgw,8004 vxsandbox/resources/tests/test_logging.py,sha256=2IbF6sPW5VaIh3XH8zra41kd605fmOdsYAnHef4xKaA,1812 vxsandbox/resources/tests/test_metrics.py,sha256=hDpf4J98HyxBGt-QnxfXW3t6mXoZgfB0Yw5dYoCJbSw,5491 vxsandbox/resources/tests/test_outbound.py,sha256=7kcBZXjJk4rUyYxd-2oSY1qhdD4bFQ94JsrnzMSV_wQ,13823 vxsandbox/resources/tests/test_utils.py,sha256=ox4bIg8iU9RQ2aWUEXD-ItYgtKWkDJP2-ofh0hkvgvo,10073 vxsandbox/resources/tests/utils.py,sha256=UVCpMaup02VRi6qxsCjZD7d1lrsBLq5eHxbTzb9D7cg,2116 vxsandbox/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 vxsandbox/tests/test_protocol.py,sha256=9gat1uGA7_fB2VTGmSixOZK8nd1qd0GaolwceoV38gk,33 vxsandbox/tests/test_rlimiter.py,sha256=RK3Zo6bnrTwtM3Ox7rUzH-ufpoZR6sv4NjJdIV7bOsA,3382 vxsandbox/tests/test_utils.py,sha256=bw_rECBflH36hLm_eXAjY2TcnKyP4HQj-FUnnVAXnbA,2907 vxsandbox/tests/test_worker.py,sha256=c-CbpPJ5w8p0_gmf-kFQ1P0hWqc9qTHZP_kHcXncw7c,22797 vxsandbox/tests/utils.py,sha256=pWIbhAnVUzqvyrDpoVrnjHd_XUzFI9uxJUNqeNYqYqQ,1240 vxsandbox-0.6.1.dist-info/DESCRIPTION.rst,sha256=ff9Ll5ie_mih_yGHMWaKkj1tvmZG1fjSZKC-xkLM3Xw,932 vxsandbox-0.6.1.dist-info/METADATA,sha256=cDwmzTv3nZABINKkbqsE6psttOupKghVduW1hRyCFB4,1581 vxsandbox-0.6.1.dist-info/RECORD,, vxsandbox-0.6.1.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 vxsandbox-0.6.1.dist-info/metadata.json,sha256=DDciQS7UsDNw62xETUFQAElUB7mVcJ51dLrv_p0A9pA,813 vxsandbox-0.6.1.dist-info/top_level.txt,sha256=LSuqawbxzJkSxp-eXxMzNKHFWrnFdnvf1kbWq3mWCfE,10 PK»YjGK;fúFFvxsandbox/utils.pyPKŠEºH6eånã ã vvxsandbox/rlimiter.pyPKŠEºHgaó僃Œ vxsandbox/protocol.pyPKLÉHÌð±ììB&vxsandbox/__init__.pyPK‹š¿HftZÆž?ž?a(vxsandbox/worker.pyPKLÉH”Äz‡ Y Y0hvxsandbox/tests/test_worker.pyPKLÉH`ê —ØØyÁvxsandbox/tests/utils.pyPK»YjGÊ"jÄ!! ‡Ævxsandbox/tests/test_protocol.pyPK»YjGæÆvxsandbox/tests/__init__.pyPKŠEºH#™Ç6 6 Çvxsandbox/tests/test_rlimiter.pyPK»YjG˰j[ [ “Ôvxsandbox/tests/test_utils.pyPK»YjG’¹®bú ú )àvxsandbox/resources/utils.pyPKLÉHtÙ¾t)t)]îvxsandbox/resources/http.pyPKLÉHИ–¹'' vxsandbox/resources/metrics.pyPK»YjG93,2± ± m'vxsandbox/resources/logging.pyPKLÉHîƒálÊÊZ5vxsandbox/resources/__init__.pyPK»YjG«ñÝÞÞa7vxsandbox/resources/kv.pyPKLÉH)y˜:à(à(vWvxsandbox/resources/outbound.pyPK»YjG †®k“€vxsandbox/resources/config.pyPKLÉHç{ØDD$ì…vxsandbox/resources/tests/test_kv.pyPKLÉHK•øEJEJ&r¥vxsandbox/resources/tests/test_http.pyPK»YjG