Skip to content

Commit

Permalink
Merge 69a065d into d07da36
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Kliment committed Mar 28, 2015
2 parents d07da36 + 69a065d commit 23fa2b2
Show file tree
Hide file tree
Showing 18 changed files with 1,379 additions and 159 deletions.
10 changes: 10 additions & 0 deletions docs/dredd-class.md
Expand Up @@ -62,6 +62,10 @@ Let's have a look at an example configuration first. (Please also see [options s

'emitter': EventEmitterInstance, // optional - listen to test progress, your own instance of EventEmitter

'hooksData': {
'pathToHook' : '...'
}

'data': {
'path/to/file': '...'
}
Expand Down Expand Up @@ -89,6 +93,12 @@ __Optional__ Object with keys as `filename` and value as `blueprint`-code.
Useful when you don't want to operate on top of filesystem and want to pass
code of your API Blueprints as a string. You get the point.

#### hooksData (Object)

__Optional__ Object with keys as `filename` and strings wirth JavaScript hooks code.

Load hooks file code from string. Must be used with sandbox mode.

```javascript
{
'data': {
Expand Down
88 changes: 88 additions & 0 deletions docs/sandboxed-hooks.md
@@ -0,0 +1,88 @@
# Sandboxed Hooks
Sandboxed hooks cen be used for running untrusted hook code. In each hook file you can use following functions:

`before(transcactionName, function)`

`after(transactionName, function)`

`beforeAll(function)`

`afterAll(function)`

`beforeEach(function)`

`afterEach(function)`


- [Transasction]() object is passed as a first argument to the hook function.
- Sandboxed hooks doesn't have asynchronous API. Loading of hooks and each hook is ran in it's own isolated, sandboxed context.
- Hook maximum execution time is 500ms.
- Memory limit is 1M
- Inside each hook you can access `stash` object variable which is passed between contexts of each hook function execution.
- Hook code is evaluated as `use strict`
- Sandboxed mode does not support CoffeScript hooks


## Examples

## CLI switch

```
$ dredd blueprint.md http://localhost:3000 --hokfiles path/to/hookfile.js --sandbox
```

## JS API

```javascript
Dredd = require('dredd');
configuration = {
server: "http://localhost",
options: {
path: "./test/fixtures/single-get.apib",
sandbox: true,
hookfiles: './test/fixtures/sandboxed-hook.js',
}
};
dredd = new Dredd(configuration);

dred.run(function(error, stats){
// your callback code here
});
```


### Stashing example
```javascript

after('First action', function(transaction){
stash['id'] = JSON.parse(transaction.real.response);
})

before('Second action', funciton(transaction){
newBody = JSON.parse(transaction.request.body);
newBody[id] = stash['id'];
transasction.request.body = JSON.stringify(newBody);
})

```


### Throwing an exception, hook function context is not shared
```javascript
var myObject = {};

after('First action', function(transaction){
myObject['id'] = JSON.parse(transaction.real.response);
})

before('Second action', funciton(transaction){
newBody = JSON.parse(transaction.request.body);
newBody[id] = myObject['id'];
transasction.request.body = JSON.stringify(newBody);
})

```

This will explode with: `ReferenceError: myOjcet is not defined`


1 change: 1 addition & 0 deletions package.json
Expand Up @@ -29,6 +29,7 @@
"markdown-it": "^4.0.1",
"node-uuid": "~1.4.2",
"optimist": "~0.6.1",
"pitboss": "git://github.com/apiaryio/pitboss",
"protagonist": "~0.17.2",
"proxyquire": "^1.3.1",
"request": "^2.53.0",
Expand Down
96 changes: 74 additions & 22 deletions src/add-hooks.coffee
@@ -1,41 +1,93 @@
path = require 'path'

require 'coffee-script/register'
path = require 'path'
proxyquire = require('proxyquire').noCallThru()
glob = require 'glob'
fs = require 'fs'
async = require 'async'

Hooks = require './hooks'
logger = require './logger'
sandboxHooksCode = require './sandbox-hooks-code'
mergeSandboxedHooks = require './merge-sandboxed-hooks'

addHooks = (runner, transactions, emitter, customConfig) ->

hooks = new Hooks()
hooks.transactions ?= {}

addHooks = (runner, transactions, callback) ->

runner.hooks = new Hooks()
runner.hooks.transactions ?= {}

customConfigCwd = runner?.configuration?.custom?.cwd

for transaction in transactions
hooks.transactions[transaction.name] = transaction
runner.hooks.transactions[transaction.name] = transaction

pattern = runner?.configuration?.options?.hookfiles
if pattern
if not pattern
if runner.configuration.hooksData?
if runner.configuration.options.sandbox == true
if typeof(runner.configuration.hooksData) != 'object' or Array.isArray(runner.configuration.hooksData) != false
return callback(new Error("hooksData option must be an object e.g. {'filename.js':'console.log(\"Hey!\")'}"))

# run code in sandbox
async.eachSeries Object.keys(runner.configuration.hooksData), (key, next) ->
data = runner.configuration.hooksData[key]

# run code in sandbox
sandboxHooksCode data, (sandboxError, result) ->
return next(sandboxError) if sandboxError

# merge stringified hooks
runner.hooks = mergeSandboxedHooks(runner.hooks, result)
next()

, callback
else
msg = """
Not sandboxed hooks loading from strings is not implemented,
Sandbox mode must me on for loading hooks from strings"
"""
callback(new Error(msg))
else
return callback()
else
files = glob.sync pattern

logger.info 'Found Hookfiles: ' + files

try
# Running in not sendboxed mode
if not runner.configuration.options.sandbox == true
try
for file in files
proxyquire path.resolve((customConfigCwd or process.cwd()), file), {
'hooks': runner.hooks
}
return callback()
catch error
logger.warn 'Skipping hook loading...'
logger.warn 'Error reading hook files (' + files + ')'
logger.warn 'This probably means one or more of your hookfiles is invalid.'
logger.warn 'Message: ' + error.message if error.message?
logger.warn 'Stack: ' + error.stack if error.stack?
return callback()

# Running in sendboxed mode
else
logger.info 'Loading hookfiles in sandboxed context' + files
for file in files
proxyquire path.resolve((customConfig?.cwd or process.cwd()), file), {
'hooks': hooks
}
catch error
logger.warn 'Skipping hook loading...'
logger.warn 'Error reading hook files (' + files + ')'
logger.warn 'This probably means one or more of your hookfiles is invalid.'
logger.warn 'Message: ' + error.message if error.message?
logger.warn 'Stack: ' + error.stack if error.stack?
return

runner.hooks ?= hooks

return hooks
resolvedPath = path.resolve((customConfigCwd or process.cwd()), file)

# load hook file content
fs.readFile resolvedPath, 'utf8', (readingError, data) ->
return callback readingError if readingError

# run code in sandbox
sandboxHooksCode data, (sandboxError, result) ->
return callback(sandboxError) if sandboxError

runner.hooks = mergeSandboxedHooks(runner.hooks, result)

callback()


module.exports = addHooks
2 changes: 2 additions & 0 deletions src/apply-configuration.coffee
Expand Up @@ -19,6 +19,7 @@ applyConfiguration = (config) ->
blueprintPath: null
server: null
emitter: new EventEmitter
hooksCode: null
custom: { # used for custom settings of various APIs or reporters
# Keep commented-out, so these values are actually set by DreddCommand
# cwd: process.cwd()
Expand All @@ -41,6 +42,7 @@ applyConfiguration = (config) ->
sorted: false
names: false
hookfiles: null
sandbox: false

# normalize options and config
for own key, value of config
Expand Down
36 changes: 30 additions & 6 deletions src/hooks.coffee
@@ -1,6 +1,6 @@
async = require 'async'

# Do not add any functinoality to this class unless you want expose it to the Hooks
# READ THIS! Disclaimer:
# Do not add any functinoality to this class unless you want expose it to the Hooks API
# This class is only an interface for users of Dredd hooks.

class Hooks
Expand All @@ -10,6 +10,8 @@ class Hooks
@transactions = {}
@beforeAllHooks = []
@afterAllHooks = []
@beforeEachHooks = []
@afterEachHooks = []

before: (name, hook) =>
@addHook(@beforeHooks, name, hook)
Expand All @@ -23,16 +25,38 @@ class Hooks
afterAll: (hook) =>
@afterAllHooks.push hook

beforeEach: (hook) =>
@beforeEachHooks.push hook

afterEach: (hook) =>
@afterEachHooks.push hook

addHook: (hooks, name, hook) ->
if hooks[name]
hooks[name].push hook
else
hooks[name] = [hook]

runBeforeAll: (callback) =>
async.series @beforeAllHooks, callback
# This is not part of hooks API
# This is here only because it has to be injected into sandboxed context
dumpHooksFunctionsToStrings: () ->
# prepare JSON friendly object
toReturn = JSON.parse(JSON.stringify(@))

# don't fiddle with transactions, they are not part of sandboxed sync API
delete toReturn['transactions']

hookTargets = Object.keys toReturn
for hookTarget in hookTargets
if Array.isArray @[hookTarget]
for index, hookFunc of @[hookTarget]
toReturn[hookTarget][index] = hookFunc.toString()

else if typeof(@[hookTarget]) == 'object' and not Array.isArray(@[hookTarget])
for transactionName, funcArray of @[hookTarget]
for index, hookFunc of funcArray
toReturn[hookTarget][transactionName][index] = hookFunc.toString()

runAfterAll: (callback) =>
async.series @afterAllHooks, callback
return toReturn

module.exports = Hooks
17 changes: 17 additions & 0 deletions src/merge-sandboxed-hooks.coffee
@@ -0,0 +1,17 @@
clone = require 'clone'

mergeSendboxedHooks = (original, toMerge) ->

newHooks = clone original

for target, functions of toMerge
if Array.isArray functions
newHooks[target] = newHooks[target].concat functions
else if typeof(functions) == "object" and not Array.isArray functions
for transactionName, funcArray of functions
newHooks[target][transactionName] = [] if not newHooks[target][transactionName]
newHooks[target][transactionName] = newHooks[target][transactionName].concat funcArray

return newHooks

module.exports = mergeSendboxedHooks
5 changes: 5 additions & 0 deletions src/options.coffee
Expand Up @@ -9,6 +9,11 @@ options =
description: 'Specifes a pattern to match files with before/after hooks for running tests'
default: null

sandbox:
alias: 'b'
description: "Load and run non trusted hooks code in sandboxed container"
default: false

names:
alias: 'n'
description: 'Only list names of requests (for use in a hookfile). No requests are made.'
Expand Down
33 changes: 33 additions & 0 deletions src/sandbox-hooks-code.coffee
@@ -0,0 +1,33 @@
{Pitboss} = require 'pitboss'
Hooks = require './hooks'

sandboxHooksCode = (hooksCode, callback) ->
hooks = new Hooks
wrappedCode = """
var _hooks = new _Hooks();
var before = _hooks.before;
var after = _hooks.after;
var beforeAll = _hooks.beforeAll;
var afterAll = _hooks.afterAll;
var beforeEach = _hooks.beforeEach;
var afterEach = _hooks.afterEach;
#{hooksCode}
try {
var output = _hooks.dumpHooksFunctionsToStrings()
} catch(e) {
console.log(e.message)
console.log(e.stack)
throw(e)
}
output
"""

pitboss = new Pitboss wrappedCode
pitboss.run {libraries: {"_Hooks": '../../../lib/hooks', "console", "console"}}, (err, result) ->
return callback err if err
callback(undefined, result)

module.exports = sandboxHooksCode

0 comments on commit 23fa2b2

Please sign in to comment.