├── .gitignore ├── .travis.yml ├── LICENSE ├── ReadMe.md ├── bin ├── binaries │ ├── lib │ │ ├── lua │ │ │ └── json.lua │ │ └── python │ │ │ └── microcule │ │ │ └── __init__.py │ ├── micro-bash │ ├── micro-clisp │ ├── micro-gst │ ├── micro-lua │ ├── micro-node │ ├── micro-ocaml │ ├── micro-perl │ ├── micro-php │ ├── micro-python │ ├── micro-python3 │ ├── micro-ruby │ ├── micro-scheme │ └── micro-tcl └── microcule ├── config ├── index.js ├── messages │ ├── README.md │ ├── childProcessSpawnError.js │ └── serviceExecutionTimeout.js └── ssl │ ├── ca-crt.pem │ ├── server-crt.pem │ └── server-key.pem ├── examples ├── ReadMe.md ├── express-any-binary.js ├── express-basicAuth-services.js ├── express-chain-services.js ├── express-compiled-languages.js ├── express-jail-chroot.js ├── express-jail-nsjail.js ├── express-multi-language.js ├── express-plugins.js ├── express-python.js ├── express-service-view.js ├── express-simple.js ├── express-source-github-gist.js ├── express-source-github-repo.js ├── http-server-simple.js └── services │ ├── ReadMe.md │ ├── echo │ ├── echo-es6-async.js │ ├── echo-py3.py │ ├── echo-stdin.js │ ├── echo-wsgi.py │ ├── echo.c │ ├── echo.coffee │ ├── echo.go │ ├── echo.js │ ├── echo.lisp │ ├── echo.lua │ ├── echo.ml │ ├── echo.php │ ├── echo.pl │ ├── echo.py │ ├── echo.r │ ├── echo.rb │ ├── echo.rs │ ├── echo.sh │ ├── echo.ss │ ├── echo.st │ ├── echo.tcl │ └── echoOld.js │ ├── hello-world │ ├── hello-wsgi.py │ ├── hello.c │ ├── hello.go │ ├── hello.java │ ├── hello.lua │ ├── hello.py │ ├── hello.r │ └── hello.rs │ ├── image │ └── index.js │ ├── middlewares │ └── basic-auth.js │ └── streams │ ├── echo.js │ ├── hello.js │ └── transform.js ├── index.js ├── json.lua ├── lib ├── plugins │ ├── Store.js │ ├── bodyParser.js │ ├── compile │ │ ├── compileServiceCode │ │ │ ├── gcc │ │ │ │ └── index.js │ │ │ ├── golang │ │ │ │ └── index.js │ │ │ ├── java │ │ │ │ └── index.js │ │ │ ├── r │ │ │ │ └── index.js │ │ │ └── rust │ │ │ │ └── index.js │ │ ├── compileServiceMappings.js │ │ └── index.js │ ├── logger.js │ ├── mschema.js │ ├── rateLimiter │ │ └── index.js │ ├── sourceGithubGist.js │ ├── sourceGithubRepo.js │ └── spawn │ │ ├── fd3 │ │ ├── index.js │ │ └── responseMethods.js │ │ ├── generateCommandLineArguments │ │ ├── bash │ │ │ └── index.js │ │ ├── clisp │ │ │ └── index.js │ │ ├── gcc │ │ │ └── index.js │ │ ├── golang │ │ │ └── index.js │ │ ├── lua │ │ │ └── index.js │ │ ├── ocaml │ │ │ └── index.js │ │ ├── perl │ │ │ └── index.js │ │ ├── php │ │ │ └── index.js │ │ ├── python │ │ │ └── index.js │ │ ├── r │ │ │ └── index.js │ │ ├── ruby │ │ │ └── index.js │ │ ├── rust │ │ │ └── index.js │ │ ├── scheme │ │ │ └── index.js │ │ ├── smalltalk │ │ │ └── index.js │ │ └── tcl │ │ │ └── index.js │ │ ├── index.js │ │ └── transpileServiceCode │ │ ├── ReadMe.md │ │ └── coffee-script │ │ └── index.js ├── requireService.js ├── requireServiceSync.js └── viewPresenter.js ├── package.json ├── release └── ReadMe.md ├── test ├── all-languages-tests.js ├── basic-tests.js ├── custom-headers-test.js ├── fixtures │ ├── assets │ │ └── file.txt │ └── invalid-services │ │ ├── ReadMe.md │ │ ├── missing-command.sh │ │ ├── missing-exports.js │ │ ├── never-responds.js │ │ ├── require-error.js │ │ ├── syntax-error.js │ │ └── writes-bad-headers.js ├── invalid-service-test.js ├── plugin-test.js ├── rate-limit-test.js ├── request-large-json-test.js ├── request-multipart-test.js ├── request-params-test.js ├── response-methods-test.js └── service-as-middleware-tests.js └── tmp └── ReadMe.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | reports/ 4 | __pycache__/ 5 | *.pyc 6 | *.pyo 7 | .nyc_output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | 6 | # safelist 7 | branches: 8 | only: 9 | - master 10 | 11 | script: 12 | - "npm test" 13 | 14 | os: 15 | - linux 16 | - osx 17 | - windows -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | microcule - Copyright (c) 2016 Marak Squires 2 | http://github.com/stackvana/microcule 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bin/binaries/lib/lua/json.lua: -------------------------------------------------------------------------------- 1 | --[[ json.lua 2 | 3 | A compact pure-Lua JSON library. 4 | The main functions are: json.stringify, json.parse. 5 | 6 | ## json.stringify: 7 | 8 | This expects the following to be true of any tables being encoded: 9 | * They only have string or number keys. Number keys must be represented as 10 | strings in json; this is part of the json spec. 11 | * They are not recursive. Such a structure cannot be specified in json. 12 | 13 | A Lua table is considered to be an array if and only if its set of keys is a 14 | consecutive sequence of positive integers starting at 1. Arrays are encoded like 15 | so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json 16 | object, encoded like so: `{"key1": 2, "key2": false}`. 17 | 18 | Because the Lua nil value cannot be a key, and as a table value is considerd 19 | equivalent to a missing key, there is no way to express the json "null" value in 20 | a Lua table. The only way this will output "null" is if your entire input obj is 21 | nil itself. 22 | 23 | An empty Lua table, {}, could be considered either a json object or array - 24 | it's an ambiguous edge case. We choose to treat this as an object as it is the 25 | more general type. 26 | 27 | To be clear, none of the above considerations is a limitation of this code. 28 | Rather, it is what we get when we completely observe the json specification for 29 | as arbitrary a Lua object as json is capable of expressing. 30 | 31 | ## json.parse: 32 | 33 | This function parses json, with the exception that it does not pay attention to 34 | \u-escaped unicode code points in strings. 35 | 36 | It is difficult for Lua to return null as a value. In order to prevent the loss 37 | of keys with a null value in a json string, this function uses the one-off 38 | table value json.null (which is just an empty table) to indicate null values. 39 | This way you can check if a value is null with the conditional 40 | `val == json.null`. 41 | 42 | If you have control over the data and are using Lua, I would recommend just 43 | avoiding null values in your data to begin with. 44 | 45 | --]] 46 | 47 | 48 | local json = {} 49 | 50 | 51 | -- Internal functions. 52 | 53 | local function kind_of(obj) 54 | if type(obj) ~= 'table' then return type(obj) end 55 | local i = 1 56 | for _ in pairs(obj) do 57 | if obj[i] ~= nil then i = i + 1 else return 'table' end 58 | end 59 | if i == 1 then return 'table' else return 'array' end 60 | end 61 | 62 | local function escape_str(s) 63 | local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} 64 | local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} 65 | for i, c in ipairs(in_char) do 66 | s = s:gsub(c, '\\' .. out_char[i]) 67 | end 68 | return s 69 | end 70 | 71 | -- Returns pos, did_find; there are two cases: 72 | -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. 73 | -- 2. Delimiter not found: pos = pos after leading space; did_find = false. 74 | -- This throws an error if err_if_missing is true and the delim is not found. 75 | local function skip_delim(str, pos, delim, err_if_missing) 76 | pos = pos + #str:match('^%s*', pos) 77 | if str:sub(pos, pos) ~= delim then 78 | if err_if_missing then 79 | error('Expected ' .. delim .. ' near position ' .. pos) 80 | end 81 | return pos, false 82 | end 83 | return pos + 1, true 84 | end 85 | 86 | -- Expects the given pos to be the first character after the opening quote. 87 | -- Returns val, pos; the returned pos is after the closing quote character. 88 | local function parse_str_val(str, pos, val) 89 | val = val or '' 90 | local early_end_error = 'End of input found while parsing string.' 91 | if pos > #str then error(early_end_error) end 92 | local c = str:sub(pos, pos) 93 | if c == '"' then return val, pos + 1 end 94 | if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end 95 | -- We must have a \ character. 96 | local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} 97 | local nextc = str:sub(pos + 1, pos + 1) 98 | if not nextc then error(early_end_error) end 99 | return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) 100 | end 101 | 102 | -- Returns val, pos; the returned pos is after the number's final character. 103 | local function parse_num_val(str, pos) 104 | local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) 105 | local val = tonumber(num_str) 106 | if not val then error('Error parsing number at position ' .. pos .. '.') end 107 | return val, pos + #num_str 108 | end 109 | 110 | 111 | -- Public values and functions. 112 | 113 | function json.stringify(obj, as_key) 114 | local s = {} -- We'll build the string as an array of strings to be concatenated. 115 | local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. 116 | if kind == 'array' then 117 | if as_key then error('Can\'t encode array as key.') end 118 | s[#s + 1] = '[' 119 | for i, val in ipairs(obj) do 120 | if i > 1 then s[#s + 1] = ', ' end 121 | s[#s + 1] = json.stringify(val) 122 | end 123 | s[#s + 1] = ']' 124 | elseif kind == 'table' then 125 | if as_key then error('Can\'t encode table as key.') end 126 | s[#s + 1] = '{' 127 | for k, v in pairs(obj) do 128 | if #s > 1 then s[#s + 1] = ', ' end 129 | s[#s + 1] = json.stringify(k, true) 130 | s[#s + 1] = ':' 131 | s[#s + 1] = json.stringify(v) 132 | end 133 | s[#s + 1] = '}' 134 | elseif kind == 'string' then 135 | return '"' .. escape_str(obj) .. '"' 136 | elseif kind == 'number' then 137 | if as_key then return '"' .. tostring(obj) .. '"' end 138 | return tostring(obj) 139 | elseif kind == 'boolean' then 140 | return tostring(obj) 141 | elseif kind == 'nil' then 142 | return 'null' 143 | else 144 | error('Unjsonifiable type: ' .. kind .. '.') 145 | end 146 | return table.concat(s) 147 | end 148 | 149 | json.null = {} -- This is a one-off table to represent the null value. 150 | 151 | function json.parse(str, pos, end_delim) 152 | pos = pos or 1 153 | if pos > #str then error('Reached unexpected end of input.') end 154 | local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. 155 | local first = str:sub(pos, pos) 156 | if first == '{' then -- Parse an object. 157 | local obj, key, delim_found = {}, true, true 158 | pos = pos + 1 159 | while true do 160 | key, pos = json.parse(str, pos, '}') 161 | if key == nil then return obj, pos end 162 | if not delim_found then error('Comma missing between object items.') end 163 | pos = skip_delim(str, pos, ':', true) -- true -> error if missing. 164 | obj[key], pos = json.parse(str, pos) 165 | pos, delim_found = skip_delim(str, pos, ',') 166 | end 167 | elseif first == '[' then -- Parse an array. 168 | local arr, val, delim_found = {}, true, true 169 | pos = pos + 1 170 | while true do 171 | val, pos = json.parse(str, pos, ']') 172 | if val == nil then return arr, pos end 173 | if not delim_found then error('Comma missing between array items.') end 174 | arr[#arr + 1] = val 175 | pos, delim_found = skip_delim(str, pos, ',') 176 | end 177 | elseif first == '"' then -- Parse a string. 178 | return parse_str_val(str, pos + 1) 179 | elseif first == '-' or first:match('%d') then -- Parse a number. 180 | return parse_num_val(str, pos) 181 | elseif first == end_delim then -- End of an object or array. 182 | return nil, pos + 1 183 | else -- Parse true, false, or null. 184 | local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} 185 | for lit_str, lit_val in pairs(literals) do 186 | local lit_end = pos + #lit_str - 1 187 | if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end 188 | end 189 | local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) 190 | error('Invalid json syntax starting at ' .. pos_info_str) 191 | end 192 | end 193 | 194 | return json -------------------------------------------------------------------------------- /bin/binaries/lib/python/microcule/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import logging 4 | import traceback 5 | import pkg_resources 6 | import wsgiref.handlers 7 | import os 8 | 9 | # open incoming connection from fd3 10 | if sys.version_info[0] < 3: 11 | fd3 = os.fdopen(3, 'w+') 12 | else: 13 | fd3 = os.fdopen(3, 'wb+', buffering=0) 14 | 15 | class FullMicroculeJSONFormatter(logging.Formatter): 16 | def format(self, record): 17 | record.message = record.getMessage() 18 | if record.exc_info and not record.exc_text: 19 | record.exc_text = self.formatException(record.exc_info) 20 | record = record.__dict__.copy() 21 | record['exc_info'] = None 22 | try: 23 | json.dumps(record['args']) 24 | except Exception: 25 | del record['args'] 26 | record['msg'] = record['message'] 27 | res = {'type': 'log', 'payload': {'entry': record}} 28 | return json.dumps(res) 29 | 30 | 31 | class SimpleMicroculeJSONFormatter(logging.Formatter): 32 | def format(self, record): 33 | msg = logging.Formatter.format(self, record) 34 | res = {'type': 'log', 'payload': {'entry': msg}} 35 | return json.dumps(res) 36 | 37 | 38 | class MicroculeExceptHook: 39 | def __init__(self, display=1, verbose=1): 40 | self.display = display 41 | self.verbose = verbose 42 | 43 | def __call__(self, etype, evalue, etb): 44 | self.handle((etype, evalue, etb)) 45 | 46 | def handle(self, info=None): 47 | self.send_exception(info) 48 | sys.exit(1) 49 | 50 | def send_exception(self, info=None): 51 | info = info or sys.exc_info() 52 | code = info[0].__name__ 53 | if getattr(info[0], '__module__ ', None): 54 | code = info[0].__module__ + '.' + code 55 | payload = {'code': code} 56 | if hasattr(info[1], 'args'): 57 | payload['args'] = repr(info[1].args) 58 | if self.verbose: 59 | payload['error'] = ''.join(traceback.format_exception(*info)) 60 | else: 61 | payload['error'] = str(info[1]) 62 | res = {'type': 'error', 'payload': payload} 63 | if isinstance(info[1], ImportError) and info[1].message.startswith('No module named '): 64 | payload['code'] = 'MODULE_NOT_FOUND' 65 | payload['module'] = info[1].message.replace('No module named ', '') 66 | if isinstance(info[1], (pkg_resources.VersionConflict, pkg_resources.DistributionNotFound)): 67 | req = None 68 | try: 69 | if hasattr(info[1], 'req'): 70 | req = info[1].req 71 | # This is check for compatibility with old version of setuptools 72 | if req is None: 73 | for arg in info[1].args: 74 | if isinstance(arg, pkg_resources.Requirement): 75 | req = arg 76 | except BaseException: 77 | # unable to parse exception to requirement - it's ok 78 | pass 79 | if req is not None: 80 | payload['code'] = 'MODULE_NOT_FOUND' 81 | payload['module'] = str(req) 82 | error = '%s(%s): %s' % (payload['code'], code, str(info[1])) 83 | if self.verbose: 84 | payload['error'] += error 85 | else: 86 | payload['error'] = error 87 | 88 | 89 | fd3.write(json.dumps(res)+'\n') 90 | fd3.flush() 91 | fd3.write(json.dumps({'type': 'statusCode', 'payload': {'value': 500}})+'\n') 92 | fd3.flush() 93 | 94 | if self.display: 95 | sys.stdout.write(payload['error'].rstrip('\n')+'\n') 96 | sys.stdout.flush() 97 | fd3.write(json.dumps({'type': 'end'})+'\n') 98 | fd3.flush() 99 | 100 | 101 | class wsgi(wsgiref.handlers.CGIHandler): 102 | def __init__(self, Hook=None): 103 | self.Hook = Hook or getattr(sys.modules.get('__main__',sys), 'Hook', None) 104 | wsgiref.handlers.BaseCGIHandler.__init__( 105 | self, sys.stdin, sys.stdout, sys.stderr, {}, 106 | multithread=False, multiprocess=True 107 | ) 108 | 109 | def send_headers(self): 110 | self.cleanup_headers() 111 | self.headers_sent = True 112 | # remark: the status code needs to be sent to the parent process as an 3 digit integer value, not a string value with label 113 | # todo: make parse int code for status more robust. 114 | head = {'type': 'writeHead', 'payload': {'code': int(self.status[:3]), 'headers': dict(self.headers)}} 115 | fd3.write(json.dumps(head)+'\n') 116 | fd3.flush() 117 | 118 | def add_cgi_vars(self): 119 | #assert not Hook['isStreaming'], 'Streaming hooks not yet supported by WSGI gateway' 120 | self.environ.update(self.Hook['env']) 121 | if 'hookAccessKey' in self.Hook: 122 | self.environ['hook_private_key'] = self.Hook['hookAccessKey'] 123 | self.environ['SERVER_NAME'] = self.Hook['req']['host'] 124 | self.environ['GATEWAY_INTERFACE'] = 'CGI/1.1' 125 | self.environ['SERVER_PORT'] = '443' 126 | self.environ['REMOTE_ADDR'] = self.environ['REMOTE_HOST'] = self.Hook['req']['connection']['remoteAddress'] 127 | self.environ['CONTENT_LENGTH'] = '' 128 | self.environ['SCRIPT_NAME'] = '' 129 | self.environ['SERVER_PROTOCOL'] = 'HTTP/1.0' 130 | self.environ['REQUEST_METHOD'] = self.Hook['req']['method'] 131 | self.environ['PATH_INFO'] = self.Hook['req']['path'] 132 | self.environ['QUERY_STRING'] = self.Hook['req']['url'][len(self.Hook['req']['path'])+1:] 133 | self.environ['CONTENT_TYPE'] = self.Hook['req']['headers'].get('content-type', '') 134 | if 'content-length' in self.Hook['req']['headers']: 135 | self.environ['CONTENT_LENGTH'] = self.Hook['req']['headers']['content-length'] 136 | for k,v in self.Hook['req']['headers'].items(): 137 | k = k.replace('-', '_').upper() 138 | v = v.strip() 139 | if k in self.environ: 140 | continue 141 | self.environ['HTTP_'+k] = v 142 | if self.Hook.get('isHookio', False): 143 | hookname = '/%(owner)s/%(hook)s/' % self.Hook['req']['params'] 144 | assert (self.Hook['req']['path']+'/').startswith(hookname) 145 | wsgiref.util.shift_path_info(self.environ) # user 146 | wsgiref.util.shift_path_info(self.environ) # hook 147 | 148 | 149 | def hookioLoggingConfig(level=None, format=None, datefmt=None): 150 | logging._acquireLock() 151 | try: 152 | if level is not None: 153 | logging.root.setLevel(level) 154 | if len(logging.root.handlers) > 0: 155 | return 156 | hdlr = logging.StreamHandler(sys.stderr) 157 | if format is None: 158 | fmt = FullMicroculeJSONFormatter() 159 | else: 160 | fmt = SimpleMicroculeJSONFormatter(format, datefmt) 161 | hdlr.setFormatter(fmt) 162 | logging.root.addHandler(hdlr) 163 | finally: 164 | logging._releaseLock() 165 | 166 | 167 | def parse_argv(argv=None): 168 | argv = argv or sys.argv 169 | 170 | # print(argv[0]) # binary 171 | # print(argv[1]) # -c 172 | # print(argv[2]) # the code 173 | code = argv[2] 174 | 175 | # print(argv[3]) # -e 176 | # print(argv[4]) # the env 177 | envJSON = argv[4] 178 | 179 | Hook = json.loads(envJSON) 180 | #print(Hook.keys()) 181 | #print(Hook['resource'].keys()) 182 | Hook['req'] = Hook['input'] 183 | # TODO: full mapping of http including streaming / multipart streaming 184 | 185 | # print(argv[5]) # -s 186 | # print(argv[6]) # the service 187 | # TODO: do something with service variable 188 | service = argv[6] 189 | 190 | debug_output = 'gateway' in Hook['resource'].get('name', 'gateway') 191 | prod_mode = Hook['resource'].get('mode', 'Debug') == 'Production' 192 | #if debug_output: 193 | # sys.stderr = sys.stdout 194 | 195 | level = [logging.DEBUG, logging.INFO][prod_mode] 196 | hookioLoggingConfig(level) 197 | sys.excepthook = MicroculeExceptHook(debug_output, not prod_mode) 198 | 199 | return code, service, Hook 200 | -------------------------------------------------------------------------------- /bin/binaries/micro-bash: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Note: Using Bash version 4 or higher will give access to bash associative arrays 4 | # Bash 3 version is used by default. Updating the configuration file is required for bash 4 functionality 5 | # 6 | 7 | # basic support for bash scripts 8 | 9 | # TODO: replace with better argument parsing function 10 | _CODE=$2 11 | _INJECT=$6 12 | 13 | # TODO: do something with $_ENV 14 | # TODO: do something with $_SERVICE 15 | 16 | # echo "$_INJECT" 17 | # echo "$_CODE" 18 | eval "$_INJECT" 19 | eval "$_CODE" -------------------------------------------------------------------------------- /bin/binaries/micro-clisp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env clisp 2 | ; TODO: better handling of null / empty lines ( end of file ) 3 | (defun read-objects-from-string (str) 4 | (loop :for i :from 0 5 | :while (< i (length str)) 6 | :collect (multiple-value-bind (object offset) 7 | (read-from-string str t nil :start i) 8 | (setf i (- offset 1)) 9 | object))) 10 | 11 | ; sixth cli argument, -e ( env ) 12 | (mapcar #'eval (read-objects-from-string (sixth *args*))) 13 | 14 | ; second cli argument, -c ( code ) 15 | (mapcar #'eval (read-objects-from-string (second *args*))) -------------------------------------------------------------------------------- /bin/binaries/micro-gst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env gst 2 | 3 | array := Array new: 7. 4 | 5 | (1 to: Smalltalk getArgc) do: [ :i | 6 | array at: i put: (Smalltalk getArgv: i) 7 | ] 8 | 9 | "(array at: 1) displayNl" 10 | "(array at: 2) displayNl" 11 | "(array at: 3) displayNl" 12 | "(array at: 4) displayNl" 13 | "(array at: 5) displayNl" 14 | "(array at: 6) displayNl" 15 | "(array at: 7) displayNl" 16 | 17 | (Behavior evaluate: (array at: 3)) -------------------------------------------------------------------------------- /bin/binaries/micro-lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- TODO: do something with -service 4 | code = arg[2] 5 | inject = (arg[6]) 6 | 7 | -- print (code) 8 | -- print (inject) 9 | 10 | -- TODO: figure out a way to require the json.lua file from ./bin/binaries/lib/micro-lua 11 | -- local handle = io.popen("pwd") 12 | -- local __dirname = handle:read("*a") 13 | -- handle:close() 14 | -- __dirname = string.gsub(__dirname, "\n", "") -- remove line breaks 15 | -- local h = __dirname .. "/bin/binaries/lib/lua/json" 16 | -- print(h) 17 | 18 | local json = require('json') 19 | 20 | function log(...) 21 | io.stderr:write('{ "type": "log", "payload": { "entry": ' .. json.stringify(arg, false) .. '}}') 22 | end 23 | 24 | local injectVars = assert(loadstring(inject)) 25 | injectVars() 26 | 27 | local func = assert(loadstring(code)) 28 | func() 29 | -------------------------------------------------------------------------------- /bin/binaries/micro-node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Spawns a service as a child process 4 | 5 | Takes in a JavaScript microservice as a string over argv, 6 | runs it using `run-service` module, and pipes results to STDOUT 7 | 8 | **Note: This script is intended to be run inside a chroot jail** 9 | 10 | Microservices are expected to take in two streams ( input / output ) 11 | 12 | In most cases these input / output streams will be http req / res. 13 | 14 | HTTP Request Input <=> process.stdin 15 | HTTP Response Output <=> process.stdout 16 | 17 | Arguments: 18 | 19 | -c - The source code of the service 20 | -s - Meta-data about the service 21 | -e - The environment config to run the service in 22 | -v - The virtual machine config to run the service in 23 | 24 | */ 25 | 26 | // TODO: make DNS punch configurable ( useful for dev ) 27 | // Punch DNS lookup, because copying BIND and system resolver into the chroot is much files to copy, 28 | // this uses node's built-in resolver ( perhaps c-ares ? ) 29 | //var _dns=require('dns'); 30 | //var dns = require('native-dns'); 31 | //var net=require('net'); 32 | //_dns.lookup = dns.lookup; 33 | 34 | // parse incoming command line arguments 35 | var argv = require('minimist')(process.argv.slice(2)); 36 | var net = require('net'); 37 | 38 | if (typeof argv.c === "undefined" || argv.c.length === 0) { 39 | console.log('Source code required. Please pass in -c option'); 40 | process.exit(); 41 | } 42 | 43 | var code = argv.c; 44 | var service = argv.s; 45 | 46 | try { 47 | service = JSON.parse(service); 48 | } catch (err) { 49 | service = { 50 | "owner": "error" 51 | }; 52 | } 53 | 54 | 55 | // pipe3 is an additional STDIO pipe we can use for HTTP response methods, debugging events, logging events, and other out of band comms 56 | var pipe3 = new net.Socket({ fd: 3 }); 57 | 58 | var rs = require('run-service'); 59 | 60 | // create a new writable stream to wrap / handle STDOUT 61 | var Writable = require('stream').Writable; 62 | var output = Writable(); 63 | 64 | // anytime output is written to, write the result to the command line 65 | output._write = function (chunk, enc, next) { 66 | //console.log('writing chunk', chunk) 67 | process.stdout.write(chunk); 68 | // console.log(chunk); // TODO: remove string? 69 | next(); 70 | }; 71 | 72 | // TODO: move response mocking code to separate module 73 | // use STDERR as a channel for sending out of stream information ( like setting headers ) 74 | output.addTrailers = function (headers) { 75 | var message = { 76 | "type": "addTrailers", 77 | "payload": { 78 | "headers": headers 79 | } 80 | }; 81 | pipe3.write(JSON.stringify(message) + '\n'); 82 | }; 83 | 84 | output.removeHeader = function (name) { 85 | var message = { 86 | "type": "removeHeader", 87 | "payload": { 88 | "name": name 89 | } 90 | }; 91 | pipe3.write(JSON.stringify(message) + '\n'); 92 | }; 93 | 94 | output.setHeader = function (name, value) { 95 | var message = { 96 | "type": "setHeader", 97 | "payload": { 98 | "name": name, 99 | "value": value 100 | } 101 | }; 102 | pipe3.write(JSON.stringify(message) + '\n'); 103 | }; 104 | 105 | output.setTimeout = function (msecs, cb) { 106 | // TODO: add optional callback argument? 107 | var message = { 108 | "type": "setTimeout", 109 | "payload": { 110 | "msecs": msecs 111 | } 112 | }; 113 | pipe3.write(JSON.stringify(message) + '\n'); 114 | }; 115 | 116 | output.sendDate = function (value) { 117 | var message = { 118 | "type": "sendDate", 119 | "payload": { 120 | "value": value 121 | } 122 | }; 123 | pipe3.write(JSON.stringify(message) + '\n'); 124 | }; 125 | 126 | 127 | output.status = function (value) { 128 | var message = { 129 | "type": "status", 130 | "payload": { 131 | "value": value 132 | } 133 | }; 134 | pipe3.write(JSON.stringify(message) + '\n'); 135 | }; 136 | 137 | 138 | output.statusMessage = function (value) { 139 | var message = { 140 | "type": "statusMessage", 141 | "payload": { 142 | "value": value 143 | } 144 | }; 145 | pipe3.write(JSON.stringify(message) + '\n'); 146 | }; 147 | 148 | // Using Object.defineProperty 149 | Object.defineProperty(output, 'statusCode', { 150 | set: function(value) { 151 | var message = { 152 | "type": "statusCode", 153 | "payload": { 154 | "value": value 155 | } 156 | }; 157 | pipe3.write(JSON.stringify(message) + '\n'); 158 | } 159 | }); 160 | 161 | output.writeContinue = function () { 162 | var message = { 163 | "type": "writeContinue", 164 | "payload": { 165 | } 166 | }; 167 | pipe3.write(JSON.stringify(message) + '\n'); 168 | }; 169 | 170 | output.writeHead = function (code, headers) { 171 | var message = { 172 | "type": "writeHead", 173 | "payload": { 174 | "code": code, 175 | "headers": headers 176 | } 177 | }; 178 | pipe3.write(JSON.stringify(message) + '\n'); 179 | }; 180 | 181 | // Capture any stream errors 182 | output.on('error', function (err) { 183 | pipe3.write(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } }) + '\n'); 184 | process.exit(); 185 | }); 186 | 187 | 188 | // When .json is called, exit the process with a JSON dump 189 | output.json = function json (data) { 190 | if (typeof data !== 'undefined') { 191 | console.log(JSON.stringify(data, true, 2)); 192 | } 193 | pipe3.write(JSON.stringify({ type: "end" }) + '\n'); 194 | process.exit(); 195 | }; 196 | 197 | // When the response has been ended, exit the process 198 | output.end = function end (data) { 199 | if (typeof data !== 'undefined') { 200 | console.log(data); 201 | } 202 | pipe3.write(JSON.stringify({ type: "end" }) + '\n'); 203 | process.exit(); 204 | }; 205 | 206 | // Custom errorHandler for `run-service` execution 207 | function errorHandler (err) { 208 | if (err) { 209 | pipe3.write(JSON.stringify({ type: "error", payload: { error: err.message, code: err.code } }) + '\n'); 210 | process.exit(1); 211 | } 212 | }; 213 | 214 | // parse the incoming service env from argv 215 | var env = JSON.parse(argv.e); 216 | 217 | /* TODO: add ability to proxy request parameters back to middleware chain 218 | //console.log(process.stdin) 219 | for (var p in env.input) { 220 | process.stdin[p] = env.input[p]; 221 | } 222 | //console.log(Object.keys(env.input)) 223 | if (typeof env.input.xxxx !== 'undefined') { 224 | //process.stdin.xxxx = env.input.xxxx; 225 | } 226 | // process.stdin.env = {}; 227 | 228 | */ 229 | 230 | // Map some familiar HTTP request information to input stream ( services need this data ) 231 | process.stdin.method = env.input.method; 232 | process.stdin.path = env.input.path; 233 | process.stdin.params = env.input.params; 234 | process.stdin.headers = env.input.headers; 235 | process.stdin.host = env.input.host; 236 | process.stdin.url = env.input.url; 237 | process.stdin.connection = env.input.connection; 238 | process.stdin.resource = { params: {}}; 239 | 240 | // Send logs to stderr as JSON message 241 | var debug = function debug () { 242 | var args = []; 243 | for (var a in arguments) { 244 | args.push(arguments[a]); 245 | } 246 | if (args.length === 1) { 247 | args = args[0]; 248 | } 249 | pipe3.write(JSON.stringify({ type: "log", payload: { entry: args } }) + '\n'); 250 | return; 251 | }; 252 | 253 | var serviceEnv = {}; 254 | 255 | // Remark: serviceEnv object is used to populate the microservice object passed into your code 256 | // You may customize this file as you wish 257 | // I've added an if statement which hook.io uses for additional SDK and microservice helper methods 258 | 259 | // Note: This block of code is unused by default 260 | if (env.isHookio) { 261 | 262 | var sdk = require('hook.io-sdk'); 263 | var clientConfig = { 264 | host: "hook.io", 265 | port: 443, 266 | protocol: "https://", 267 | uri: "https://hook.io", 268 | hook_private_key: env.hookAccessKey 269 | } 270 | var client = sdk.createClient(clientConfig); 271 | serviceEnv.client = client; 272 | serviceEnv.datastore = client.datastore; 273 | serviceEnv.keys = client.keys; 274 | serviceEnv.fs = client.files; 275 | serviceEnv.logs = client.logs; 276 | serviceEnv.sdk = client; 277 | } 278 | 279 | function _require (module) { 280 | // console.log(module, require.paths, process.cwd()) 281 | // Note: This seems wrong, but we want to require from the path microcule binary is running at 282 | // In other cases, we may want to instead fallback to normal node.js lookups ( which are looking for modules in *this* project directory ) 283 | 284 | // first look for module in current working directory 285 | try { 286 | return require(process.cwd() + '/node_modules/' + module); 287 | } catch (err) { 288 | 289 | } 290 | // if that fails, fallback to normal node.js lookups 291 | return require(module); 292 | } 293 | 294 | // Note: psr has been added back to child process, may be required for streaming multipart uploads 295 | // TODO: Add tests for multipart file uploads ( may not be working ) 296 | var psr = require('parse-service-request'); 297 | 298 | psr(process.stdin, output, function (req, res, fields) { 299 | 300 | // re-map any potential resource params that have been extracted from the request 301 | for (var p in req.resource.params) { 302 | env.params[p] = req.resource.params[p]; 303 | } 304 | process.stdin.body = env.params; 305 | /* TODO: add ability to proxy request parameters to middleware chain 306 | var proxy = new Proxy(process.stdin, { 307 | get: function(target, name) { 308 | // console.log("Getting pproperty '" + name + "'", env.input[name]); 309 | if (!(name in target)) { 310 | //console.log("Getting non-existant property '" + name + "'"); 311 | return undefined; 312 | } 313 | return target[name]; 314 | }, 315 | set: function(target, name, value) { 316 | //console.log("Setting property '" + name + "', initial value: " + value); 317 | if (!(name in target)) { 318 | // console.log("Setting non-existant property '" + name + "', initial value: " + value); 319 | pipe3.write(JSON.stringify({ type: "setvar", payload: { key: name, value: value } })); 320 | } 321 | target[name] = value; 322 | return true; 323 | } 324 | }); 325 | */ 326 | 327 | /* TODO: add ability to send proxied params back to parent process 328 | // sets key value on input stream ( useful for middleware processing later ) 329 | process.stdin.set = function (key, value) { 330 | pipe3.write(JSON.stringify({ type: "setvar", payload: { key: key, value: value } })); 331 | }; 332 | */ 333 | 334 | // serviceEnv.req = proxy; 335 | serviceEnv.req = process.stdin; 336 | serviceEnv.res = output; 337 | serviceEnv.env = env.env; 338 | serviceEnv.params = env.params; 339 | serviceEnv.resource = env.resource; 340 | serviceEnv.isStreaming = env.isStreaming; 341 | serviceEnv.customTimeout = env.customTimeout; 342 | serviceEnv.debug = debug; 343 | 344 | rs({ 345 | service: code, 346 | env: serviceEnv, 347 | vm: { 348 | console: { 349 | log: debug, 350 | error: debug 351 | }, 352 | setTimeout: setTimeout, 353 | __dirname: __dirname, 354 | require: _require, 355 | Buffer: Buffer 356 | }, 357 | errorHandler: errorHandler 358 | })(function (err, result) { 359 | // callback / promise API 360 | if (err) { 361 | errorHandler(err); 362 | } 363 | // Note: Do not write undefined output 364 | if (result) { 365 | output.write(result); 366 | } 367 | // Remark: We are no longed calling output.end() and instead we exit the process 368 | // This should allow for services which don't explictly end requests ( like middlewares ) 369 | process.exit(); 370 | }); 371 | }); -------------------------------------------------------------------------------- /bin/binaries/micro-ocaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # easiest way to implement ocaml seems to be piping the source code directly into stdin of utop 3 | echo $2 | utop -stdin -------------------------------------------------------------------------------- /bin/binaries/micro-perl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | $code = @ARGV[1]; 3 | 4 | # TODO: map $env to $Hook 5 | # TODO: perl json_decode to decode incoming env vars 6 | $env = @ARGV[3]; 7 | 8 | # TODO: do something with $service 9 | # $service = @ARGV[5]; 10 | 11 | $inject = @ARGV[5]; 12 | eval $inject; 13 | 14 | # TODO: better error handling in user-code 15 | eval $code; -------------------------------------------------------------------------------- /bin/binaries/micro-php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | 11 | * @see https://github.com/pwfisher/CommandLine.php 12 | */ 13 | function parseArgs($argv = null) { 14 | $argv = $argv ? $argv : $_SERVER['argv']; array_shift($argv); $o = array(); 15 | for ($i = 0, $j = count($argv); $i < $j; $i++) { $a = $argv[$i]; 16 | if (substr($a, 0, 2) == '--') { $eq = strpos($a, '='); 17 | if ($eq !== false) { $o[substr($a, 2, $eq - 2)] = substr($a, $eq + 1); } 18 | else { $k = substr($a, 2); 19 | if ($i + 1 < $j && $argv[$i + 1][0] !== '-') { $o[$k] = $argv[$i + 1]; $i++; } 20 | else if (!isset($o[$k])) { $o[$k] = true; } } } 21 | else if (substr($a, 0, 1) == '-') { 22 | if (substr($a, 2, 1) == '=') { $o[substr($a, 1, 1)] = substr($a, 3); } 23 | else { 24 | foreach (str_split(substr($a, 1)) as $k) { if (!isset($o[$k])) { $o[$k] = true; } } 25 | if ($i + 1 < $j && $argv[$i + 1][0] !== '-') { $o[$k] = $argv[$i + 1]; $i++; } } } 26 | else { $o[] = $a; } } 27 | return $o; 28 | } 29 | 30 | $args = parseArgs($_SERVER['argv']); 31 | 32 | // code 33 | $code = $args["c"]; 34 | 35 | // env 36 | $env = $args["e"]; 37 | 38 | // prepend $Hook variable code to user's service 39 | // this variable provides data such as $Hook["params"] 40 | $code = "$" . "Hook=" . var_export(json_decode($env, true), true) . ";" . $code; 41 | 42 | // evaluate the source code, it's response will stream to STDOUT 43 | eval($code); 44 | 45 | // TODO: do something with service data 46 | // service 47 | //echo $args["s"]; 48 | 49 | ?> -------------------------------------------------------------------------------- /bin/binaries/micro-python: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | def __prepare(): 4 | global code, service, Hook 5 | 6 | import sys 7 | import os.path 8 | sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), 'lib', 'python')) 9 | # print(sys.argv) 10 | # print(sys.path) 11 | 12 | import microcule 13 | 14 | code, service, Hook = microcule.parse_argv() 15 | 16 | __prepare() 17 | del __prepare 18 | 19 | # inject microcule WSGI handler code 20 | # code = code + '\nimport microcule\nmicrocule.wsgi(Hook).run(app)\n' 21 | 22 | # Execute the code as a string in ( this ) context 23 | # print(code) 24 | exec(code) 25 | -------------------------------------------------------------------------------- /bin/binaries/micro-python3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | def __prepare(): 4 | global code, service, Hook 5 | 6 | import sys 7 | import os.path 8 | sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), 'lib', 'python')) 9 | # print(sys.argv) 10 | # print(sys.path) 11 | 12 | import microcule 13 | 14 | code, service, Hook = microcule.parse_argv() 15 | 16 | __prepare() 17 | del __prepare 18 | 19 | # Execute the code as a string in ( this ) context 20 | # print(code) 21 | exec(code) 22 | -------------------------------------------------------------------------------- /bin/binaries/micro-ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | 5 | # simple logging method 6 | def log(*args) 7 | jsonMessage = "{ \"type\": \"log\", \"payload\" : { \"entry\": #{args.to_json} } }" 8 | STDERR.puts jsonMessage 9 | end 10 | 11 | # service source code is passed in through -c "string" ( second item in argv array ) 12 | code = ARGV[1] 13 | 14 | # service enviroment code is passed in through -e "json string" ( third item in argv array ) 15 | _env = JSON.parse(ARGV[3]) 16 | 17 | # assign hook enviroment to Hook variables ( contains params / env / etc ) 18 | Hook = _env 19 | 20 | # map Hook.env variables to ENV ruby scope ( for convenience ) 21 | _env['env'].each do |k, v| 22 | # string values can be set as-is 23 | if v.is_a?(String) 24 | # puts "string #{k} #{v}" 25 | ENV[k] = v 26 | end 27 | 28 | # object values need to be converted to JSON, or else Ruby will error 29 | if v.is_a?(Object) 30 | # puts "object #{k} #{v}" 31 | ENV[k] = v.to_json 32 | end 33 | 34 | end 35 | 36 | # TODO: do something with service 37 | service = ARGV[5] 38 | 39 | # TODO: better error checking around eval 40 | eval code -------------------------------------------------------------------------------- /bin/binaries/micro-scheme: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scheme 2 | 3 | ;;; It may look like this doesn't do anything yet... 4 | ;;; ...but for some reason the tinyscheme binary works by passing in -c as code 5 | ;;; 6 | ;;; This means we have prepared a scheme payload as a string which was passed here -------------------------------------------------------------------------------- /bin/binaries/micro-tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/tclsh 2 | 3 | package require cmdline 4 | 5 | # Show argv before processing 6 | # puts "Before, argv = '$argv'" 7 | 8 | # Process the command line 9 | set parameters { 10 | {s.arg "" "The service"} 11 | {c.arg "" "The code to run"} 12 | {p.arg "" "Additional inject payload"} 13 | {e.arg "" "The environment"} 14 | {debug "Output extra debug info"} 15 | } 16 | array set arg [cmdline::getoptions argv $parameters] 17 | 18 | # parray arg 19 | eval $arg(p) 20 | eval $arg(c) 21 | # TODO: do something with service and env 22 | #puts $arg(s) 23 | -------------------------------------------------------------------------------- /bin/microcule: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var microcule = require('../'); 4 | var config = require('../config'); 5 | // parse incoming command line arguments 6 | var argv = require('minimist')(process.argv.slice(2)); 7 | var script = argv._[0]; 8 | var stdin = process.stdin; 9 | var http; 10 | 11 | config.SERVICE_MAX_TIMEOUT = argv.t || argv.timeout || config.SERVICE_MAX_TIMEOUT; 12 | config.http = config.http || {}; 13 | config.http.host = argv.h || argv.host || config.http.host; 14 | config.http.port = argv.p || argv.port || config.http.port; 15 | config.watch = argv.w || argv.watch || config.watch; 16 | config.stream = argv.s || argv.stream || config.stream; 17 | 18 | // default to releasing compiled binaries to current working directory of `microcule` binary 19 | config.releaseDir = config.releaseDir || process.cwd(); 20 | 21 | if (typeof argv.v !== "undefined" || typeof argv.version !== "undefined") { 22 | var pkg = require('../package.json') 23 | console.log(pkg.version) 24 | process.exit(); 25 | } 26 | 27 | if (script === "help") { 28 | showHelp(); 29 | process.exit(); 30 | } 31 | 32 | if (typeof script === "undefined" && stdin.isTTY) { 33 | console.log('path to script is a required argument'); 34 | showHelp(); 35 | process.exit(); 36 | } 37 | 38 | var path = require('path'); 39 | var requireServiceSync = require('../lib/requireServiceSync'); 40 | 41 | var logger = require('../lib/plugins/logger'); 42 | var mschema = require('../lib/plugins/mschema'); 43 | var viewPresenter = require('../lib/viewPresenter'); 44 | 45 | var servicePath = path.resolve(process.cwd() + "/" + script); 46 | var _service; 47 | 48 | try { 49 | _service = requireServiceSync({ path: servicePath }); 50 | } catch (err) { 51 | if (err.code === 'ENOENT') { 52 | // if we could not find the service, attempt to see if it's a globally available binary 53 | // TODO: consider using https://github.com/mathisonian/command-exists library 54 | var commandExists = true; 55 | // if the command exists, assume we are spawning an arbitrary binary with argv as it's arguments 56 | if (commandExists) { 57 | _service = { 58 | bin: script, 59 | argv: process.argv.slice(3) 60 | } 61 | } else { 62 | throw err; 63 | } 64 | } 65 | } 66 | 67 | // language override 68 | if (typeof argv.l !== "undefined" || typeof argv.language !== "undefined") { 69 | _service.language = argv.l || argv.language; 70 | } 71 | 72 | if (stdin.isTTY) { 73 | // console.log('ignoring will not run in TTY mode') 74 | // helps with peformance of STDIN tool 75 | http = require('resource-http'); 76 | startServer(_service); 77 | return; 78 | } else { 79 | startSTDIN(_service); 80 | } 81 | 82 | function showHelp () { 83 | console.log('Usage: microcule [command] [options] ./path/to/script.foo'); 84 | console.log(' '); 85 | console.log('Commands:'); 86 | console.log(' '); 87 | console.log(' help Display help'); 88 | console.log(' '); 89 | console.log('Options:'); 90 | console.log(' -t, --timeout Sets max timeout of service in milliseconds'); 91 | console.log(' -h, --host Host to listen on'); 92 | console.log(' -p, --port Port to listen on'); 93 | console.log(' -l, --language Target programming language'); 94 | console.log(' -s, --stream Enables stream processing for ( STDIN usage only ) '); 95 | console.log(' -w, --watch Reloads source files on every request ( dev only )'); 96 | console.log(' -v, --version Output the version number'); 97 | } 98 | 99 | function startSTDIN (service) { 100 | 101 | var ret = ''; 102 | 103 | var opts = { 104 | mode: 'buffer' 105 | }; 106 | 107 | var opts = { 108 | mode: 'stream' 109 | }; 110 | 111 | var Readable = require('stream').Readable; 112 | var Writable = require('stream').Writable; 113 | 114 | var input = new Readable; 115 | var output = Writable(); 116 | 117 | output._write = function (chunk, enc, next) { 118 | // console.log('wirtint out',chunk.toString()); 119 | process.stdout.write(chunk); 120 | next(); 121 | }; 122 | 123 | output.on('error', function(err){ 124 | console.log('err', err) 125 | }); 126 | 127 | output.on('end', function(err){ 128 | process.exit(); 129 | }); 130 | 131 | stdin.setEncoding('utf8'); 132 | if (config.stream) { 133 | // if we are in streaming mode, check to see if existing stream exists 134 | // if so, write some data to it 135 | // TODO: write to spawned MC stream 136 | // if no, spawn a new stream and write this first data to it 137 | // TODO: mc.spawn 138 | stdin.resource = { 139 | params: { 140 | STDIN: result 141 | } 142 | }; 143 | microcule.plugins.spawn({ 144 | bin: service.bin, 145 | argv: service.argv, 146 | code: service.code, 147 | schema: service.schema, 148 | view: service.view, 149 | presenter: service.presenter, 150 | releaseDir: config.releaseDir, 151 | language: service.language, 152 | config: config, 153 | log: console.log 154 | })(stdin, output, function(){ 155 | // console.log("COMPLETED STREAMING SERVICE") 156 | }); 157 | } else { 158 | var result = ''; 159 | stdin.on('end', function () { 160 | // console.log(ret); 161 | stdin.resource = { 162 | params: { 163 | STDIN: result 164 | } 165 | }; 166 | // console.log('spawning service'.yellow) 167 | microcule.plugins.spawn({ 168 | bin: service.bin, 169 | argv: service.argv, 170 | code: service.code, 171 | schema: service.schema, 172 | view: service.view, 173 | presenter: service.presenter, 174 | releaseDir: config.releaseDir, 175 | language: service.language, 176 | config: config, 177 | log: console.log 178 | })(stdin, output, function(){ 179 | // console.log("COMPLETED BUFFER SERVICE") 180 | }); 181 | }); 182 | // if we are in non-streaming buffer mode, 183 | // then every time STDIN is received from `microcule`, we will execute a fresh function 184 | stdin.on('readable', function () { 185 | var chunk; 186 | while ((chunk = stdin.read())) { 187 | result += chunk.toString(); 188 | } 189 | }); 190 | } 191 | } 192 | 193 | function startServer (_service) { 194 | http.listen(config.http, function(err, app){ 195 | var addr = app.server.address(); 196 | 197 | if (typeof _service.schema === "object") { 198 | console.log('using schema for service. see: mschema for more details') 199 | console.log(_service.schema); 200 | } 201 | 202 | if (_service.view && _service.view.length > 0 ) { 203 | console.log('using view for service') 204 | } 205 | 206 | if (typeof _service.presenter === "function") { 207 | console.log('using presenter for service') 208 | } 209 | 210 | var serviceType = _service.language || _service.bin; 211 | console.log(serviceType + ' microcule started at: http://' + addr.address + ":" + addr.port); 212 | 213 | // Remark: Will automatically map the process.env `microcule` was spawned in to the service.env of the spawned function 214 | config.env = process.env; 215 | 216 | app.use(logger()); 217 | 218 | if (_service.schema) { 219 | app.use(mschema(_service.schema)); 220 | } 221 | 222 | app.use(function watchSpawn (req, res, next) { 223 | // Remark: If config.watch or argv is detected, reload the script on every request 224 | // TODO: we could use an MD5 checksum or mtime to only reload the script on actual changes 225 | if (config.watch === "true" || config.watch === true) { 226 | // TODO: fix / add back the working async requireService method 227 | /* 228 | requireService({ path: process.cwd() + "/" + script, language: targetLang }, function (err, __service) { 229 | if (err) { 230 | throw err; 231 | } 232 | spawnService(__service); 233 | }); 234 | */ 235 | spawnService(requireServiceSync({ path: servicePath })); 236 | } else { 237 | spawnService(_service) 238 | } 239 | 240 | function spawnService (service) { 241 | // TODO: move viewPresenter back into proper plugin 242 | if (_service.view) { 243 | viewPresenter({ 244 | view: _service.view, 245 | presenter: _service.presenter 246 | }, req, res, function(err, req, output){ 247 | if (err) { 248 | return next(err); 249 | } 250 | microcule.plugins.spawn({ 251 | bin: service.bin, 252 | argv: service.argv, 253 | code: service.code, 254 | schema: service.schema, 255 | view: service.view, 256 | presenter: service.presenter, 257 | releaseDir: config.releaseDir, 258 | language: service.language, 259 | config: config, 260 | log: console.log 261 | })(req, output, function(){ 262 | // if we made it here, it means no services called res.end() 263 | // we should end the service ( or else it will hang forever ) 264 | output.end(); 265 | }); 266 | }); 267 | } else { 268 | microcule.plugins.spawn({ 269 | bin: service.bin, 270 | argv: service.argv, 271 | code: service.code, 272 | schema: service.schema, 273 | view: service.view, 274 | presenter: service.presenter, 275 | releaseDir: config.releaseDir, 276 | language: service.language, 277 | config: config, 278 | log: console.log 279 | })(req, res, function(){ 280 | // if we made it here, it means no services called res.end() 281 | // we should end the service ( or else it will hang forever ) 282 | res.end(); 283 | }); 284 | } 285 | } 286 | }); 287 | }); 288 | } 289 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | bash: { 5 | version: 3 6 | }, 7 | http: { 8 | port: 3000, 9 | host: "0.0.0.0", 10 | https: false, // set to `true` to enable SSL server. cert, key, and ca will be required. 11 | key: fs.readFileSync(__dirname + "/ssl/server-key.pem").toString(), 12 | cert: fs.readFileSync(__dirname + "/ssl/server-crt.pem").toString(), 13 | ca: [fs.readFileSync(__dirname + '/ssl/ca-crt.pem').toString()], 14 | sslRequired: false, // redirects all http traffic to https, optional 15 | onlySSL: false // will only start https server with no unprotected http interface, optional 16 | }, 17 | SERVICE_MAX_TIMEOUT: 10000, 18 | messages: { 19 | childProcessSpawnError: require('./messages/childProcessSpawnError'), 20 | serviceExecutionTimeout: require('./messages/serviceExecutionTimeout') 21 | } 22 | }; -------------------------------------------------------------------------------- /config/messages/README.md: -------------------------------------------------------------------------------- 1 | This directory contains messages which may be presented to the user. 2 | 3 | These messages are configurable, so we can adjust the copy of certain messages without having to touch any sensitive code paths. 4 | 5 | This is also useful if you intend to white-label this project. -------------------------------------------------------------------------------- /config/messages/childProcessSpawnError.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function childProcessSpawnErrorMessage (args) { 2 | // TODO: use a template instead of str concat 3 | var str = ''; 4 | str += 'Error in spawning child process. Error code: 1\n'; 5 | str += 'We attempted to run the following command: \n\n' 6 | str += args.join(" "); 7 | return str; 8 | } -------------------------------------------------------------------------------- /config/messages/serviceExecutionTimeout.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function serviceExecutionTimeoutMessage (seconds) { 2 | // TODO: use a template instead of str concat 3 | var str = ''; 4 | str += 'Timeout Limit Hit. Request Aborted! \n\Service source code took more than '; 5 | str += seconds; 6 | str += ' complete.\n\n'; 7 | str += 'A delay of this long may indicate there is an error in the source code for the Service. \n\n'; 8 | str += 'If there are no errors and the Service requires more than '; 9 | str += seconds; 10 | str += ' seconds to execute, you can increase SERVICE_MAX_TIMEOUT variable.'; 11 | return str; 12 | } -------------------------------------------------------------------------------- /config/ssl/ca-crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFgDCCA2gCCQCJw+oyzjLl9zANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UEBhMC 3 | VVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEzARBgNVBAoMCkV4YW1w 4 | bGUgQ28xEDAOBgNVBAsMB3RlY2hvcHMxCzAJBgNVBAMMAmNhMSAwHgYJKoZIhvcN 5 | AQkBFhFjZXJ0c0BleGFtcGxlLmNvbTAeFw0xNzA4MjMyMjMxMjJaFw00NTAxMDcy 6 | MjMxMjJaMIGBMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJv 7 | c3RvbjETMBEGA1UECgwKRXhhbXBsZSBDbzEQMA4GA1UECwwHdGVjaG9wczELMAkG 8 | A1UEAwwCY2ExIDAeBgkqhkiG9w0BCQEWEWNlcnRzQGV4YW1wbGUuY29tMIICIjAN 9 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7lGBPJD8B1n2rC3VD9ZZPPbdNIuo 10 | a1jZzjjqT4pVSMKeQIcHnO7Oq5CLGVltpiQJO/xrbFmiJCqN1uMTbfhcAkHdEOby 11 | P6M/DapBYsl+IFH9W7nr0RG1sduANNnhBI4aO03zlKmlmmhQrMaIdHK02WwzbDhx 12 | i4oR4lf/mBbGdpCeJNylC32yN6PNT6s/HbD/DytKHi4VZnCOfu2enXx8OGEddU9u 13 | KoHbY6pPRvmB+5wk30OWOtGNy+p+P7UYb0TrGcgJb/lWtgHB7XpvwYOKatNecVsy 14 | KHbUO1EbF1vyL4upddID8Cu+eDNU1mjsbM3O6ANY4UXq8jwKOWrlL1iuuN48AWJn 15 | vFaYFyM8HhWzRA3bvAiUHgCAkd5xBFkc70Wx7p8f97xVe4heucE+e8Q7QDBNc4+C 16 | vVPHhiFuGYisgBvABqobPJ0qYu5mEG9Y9DmEjb8SoGqruWvWc2qeAm9J+gQFq1Lj 17 | JYSUzxbmvmj5mnGzEh462R/QQ1I1ZR7zzX5UkpV/1XD+Bs41oRJzQ/Iy8uXGH5H0 18 | PZd3N0BSls16RcZx4THWxkCFx70ko1dSgnIP+GhfCAus9XuM+O2B4WMCA1rDk3f6 19 | gMnp6iUCI0MOb7Zd2JGAoFzZv+OhEHf1700rlfHfYKi7qbLcdtu/JL8mLwFEY9uO 20 | DDF5jgM6joH5VoMCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA3Jpj75Nz+aDc+RuP 21 | L8cSj53Xi7D/8uZJYsWoRBwO4KerHp4jIBwW63eVzt3OIDkDz4+BYwXeNHi1brGY 22 | sWC0BVco5GTw9uiV3KVrVmzqvRahFsuo290lJYtU2dwcRVR/KIfGhwn37x9iUQyd 23 | LVjZLUfTJ7enPljAIptmoKCdSgghDL/ZBItxkn4UsTdDYmhcYwmpJCNac+WjVL/b 24 | JcE3DSHMfCrnw8Z/QvUvJzpLbP0JXXc6l3bouoPBvg6d2m+KZ0Z7Pz4AV/7Wrr2/ 25 | v1cIlfiRAAEMrhZtX6Zz0HKFEVZXvzh1gn+G72lGoyH9/CQQZ5CXHuFHSPSGdKUD 26 | hGqPZuBd34bwKO7qn/3YMdcX1iq/TOXgleTnBXLCoMcJhXOSnBGHYwMndqv4jeRy 27 | 1PZVgXpycSiWwrKgBW7Ioh7F820bNfTjq2xgRPKi7EsMzAjE6PYYA6W8MDrdJqu5 28 | NFJWLbiAB9HKmadjCl5nYsIUBZt1UZOJ4W32A+Z5Y42KR0LQIABm7BQ1WxaI+pD1 29 | 91Kk35Qo7rewKBMN7UllFv1KdPVdGKz9sugjuM1QQkEDcp3nZSc8DywhVeCSjA4Q 30 | +ZDQnnpz4x5QbW6KAROmwQE8PTnLitHgSLAb12qPhF2NOx76z32Tk6ZxSlw9qFBZ 31 | y1/ddjtt4PIYRpsKvlpqw1cKVRU= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /config/ssl/server-crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFjDCCA3SgAwIBAgIJAKb1y7ptPOxWMA0GCSqGSIb3DQEBBQUAMIGBMQswCQYD 3 | VQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjETMBEGA1UECgwK 4 | RXhhbXBsZSBDbzEQMA4GA1UECwwHdGVjaG9wczELMAkGA1UEAwwCY2ExIDAeBgkq 5 | hkiG9w0BCQEWEWNlcnRzQGV4YW1wbGUuY29tMB4XDTE3MDgyMzIyMzE0MVoXDTIw 6 | MDUxODIyMzE0MVowgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNQTEPMA0GA1UE 7 | BwwGQm9zdG9uMRMwEQYDVQQKDApFeGFtcGxlIENvMRAwDgYDVQQLDAd0ZWNob3Bz 8 | MRIwEAYDVQQDDAlsb2NhbGhvc3QxIDAeBgkqhkiG9w0BCQEWEWNlcnRzQGV4YW1w 9 | bGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuO35PfnJqbml 10 | So77I2QLP8mV4mlyJYOJbwTbE8VaZJMmjZ9/wkH6ax8ywUkcl9d5CKHc7W/NNI4k 11 | z62X4BVW1BsIfiIa3TuoMJiItFzRbenLK/mYLD+69QwOnBY4ftZS8Xy25rdQ8HYE 12 | zIpsdGmTtJZph2W0/EoVu0JlcZ9q4nzVpbOv60E261zvIMlg/7mRU/pS59ZGoXyw 13 | ZRwwZdTdwDzkxGCXS8Y1NDEc/byt18tZhxiM71+QXmO11zlez/7bRRqTokeRkyNP 14 | sxwdCre6F+WdlKUkt1O2PvzBOA3230+usT7o+3OMDODQHKNLGl+pjHlZB12A0Xfj 15 | zPEuWhGV3jUG9Qv9jaR/QHKutNzUjqBH/ILW4RfI+K1WuTnT2pIi6WwF12kllmQd 16 | Py2qjrXdpHULaSyTCnui4MAlHM5q6hHFWc70+k5nNd5U4BojtILsI14ARkqOiFUr 17 | M6+pR8AOastBDuzyzzq+NwlQbJWKNE0iDBZcV4hugopvsqVmjaDVLno6iYitPUE3 18 | uW8tszWRbm6Rz+j+ueA2KW7kEw0hd9mUgycbolc9lxy8MkW0cwsu+xAzRnESkYVi 19 | o3201lp5/7jIA/3O7I/ITX8rt7wCFMJkrm51c2zLcpKF09d8kw2vmBqfMV7Sggzv 20 | +Lz10iHZAJiXYT0oBIaVbwgy6NfNs20CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA 21 | jxZslB7aADfCMzHp0smFAGGmz9kuDYitpTzjfWQml+ZmSD6PV64dkPQD8qwJXMgF 22 | e5cU9DAoAdkBDh7+zlH+V+74zMt2bR64z9oiJ3nEPuwxHAsyjE+k8WP1Hd9Ax/ZM 23 | LgckO3x+l4mmpR8cBYR1jBCxNbLqTtDyVEj8OqSYYi9QHnS3SKiFr4c78+hNUHda 24 | CzNGb5wdJFhrT3ODx9Xy83zCKXmCsF8GH/soThk9vy/QHmYTT8W7WzgIg5+rbava 25 | X4AwmQeW8N+guQIeyifroqODnVFcgxhaghggthNM1AGY76wm1bCzwQwODS16Ddhe 26 | HcMXvKO5DsRNLp/toWb1lfZk0AZmTHCjGl0pwuC9liZtcJ2DXgKZYxKI2KWi0J3T 27 | YZCJMnNBCjdnyivmVL4zvOxjPoV1B7u0oJ564YjYOnkxKg4U6zXVwHaAIC2Ph9t6 28 | F+JPdn2NbBHear0ptnsMGNvmZWXGInrW+Ebdw098O4Kv05TNgXefM5d+Hn3CHQUN 29 | A0ilvWmox4V7RSZFUhnnB+PNX26Sp0lLzsaP90H26ihjV7UvoQZAjWzSa3pt1wTw 30 | Avnosg88bl2Y9NgeYW8KG0YXLa1JIxIqTNDfVddnQdj9Ct/ZAhpaxE12XHEV0YZI 31 | S6JbfpsoA2rCNpSSby62PxYzzdlKIAA7B6DjhGj5R/s= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /config/ssl/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEAuO35PfnJqbmlSo77I2QLP8mV4mlyJYOJbwTbE8VaZJMmjZ9/ 3 | wkH6ax8ywUkcl9d5CKHc7W/NNI4kz62X4BVW1BsIfiIa3TuoMJiItFzRbenLK/mY 4 | LD+69QwOnBY4ftZS8Xy25rdQ8HYEzIpsdGmTtJZph2W0/EoVu0JlcZ9q4nzVpbOv 5 | 60E261zvIMlg/7mRU/pS59ZGoXywZRwwZdTdwDzkxGCXS8Y1NDEc/byt18tZhxiM 6 | 71+QXmO11zlez/7bRRqTokeRkyNPsxwdCre6F+WdlKUkt1O2PvzBOA3230+usT7o 7 | +3OMDODQHKNLGl+pjHlZB12A0XfjzPEuWhGV3jUG9Qv9jaR/QHKutNzUjqBH/ILW 8 | 4RfI+K1WuTnT2pIi6WwF12kllmQdPy2qjrXdpHULaSyTCnui4MAlHM5q6hHFWc70 9 | +k5nNd5U4BojtILsI14ARkqOiFUrM6+pR8AOastBDuzyzzq+NwlQbJWKNE0iDBZc 10 | V4hugopvsqVmjaDVLno6iYitPUE3uW8tszWRbm6Rz+j+ueA2KW7kEw0hd9mUgycb 11 | olc9lxy8MkW0cwsu+xAzRnESkYVio3201lp5/7jIA/3O7I/ITX8rt7wCFMJkrm51 12 | c2zLcpKF09d8kw2vmBqfMV7Sggzv+Lz10iHZAJiXYT0oBIaVbwgy6NfNs20CAwEA 13 | AQKCAgEAuNP34O5vlLfcMRmYfXW3HIjAyiqzkDTYPmJvB2KfBphf1vpL+X5x53Cm 14 | DLKi5kvQR0VnhtPQF6/dsMeCXM9XGeTu5wn1KZo/blp1fzaphp49lvT+F3OWyuXD 15 | EFPRIUvaWEtajls3hS2ffEL5RJxkqQAP9ug7LFBrSd6Bfz9i130HF3bw40MpvbED 16 | uxQqY0w3qOQuylHKBAqYTmYPJSfMfh8eUftsG6q9cC2KGTvj0CMIltJ9wZ0UsJId 17 | oz9OuLmys21tkqrPH20SciQuDpE0aD/w8Cjh+myrYGbEQt8KF4UPR01mFDMXDaP/ 18 | vC+kl3Z3AdQinA7i+fhGcIW42wJqcpeEimtgqMWTSsqagwR0Qvv+quGra7XGVO7w 19 | 5AYr1eKufo5owTfO5zbzeorJTf8I5QOViD9zxrGh1F54jqn+qumvSbAj4KnzvJcn 20 | AGhCczr+c2JY5AjeTgLdCR65kAj+MIJCmu+jkdREg5JGqHdnf8ZRq3I8EYUWNQLa 21 | 80b96o0WevvnGXzI7DIxpBanUI0YW3C6Z/TtuZGlqYTvtsPr+vMasnqvmszmBGcT 22 | 1Jt5Dg0k0LxQfiWaZoHCDKXWrgkZOUMO0F8HLAWxCEx9NmDCW8rhfmifWEMb5W7D 23 | G4xqipG9goOjdNkg8mVSgb/YqI9KIkhfQMBkQk0JcxToaudXMuECggEBAPN2/yJF 24 | lb8kIHeSUYGpwqCKboglFH3142hafhEiZ04tQYA83gFmFzytN/yXh4l2NBszPtaQ 25 | gdEVdTvyZKyspXY5chB9w2T3JzE77GFg62azQ+37p2C71MWnrCZ6MwKaPCik/sjY 26 | 0d03QzKqZGoesNcXCkQheSDRFs9yhVKZZyAM6DoOTeebS9p1FLEQFJM2aSktMOt9 27 | Jl2j8C9fxkEFXXJsV2aX53zkxLxnytNlGlydeQx+r9ieWiq5e/zSLMmpqCCv5kA/ 28 | IxSAJi8YcBTHn+2KXjx6SwEj0CqpeOm0XpauPXZEbL+xPeyD1OM+VKmK3tesc3NC 29 | Rc3fMvCBeY6Ov1UCggEBAMJzcxPpqIpFm6wgSUSib+rlDYDK1KS6iDRtJKGNHT2u 30 | avGLc13cBggGCjU4v4xFp67uQIYPFDw50BWlrbbET4av2kkynbqqQxdf49xUypT6 31 | s484nKPAyDsr85Qx512x3ZETjUf3YyqbpdQxd32Tx8mQ+19xqSQteVPJGxJdJaWp 32 | SPEoWLUpR5xuJOhX/WjnQeiZFnqB/BF+iGO9BNmjNQC1hH5DlpjjDfliGKI7PGiB 33 | D+L44ni7VPeNDEFFozkrGaRXtwGsjl1WATdYYzwHEEDcyz6wJEeijStMiPSNCmP4 34 | rDNbWTbcNylhdlGjelPtdvcPy7SX0mAaXqoP8mVps7kCggEAMxZtQwvG+GzkQzL4 35 | 13R/2qAxwW/GdQf7yBH1EjNd6hGlBPvAuhpqFdljPiWt0zJyjKKcNPntw6n1B7c9 36 | WN6BeeVkAgHh1nXmS3Z/i4+C1fIX9wAvhyTi6PbeMNTDj3A932l/7TVSFFFcUG1g 37 | 1MPL59Z12QYNHM3DM5ScVVDLSvZqZIRL5KW5rQWUafMt3hY0yucotMF2I4AbDlXS 38 | jCJMEY6aaHTHpnV6su40qVc1yOliVTTgQVw5H238jnir3UU0emdusVyOR+b8HDXM 39 | jw+0h2vZimc/BeY18D3PmXq3vnahfwesqizPCapAKc8ShOEXEbqmHzFw5FpJnh4N 40 | G9ToYQKCAQBdJzml/HYFZ7tgBkeTXc4H1ZtWa9Ta5ZGCrBa0Xgn4Fqqc9JVcDZ/G 41 | ED6rqHf7FxNxtimrD2Y+Q+PvuuMxf3Ipr+z+zATL++0QMZvTXbt/C2sh3ZkMyboi 42 | vrd98zpHpIHkkl1IcLdNHiufL+NygW1gntgNrUG+VxqcjOcvMPhsGGEdRprYjuID 43 | irCbrit0KfVlHJGsNsEvEFL+lPrR2GH36P2ED5UBwSRcqCXs8jvKRKn7rgewlu3p 44 | eeS5EEdVh9RM6sh9QNEi3aTPgsRnaWrb5+pmKbP3rWO7rnqdzUOBCQxVvhlLMswq 45 | QsqP6aUct9IaIbMQ3Pqnl3pqNiGFe9HhAoIBAQDTOTwCxFfopigM03FiGgm7AM42 46 | xJLwNsaG81zBSeF57PZaoXQ/tFwlIpJTZnf8jqdCKNEKecyIKF6/kuEYdGHQaSsq 47 | BUZFXHhn2qPYc2Gj/fyKJQDOTOQ3o+m/sX/alxQ3ElLMCKe3pWkGWQyl8sQP9AbF 48 | Wu4CC1zblXAIhaB0qo9z9/K/ZsTVHZzGuphLFsPN1yuAKpQKloF3vn/p7tOCbheO 49 | phPMuHdN1MskFKRS/yj14pfLYSfj4Oi4LO/Abnq93x6lPLxShNzdy71l0pnHqTM/ 50 | ooW1K+RTAVXPwwJhWrxltxjHaoGjiUaSoKonbvSJtRE2PBSfQL0F6LqHvFCO 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /examples/ReadMe.md: -------------------------------------------------------------------------------- 1 | # microcule examples 2 | 3 | This folder contains several unique examples and ways to run / use microcule. 4 | 5 | ## Using `microcule` CLI to spawn server 6 | 7 | To use the `microcule` CLI to start one of these examples, simply run: 8 | 9 | ``` 10 | microcule ./examples/services/echo/echo.js 11 | ``` 12 | 13 | You can change the path argument to any of the services found in `./examples/services` 14 | 15 | ## Create custom stand-alone servers 16 | 17 | The `microcule` CLI spawns an HTTP server bound to the service argument. 18 | 19 | If you wish to create a custom HTTP server, you can run any of the following examples: 20 | 21 | ``` 22 | node express-plugins.js 23 | node express-simple.js 24 | node http-server-simple.js 25 | ``` 26 | 27 | These examples show how to use `microcule` as an HTTP middleware in Express or any standard Node.js HTTP server. 28 | 29 | ## 60+ Microservice Examples using Microcule 30 | 31 | see: https://github.com/Stackvana/microcule-examples 32 | -------------------------------------------------------------------------------- /examples/express-any-binary.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var handler = microcule.plugins.spawn({ 6 | bin: 'echo', 7 | argv: ['hello', 'world'] 8 | }); 9 | 10 | var handler = microcule.plugins.spawn({ 11 | bin: 'sh', 12 | argv: ['-c', 'echo "foo" | cat'] 13 | }); 14 | 15 | // spawn simple ls command to show current directories 16 | /* 17 | var handler = microcule.plugins.spawn({ 18 | bin: 'ls', 19 | argv: ['.'] 20 | }); 21 | */ 22 | 23 | // spawn a gcc program ( needs to be precompiled ) 24 | // to compile go to gcc folder and run: `gcc -o hello hello.c` 25 | /* 26 | var handler = microcule.plugins.spawn({ 27 | bin: __dirname + '/services/echo/gcc/hello' 28 | }); 29 | */ 30 | 31 | app.use(handler); 32 | 33 | app.listen(3000, function () { 34 | console.log('server started on port 3000'); 35 | }); -------------------------------------------------------------------------------- /examples/express-basicAuth-services.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function authedService (req, res, next) { 6 | res.end('logged in'); 7 | }; 8 | 9 | var basicAuth = require('./services/middlewares/basic-auth'); 10 | 11 | var nodeHandler = microcule.plugins.spawn({ 12 | code: nodeService, 13 | language: "javascript" 14 | }); 15 | 16 | var basicAuthHandler = microcule.plugins.spawn({ 17 | code: basicAuth, 18 | language: "javascript" 19 | }); 20 | 21 | var logger = microcule.plugins.logger; 22 | 23 | app.use([logger(), basicAuthHandler, nodeHandler], function (req, res) { 24 | res.end('no middlewares ended response, ending now'); 25 | }); 26 | 27 | app.listen(3000, function () { 28 | console.log('server started on port 3000'); 29 | }); -------------------------------------------------------------------------------- /examples/express-chain-services.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (req, res, next) { 6 | console.log('inside a') 7 | res.write('HELLO\n') 8 | next(); 9 | }; 10 | 11 | var nodeServiceB = function testService (req, res, next) { 12 | console.log('inside b') 13 | res.write('WORLD\n') 14 | res.end('ended request'); 15 | }; 16 | 17 | var bashService = 'echo "hello bash"' 18 | 19 | 20 | var handlerA = microcule.plugins.spawn({ 21 | code: nodeService, 22 | language: "javascript", 23 | chain: true 24 | }); 25 | 26 | var handlerB = microcule.plugins.spawn({ 27 | code: nodeServiceB, 28 | language: "javascript", 29 | chain: true 30 | }); 31 | 32 | var bashHandler = microcule.plugins.spawn({ 33 | code: bashService, 34 | language: "bash", 35 | chain: true 36 | }); 37 | 38 | 39 | var logger = microcule.plugins.logger; 40 | 41 | app.use([logger(), bashHandler, handlerA, handlerB], function (req, res) { 42 | console.log("No middlewares ended response, made it to end"); 43 | res.end('caught end') 44 | }); 45 | 46 | 47 | app.listen(3000, function () { 48 | console.log('server started on port 3000'); 49 | }); -------------------------------------------------------------------------------- /examples/express-compiled-languages.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var service = { 6 | language: 'gcc', 7 | code: require('fs').readFileSync(__dirname + '/services/hello-world/hello.c').toString() 8 | }; 9 | 10 | var spawn = microcule.plugins.spawn(service); 11 | 12 | app.use(function(req, res, next){ 13 | console.log('attempting to spawn', service) 14 | spawn(req, res, next); 15 | }); 16 | 17 | app.listen(3000, function () { 18 | console.log('server started on port 3000'); 19 | }); -------------------------------------------------------------------------------- /examples/express-jail-chroot.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (opts) { 6 | var res = opts.res; 7 | console.log('logging to console'); 8 | res.end('ran service'); 9 | }; 10 | 11 | var handler = microcule.plugins.spawn({ 12 | code: nodeService, 13 | language: "javascript", 14 | jail: "chroot", 15 | jailArgs: [ '/Users/worker'] 16 | }); 17 | 18 | app.use(handler); 19 | 20 | app.listen(3000, function () { 21 | console.log('server started on port 3000'); 22 | }); -------------------------------------------------------------------------------- /examples/express-jail-nsjail.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (opts) { 6 | var res = opts.res; 7 | console.log('logging to console'); 8 | setTimeout(function(){ 9 | res.end('ran service'); 10 | }, 10) 11 | }; 12 | 13 | var bash = microcule.plugins.spawn({ 14 | code: 'ps -ax', 15 | language: "bash", 16 | jail: "nsjail", 17 | jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] 18 | }); 19 | 20 | var handler = microcule.plugins.spawn({ 21 | code: nodeService, 22 | language: "javascript", 23 | jail: "nsjail", 24 | jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] 25 | }); 26 | 27 | var ps = microcule.plugins.spawn({ 28 | bin: "/bin/ps", 29 | argv: ['-ax' ] 30 | }); 31 | 32 | var psJail = microcule.plugins.spawn({ 33 | bin: "/bin/ps", 34 | argv: ['-ax' ], 35 | jail: "nsjail", 36 | jailArgs: [ '-Mo', '--chroot', '/var/chroot/', '--user', '99999', '--group', '99999'] 37 | }); 38 | 39 | app.use('/node', handler); 40 | app.use('/ps', ps); 41 | app.use('/psjail', psJail); 42 | 43 | app.listen(3000, function () { 44 | console.log('server started on port 3000'); 45 | }); -------------------------------------------------------------------------------- /examples/express-multi-language.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | var plugins = microcule.plugins; 5 | 6 | var nodeService = express(); 7 | nodeService.use(plugins.logger()); 8 | nodeService.use(plugins.mschema({ 9 | "hello": { 10 | "type": "string", 11 | "required": true 12 | } 13 | })); 14 | nodeService.use(plugins.rateLimiter({ 15 | maxLimit: 1000, 16 | maxConcurrency: 2 17 | })); 18 | nodeService.use(plugins.spawn({ 19 | code: function testService (opts) { 20 | var res = opts.res; 21 | res.write('hello node!'); 22 | res.end(); 23 | }, 24 | language: "javascript" 25 | })) 26 | app.use('/node', nodeService); 27 | 28 | var bashService = express(); 29 | bashService.use(plugins.logger()); 30 | bashService.use(plugins.mschema({ 31 | "hello": { 32 | "type": "string", 33 | "required": true 34 | } 35 | })); 36 | bashService.use(plugins.rateLimiter({ 37 | maxLimit: 1000, 38 | maxConcurrency: 2 39 | })); 40 | bashService.use(plugins.spawn({ 41 | code: 'echo "hello world"', 42 | language: "bash" 43 | })) 44 | app.use('/bash', bashService); 45 | 46 | app.listen(3000, function () { 47 | console.log('server started on port 3000'); 48 | console.log('node endpoint mount at /node'); 49 | console.log('bash endpoint mount at /bash') 50 | }); -------------------------------------------------------------------------------- /examples/express-plugins.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (opts) { 6 | var res = opts.res; 7 | // console.log('logging to console'); 8 | setTimeout(function(){ 9 | res.json(opts.params); 10 | }, 50); 11 | }; 12 | 13 | var logger = require('../lib/plugins/logger'); 14 | var mschema = require('../lib/plugins/mschema'); 15 | var rateLimiter = require('../lib/plugins/rateLimiter'); 16 | var spawn = require('../lib/plugins/spawn'); 17 | 18 | var handler = spawn({ 19 | code: nodeService, 20 | language: "javascript" 21 | }); 22 | 23 | app.use(logger()); 24 | app.use(mschema({ 25 | "hello": { 26 | "type": "string", 27 | "required": true 28 | } 29 | })); 30 | app.use(rateLimiter({ 31 | maxLimit: 1000, 32 | maxConcurrency: 2 33 | })); 34 | app.use(handler); 35 | app.use(function(req, res, next){ 36 | // Note: It's most likely you will not be able to call res.end or res.write here, 37 | // as the microcule.plugins.spawn handler should end the response 38 | // Any middlewares places after microcule.plugins.spawn should be considered "post processing" logic 39 | console.log('post process service'); 40 | }) 41 | 42 | app.listen(3000, function () { 43 | console.log('server started on port 3000'); 44 | }); -------------------------------------------------------------------------------- /examples/express-python.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var config = require('../config'); 3 | var express = require('express'); 4 | var app = express(); 5 | 6 | var nodeService = 'import sys\n\ 7 | import logging\n\ 8 | import pprint\n\ 9 | pprint.pprint(globals())\n\ 10 | logging.getLogger("python").info("%s", globals())'; 11 | 12 | var handler = microcule.plugins.spawn({ 13 | code: nodeService, 14 | language: "python" 15 | }); 16 | 17 | app.use(handler); 18 | 19 | // in-case we didn't end the response in the python script, create a middleware at the end of the request to catch and close the response 20 | app.use(function (req, res) { 21 | res.end(); 22 | }); 23 | 24 | app.listen(config.http.port, function () { 25 | console.log('server started on port '+config.http.port); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/express-service-view.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (opts) { 6 | var res = opts.res; 7 | console.log('logging to console'); 8 | res.end('Hello world!'); 9 | }; 10 | 11 | var handler = microcule.plugins.spawn({ 12 | code: nodeService, 13 | language: "javascript" 14 | }); 15 | 16 | //var view = microcule.plugins.viewPresenter({}, req, res, next); 17 | 18 | app.use('/view', function(req, res, next){ 19 | microcule.viewPresenter({ 20 | view: "Service outputs: {{hook.output}}" 21 | }, req, res, function(err, req, output){ 22 | handler(req, output, next) 23 | }) 24 | }); 25 | 26 | /* presenter API has been removed ( for now ) 27 | app.use('/view-presenter', function(req, res, next){ 28 | microcule.viewPresenter({ 29 | view: "Service outputs: {{hook.output}}
", 30 | presenter: function (opts, cb) { 31 | opts.res.setHeader('Content-type', 'text/html'); 32 | var $ = this.$; 33 | $('.output').html('presenters can use $ selectors'); 34 | cb(null, $.html()); 35 | } 36 | }, req, res, function(err, req, output){ 37 | handler(req, output, next) 38 | }) 39 | }); 40 | */ 41 | 42 | app.listen(3000, function () { 43 | console.log('server started on port 3000'); 44 | }); -------------------------------------------------------------------------------- /examples/express-simple.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var nodeService = function testService (req, res, next) { 6 | res.json(req.params); 7 | }; 8 | 9 | var handler = microcule.plugins.spawn({ 10 | code: nodeService, 11 | language: "javascript" 12 | }); 13 | 14 | app.use(handler); 15 | 16 | app.listen(3000, function () { 17 | console.log('server started on port 3000'); 18 | }); -------------------------------------------------------------------------------- /examples/express-source-github-gist.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var logger = require('../lib/plugins/logger'); 6 | var mschema = require('../lib/plugins/mschema'); 7 | var bodyParser = require('../lib/plugins/bodyParser'); 8 | var sourceGithubGist = require('../lib/plugins/sourceGithubGist'); 9 | var sourceGithubRepo = require('../lib/plugins/sourceGithubRepo'); 10 | var spawn = require('../lib/plugins/spawn'); 11 | 12 | var handler = spawn({ 13 | // code: nodeService, 14 | language: "javascript" 15 | }); 16 | 17 | app.use(logger()); 18 | 19 | // source from github gist 20 | app.use(sourceGithubGist({ 21 | token: "1234", 22 | main: "echoHttpRequest.js", 23 | gistID: "357645b8a17daeb17458" 24 | })); 25 | 26 | app.use(mschema({ 27 | "hello": { 28 | "type": "string", 29 | "required": true 30 | } 31 | })); 32 | app.use(handler); 33 | app.use(function(req, res, next){ 34 | // Note: It's most likely you will not be able to call res.end or res.write here, 35 | // as the microcule.plugins.spawn handler should end the response 36 | // Any middlewares places after microcule.plugins.spawn should be considered "post processing" logic 37 | console.log('post process service'); 38 | }) 39 | 40 | app.listen(3000, function () { 41 | console.log('server started on port 3000'); 42 | }); -------------------------------------------------------------------------------- /examples/express-source-github-repo.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var express = require('express'); 3 | var app = express(); 4 | 5 | var logger = require('../lib/plugins/logger'); 6 | var mschema = require('../lib/plugins/mschema'); 7 | var bodyParser = require('../lib/plugins/bodyParser'); 8 | var sourceGithubGist = require('../lib/plugins/sourceGithubGist'); 9 | var sourceGithubRepo = require('../lib/plugins/sourceGithubRepo'); 10 | var spawn = require('../lib/plugins/spawn'); 11 | 12 | var handler = spawn({ 13 | // code: nodeService, 14 | language: "python" 15 | }); 16 | 17 | app.use(logger()); 18 | 19 | // source from github repo 20 | app.use(sourceGithubRepo({ 21 | token: "1234", 22 | repo: "stackvana/microcule-examples", 23 | branch: "master", 24 | main: "python/index.py", 25 | })); 26 | 27 | app.use(mschema({ 28 | "hello": { 29 | "type": "string", 30 | "required": true 31 | } 32 | })); 33 | app.use(handler); 34 | app.use(function(req, res, next){ 35 | // Note: It's most likely you will not be able to call res.end or res.write here, 36 | // as the microcule.plugins.spawn handler should end the response 37 | // Any middlewares places after microcule.plugins.spawn should be considered "post processing" logic 38 | console.log('post process service'); 39 | }) 40 | 41 | app.listen(3000, function () { 42 | console.log('server started on port 3000'); 43 | }); -------------------------------------------------------------------------------- /examples/http-server-simple.js: -------------------------------------------------------------------------------- 1 | var microcule = require('../'); 2 | var http = require('http'); 3 | 4 | var service = function testService (opts) { 5 | opts.res.write('hello node!'); 6 | opts.res.end(); 7 | }; 8 | 9 | /* could also be bash or any supported language */ 10 | var handler = microcule.plugins.spawn({ 11 | code: service, 12 | language: "javascript" 13 | }); 14 | 15 | var server = http.createServer(function(req, res){ 16 | handler(req, res, function (err) { 17 | if (err) { 18 | return res.end(err.message); 19 | } 20 | console.log('request completed', req.url); 21 | }); 22 | }); 23 | 24 | server.listen(3000, function () { 25 | console.log('http server started on port 3000'); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/services/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## 100+ Microservice Examples using Microcule 2 | 3 | see: https://github.com/Stackvana/microcule-examples 4 | -------------------------------------------------------------------------------- /examples/services/echo/echo-es6-async.js: -------------------------------------------------------------------------------- 1 | import sleep from 'then-sleep'; 2 | export default async function (hook) { 3 | await sleep(1); 4 | hook.res.end("Hello world!"); 5 | } -------------------------------------------------------------------------------- /examples/services/echo/echo-py3.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import logging 3 | import microcule 4 | 5 | log = logging.getLogger('echo-py3') 6 | 7 | 8 | def app(environ, start_response): 9 | start_response('200 OK', [('content-type', 'text/plain')]) 10 | res = ["Hello, this is a Python script."] 11 | res.append("Hook['params'] is populated with request parameters") 12 | res.append(pprint.pformat(Hook['params'])) 13 | res.append("Hook['req'] is the http request") 14 | res.append(pprint.pformat['req']['url'])) 15 | log.info('hello logs') 16 | log.warn('%s', Hook['params']) 17 | return '\n'.join(res) 18 | 19 | if __name__ == '__main__': 20 | microcule.wsgi(Hook).run(app) 21 | -------------------------------------------------------------------------------- /examples/services/echo/echo-stdin.js: -------------------------------------------------------------------------------- 1 | module.exports = function (hook) { 2 | hook.res.write(hook.params.STDIN); 3 | hook.res.end('\n'); 4 | }; -------------------------------------------------------------------------------- /examples/services/echo/echo-wsgi.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import logging 3 | 4 | log = logging.getLogger('echo-py') 5 | 6 | def app(environ, start_response): 7 | start_response('200 OK', [('content-type', 'text/plain')]) 8 | res = ["Hello, this is a Python script."] 9 | res.append("Hook['params'] is populated with request parameters") 10 | res.append(pprint.pformat(Hook['params'])) 11 | res.append("Hook['req'] is the http request") 12 | res.append(pprint.pformat(Hook['req']['url'])) 13 | log.info('hello logs') 14 | log.warn('%s', Hook['params']) 15 | return '\n'.join(res) -------------------------------------------------------------------------------- /examples/services/echo/echo.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int // Specifies that type of variable the function returns. 4 | // main() must return an integer 5 | main ( int argc, char **argv ) { 6 | // code 7 | printf("Hello argv!\n"); 8 | printf( "argc len = %d\n", argc ); 9 | for ( int i = 0; i < argc; ++i ) { 10 | printf( "argv[ %d ] = %s\n", i, argv[ i ] ); 11 | } 12 | 13 | return 0; // Indicates that everything went well. 14 | } -------------------------------------------------------------------------------- /examples/services/echo/echo.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (hook) -> 2 | hook.res.write 'Hello, this is a Coffee-script function.\n' 3 | hook.res.write 'hook.params is populated with request parameters\n' 4 | hook.res.write JSON.stringify(hook.params, true, 2) 5 | hook.res.end '' 6 | return -------------------------------------------------------------------------------- /examples/services/echo/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | for _,element := range os.Args { 10 | fmt.Println(element); 11 | } 12 | os.Exit(1); 13 | } -------------------------------------------------------------------------------- /examples/services/echo/echo.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | res.write('Hello, this is a JavaScript function.\n'); 3 | res.write('hook.params is populated with request parameters\n'); 4 | res.write(JSON.stringify(req.params, true, 2)); 5 | res.end(''); 6 | }; -------------------------------------------------------------------------------- /examples/services/echo/echo.lisp: -------------------------------------------------------------------------------- 1 | (format t "The *Hook* hash contains all HTTP and ENV variables") 2 | (print-object *Hook* t) -------------------------------------------------------------------------------- /examples/services/echo/echo.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | print ("Hello, this is a Lua script.") 3 | print ("Hook['params'] is populated with request parameters") 4 | print ("Hook['env'] is populated with env variables") 5 | print(json.stringify(Hook['params'])) 6 | print(json.stringify(Hook['env'])) 7 | 8 | -- gets sent to logging handler 9 | log('Hook params', Hook['params']) 10 | -------------------------------------------------------------------------------- /examples/services/echo/echo.ml: -------------------------------------------------------------------------------- 1 | open Printf;; 2 | Hashtbl.iter (fun k v -> printf "%s: %s\n" k v) hook_params; -------------------------------------------------------------------------------- /examples/services/echo/echo.php: -------------------------------------------------------------------------------- 1 | echo "Hello, this is a PHP script.\n"; 2 | echo "\$Hook['params'] is populated with request parameters\n"; 3 | var_dump($Hook["params"]); -------------------------------------------------------------------------------- /examples/services/echo/echo.pl: -------------------------------------------------------------------------------- 1 | print "Hello, this is a Perl script.\n"; 2 | print "\$Hook_params_* is populated with request parameters\n"; 3 | print "\$Hook_params_foo: " . $Hook_params_foo . "\n"; 4 | -------------------------------------------------------------------------------- /examples/services/echo/echo.py: -------------------------------------------------------------------------------- 1 | import pprint, sys, json 2 | print "Hello, this is a Python script." 3 | print "Hook['params'] is populated with request parameters" 4 | pprint.pprint(Hook['params']) 5 | print "Hook['env'] is populated with env variables" 6 | pprint.pprint(Hook['env']) 7 | log('hello logs') 8 | log(Hook['params']) 9 | -------------------------------------------------------------------------------- /examples/services/echo/echo.r: -------------------------------------------------------------------------------- 1 | # hook contains List of http params as key / value 2 | print(hook) -------------------------------------------------------------------------------- /examples/services/echo/echo.rb: -------------------------------------------------------------------------------- 1 | puts 'Hello, this is a Ruby script.' 2 | puts "Hook['params'] is populated with request parameters" 3 | puts Hook['params'] -------------------------------------------------------------------------------- /examples/services/echo/echo.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | // This is the main function 4 | fn main() { 5 | // The statements here will be executed when the compiled binary is called 6 | // Prints each argument on a separate line 7 | for argument in env::args() { 8 | println!("{}", argument); 9 | } 10 | } -------------------------------------------------------------------------------- /examples/services/echo/echo.sh: -------------------------------------------------------------------------------- 1 | # bash 4 is required for Hook_params nested object 2 | for k in "${!Hook_params[@]}" 3 | do 4 | echo "$k=${Hook_params[$k]}" 5 | done 6 | 7 | # with bash 3 properties are accessed using the syntax: 8 | echo $Hook_params_foo; 9 | # where "foo" is the name of the http request parameter -------------------------------------------------------------------------------- /examples/services/echo/echo.ss: -------------------------------------------------------------------------------- 1 | (begin 2 | (display "Hello, this is a Scheme script.") 3 | (newline) 4 | (display "Hook_params_* is populated with request parameters") 5 | (newline) 6 | (display "Hook_params_foo: ") (display Hook_params_foo) 7 | ) -------------------------------------------------------------------------------- /examples/services/echo/echo.st: -------------------------------------------------------------------------------- 1 | 'Hello, this is a Smalltalk script.' displayNl ! 2 | 'Hook_params_* is populated with request parameters' displayNl ! 3 | ('Hook_params_foo: ' , Hook_params_foo) displayNl ! 4 | -------------------------------------------------------------------------------- /examples/services/echo/echo.tcl: -------------------------------------------------------------------------------- 1 | puts {Hello, this is a tcl script.} 2 | puts {\$Hook_params_* is populated with request parameters} 3 | puts {\$Hook_params_foo $Hook_params_foo} 4 | -------------------------------------------------------------------------------- /examples/services/echo/echoOld.js: -------------------------------------------------------------------------------- 1 | module.exports = function (hook) { 2 | var req = hook.req, res = hook.res; 3 | res.write('Hello, this is a JavaScript function.\n'); 4 | res.write('hook.params is populated with request parameters\n'); 5 | res.write(JSON.stringify(req.params, true, 2)); 6 | res.end(''); 7 | }; -------------------------------------------------------------------------------- /examples/services/hello-world/hello-wsgi.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import logging 3 | 4 | log = logging.getLogger('echo-py') 5 | 6 | def app(environ, start_response): 7 | start_response('200 OK', [('content-type', 'text/plain')]) 8 | res = ["hello world"] 9 | return '\n'.join(res) -------------------------------------------------------------------------------- /examples/services/hello-world/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) 4 | { // This is a comments 5 | printf("Hello world!\n"); 6 | return 0; 7 | } -------------------------------------------------------------------------------- /examples/services/hello-world/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | import "fmt" 3 | func main() { 4 | fmt.Println("hello world") 5 | } -------------------------------------------------------------------------------- /examples/services/hello-world/hello.java: -------------------------------------------------------------------------------- 1 | class hook { public static void main(String args[]) { System.out.println("Hello JavaWorld!!!!"); } } -------------------------------------------------------------------------------- /examples/services/hello-world/hello.lua: -------------------------------------------------------------------------------- 1 | print ("hello world") -------------------------------------------------------------------------------- /examples/services/hello-world/hello.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | print "hello world" -------------------------------------------------------------------------------- /examples/services/hello-world/hello.r: -------------------------------------------------------------------------------- 1 | # the R app 2 | myString <- "Hello, World!" 3 | print ( myString) -------------------------------------------------------------------------------- /examples/services/hello-world/hello.rs: -------------------------------------------------------------------------------- 1 | // This is the main function 2 | fn main() { 3 | // The statements here will be executed when the compiled binary is called 4 | 5 | // Print text to the console 6 | println!("Hello RustWorld!"); 7 | } -------------------------------------------------------------------------------- /examples/services/image/index.js: -------------------------------------------------------------------------------- 1 | // GraphicsMagick fully supported 2 | var gm = require('gm'); 3 | var request = require('request'); 4 | 5 | module['exports'] = function imageResize (hook) { 6 | // grab an image stream using request 7 | var stream = request('https://hook.io/img/robotcat.png'); 8 | hook.res.writeHead(200, { 'Content-Type': 'image/png' }); 9 | // create a gm resize stream and pipe to response 10 | gm(stream) 11 | .options({imageMagick: true }) // set to `true` for MacOS ( gm by default ) 12 | .resize(150, 150) 13 | .stream() 14 | .pipe(hook.res); 15 | }; -------------------------------------------------------------------------------- /examples/services/middlewares/basic-auth.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res, next) { 2 | 3 | var auth = require('basic-auth') 4 | 5 | var credentials = auth(req) 6 | 7 | if (!credentials || credentials.name !== 'admin' || credentials.pass !== 'password') { 8 | //res.statusCode(401); 9 | res.setHeader('WWW-Authenticate', 'Basic realm="examples"') 10 | res.writeHead(401); 11 | res.end('Access denied'); 12 | } else { 13 | // req.user = 'john'; 14 | next(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /examples/services/streams/echo.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function echoStream (req, res) { 2 | // If the hook is not currently streaming, 3 | // the req has already been fully buffered, 4 | // and can no longer be streamed! 5 | if (!req.streaming) { 6 | return res.end('No streaming request detected. \n\nTo test streaming data to this Hook try running this Curl command: \n\n echo "foo" | curl --header "content-type: application/octet-stream" --data-binary @- https://io/examples/javascript-stream-transform'); 7 | } 8 | req.on('end', function(){ 9 | res.end(); 10 | }); 11 | req.on('data', function(chunk){ 12 | res.write(chunk.toString()) 13 | }); 14 | }; -------------------------------------------------------------------------------- /examples/services/streams/hello.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function transformStream (req, res, next) { 2 | // If the hook is not currently streaming, 3 | // the req has already been fully buffered, 4 | // and can no longer be streamed! 5 | if (!req.streaming) { 6 | return hook.res.end('No streaming request detected. \n\nTo test streaming data to this Hook try running this Curl command: \n\n echo "foo" | curl --header "content-type: application/octet-stream" --data-binary @- https://hook.io/examples/javascript-stream-transform'); 7 | } 8 | req.on('end', function(){ 9 | console.log('called end event') 10 | res.end(); 11 | }); 12 | req.on('data', function(chunk){ 13 | res.write('hello world'); 14 | }); 15 | }; -------------------------------------------------------------------------------- /examples/services/streams/transform.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function echoStream (req, res) { 2 | // If the hook is not currently streaming, 3 | // the req has already been fully buffered, 4 | // and can no longer be streamed! 5 | if (!req.streaming) { 6 | return hook.res.end('No streaming request detected. \n\nTo test streaming data to this Hook try running this Curl command: \n\n echo "foo" | curl --header "content-type: application/octet-stream" --data-binary @- https://hook.io/examples/javascript-stream-transform'); 7 | } 8 | req.on('end', function(){ 9 | res.end(); 10 | }); 11 | req.on('data', function(chunk) { 12 | res.write(chunk.toString().toUpperCase()) 13 | }); 14 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var microcule = {}; 2 | 3 | microcule.config = require('./config'); 4 | 5 | microcule.requireService = require('./lib/requireService'); 6 | microcule.requireServiceSync = require('./lib/requireServiceSync'); 7 | 8 | microcule.plugins = { 9 | "bodyParser": require('./lib/plugins/bodyParser'), 10 | "compile": require('./lib/plugins/compile'), 11 | "logger": require('./lib/plugins/logger'), 12 | "mschema": require('./lib/plugins/mschema'), 13 | "RateLimiter": require('./lib/plugins/rateLimiter'), 14 | "sourceGithubGist": require('./lib/plugins/sourceGithubGist'), 15 | "sourceGithubRepo": require('./lib/plugins/sourceGithubRepo'), 16 | "spawn": require('./lib/plugins/spawn') 17 | }; 18 | 19 | // TODO: refactor viewPresenter into plugin 20 | microcule.viewPresenter = require('./lib/viewPresenter'); 21 | 22 | module.exports = microcule; -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | --[[ json.lua 2 | 3 | A compact pure-Lua JSON library. 4 | The main functions are: json.stringify, json.parse. 5 | 6 | ## json.stringify: 7 | 8 | This expects the following to be true of any tables being encoded: 9 | * They only have string or number keys. Number keys must be represented as 10 | strings in json; this is part of the json spec. 11 | * They are not recursive. Such a structure cannot be specified in json. 12 | 13 | A Lua table is considered to be an array if and only if its set of keys is a 14 | consecutive sequence of positive integers starting at 1. Arrays are encoded like 15 | so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json 16 | object, encoded like so: `{"key1": 2, "key2": false}`. 17 | 18 | Because the Lua nil value cannot be a key, and as a table value is considerd 19 | equivalent to a missing key, there is no way to express the json "null" value in 20 | a Lua table. The only way this will output "null" is if your entire input obj is 21 | nil itself. 22 | 23 | An empty Lua table, {}, could be considered either a json object or array - 24 | it's an ambiguous edge case. We choose to treat this as an object as it is the 25 | more general type. 26 | 27 | To be clear, none of the above considerations is a limitation of this code. 28 | Rather, it is what we get when we completely observe the json specification for 29 | as arbitrary a Lua object as json is capable of expressing. 30 | 31 | ## json.parse: 32 | 33 | This function parses json, with the exception that it does not pay attention to 34 | \u-escaped unicode code points in strings. 35 | 36 | It is difficult for Lua to return null as a value. In order to prevent the loss 37 | of keys with a null value in a json string, this function uses the one-off 38 | table value json.null (which is just an empty table) to indicate null values. 39 | This way you can check if a value is null with the conditional 40 | `val == json.null`. 41 | 42 | If you have control over the data and are using Lua, I would recommend just 43 | avoiding null values in your data to begin with. 44 | 45 | --]] 46 | 47 | 48 | local json = {} 49 | 50 | 51 | -- Internal functions. 52 | 53 | local function kind_of(obj) 54 | if type(obj) ~= 'table' then return type(obj) end 55 | local i = 1 56 | for _ in pairs(obj) do 57 | if obj[i] ~= nil then i = i + 1 else return 'table' end 58 | end 59 | if i == 1 then return 'table' else return 'array' end 60 | end 61 | 62 | local function escape_str(s) 63 | local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} 64 | local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} 65 | for i, c in ipairs(in_char) do 66 | s = s:gsub(c, '\\' .. out_char[i]) 67 | end 68 | return s 69 | end 70 | 71 | -- Returns pos, did_find; there are two cases: 72 | -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. 73 | -- 2. Delimiter not found: pos = pos after leading space; did_find = false. 74 | -- This throws an error if err_if_missing is true and the delim is not found. 75 | local function skip_delim(str, pos, delim, err_if_missing) 76 | pos = pos + #str:match('^%s*', pos) 77 | if str:sub(pos, pos) ~= delim then 78 | if err_if_missing then 79 | error('Expected ' .. delim .. ' near position ' .. pos) 80 | end 81 | return pos, false 82 | end 83 | return pos + 1, true 84 | end 85 | 86 | -- Expects the given pos to be the first character after the opening quote. 87 | -- Returns val, pos; the returned pos is after the closing quote character. 88 | local function parse_str_val(str, pos, val) 89 | val = val or '' 90 | local early_end_error = 'End of input found while parsing string.' 91 | if pos > #str then error(early_end_error) end 92 | local c = str:sub(pos, pos) 93 | if c == '"' then return val, pos + 1 end 94 | if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end 95 | -- We must have a \ character. 96 | local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} 97 | local nextc = str:sub(pos + 1, pos + 1) 98 | if not nextc then error(early_end_error) end 99 | return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) 100 | end 101 | 102 | -- Returns val, pos; the returned pos is after the number's final character. 103 | local function parse_num_val(str, pos) 104 | local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) 105 | local val = tonumber(num_str) 106 | if not val then error('Error parsing number at position ' .. pos .. '.') end 107 | return val, pos + #num_str 108 | end 109 | 110 | 111 | -- Public values and functions. 112 | 113 | function json.stringify(obj, as_key) 114 | local s = {} -- We'll build the string as an array of strings to be concatenated. 115 | local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. 116 | if kind == 'array' then 117 | if as_key then error('Can\'t encode array as key.') end 118 | s[#s + 1] = '[' 119 | for i, val in ipairs(obj) do 120 | if i > 1 then s[#s + 1] = ', ' end 121 | s[#s + 1] = json.stringify(val) 122 | end 123 | s[#s + 1] = ']' 124 | elseif kind == 'table' then 125 | if as_key then error('Can\'t encode table as key.') end 126 | s[#s + 1] = '{' 127 | for k, v in pairs(obj) do 128 | if #s > 1 then s[#s + 1] = ', ' end 129 | s[#s + 1] = json.stringify(k, true) 130 | s[#s + 1] = ':' 131 | s[#s + 1] = json.stringify(v) 132 | end 133 | s[#s + 1] = '}' 134 | elseif kind == 'string' then 135 | return '"' .. escape_str(obj) .. '"' 136 | elseif kind == 'number' then 137 | if as_key then return '"' .. tostring(obj) .. '"' end 138 | return tostring(obj) 139 | elseif kind == 'boolean' then 140 | return tostring(obj) 141 | elseif kind == 'nil' then 142 | return 'null' 143 | else 144 | error('Unjsonifiable type: ' .. kind .. '.') 145 | end 146 | return table.concat(s) 147 | end 148 | 149 | json.null = {} -- This is a one-off table to represent the null value. 150 | 151 | function json.parse(str, pos, end_delim) 152 | pos = pos or 1 153 | if pos > #str then error('Reached unexpected end of input.') end 154 | local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. 155 | local first = str:sub(pos, pos) 156 | if first == '{' then -- Parse an object. 157 | local obj, key, delim_found = {}, true, true 158 | pos = pos + 1 159 | while true do 160 | key, pos = json.parse(str, pos, '}') 161 | if key == nil then return obj, pos end 162 | if not delim_found then error('Comma missing between object items.') end 163 | pos = skip_delim(str, pos, ':', true) -- true -> error if missing. 164 | obj[key], pos = json.parse(str, pos) 165 | pos, delim_found = skip_delim(str, pos, ',') 166 | end 167 | elseif first == '[' then -- Parse an array. 168 | local arr, val, delim_found = {}, true, true 169 | pos = pos + 1 170 | while true do 171 | val, pos = json.parse(str, pos, ']') 172 | if val == nil then return arr, pos end 173 | if not delim_found then error('Comma missing between array items.') end 174 | arr[#arr + 1] = val 175 | pos, delim_found = skip_delim(str, pos, ',') 176 | end 177 | elseif first == '"' then -- Parse a string. 178 | return parse_str_val(str, pos + 1) 179 | elseif first == '-' or first:match('%d') then -- Parse a number. 180 | return parse_num_val(str, pos) 181 | elseif first == end_delim then -- End of an object or array. 182 | return nil, pos + 1 183 | else -- Parse true, false, or null. 184 | local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} 185 | for lit_str, lit_val in pairs(literals) do 186 | local lit_end = pos + #lit_str - 1 187 | if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end 188 | end 189 | local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) 190 | error('Invalid json syntax starting at ' .. pos_info_str) 191 | end 192 | end 193 | 194 | return json -------------------------------------------------------------------------------- /lib/plugins/Store.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Store.js - in-memory store which mocks some very simple redis commands for local development / testing 4 | 5 | Important: You should not be using this Store in production settings. 6 | Instead of using the default memory store, pass in a node-redis client instead 7 | 8 | */ 9 | 10 | var Store = function Store (provider, plugin) { 11 | var self = this; 12 | self.services = {}; 13 | self.files = {}; 14 | console.log('Warning: ' + plugin + ' is using default memory store. This is not suitable for production. Instead pass in a node-redis client.'); 15 | return self; 16 | }; 17 | module.exports = Store; 18 | Store.prototype.get = function (service, cb) { 19 | cb(null, this.services[service] || 0) 20 | }; 21 | Store.prototype.set = function (key, val, cb) { 22 | this.services[key] = val; 23 | cb(null, true) 24 | }; 25 | 26 | Store.prototype.mget = function (services, cb) { 27 | var res = [], self = this; 28 | services.forEach(function(s){ 29 | self.get(s, function(err, r){ 30 | res.push(r); 31 | }) 32 | }); 33 | cb(null, res) 34 | }; 35 | 36 | Store.prototype.incr = function (service) { 37 | // callback not required, fire and forget to update value 38 | this.services[service] = this.services[service] || 0; 39 | this.services[service] += 1; 40 | }; 41 | 42 | Store.prototype.incrby = function (service, by) { 43 | if (typeof by === "undefined") { 44 | by = 1; 45 | } 46 | this.services[service] = this.services[service] || 0; 47 | this.services[service] += by; 48 | }; 49 | 50 | // 51 | // Important: Redis sorted set commands ( zscore and zincrby ) are mocked here, 52 | // and will not perform a true sorted set operation. 53 | // These methods are only used for local dev and testing. 54 | // 55 | Store.prototype.zscore = function zscore (zset, member, cb) { 56 | this.get(zset + '/' + member, cb) 57 | }; 58 | Store.prototype.zincrby = function zincrby (args, cb) { 59 | if (typeof cb !== "function") { 60 | cb = function (err, res) { 61 | } 62 | } 63 | this.incrby(args[0] + "/" + args[2], args[1]) 64 | cb(null); 65 | }; 66 | 67 | Store.prototype.sadd = function sadd (key, value, cb) { 68 | this.services[key] = this.services[key] || []; 69 | // Note: will always add new and not overwrite ( this is not standard sadd behavior ) 70 | this.services[key].push(value); 71 | cb(null, 'added'); 72 | }; 73 | 74 | Store.prototype.zadd = function zadd (set, member, val, cb) { 75 | // TODO: add this method 76 | cb(null, 'added'); 77 | }; 78 | 79 | Store.prototype.smembers = function (key, cb) { 80 | cb(null, this.services[key]); 81 | }; 82 | 83 | // 84 | // hash type 85 | // 86 | 87 | /* 88 | Store.prototype.hdel = function hdel (key, cb) { 89 | cb(null, this.services[key]); 90 | }; 91 | */ 92 | 93 | Store.prototype.hget = function hget (key, field, cb) { 94 | if (typeof this.services[key] === 'undefined') { 95 | return cb(null, null); 96 | } 97 | cb(null, this.services[key][field]); 98 | }; 99 | 100 | Store.prototype.hkeys = function hkeys (key, cb) { 101 | cb(null, Object.keys(this.services[key])); 102 | }; 103 | 104 | Store.prototype.hset = function hset (key, field, value, cb) { 105 | cb = cb || function () {}; 106 | this.services[key] = this.services[key] || {}; 107 | this.services[key][field] = value; 108 | cb(null, 1); 109 | }; 110 | 111 | Store.prototype.hgetall = function hgetall (key, cb) { 112 | cb(null, this.services[key]); 113 | }; 114 | 115 | Store.prototype.hincrby = function hincrby (key, field, value, cb) { 116 | this.services[key] = this.services[key] || {}; 117 | if (typeof this.services[key][field] !== 'number') { 118 | this.services[key][field] = 0; 119 | } 120 | this.services[key][field] = this.services[key][field] + value; // could be negative value to decrement 121 | cb(null, 1); 122 | }; -------------------------------------------------------------------------------- /lib/plugins/bodyParser.js: -------------------------------------------------------------------------------- 1 | var psr = require('parse-service-request'); 2 | 3 | module.exports = function bodyParserMiddleware () { 4 | return function bodyParserHandler (input, output, next) { 5 | psr(input, output, function(req, res){ 6 | next(); 7 | }); 8 | } 9 | } -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceCode/gcc/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var crypto = require('crypto'); 4 | var path = require('path'); 5 | 6 | module.exports = function (req, res, cb) { 7 | 8 | var service = req.service; 9 | 10 | var hash = service.sha1; 11 | 12 | // first, we need to write the file to a temporary location on the disk 13 | // it could be possible to bypass writing to the disk entirely here, but it will be easier to debug user-scripts, 14 | // and also interact with various compilers without having to look up special use cases for stdin / argv code parsing 15 | var outputPath = service.buildDir + '/' + hash + '.c'; 16 | console.log('writing to', outputPath) 17 | fs.writeFile(outputPath, service.code, function (err, result) { 18 | 19 | if (err) { 20 | return cb(err); 21 | } 22 | console.log('wrote file with sucess!'); 23 | console.log('starting compiler gcc', 'gcc', ['-o', hash, outputPath]); 24 | 25 | var compiler = spawn('gcc', ['-std=c99', '-o', hash, outputPath], { 26 | cwd: service.releaseDir // ensure binary compiles to target directory 27 | }); 28 | 29 | var stderr = '', stdout = ''; 30 | 31 | compiler.on('error', function(data){ 32 | console.log('error', data) 33 | cb(data) 34 | }); 35 | 36 | /* 37 | vm.stdin.error 38 | vm.exit 39 | vm.stdout.end 40 | vm.stderr 41 | */ 42 | 43 | compiler.on('data', function(data){ 44 | console.log('got data', data) 45 | }); 46 | 47 | compiler.on('close', function(data){ 48 | console.log('close', data) 49 | //cb(null, 'close') 50 | }); 51 | 52 | compiler.stdin.on('error', function(data){ 53 | console.log('stdin.error', data); 54 | // cb(null, 'close') 55 | }); 56 | 57 | compiler.stdout.on('data', function(data){ 58 | console.log('stdout', data); 59 | stdout += data.toString(); 60 | //cb(null, 'close') 61 | }); 62 | 63 | compiler.stderr.on('data', function(data){ 64 | // console.log('stderr', data); 65 | stderr += data.toString(); 66 | //cb(null, 'close') 67 | }); 68 | 69 | compiler.on('exit', function(data){ 70 | console.log('exit', data) 71 | var result = { 72 | tmpSourceFile: outputPath, 73 | bin: service.releaseDir + '/' + hash, 74 | stderr: stderr, 75 | stdout: stdout, 76 | exitCode: data 77 | }; 78 | cb(null, result); 79 | }); 80 | 81 | }); 82 | 83 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceCode/golang/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var crypto = require('crypto'); 4 | var path = require('path'); 5 | 6 | module.exports = function (req, res, cb) { 7 | 8 | var service = req.service; 9 | 10 | var hash = service.sha1; 11 | 12 | // first, we need to write the file to a temporary location on the disk 13 | // it could be possible to bypass writing to the disk entirely here, but it will be easier to debug user-scripts, 14 | // and also interact with various compilers without having to look up special use cases for stdin / argv code parsing 15 | var outputPath = service.buildDir + '/' + hash + '.go'; 16 | console.log('writing to', outputPath) 17 | fs.writeFile(outputPath, service.code, function (err, result) { 18 | 19 | if (err) { 20 | return cb(err); 21 | } 22 | console.log('wrote file with sucess!'); 23 | console.log('starting compiler', 'go', ['build', outputPath]); 24 | 25 | var compiler = spawn('go', ['build', outputPath], { 26 | cwd: service.releaseDir // ensure binary compiles to target directory 27 | }); 28 | 29 | var stderr = '', stdout = ''; 30 | 31 | compiler.on('error', function(data){ 32 | console.log('error', data) 33 | cb(data) 34 | }); 35 | 36 | /* 37 | vm.stdin.error 38 | vm.exit 39 | vm.stdout.end 40 | vm.stderr 41 | */ 42 | 43 | compiler.on('data', function(data){ 44 | console.log('got data', data) 45 | }); 46 | 47 | compiler.on('close', function(data){ 48 | console.log('close', data) 49 | //cb(null, 'close') 50 | }); 51 | 52 | compiler.stdin.on('error', function(data){ 53 | console.log('stdin.error', data); 54 | // cb(null, 'close') 55 | }); 56 | 57 | compiler.stdout.on('data', function(data){ 58 | console.log('stdout', data); 59 | stdout += data.toString(); 60 | //cb(null, 'close') 61 | }); 62 | 63 | compiler.stderr.on('data', function(data){ 64 | // console.log('stderr', data); 65 | stderr += data.toString(); 66 | //cb(null, 'close') 67 | }); 68 | 69 | compiler.on('exit', function(data){ 70 | console.log('exit', data) 71 | var result = { 72 | tmpSourceFile: outputPath, 73 | bin: service.releaseDir + '/' + hash, 74 | stderr: stderr, 75 | stdout: stdout, 76 | exitCode: data 77 | }; 78 | cb(null, result); 79 | }); 80 | 81 | }); 82 | 83 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceCode/java/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var crypto = require('crypto'); 4 | var path = require('path'); 5 | var mkdirp = require('mkdirp'); 6 | 7 | module.exports = function (req, res, cb) { 8 | 9 | var service = req.service; 10 | 11 | var hash = service.sha1; 12 | 13 | // first, we need to write the file to a temporary location on the disk 14 | // it could be possible to bypass writing to the disk entirely here, but it will be easier to debug user-scripts, 15 | // and also interact with various compilers without having to look up special use cases for stdin / argv code parsing 16 | var outputPath = service.buildDir + '/' + hash + '/' + 'hook' + '.java'; 17 | // service.releaseDir = service.releaseDir + '/' + hash; 18 | console.log('writing to', outputPath) 19 | 20 | // Java requires that the .class file be the same name as the exported code class 21 | // This means, we have to create a unique directory for the binary ( unlike other compiled langs which use only uninque file names ) 22 | var outputDir = service.buildDir + '/' + hash; 23 | mkdirp(outputDir, function(err){ 24 | if (err) { 25 | return res.end(err.message); 26 | } 27 | fs.writeFile(outputPath, service.code, function (err, result) { 28 | 29 | if (err) { 30 | return cb(err); 31 | } 32 | console.log('wrote file with sucess!'); 33 | console.log('starting compiler javac', 'javac', [outputPath]); 34 | 35 | var compiler = spawn('javac', [outputPath], { 36 | cwd: service.releaseDir // ensure binary compiles to target directory 37 | }); 38 | 39 | var stderr = '', stdout = ''; 40 | 41 | compiler.on('error', function(data){ 42 | console.log('error', data) 43 | cb(data) 44 | }); 45 | 46 | /* 47 | vm.stdin.error 48 | vm.exit 49 | vm.stdout.end 50 | vm.stderr 51 | */ 52 | 53 | compiler.on('data', function(data){ 54 | console.log('got data', data) 55 | }); 56 | 57 | compiler.on('close', function(data){ 58 | console.log('close', data) 59 | //cb(null, 'close') 60 | }); 61 | 62 | compiler.stdin.on('error', function(data){ 63 | console.log('stdin.error', data); 64 | // cb(null, 'close') 65 | }); 66 | 67 | compiler.stdout.on('data', function(data){ 68 | console.log('stdout', data); 69 | stdout += data.toString(); 70 | //cb(null, 'close') 71 | }); 72 | 73 | compiler.stderr.on('data', function(data){ 74 | // console.log('stderr', data); 75 | stderr += data.toString(); 76 | //cb(null, 'close') 77 | }); 78 | 79 | compiler.on('exit', function(data){ 80 | console.log('exit', data) 81 | var result = { 82 | tmpSourceFile: outputPath, 83 | buildDir: service.buildDir + '/' + hash, 84 | sha1: hash, 85 | bin: service.buildDir + '/' + hash + '/' + 'hook', // java is strange 86 | stderr: stderr, 87 | stdout: stdout, 88 | exitCode: data 89 | }; 90 | cb(null, result); 91 | }); 92 | 93 | }); 94 | }); 95 | 96 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceCode/r/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var crypto = require('crypto'); 4 | var path = require('path'); 5 | 6 | module.exports = function (req, res, cb) { 7 | 8 | var service = req.service; 9 | var params = req.resource.params; 10 | 11 | var hash = service.sha1; 12 | 13 | var outputPath = service.buildDir + '/' + hash + '.r'; 14 | // console.log('writing to', outputPath) 15 | var RInject = "hook <- list()\n"; 16 | 17 | for (var p in params) { 18 | if (typeof params[p] === "object") { 19 | for (var s in params[p]) { 20 | RInject += 'hook[[ "' + p + '" ]] <- "' + params[p][s] + '"\n'; 21 | } 22 | } else { 23 | RInject += 'hook[[ "' + p + '" ]] <- "' + params[p] + '"\n'; 24 | } 25 | } 26 | 27 | // inject hook service code into "compiled" R script 28 | service.code = RInject + '\n\n' + service.code; 29 | fs.writeFile(outputPath, service.code, function (err, result) { 30 | if (err) { 31 | return cb(err); 32 | } 33 | var result = { 34 | tmpSourceFile: outputPath, 35 | bin: service.releaseDir + '/' + hash, 36 | stderr: "", 37 | stdout: "", 38 | exitCode: 0 39 | }; 40 | cb(null, result); 41 | }); 42 | 43 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceCode/rust/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var crypto = require('crypto'); 4 | var path = require('path'); 5 | 6 | module.exports = function (req, res, cb) { 7 | 8 | var service = req.service; 9 | 10 | var hash = service.sha1; 11 | 12 | // first, we need to write the file to a temporary location on the disk 13 | // it could be possible to bypass writing to the disk entirely here, but it will be easier to debug user-scripts, 14 | // and also interact with various compilers without having to look up special use cases for stdin / argv code parsing 15 | var outputPath = service.buildDir + '/' + hash + '.rs'; 16 | console.log('writing to', outputPath) 17 | fs.writeFile(outputPath, service.code, function (err, result) { 18 | 19 | if (err) { 20 | return cb(err); 21 | } 22 | console.log('wrote file with sucess!'); 23 | console.log('starting compiler rust', 'rust', [outputPath]); 24 | 25 | var compiler = spawn('rustc', [outputPath], { 26 | cwd: service.releaseDir // ensure binary compiles to target directory 27 | }); 28 | 29 | var stderr = '', stdout = ''; 30 | 31 | compiler.on('error', function(data){ 32 | console.log('error', data) 33 | cb(data); 34 | }); 35 | 36 | /* 37 | vm.stdin.error 38 | vm.exit 39 | vm.stdout.end 40 | vm.stderr 41 | */ 42 | 43 | compiler.on('data', function(data){ 44 | console.log('got data', data) 45 | }); 46 | 47 | compiler.on('close', function(data){ 48 | console.log('close', data) 49 | //cb(null, 'close') 50 | }); 51 | 52 | compiler.stdin.on('error', function(data){ 53 | console.log('stdin.error', data); 54 | // cb(null, 'close') 55 | }); 56 | 57 | compiler.stdout.on('data', function(data){ 58 | console.log('stdout', data); 59 | stdout += data.toString(); 60 | //cb(null, 'close') 61 | }); 62 | 63 | compiler.stderr.on('data', function(data){ 64 | // console.log('stderr', data); 65 | stderr += data.toString(); 66 | //cb(null, 'close') 67 | }); 68 | 69 | compiler.on('exit', function(data){ 70 | console.log('exit', data) 71 | var result = { 72 | tmpSourceFile: outputPath, 73 | bin: service.releaseDir + '/' + hash, 74 | stderr: stderr, 75 | stdout: stdout, 76 | exitCode: data 77 | }; 78 | cb(null, result); 79 | }); 80 | 81 | }); 82 | 83 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/compileServiceMappings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "java": require('./compileServiceCode/java'), 3 | "gcc": require('./compileServiceCode/gcc'), 4 | "go": require('./compileServiceCode/golang'), 5 | "rust": require('./compileServiceCode/rust'), 6 | "r": require('./compileServiceCode/r') 7 | }; -------------------------------------------------------------------------------- /lib/plugins/compile/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var shasum = require('shasum'); 3 | var compileService = require('./compileServiceMappings'); 4 | var fs = require('fs'); 5 | require('colors'); 6 | module.exports = function compile (config) { 7 | // TODO: update API to use config... 8 | var Store = require('../Store'); 9 | 10 | config = config || {}; 11 | 12 | config.buildDir = path.resolve(__dirname + '/../../../tmp'); 13 | 14 | // if no releaseDir is specified, will default to ./microcule/release directory 15 | config.releaseDir = config.releaseDir || config.service.home + '/release'; 16 | 17 | // console.log('using compile config', config) 18 | 19 | config.errorHandler = config.errorHandler || function (err, next) { 20 | if (err.message === "Bad credentials") { 21 | return next(new Error('Invalid Github oauth key: ' + config.token)); 22 | } 23 | if (err.message === "Not Found") { 24 | return next(new Error('Could not load: ' + config.gistID)); 25 | } 26 | return next(err); 27 | } 28 | 29 | // provider should be an instance of a node-redis client 30 | var provider = config.provider || new Store('memory', 'Compiled Language Plugin'); 31 | 32 | var _service = {}; 33 | _service.home = config.service.home; 34 | 35 | _service.code = config.service.code; 36 | // _service.schema = service.schema; 37 | _service.language = config.service.lang || config.service.language; 38 | 39 | _service.buildDir = config.buildDir; 40 | _service.releaseDir = config.releaseDir; 41 | 42 | // TODO: add provider pattern for state cache ( look into babel / coffee ) 43 | 44 | if (typeof _service.code === "undefined" || typeof _service.language === "undefined") { 45 | throw new Error('invalid service object'); 46 | } 47 | 48 | var targetCompiler = compileService[_service.language]; 49 | if (typeof targetCompiler === "undefined") { 50 | throw new Error('invalid language choice: ' + _service.language); 51 | } 52 | 53 | // create shasum1 of service source code contents 54 | var sha = shasum(_service.code); 55 | _service.sha1 = sha; 56 | // console.log('what is sha1', sha); 57 | 58 | return function compileMiddleware (req, res, next) { 59 | req.service = _service; 60 | 61 | // check if binary of same sha1 already exists ( has been previously compiled ) 62 | // if compiled service already exists, simply continue with spawn and defined binary path 63 | 64 | var absoluteBinLocation, relatitveBinLocation; 65 | 66 | absoluteBinLocation = _service.releaseDir + '/' + sha; 67 | relatitveBinLocation = '/release/' + sha; 68 | 69 | // java requires a fixed class name, so we must create a unique directory instead 70 | if (_service.language === 'java') { 71 | absoluteBinLocation = _service.buildDir + '/' + sha + '/' + 'hook.class'; 72 | } 73 | 74 | // TODO: check that sha matches as well as language? potential issue here with same content files, but diffirent target languages 75 | // we will assume that if the binary file exists, it has completed it's build step and is ready to be run 76 | fs.stat(absoluteBinLocation, function (err, _stat) { 77 | // console.log('back from stat', err, _stat); 78 | if (err) { 79 | // could not find the file, attempt to compile it 80 | // console.log('could not find file, so lets compile it'.red); 81 | 82 | // Remark: Implementes a mutually exclusive lock for compilation step ( do not attempt to compile twice ) 83 | // If service is compiling, simply return a 202 Accepted status until it's ready 84 | // mutually exclusive lock state is stored in Provider ( could be in-memory or in Redis ) 85 | 86 | // check to see if provider indicates that service is building 87 | provider.get('/builds/' + sha, function (err, result){ 88 | // console.log('back from provider get'.green, err, result) 89 | if (err) { 90 | return res.end(err.message); 91 | } 92 | if (result === 0) { 93 | // TODO: null result means no build status, create a new build status 94 | provider.set('/builds/' + sha, { status: 'building' }, function (err, result){ 95 | // console.log('back from provider set', err, result) 96 | if (err) { 97 | return res.end(err.message); 98 | } 99 | // introduce delay for dev of mutex 100 | // setTimeout(function(){ 101 | targetCompiler(req, res, function(err, result) { 102 | if (err) { 103 | res.end(err.message); 104 | } 105 | next(null, result); 106 | }); 107 | // }, 10000); 108 | }); 109 | } else { 110 | // if we couldn't find the compiled binary, and any entry exists for it in the DB, its probably building 111 | // this way, we can easily just delete the binary on the file-system to trigger a safe rebuild 112 | res.writeHead(202); 113 | res.end('currently building. please try again shortly'); 114 | } 115 | }); 116 | } else { 117 | // we find the file, attempt to execute it 118 | // if the stat returned ( a file ) then use that path instead of compiling a new one 119 | // console.log('using compiled version of service', binLocation); 120 | var result = { 121 | bin: _service.home + relatitveBinLocation, 122 | buildDir: _service.buildDir + '/' + sha, 123 | sha1: sha, 124 | compiledFresh: false, 125 | foundCompiledCache: true, 126 | stderr: '', 127 | stdout: '' 128 | }; 129 | next(null, result); 130 | } 131 | 132 | }); 133 | } 134 | } -------------------------------------------------------------------------------- /lib/plugins/logger.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | return function loggerHandler (req, res, next) { 3 | console.log('running service ' + req.url); 4 | next(); 5 | } 6 | } -------------------------------------------------------------------------------- /lib/plugins/mschema.js: -------------------------------------------------------------------------------- 1 | var mschema = require("mschema"); 2 | var bodyParser = require('./bodyParser'); 3 | 4 | module.exports = function validateParamsMiddleware (schema) { 5 | return function validateParamsHandler (input, output, next) { 6 | // if no schema is provided, bypass body parsing and validation code 7 | // this helps performance of parent function in case it sends an empty schema 8 | if (typeof schema !== 'object' || Object.keys(schema).length === 0) { 9 | return next(); 10 | } 11 | //console.log('mschema validate', input.resource.params, input.resource.schema); 12 | // we need to parse the body in order to perform schema validation of incoming POST data ( JSON or form ) 13 | bodyParser()(input, output, function () { 14 | input.resource = input.resource || {}; 15 | input.resource.schema = schema || input.resource.schema || {}; 16 | input.resource.params = input.resource.params || {}; 17 | var validate = mschema.validate(input.resource.params, input.resource.schema, { strict: false }); 18 | //console.log(validate.instance) 19 | if (validate.valid) { 20 | input.resource.instance = validate.instance; 21 | next(); 22 | } else { 23 | // if schema did not validate, end the response immediately 24 | // send the error as JSON back to the client 25 | return output.json(validate.errors) 26 | } 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /lib/plugins/rateLimiter/index.js: -------------------------------------------------------------------------------- 1 | // rateLimiter/index.js 2 | 3 | /* 4 | 5 | HTTP Plugin responsible for rate-limiting requests based on recorded usage metrics 6 | 7 | The rateLimiter plugin expects a 'provider' option is passed into it's configuration for storing usage metrics 8 | This provider will default to an in-memory, but can easily be extended to use Redis by passing in a Redis client 9 | 10 | The rateLimiter 'provider' requires the following methods: 11 | 12 | provider.hincrby 13 | provider.hget 14 | provider.hset 15 | 16 | If you are using a redis client or the build-in memory store as the provider, these methods should already available. 17 | 18 | By default, Rate-limiting information is sent back to the client request with the following HTTP headers: 19 | 20 | X-RateLimit-Limit - Total amount of requests processed during current period 21 | X-RateLimit-Remaining - Amount of requests remaining during current period 22 | X-RateLimit-Running - Total amount of currently running services ( current concurrency count ) 23 | TODO: X-RateLimit-Concurrency - Amount of concurrency supported by this request 24 | TODO: X-RateLimit-Reset - Estimated time concurrency will reset 25 | 26 | */ 27 | 28 | /* 29 | 30 | Usage Metric Formats 31 | 32 | There are currently three unique metric reports ( hashes ) that we are tracking on each request. 33 | 34 | 1. The system usage report, contains global stats for system. This is used primarily for admins or system-wide dashboards. 35 | 36 | var systemReport = { 37 | running: 40, 38 | totalHits: 10000000000 39 | }; 40 | 41 | 2. The user usage report, contains stats for single user. This is used to track service plan limits per user. 42 | var userReport = { 43 | running: 8, 44 | hits: 10000, 45 | totalHits: 400000 46 | }; 47 | 48 | 3. The service usage report, contains stats for single service. This is used to help track the state of an individual service. 49 | var serviceReport = { 50 | running: 4, 51 | hits: 1000, 52 | totalHits: 40000, 53 | lastRun: new Date(), 54 | lastCompleted: new Date(), 55 | lastStatusCode: 200 56 | } 57 | 58 | */ 59 | 60 | var Store = require('../Store'); 61 | 62 | function RateLimiter (config) { 63 | var self = this; 64 | config = config || {}; 65 | // provider should be an instance of a node-redis client 66 | self.provider = config.provider || new Store('memory', 'Rate-Limiter'); 67 | return self; 68 | } 69 | 70 | RateLimiter.prototype.registerService = function (data, cb) { 71 | var self = this; 72 | cb = cb || function () {}; 73 | var serviceReportKey = '/' + data.owner + '/' + data.name + '/report'; 74 | self.provider.hset(serviceReportKey, 'totalHits', 0, function (err) { 75 | cb(err); 76 | }); 77 | }; 78 | 79 | RateLimiter.prototype.removeService = function (data, cb) { 80 | var self = this; 81 | cb = cb || function () {}; 82 | var serviceReportKey = '/' + data.owner + '/' + data.name + '/report'; 83 | self.provider.del(serviceReportKey, function (err) { 84 | cb(err); 85 | }); 86 | }; 87 | 88 | // TODO: rate limiter needs to be constructed instance in order to support instance methods for register / remove 89 | 90 | RateLimiter.prototype.middle = function rateLimitingMiddleware (config) { 91 | var self = this; 92 | var provider = self.provider; 93 | 94 | config.maxLimit = config.maxLimit || 1000; 95 | config.maxConcurrency = config.maxConcurrency || 2; 96 | 97 | config.maxConcurrencyMessage = 'Rate limited: Max concurrency limit hit: ' + config.maxConcurrency; 98 | config.maxLimitMessage = 'Rate limited: Max services limit hit: ' + config.maxLimit; 99 | 100 | return function rateLimitingHandler (req, res, next) { 101 | 102 | // TODO: better default identity provider, perhaps get user name from system 103 | var owner = req.params.owner || 'anonymous'; 104 | var hook = req.params.hook || req.url.replace('/', ''); 105 | 106 | var now = new Date(); 107 | var systemKey = '/system/report'; 108 | var userKey = '/' + owner + '/report'; 109 | var serviceKey = '/' + owner + '/' + hook; 110 | var serviceReportKey = '/' + owner + '/' + hook + '/report'; 111 | var monthlyHitsKey = 'monthlyHits - ' + (now.getMonth() + 1) + '/' + now.getFullYear(); 112 | 113 | function incrementHits (cb) { 114 | // 115 | // Remark: node-redis client should be able to pipeline these requests automatically since they are started from the same context 116 | // We could consider using client.multi() to improve performance 117 | 118 | // 119 | // System metrics 120 | // 121 | // sorted set for containing total top hits and running ( useful for tracking active services and users ) 122 | provider.zincrby(['hits', 1, owner]); 123 | provider.zincrby(['running', 1, owner]); 124 | 125 | // 126 | // Update Service Usage Report 127 | // 128 | // how many of this service is running 129 | provider.hincrby(serviceReportKey, 'running', 1, function (err) { 130 | if (err) { 131 | return console.log('error: saving metrics', serviceReportKey); 132 | } 133 | }); 134 | 135 | // last time this service was started 136 | provider.hset(serviceReportKey, 'lastStart', new Date().getTime(), function (err) { 137 | if (err) { 138 | return console.log('error: saving metrics', serviceReportKey); 139 | } 140 | }); 141 | req._lastStart = new Date().getTime(); 142 | 143 | // total times this service was run 144 | provider.hincrby(serviceReportKey, 'totalHits', 1, function (err) { 145 | if (err) { 146 | return console.log('error: saving metrics', serviceReportKey); 147 | } 148 | }); 149 | 150 | // monthlyHits 151 | provider.hincrby(serviceReportKey, monthlyHitsKey, 1, function (err) { 152 | if (err) { 153 | return console.log('error: saving metrics', serviceReportKey); 154 | } 155 | }); 156 | 157 | // 158 | // Update User Usage Report 159 | // 160 | 161 | // how many of this service is running 162 | var userKey = '/' + owner + '/report'; 163 | provider.hincrby(userKey, 'running', 1, function (err) { 164 | if (err) { 165 | return console.log('error: saving metrics', serviceReportKey); 166 | } 167 | // most important metric, must wait for result 168 | cb(); 169 | }); 170 | 171 | // total hits user has accumlated 172 | provider.hincrby(userKey, 'totalHits', 1, function (err) { 173 | if (err) { 174 | return console.log('error: saving metrics', serviceReportKey); 175 | } 176 | }); 177 | 178 | // total monthly hits user has accumlated 179 | provider.hincrby(userKey, monthlyHitsKey, 1, function (err) { 180 | if (err) { 181 | return console.log('error: saving metrics', serviceReportKey); 182 | } 183 | }); 184 | 185 | // 186 | // Update System Report with new stats 187 | // 188 | 189 | // total running services on system 190 | provider.hincrby(systemKey, 'running', 1, function (err) { 191 | if (err) { 192 | return console.log('error: saving metrics', serviceReportKey); 193 | } 194 | }); 195 | 196 | // total hits system has accumlated 197 | provider.hincrby(systemKey, 'totalHits', 1, function (err) { 198 | if (err) { 199 | return console.log('error: saving metrics', serviceReportKey); 200 | } 201 | }); 202 | } 203 | 204 | function incrementRunning (res, val) { 205 | provider.hincrby(userKey, 'running', val, function (err) { 206 | if (err) { 207 | return console.log('error: saving metrics', serviceReportKey); 208 | } 209 | }); 210 | provider.hincrby(systemKey, 'running', val, function (err) { 211 | if (err) { 212 | return console.log('error: saving metrics', serviceReportKey); 213 | } 214 | }); 215 | 216 | let recentKey = 'recent'; 217 | if (res.statusCode === 500) { 218 | recentKey += ':500'; 219 | } 220 | 221 | provider.zadd(recentKey, new Date().getTime(), serviceKey, function (err) { 222 | if (err) { 223 | return console.log('error: saving metrics/recent', serviceKey, err); 224 | } 225 | }); 226 | 227 | // 228 | // update service report properties ( such as last known statusCode, lastTime, lastEnd ) 229 | // 230 | provider.hincrby(serviceReportKey, 'running', val, function (err) { 231 | if (err) { 232 | return console.log('error: saving metrics', serviceReportKey); 233 | } 234 | }); 235 | 236 | provider.hset(serviceReportKey, 'lastEnd', new Date().getTime(), function (err) { 237 | if (err) { 238 | return console.log('error: saving metrics', serviceReportKey); 239 | } 240 | }); 241 | 242 | provider.hset(serviceReportKey, 'lastTime', new Date().getTime() - req._lastStart, function (err) { 243 | if (err) { 244 | return console.log('error: saving metrics', serviceReportKey); 245 | } 246 | }); 247 | 248 | provider.hset(serviceReportKey, 'statusCode', res.statusCode, function (err) { 249 | if (err) { 250 | return console.log('error: saving metrics', serviceReportKey); 251 | } 252 | }); 253 | provider.zincrby(['running', val, owner]); 254 | } 255 | 256 | res.on('close', function(){ 257 | // console.log("res.close".magenta, res.statusCode); 258 | if (req.reduceCount === false) { 259 | // do nothing 260 | } else { 261 | // provider.zincrby(['running', -1, owner]); 262 | // decrement running total for user, system, and service reports 263 | incrementRunning(res, -1); 264 | } 265 | 266 | }); 267 | 268 | res.on('finish', function(){ 269 | // console.log("res.finish".magenta, res.statusCode); 270 | if (req.reduceCount === false) { 271 | // do nothing 272 | } else { 273 | // decrement running total for user, system, and service reports 274 | incrementRunning(res, -1); 275 | } 276 | }); 277 | 278 | res.setHeader('X-RateLimit-Limit', config.maxLimit); 279 | 280 | // get monthly usage from user metric report 281 | provider.hget(userKey, monthlyHitsKey, function (err, monthlyHits) { 282 | monthlyHits = monthlyHits || 0; 283 | provider.hget(serviceReportKey, 'totalHits', function (err, totalHits) { 284 | if (totalHits === null || typeof totalHits === 'undefined') { 285 | req.reduceCount = false; 286 | // TODO: use callback with new error 287 | console.log('error: will not route unregistered service ' + serviceKey); 288 | var error = new Error('Will not route unregistered service: ' + serviceKey); 289 | error.code = 'UNREGISTERED_SERVICE'; 290 | // Remark: Rely on calling function to handle http status and response after rate limit event 291 | return next(error); 292 | } 293 | 294 | // if total hits for user account is exceeded, rate-limit 295 | if (Number(monthlyHits) >= config.maxLimit) { 296 | res.setHeader('X-RateLimit-Remaining', '0'); 297 | req.reduceCount = false; 298 | var error = new Error('Rate Limit Exceeded!'); 299 | error.code = 'RATE_LIMIT_EXCEEDED'; 300 | error.currentMonthlyHits = monthlyHits; 301 | error.monthlyLimit = config.maxLimit; 302 | return incrementHits(function(){ 303 | next(error); 304 | }); 305 | } 306 | 307 | res.setHeader('X-RateLimit-Remaining', (config.maxLimit - monthlyHits).toString()); 308 | 309 | // Get total amount of running hooks for current user 310 | // get currently running from user metric report 311 | provider.hget(userKey, 'running', function (err, totalRunning) { 312 | if (err) { 313 | return res.end(err.message); 314 | } 315 | if (totalRunning === null) { 316 | totalRunning = 0; 317 | } 318 | res.setHeader('X-RateLimit-Running', totalRunning.toString()); 319 | // console.log('metric.' + owner + '.running'.green, total, config.maxConcurrency) 320 | // if total running is greater than account concurrency limit, rate-limit the request 321 | if (Number(totalRunning) >= config.maxConcurrency) { 322 | req.reduceCount = false; 323 | var error = new Error('Rate Concurrency Exceeded!'); 324 | error.currentTotalRunning = totalRunning; 325 | error.maxConcurrency = config.maxConcurrency; 326 | error.code = 'RATE_CONCURRENCY_EXCEEDED'; 327 | // Remark: Rely on calling function to handle http status and response after rate limit event 328 | return next(error); 329 | } 330 | 331 | return incrementHits(function(){ 332 | next(); 333 | }); 334 | 335 | }); 336 | 337 | }); 338 | }); 339 | }; 340 | }; 341 | 342 | module.exports = RateLimiter; 343 | -------------------------------------------------------------------------------- /lib/plugins/sourceGithubGist.js: -------------------------------------------------------------------------------- 1 | // sourceGithubGist.js - pulls microservice source code from Github Gist 2 | // requires the `octonode` package and uses Github V3 REST API for pulling content 3 | 4 | module.exports = function sourceGithubGist (config) { 5 | 6 | var github = require('octonode'); 7 | 8 | var Store = require('./Store'); 9 | 10 | config = config || {}; 11 | config.errorHandler = config.errorHandler || function (err, next) { 12 | if (err.message === "Bad credentials") { 13 | return next(new Error('Invalid Github oauth key: ' + config.token)); 14 | } 15 | if (err.message === "Not Found") { 16 | return next(new Error('Could not load: ' + config.gistID)); 17 | } 18 | return next(err); 19 | } 20 | 21 | // provider should be an instance of a node-redis client 22 | var provider = config.provider || new Store('memory', 'Source Github Repo'); 23 | 24 | return function sourceGithubGistHandler (req, res, next) { 25 | 26 | var key = config.gistID; 27 | 28 | // TODO: better support for pulling schema / view / presenter 29 | // TODO: better package.json support for main 30 | // check to see if there is a copy of the source code in the cache 31 | provider.get(key, function (err, repo) { 32 | if (err) { 33 | return next(err); 34 | } 35 | if (repo === 0) { 36 | // if a copy of the source code was not found, load it, use it, and set it 37 | fetchSourceCodeFromGithubGist(); 38 | } else { 39 | // if a copy of the source code exists in the cache, use it 40 | req.code = repo.code; 41 | next(); 42 | } 43 | }); 44 | 45 | function fetchSourceCodeFromGithubGist () { 46 | // console.log('fetchSourceCodeFromGithubGist'); 47 | // in the middleware configuration, assume we already have an access token 48 | // access token must be done through github oauth login 49 | var token = config.token; 50 | 51 | if (typeof token === "undefined") { 52 | throw new Error('token is a required option!'); 53 | } 54 | 55 | var client = github.client(token); 56 | 57 | var main = config.main; 58 | var gistID = config.gistID || null; 59 | var ghgist = client.gist(); 60 | 61 | ghgist.get(gistID, function(err, g){ 62 | if (err) { 63 | if (err.message === "Bad credentials") { 64 | return next(new Error('Invalid Github oauth key: ' + token)); 65 | } 66 | if (err.message === "Not Found") { 67 | return next(new Error('Could not find gist: ' + gistID)); 68 | } 69 | return next(err); 70 | } 71 | // TODO: Better package.json / main support 72 | // for gists, main will either be the first file found, or the main specified 73 | if (typeof main === "string" && main.length > 0) { 74 | if (typeof g.files[main] === "undefined") { 75 | return next(new Error('Could not find main entry point: ' + main)); 76 | } 77 | req.code = g.files[main].content; 78 | // res.end(g.files[main].content); 79 | } else { 80 | var keys = Object.keys(g.files) 81 | req.code = g.files[keys[0]].content; 82 | // res.end(g.files[0].content); 83 | } 84 | provider.set(key, req.code, function (err, _set) { 85 | if (err) { 86 | console.log(err) 87 | } 88 | }); 89 | return next(); 90 | }); 91 | } 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /lib/plugins/sourceGithubRepo.js: -------------------------------------------------------------------------------- 1 | // sourceGithubRepo.js - pulls microservice source code from Github Repository 2 | // requires the `octonode` package and uses Github V3 REST API for pulling content 3 | 4 | module.exports = function sourceGithubRepo (config) { 5 | 6 | var github = require('octonode'); 7 | 8 | var Store = require('./Store'); 9 | 10 | config = config || {}; 11 | config.errorHandler = config.errorHandler || function (err, next) { 12 | if (err.message === "Bad credentials") { 13 | return next(new Error('Invalid Github oauth key: ' + config.token)); 14 | } 15 | if (err.message === "Not Found") { 16 | return next(new Error('Could not load: ' + config.repo + "@" + config.branch + ":" + config.main)); 17 | } 18 | return next(err); 19 | } 20 | 21 | // provider should be an instance of a node-redis client 22 | var provider = config.provider || new Store('memory', 'Source Github Repo'); 23 | 24 | return function sourceGithubRepoHandler (req, res, next) { 25 | 26 | var key = config.repo + "@" + config.branch + ":" + config.main; 27 | 28 | // TODO: better support for pulling schema / view / presenter 29 | // TODO: better package.json support for main 30 | // check to see if there is a copy of the source code in the cache 31 | provider.get(key, function (err, repo) { 32 | if (err) { 33 | return next(err); 34 | } 35 | if (repo === 0) { 36 | // if a copy of the source code was not found, load it, use it, and set it 37 | fetchSourceCodeFromGithubRepo(); 38 | } else { 39 | // if a copy of the source code exists in the cache, use it 40 | req.code = repo.code; 41 | next(); 42 | } 43 | }); 44 | 45 | function fetchSourceCodeFromGithubRepo () { 46 | 47 | // console.log('fetch source code from github') 48 | 49 | // in the middleware configuration, assume we already have an access token 50 | // access token must be done through github oauth login 51 | var token = config.token; 52 | 53 | if (typeof token === "undefined") { 54 | throw new Error('token is a required option!'); 55 | } 56 | 57 | var client = github.client(token); 58 | 59 | var repo = config.repo || "Stackvana/microcule-examples"; 60 | var branch = config.branch || "master"; 61 | var main = config.main || "index.js"; 62 | 63 | var ghrepo = client.repo(repo, branch); 64 | 65 | ghrepo.contents(main, function (err, file) { 66 | if (err) { 67 | return config.errorHandler(err, next); 68 | } 69 | req.code = base64Decode(file.content).toString(); 70 | next(); 71 | provider.set(key, req.code, function(err, _set){ 72 | }); 73 | }); 74 | 75 | }; 76 | 77 | } 78 | 79 | } 80 | 81 | function base64Decode (str) { 82 | var buf; 83 | // add guard in case non-string has been passed in 84 | if (typeof str !== 'string') { 85 | str = ""; 86 | } 87 | if (typeof Buffer.from === "function") { 88 | // Node 5.10+ 89 | buf = Buffer.from(str, 'base64'); // Ta-da 90 | } else { 91 | // older Node versions 92 | buf = new Buffer(str, 'base64'); // Ta-da 93 | } 94 | return buf; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /lib/plugins/spawn/fd3/index.js: -------------------------------------------------------------------------------- 1 | // fd3/index.js - handles all STDERR output from spawned service 2 | // Remark: STDERR is currently overloaded to support JSON messages 3 | // Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage 4 | 5 | var responseMethods = require('./responseMethods'); 6 | var fd3 = {}; 7 | module['exports'] = fd3; 8 | 9 | // processes incoming fd3 buffer 10 | fd3.onData = function onFD3Data (data, status, debug, output, input) { 11 | var messages = data.toString(); 12 | messages = messages.split('\n'); 13 | messages.forEach(function(message){ 14 | if (message.length === 0) { 15 | return; 16 | } 17 | // attempt to parse incoming FD3 as JSON message 18 | try { 19 | message = JSON.parse(message.toString()); 20 | } catch (err) { 21 | // don't do anything, ignore 22 | // message = { "type": "error", "payload": { "error": message.toString() }}; 23 | } 24 | handleMessage(message, status, debug, output, input); 25 | }); 26 | }; 27 | 28 | var handleMessage = fd3.handleMessage = function (message, status, debug, output, input) { 29 | 30 | var request = require('request'); 31 | 32 | /* 33 | fd3 message types: 34 | 35 | error: error event from vm, send error stack as plaintext to client. 36 | log: console.log logging event, send log entry to logging system 37 | end: hook.res.end was called inside the vm, call output.end() 38 | untyped: any untyped messages are considered type `error` and will be wrapped as error types 39 | 40 | */ 41 | // console.log('handling message', message) 42 | // check to see if incoming message is a response method ( like res.writeHead ) 43 | if (typeof responseMethods[message.type] === "function") { 44 | responseMethods[message.type](message, output); 45 | return; 46 | } 47 | 48 | // if the incoming message is end event, signal that is time to end the response 49 | if (message.type === "end") { 50 | status.serviceEnded = true; 51 | } 52 | 53 | // send logging messages to the debug function ( redis logs at this point ) 54 | if (message.type === "log") { 55 | debug(message.payload.entry); 56 | return; 57 | } 58 | 59 | /* 60 | TODO: implement req param setters for middlewares 61 | if (message.type === "setvar") { 62 | console.log('calling setvar...', message.payload) 63 | input[message.payload.key] = message.payload.value; 64 | return; 65 | } 66 | */ 67 | 68 | // if the incoming message is an error 69 | if (message.type === "error") { 70 | status.erroring = true; 71 | 72 | // could this 500 header try to be written twice? potential double header writing? needs more testing 73 | try { 74 | output.status(500) 75 | } catch (err) { 76 | 77 | } 78 | 79 | // write the error message to the response 80 | // we could make this configurable with 'redirectStderrToStdout' property 81 | try { 82 | output.write(message.payload.error); 83 | } catch (err) { 84 | console.log('output.write error: ', err); 85 | } 86 | 87 | // write the error message to the logs 88 | debug(message.payload.error) 89 | 90 | status.serviceEnded = true; 91 | 92 | /* 93 | Removed with new middleware API 94 | Note: Should this be added back? 95 | // we don't know what happened at this point, or how much more error information is coming 96 | // let's just set a timer to end the request after a few moments 97 | // this ensures that most ( if not the entire ) error stack gets sent to the client 98 | // status.ended = true; 99 | // wait 200 ms to account for any errors to flush 100 | setTimeout(function(){ 101 | output.end(); 102 | }, 200) 103 | */ 104 | } 105 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/fd3/responseMethods.js: -------------------------------------------------------------------------------- 1 | // responseMethods.js 2 | // This file handles mappings of JSON encoded HTTP response methods to actually HTTP response streams 3 | // HTTP response methods are handled over STDERR encoded JSON instead of STDIO 3 and 4 4 | // Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage 5 | 6 | var methods = {}; 7 | module['exports'] = methods; 8 | 9 | /* 10 | The following HTTP methods are implemented... 11 | 12 | ✓ hook.res.writeHead 13 | ✓ hook.res.write 14 | ✓ hook.res.end 15 | ✓ hook.res.writeContinue 16 | ✓ hook.res.setTimeout ( missing callback argument ) 17 | ✓ hook.res.statusCode (getter and setter) 18 | ✓ hook.res.statusMessage 19 | ✓ hook.res.setHeader 20 | ✓ hook.res.sendDate 21 | ✓ hook.res.removeHeader 22 | ✓ hook.res.addTrailers 23 | 24 | TODO: 25 | 26 | hook.res.headersSent 27 | hook.res.getHeader 28 | 29 | */ 30 | 31 | /* 32 | Remark: All methods carry the following signature: 33 | 34 | function (message, res) 35 | 36 | message: hash containing the hook.res.foo method payload 37 | res: the http response object 38 | 39 | */ 40 | 41 | methods.addTrailers = function (message, res) { 42 | res.addTrailers(message.payload.headers); 43 | }; 44 | 45 | methods.removeHeader = function (message, res) { 46 | res.removeHeader(message.payload.name); 47 | }; 48 | 49 | methods.setHeader = function (message, res) { 50 | try { 51 | res.setHeader(message.payload.name, message.payload.value); 52 | } catch (err) { 53 | // do nothing 54 | } 55 | }; 56 | 57 | methods.setTimeout = function (message, res) { 58 | // TODO: add optional callback argument? 59 | res.setTimeout(message.payload.msecs); 60 | }; 61 | 62 | methods.sendDate = function (message, res) { 63 | res.sendDate = message.payload.value; 64 | }; 65 | 66 | methods.statusMessage = function (message, res) { 67 | res.statusMessage = message.payload.value; 68 | }; 69 | 70 | methods.status = function (message, res) { 71 | if (typeof message.payload.value === "number" && message.payload.value >= 100 && message.payload.value < 1000) { 72 | res.status(message.payload.value); 73 | } 74 | }; 75 | 76 | methods.statusCode = function (message, res) { 77 | if (typeof message.payload.value === "number" && message.payload.value >= 100 && message.payload.value < 1000) { 78 | res.statusCode = message.payload.value; 79 | } else { 80 | // TODO: status.closed = true 81 | // res.end('bad res.statusCode values'); 82 | } 83 | }; 84 | 85 | methods.writeContinue = function (message, res) { 86 | res.writeContinue(); 87 | }; 88 | 89 | methods.writeHead = function (message, res) { 90 | if (typeof message.payload.code === "number" && message.payload.code >= 100 && message.payload.code < 1000) { 91 | try { 92 | res.writeHead(message.payload.code, message.payload.headers); 93 | } catch (err) { 94 | console.log('write head error', message); 95 | } 96 | } else { 97 | // TODO: status.closed = true 98 | // res.end('bad res.writeHead values'); 99 | } 100 | }; -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/bash/index.js: -------------------------------------------------------------------------------- 1 | var config = require('../../../../../config'); // used to track bash version 2 | 3 | function bashEscape (arg) { 4 | if (typeof arg === "undefined") { 5 | return ""; 6 | } 7 | var str; 8 | str = arg.toString(); 9 | /* 10 | if (typeof arg === "object") { 11 | str = JSON.stringify(arg); 12 | } else { 13 | } 14 | */ 15 | str = str.replace(/`/g, ''); 16 | str = str.replace(/"/g, '\''); 17 | str = str.split("\r\n"); 18 | str = str.join(" "); 19 | return str; 20 | } 21 | 22 | module['exports'] = function generateBashArguments (service, env) { 23 | 24 | // parsing JSON in bash is not so great 25 | // instead, just iterate through all our properties, 26 | // and generate a bunch of unique keys 27 | var args = []; 28 | var bashInject = ""; 29 | bashInject += 'Hook="The Hook object isnt a bash object.";\n'; 30 | 31 | // will default to bash version 4 API by default 32 | var bash3 = false; 33 | // setting bash to version 3 requires configuration parameter 34 | if (config.bash && typeof config.bash.version === 'number') { 35 | if (config.bash.version === 3) { 36 | bash3 = true; 37 | } 38 | } 39 | 40 | if (!bash3) { 41 | bashInject += 'declare -A Hook_params;\n'; 42 | } 43 | 44 | for (var p in env) { 45 | if (typeof env[p] === "object") { 46 | for (var s in env[p]) { 47 | if (s !== 'code') { 48 | // slight hack to not send bash code to service as parameters 49 | // causing some hard to catch escape issues 50 | bashInject += 'Hook_' + p + '_' + s + '="' + bashEscape(env[p][s]) + '";\n' 51 | } 52 | if (p === 'params') { 53 | if (!bash3) { 54 | bashInject += 'Hook_params[' + s +']="' + bashEscape(env[p][s]) + '";\n'; 55 | } 56 | } 57 | } 58 | } else { 59 | // bashInject += 'Hook_params[' + p +']=' + bashEscape(env[p]) + ';\n'; 60 | bashInject += 'Hook_' + p + '="' + bashEscape(env[p]) + '";\n' 61 | } 62 | } 63 | args = [ 64 | '-code', service.code, 65 | '-service', JSON.stringify(service), 66 | '-payload', bashInject 67 | ]; 68 | return args; 69 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/clisp/index.js: -------------------------------------------------------------------------------- 1 | function clispEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str = arg.toString(); 6 | str = str.replace(/"/g, '\\"') 7 | return str; 8 | } 9 | 10 | module['exports'] = function generateClispArguments (service, env) { 11 | var args = []; 12 | var clispInject = ""; 13 | 14 | clispInject += '; declare the hash tables for Hook.params and Hook.env \n'; 15 | 16 | clispInject += '(defparameter *Hook* (make-hash-table))\n'; 17 | clispInject += '(defparameter *params* (make-hash-table))\n'; 18 | clispInject += '(defparameter *env* (make-hash-table))\n'; 19 | 20 | clispInject += '; assign the params and env hashes to the Hook \n'; 21 | clispInject += '(setf (gethash \'params *Hook*) *params*)\n'; 22 | clispInject += '(setf (gethash \'env *Hook*) *env*)\n'; 23 | 24 | clispInject += '; assign http parameters \n'; 25 | clispInject += '(setf (gethash \'a *params*) "1")\n'; 26 | 27 | clispInject += '; assign env parameters \n'; 28 | clispInject += '(setf (gethash \'d *env*) "4")\n'; 29 | 30 | for (var p in env) { 31 | 32 | if (typeof env[p] === "object") { 33 | clispInject += '(defparameter *' + p + '* (make-hash-table))\n'; 34 | clispInject += '(setf (gethash \'' + p + ' *Hook*) *' + p + '*)\n'; 35 | for (var s in env[p]) { 36 | var val = env[p][s]; 37 | if (typeof val === "object") { 38 | // TODO: serialize value 39 | val = "unavailable"; 40 | } 41 | if (s !== 'code') { 42 | clispInject += '(setf (gethash \'' + s + ' *' + p + '*) "' + clispEscape(val) + '")\n'; 43 | } 44 | } 45 | } else { 46 | clispInject += '(setf (gethash \'' + p + ' *Hook*) "' + clispEscape(env[p]) + '")\n'; 47 | } 48 | } 49 | 50 | clispInject = clispInject.substr(0, clispInject.length - 1); 51 | 52 | args = [ 53 | '-c', service.code, 54 | '-s', JSON.stringify(service), 55 | '-p', clispInject 56 | ]; 57 | 58 | return args; 59 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/gcc/index.js: -------------------------------------------------------------------------------- 1 | function gccEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str; 6 | str = arg.toString(); 7 | /* 8 | if (typeof arg === "object") { 9 | str = JSON.stringify(arg); 10 | } else { 11 | } 12 | */ 13 | str = arg.toString().replace(/"/g, '\''); 14 | str = str.split("\r\n"); 15 | str = str.join(" "); 16 | return str; 17 | } 18 | 19 | module['exports'] = function generategccArguments (service, env) { 20 | 21 | // parsing JSON in gcc is not so great 22 | // instead, just iterate through all our properties, 23 | // and generate a bunch of unique keys 24 | var gccArgv; 25 | 26 | // iterate through all incoming env parameters and map them to a flat argv structure 27 | // these will be passed in over the command line and be avaialble in the compiled application as argv / argc scope 28 | // the service can then parse the incoming options however it wants 29 | var gccArgv = []; 30 | for (var p in env) { 31 | 32 | if (typeof env[p] === "object") { 33 | for (var s in env[p]) { 34 | var key = '-hook_' + p + '_' + s; 35 | var value = env[p][s]; 36 | if (typeof value === "object") { // TODO: recursive flatten() function 37 | value = JSON.stringify(value); 38 | } 39 | gccArgv.push(key); 40 | gccArgv.push(value); 41 | //gccInject += 'Hook_' + p + '_' + s + '="' + gccEscape(env[p][s]) + '";\n' 42 | } 43 | } else { 44 | var key = '-hook_' + p; 45 | gccArgv.push(key); 46 | gccArgv.push(env[p]); 47 | } 48 | } 49 | return gccArgv; 50 | // return args; 51 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/golang/index.js: -------------------------------------------------------------------------------- 1 | function golangEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str; 6 | str = arg.toString(); 7 | /* 8 | if (typeof arg === "object") { 9 | str = JSON.stringify(arg); 10 | } else { 11 | } 12 | */ 13 | str = arg.toString().replace(/"/g, '\''); 14 | str = str.split("\r\n"); 15 | str = str.join(" "); 16 | return str; 17 | } 18 | 19 | module['exports'] = function generategolangArguments (service, env) { 20 | 21 | // parsing JSON in golang is not so great 22 | // instead, just iterate through all our properties, 23 | // and generate a bunch of unique keys 24 | var golangArgv; 25 | 26 | // iterate through all incoming env parameters and map them to a flat argv structure 27 | // these will be passed in over the command line and be avaialble in the compiled application as argv / argc scope 28 | // the service can then parse the incoming options however it wants 29 | var golangArgv = []; 30 | for (var p in env) { 31 | 32 | if (typeof env[p] === "object") { 33 | for (var s in env[p]) { 34 | var key = '-hook_' + p + '_' + s; 35 | var value = env[p][s]; 36 | if (typeof value === "object") { // TODO: recursive flatten() function 37 | value = JSON.stringify(value); 38 | } 39 | golangArgv.push(key); 40 | golangArgv.push(value); 41 | //golangInject += 'Hook_' + p + '_' + s + '="' + golangEscape(env[p][s]) + '";\n' 42 | } 43 | } else { 44 | var key = '-hook_' + p; 45 | golangArgv.push(key); 46 | golangArgv.push(env[p]); 47 | } 48 | } 49 | return golangArgv; 50 | // return args; 51 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/lua/index.js: -------------------------------------------------------------------------------- 1 | function luaEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | if (typeof arg === "object") { 6 | arg = JSON.stringify(arg); 7 | } 8 | var str = arg.toString(); 9 | str = str.split("\r\n"); 10 | str = str.join(" "); 11 | return str; 12 | } 13 | 14 | module['exports'] = function generateLuaArguments (service, env) { 15 | var args = []; 16 | var luaInject = "Hook = {}\n"; 17 | luaInject += "local json = require('json')\n" 18 | // TODO: fix issue with escaped values 19 | luaInject += "Hook = json.parse('" + JSON.stringify(env) + "')\n"; 20 | // Note: The following block of code is legacy and not needed for new services 21 | // We will maintain this code path for a few moves until all legacy services have migrated to the new API 22 | for (var p in env) { 23 | if (typeof env[p] === "object") { 24 | for (var s in env[p]) { 25 | luaInject += 'Hook_' + p + '_' + s + ' = \'' + luaEscape(env[p][s]) + '\'\n' 26 | } 27 | } else { 28 | luaInject += 'Hook_' + p + ' = \'' + luaEscape(env[p]) + '\'\n' 29 | } 30 | } 31 | args = [ 32 | '-code', service.code, 33 | '-service', JSON.stringify(service), 34 | '-payload', luaInject 35 | ]; 36 | return args; 37 | 38 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/ocaml/index.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function generateOcamlArguments (service, env) { 2 | var args = []; 3 | 4 | var ocamlInject = "let hook_params = Hashtbl.create 123456;;\n"; 5 | ocamlInject += "let hook_env = Hashtbl.create 123456;;\n"; 6 | 7 | for (var p in env.params) { 8 | ocamlInject += 'Hashtbl.add hook_params "' + p + '" "' + env.params[p] + '";;' 9 | } 10 | 11 | for (var p in env.env) { 12 | ocamlInject += 'Hashtbl.add hook_env "' + p + '" "' + env.env[p] + '";;' 13 | } 14 | 15 | service.code = ocamlInject + '\n' + service.code; 16 | 17 | args = [ 18 | '-code', service.code, 19 | '-service', JSON.stringify(service) 20 | ]; 21 | 22 | return args; 23 | 24 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/perl/index.js: -------------------------------------------------------------------------------- 1 | function perlEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str = arg.toString(); 6 | str = str.replace(/`/g, ''); 7 | str = str.split("\r\n"); 8 | str = str.join(" "); 9 | return str; 10 | } 11 | 12 | module['exports'] = function generatePerlArguments (service, env) { 13 | 14 | var args = []; 15 | var perlInject = ""; 16 | 17 | perlInject += '$Hook = "The Hook object isnt an object.";\n'; 18 | perlInject += '$Hook_params = "The Hook.params object isnt an object.";\n'; 19 | 20 | for (var p in env) { 21 | if (typeof env[p] === "object") { 22 | for (var s in env[p]) { 23 | perlInject += '$Hook_' + p + '_' + s + ' = \'' + perlEscape(env[p][s]) + '\';\n' 24 | } 25 | } else { 26 | perlInject += '$Hook_' + p + ' = \'' + perlEscape(env[p]) + '\';\n' 27 | } 28 | } 29 | args = [ 30 | '-code', service.code, 31 | '-service', JSON.stringify(service), 32 | '-payload', perlInject 33 | ]; 34 | return args; 35 | 36 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/php/index.js: -------------------------------------------------------------------------------- 1 | // Currently not used 2 | // PHP uses JSON arguments and spawns with the default spawning options ( the same as JavaScript ) -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/python/index.js: -------------------------------------------------------------------------------- 1 | // Currently not used 2 | // Python uses JSON arguments and spawns with the default spawning options ( the same as JavaScript ) -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/r/index.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function generateRArguments (service, env) { 2 | // Remark: Currently not being used 3 | var args = []; 4 | return args; 5 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/ruby/index.js: -------------------------------------------------------------------------------- 1 | // Currently not used 2 | // Ruby uses JSON arguments and spawns with the default spawning options ( the same as JavaScript ) -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/rust/index.js: -------------------------------------------------------------------------------- 1 | function rustEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str; 6 | str = arg.toString(); 7 | /* 8 | if (typeof arg === "object") { 9 | str = JSON.stringify(arg); 10 | } else { 11 | } 12 | */ 13 | str = arg.toString().replace(/"/g, '\''); 14 | str = str.split("\r\n"); 15 | str = str.join(" "); 16 | return str; 17 | } 18 | 19 | module['exports'] = function generaterustArguments (service, env) { 20 | 21 | // parsing JSON in rust is not so great 22 | // instead, just iterate through all our properties, 23 | // and generate a bunch of unique keys 24 | var rustArgv; 25 | 26 | // iterate through all incoming env parameters and map them to a flat argv structure 27 | // these will be passed in over the command line and be avaialble in the compiled application as argv / argc scope 28 | // the service can then parse the incoming options however it wants 29 | var rustArgv = []; 30 | for (var p in env) { 31 | 32 | if (typeof env[p] === "object") { 33 | for (var s in env[p]) { 34 | var key = '-hook_' + p + '_' + s; 35 | var value = env[p][s]; 36 | if (typeof value === "object") { // TODO: recursive flatten() function 37 | value = JSON.stringify(value); 38 | } 39 | rustArgv.push(key); 40 | rustArgv.push(value); 41 | //rustInject += 'Hook_' + p + '_' + s + '="' + rustEscape(env[p][s]) + '";\n' 42 | } 43 | } else { 44 | var key = '-hook_' + p; 45 | rustArgv.push(key); 46 | rustArgv.push(env[p]); 47 | } 48 | } 49 | return rustArgv; 50 | // return args; 51 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/scheme/index.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function generateSchemeArguments (service, env) { 2 | var args = []; 3 | var schemeInject = ""; 4 | 5 | //schemeInject += '$Hook = "The Hook object isnt an object.";\n'; 6 | //schemeInject += '$Hook_params = "The Hook.params object isnt an object.";\n'; 7 | 8 | for (var p in env) { 9 | if (typeof env[p] === "object") { 10 | for (var s in env[p]) { 11 | schemeInject += "(define Hook_" + p + "_" + s + " '" + env[p][s] + ")\n"; 12 | } 13 | } else { 14 | schemeInject += "(define Hook_" + p + " '" + env[p] + ")\n"; 15 | } 16 | } 17 | args = [ 18 | '-c', schemeInject + '\n' + service.code 19 | ]; 20 | 21 | return args; 22 | } 23 | -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/smalltalk/index.js: -------------------------------------------------------------------------------- 1 | function smalltalkEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str = arg.toString(); 6 | str = str.split("\r\n"); 7 | str = str.join(" "); 8 | str = arg.toString().replace(/'/g, '\'\''); 9 | return str; 10 | } 11 | 12 | module['exports'] = function generateSmalltalkArguments (service, env) { 13 | var args = []; 14 | var smalltalkInject = ""; 15 | 16 | //schemeInject += '$Hook = "The Hook object isnt an object.";\n'; 17 | //schemeInject += '$Hook_params = "The Hook.params object isnt an object.";\n'; 18 | 19 | for (var p in env) { 20 | if (typeof env[p] === "object") { 21 | for (var s in env[p]) { 22 | var val = env[p][s]; 23 | if (typeof val === "object") { 24 | // TODO: serialize value 25 | val = "unavailable"; 26 | } 27 | //schemeInject += '$Hook_' + p + '_' + s + ' = \'' + env[p][s] + '\';\n' 28 | smalltalkInject += "Hook_" + p + "_" + s + " := \'" + smalltalkEscape(val) + "\'.\n"; 29 | } 30 | } else { 31 | smalltalkInject += "Hook_" + p + " := \'" + smalltalkEscape(env[p]) + "\'.\n"; 32 | } 33 | } 34 | 35 | args = [ 36 | '-a', '', 37 | '-code', smalltalkInject + service.code, 38 | '-env', JSON.stringify(env), 39 | '-service', JSON.stringify(service) 40 | ]; 41 | return args; 42 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/generateCommandLineArguments/tcl/index.js: -------------------------------------------------------------------------------- 1 | function tclEscape (arg) { 2 | if (typeof arg === "undefined") { 3 | return ""; 4 | } 5 | var str = arg.toString(); 6 | str = str.split("\r\n"); 7 | str = str.join(";"); 8 | return str; 9 | } 10 | 11 | module['exports'] = function generateTclArguments (service, env) { 12 | var args = []; 13 | var tclInject = ""; 14 | // TODO: fix issues with escaping " and ' in property values 15 | 16 | for (var p in env) { 17 | if (typeof env[p] === "object") { 18 | for (var s in env[p]) { 19 | var val = env[p][s]; 20 | if (typeof val === "object") { 21 | // TODO: serialize value 22 | val = "unavailable"; 23 | } 24 | tclInject += "set Hook_" + p + "_" + s + " \"" + tclEscape(val) + "\"\n"; 25 | } 26 | } else { 27 | tclInject += "set Hook_" + p + " \"" + tclEscape(env[p]) + "\"\n"; 28 | } 29 | } 30 | args = [ 31 | '-c', service.code, 32 | '-s', JSON.stringify(service), 33 | '-p', tclInject 34 | ]; 35 | return args; 36 | } -------------------------------------------------------------------------------- /lib/plugins/spawn/transpileServiceCode/ReadMe.md: -------------------------------------------------------------------------------- 1 | Certain languages require a compile / transpile step before they are capable of executing the microservice. 2 | 3 | Babel / Coffee-Script are examples of this, as they must be compiled to JavaScript before execution. 4 | 5 | Since real-time compilation takes a signifiant amount of time, we add the ability to cache compiled versions of code through a custom callback. -------------------------------------------------------------------------------- /lib/plugins/spawn/transpileServiceCode/coffee-script/index.js: -------------------------------------------------------------------------------- 1 | module['exports'] = function compileCoffee (code, cb) { 2 | 3 | var CoffeeScript = require('coffee-script'); // don't require unless we need it 4 | 5 | // service is coffee-script, so convert it to javascript 6 | // TODO: This may cause peformance issues, better to cache it 7 | code = CoffeeScript.compile(code); 8 | // brittle approach to remove coffee-script wrap, 9 | // we don't need / can't use the wrap as it was breaking our module['exports'] metaprogramming 10 | // TODO: better integration with generated JS 11 | code = code.split('\n'); 12 | code.pop(); 13 | code.pop(); 14 | code.shift(); 15 | // removes top and bottom generated lines 16 | code = code.join('\n'); 17 | code = code.substr(0, code.length - 2); 18 | 19 | return code; 20 | 21 | }; -------------------------------------------------------------------------------- /lib/requireService.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'); 3 | 4 | module['exports'] = function requireService (opts, cb) { 5 | // removed from API ( for now ) 6 | cb(new Error('Not implemented')); 7 | }; -------------------------------------------------------------------------------- /lib/requireServiceSync.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'); 3 | 4 | function extToLang (ext) { 5 | var lang; 6 | 7 | return lang; 8 | } 9 | 10 | // same as requireService, only with sync interface ( useful for requiring services as packages ) 11 | module['exports'] = function requireServiceSync (opts) { 12 | 13 | var service = {}; 14 | // console.log(opts.path) 15 | opts.path = opts.path; 16 | 17 | if (typeof opts.path === "undefined") { 18 | throw new Error('`path` is a required argument.'); 19 | } 20 | 21 | // take stat of path to determine if its file / folder / missing 22 | var stat; 23 | 24 | try { 25 | stat = fs.statSync(opts.path); 26 | } catch (err) { 27 | throw err; 28 | } 29 | 30 | if (stat.isDirectory()) { 31 | return loadService(opts.path, 'dir'); 32 | } else { 33 | return loadService(opts.path, 'file'); 34 | } 35 | 36 | }; 37 | 38 | function loadService (p, type) { 39 | 40 | var service = { 41 | name: p, 42 | owner: "examples" 43 | }; 44 | 45 | var _service = { 46 | service: "index.js", 47 | view: "view.html", 48 | presenter: "presenter.js", 49 | schema: "schema.js" 50 | }; 51 | 52 | var pkg; 53 | 54 | if (type === "file") { 55 | service.localPath = path.resolve(p); 56 | service.name = p.replace(/\.[^/.]+$/, ""); 57 | } else { 58 | try { 59 | pkg = JSON.parse(fs.readFileSync(path.resolve(p) + "/package.json").toString()); 60 | service.pkg = pkg; 61 | service.localPath = path.resolve(p) + "/" + pkg.main; 62 | } catch (err) { 63 | // pkg not available, dont use 64 | throw err; 65 | } 66 | } 67 | 68 | if (typeof pkg === "object") { 69 | if (pkg.name) { 70 | service.name = pkg.name; 71 | } 72 | if (pkg.author) { 73 | service.owner = pkg.author; 74 | } 75 | if (pkg.main) { 76 | _service.service = pkg.main; 77 | } 78 | if (pkg.view) { 79 | _service.view = pkg.view; 80 | } 81 | if (pkg.presenter) { 82 | _service.presenter = pkg.presenter; 83 | } 84 | if (pkg.language) { 85 | service.language = pkg.language; 86 | } 87 | } 88 | 89 | var extentions = { 90 | ".c": "gcc", 91 | ".coffee": "coffee-script", 92 | ".go": "go", 93 | ".java": "java", 94 | ".js": "javascript", 95 | ".lua": "lua", 96 | ".lisp": "clisp", 97 | ".ml": "ocaml", 98 | ".php": "php", 99 | ".pl": "perl", 100 | ".py": "python", // Remark: You can also use the "--language python" option 101 | ".py3": "python3", // Remark: You can also use the "--language python3" option 102 | ".sh": "bash", 103 | ".r": "r", 104 | ".rb": "ruby", 105 | ".rs": "rust", 106 | ".tcl": "tcl", 107 | ".ss": "scheme", 108 | ".st": "smalltalk" 109 | }; 110 | var ext; 111 | 112 | // source is legacy parameter, now replaced with code 113 | if (type === "file") { 114 | ext = path.extname(path.resolve(p)); 115 | service.source = fs.readFileSync(path.resolve(p)).toString(); 116 | service.code = fs.readFileSync(path.resolve(p)).toString(); 117 | } else { 118 | ext = path.extname(_service.service); 119 | service.source = fs.readFileSync(path.resolve(p) + "/" + _service.service).toString(); 120 | service.code = fs.readFileSync(path.resolve(p) + "/" + _service.service).toString(); 121 | } 122 | 123 | if (typeof service.language === "undefined") { 124 | service.language = extentions[ext]; 125 | } 126 | 127 | try { 128 | service.presenter = fs.readFileSync(path.resolve(p) + "/" + _service.presenter).toString(); 129 | //service.presenter = require(path.resolve(p) + "/" + _service.presenter); 130 | } catch (err) { 131 | // console.log(err); 132 | } 133 | 134 | try { 135 | service.view = fs.readFileSync(path.resolve(p) + "/" + _service.view).toString(); 136 | } catch (err) { 137 | // console.log(err); 138 | } 139 | 140 | try { 141 | // service.schema = fs.readFileSync(path.resolve(p) + "/" + _service.schema).toString(); 142 | service.schema = require(path.resolve(p) + "/" + _service.schema); 143 | } catch (err) { 144 | // console.log(err); 145 | } 146 | return service; 147 | } -------------------------------------------------------------------------------- /lib/viewPresenter.js: -------------------------------------------------------------------------------- 1 | // TODO: remove this file and replace with /lib/plugins/viewPresenter.js 2 | // Legacy v2.x.x 3 | // The reason this file still exists, is that there seems to be an issue using the monkey patch express res / write middleware hack outside of express 4 | // This was causing problem upstream in hook.io, because hook.io uses microcules middlewares directly ( not through .use() ) 5 | // Ideally, we should be able to fix /lib/plugins/viewPresenter.js to accomdate for both usages and remove this file 6 | 7 | var through = require('through2'); 8 | var streamBuffers = require('stream-buffers'); 9 | var Mustache = require("mustache"); 10 | 11 | module.exports = function viewPresenter (service, req, res, cb) { 12 | 13 | // console.log('calling View-Presenter'.yellow, service) 14 | 15 | // create a new buffer and output stream for capturing the hook.res.write and hook.res.end calls from inside the hook 16 | // this is used as an intermediary to pipe hook output to other streams ( such as another hook ) 17 | var hookOutput = new streamBuffers.WritableStreamBuffer({ 18 | initialSize: (100 * 1024), // start as 100 kilobytes. 19 | incrementAmount: (10 * 1024) // grow by 10 kilobytes each time buffer overflows. 20 | }); 21 | 22 | var debugOutput = []; 23 | 24 | var _headers = { 25 | code: null, 26 | headers: {} 27 | }; 28 | 29 | var output = through(function (chunk, enc, callback) { 30 | //console.log('getting output', chunk.toString()) 31 | hookOutput.write(chunk); 32 | callback(); 33 | }, function () { 34 | var content = hookOutput.getContents(); 35 | 36 | // Apply basic mustache replacements 37 | // renamed themeSource -> viewSource 38 | var strTheme = service.view || service.themeSource; 39 | 40 | strTheme = Mustache.render(strTheme, { 41 | /* 42 | config: { 43 | baseUrl: config.app.url // TODO: replace with config options 44 | }, 45 | */ 46 | output: content.toString(), 47 | hook: { 48 | output: content.toString(), 49 | debug: JSON.stringify(debugOutput, true, 2), 50 | params: req.resource.instance, 51 | request: { 52 | headers: req.headers 53 | }, 54 | response: { 55 | statusCode: res.statusCode, 56 | body: content.toString() 57 | }, 58 | request: { 59 | headers: { 'fo': 'bar'} 60 | }, 61 | schema: service.schema 62 | } 63 | }); 64 | console.log(strTheme) 65 | res.end(strTheme); 66 | return; 67 | 68 | 69 | /* 70 | 71 | viewPresenter code has been removed ( for now ). not many people were using it and it's implementation seems brittle 72 | we should refactor this kind of post request template processing logic into plugins or post-middlewares 73 | 74 | var View = require('view').View; 75 | 76 | var _view = new View({ template: strTheme, presenter: _presenter }); 77 | 78 | var _presenter = service.presenter || service.presenterSource || function (opts, cb) { 79 | // default presenter will load view into cheerio server-side dom 80 | // this is useful for escape / validation, it also makes the designer simplier 81 | var $ = this.$; 82 | cb(null, $.html()); 83 | }; 84 | 85 | // if presenter is a string, assume it's a Node.js module and attempt to compile it 86 | // this will somewhat safetly turn the string version of the function back into a regular function 87 | if (typeof _presenter === "string" && _presenter.length > 0) { 88 | console.log('ppp', _presenter) 89 | var Module = module.constructor; 90 | var __presenter = new Module(); 91 | __presenter.paths = module.paths; 92 | var error = null; 93 | try { 94 | __presenter._compile(_presenter, 'presenter-' + service.name); 95 | } catch (err) { 96 | error = err; 97 | } 98 | // console.log("ERRR", _presenter, __presenter.exports, error) 99 | if (error !== null) { 100 | return cb(new Error('Could not compile presenter into module: ' + error.message)); 101 | } 102 | _presenter = __presenter.exports; 103 | } 104 | 105 | if (typeof _view.present !== "function") { 106 | return res.end('Unable to load View-Presenter for hook service. We made a mistake. Please contact support.'); 107 | } 108 | // give the presenter 3 seconds to render, or else it has failed 109 | var completedTimer = setTimeout(function(){ 110 | if (!completed) { 111 | return cb(new Error('Hook presenter took more than 3 seconds to load. Aborting request. \n\nA delay of this long usually indicates the presenter never fired it\'s callback. Check the presenter code for error. \n\nIf this is not the case and you require more than 3 seconds to present your view, please contact hookmaster@hook.io')); 112 | } 113 | }, 3000); 114 | var completed = false; 115 | // replay headers? 116 | // res.writeHead(_headers.code, _headers.headers); 117 | 118 | var c = content.toString(); 119 | try { 120 | c = JSON.parse(c); 121 | } catch (err) { 122 | 123 | } 124 | 125 | console.log('pre doing the view', c) 126 | try { // this will catch user run-time errors in the presenter 127 | _view.present({ 128 | request: req, 129 | response: res, 130 | service: service, 131 | req: req, 132 | res: res, 133 | output: c, 134 | debug: debugOutput, 135 | instance: req.resource.instance, 136 | params: req.resource.params, 137 | headers: _headers 138 | }, function(err, rendered){ 139 | completed = true; 140 | console.log('made it to completed', err) 141 | completedTimer = clearTimeout(completed); 142 | try { 143 | console.log('ending view', rendered) 144 | res.end(rendered); 145 | } catch(err) { 146 | res.end('Failed to call res.end ' + err.message); 147 | } 148 | }); 149 | } catch (err) { 150 | completed = true; 151 | completedTimer = clearTimeout(completed); 152 | // Remark: cb(err) doesn't seem to be working here? 153 | // ending response directly seem to work 154 | // 155 | // return cb(new Error('Error in Presenter code: ' + err.message)); 156 | res.end('Error in Presenter code: ' + err.message + '\n\n' + err.stack); 157 | } 158 | */ 159 | }); 160 | 161 | output.on('error', function (err) { 162 | // Does this ever actually fire? 163 | console.log('OUTPUT ERROR', err.message); 164 | // TODO: add erroring boolean with timer? 165 | return res.end(err.message); 166 | }); 167 | // TODO: implement all http.Response methods 168 | // TODO: do something with these writeHead events in the view 169 | output.writeHead = function (code, headers) { 170 | _headers.code = code; 171 | for(var h in headers) { 172 | _headers.headers[h] = headers[h]; 173 | } 174 | }; 175 | 176 | output._headers = _headers; 177 | cb(null, req, output); 178 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microcule", 3 | "version": "6.1.0", 4 | "description": "SDK and CLI for managing multi-language microservices", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap test/*.*" 8 | }, 9 | "bin": { 10 | "microcule": "./bin/microcule" 11 | }, 12 | "author": "Marak Squires", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/stackvana/microcule.git" 17 | }, 18 | "dependencies": { 19 | "async": "^2.0.1", 20 | "coffee-script": "^1.12.7", 21 | "cron-parser": "^2.3.0", 22 | "cross-spawn": "^5.0.0", 23 | "hook.io-sdk": "^3.2.2", 24 | "hyperquest": "^2.0.0", 25 | "install": "^0.10.1", 26 | "merge-params": "^1.1.0", 27 | "minimist": "^1.2.0", 28 | "mkdirp": "^0.5.1", 29 | "mschema": "^0.5.6", 30 | "mustache": "^2.2.1", 31 | "npm": "^5.5.1", 32 | "parse-service-request": "1.3.x", 33 | "resource-http": "^1.1.0", 34 | "run-service": "3.x.x", 35 | "shasum": "^1.0.2", 36 | "stream-buffers": "^3.0.1", 37 | "through2": "^2.0.1", 38 | "tree-kill": "^1.1.0", 39 | "view": "1.0.0" 40 | }, 41 | "devDependencies": { 42 | "basic-auth": "^1.1.0", 43 | "colors": "*", 44 | "express": "^4.14.0", 45 | "gm": "^1.23.0", 46 | "microcule-examples": "6.x.x", 47 | "octonode": "^0.7.6", 48 | "passport": "^0.3.2", 49 | "passport-github": "^1.1.0", 50 | "request": "^2.75.0", 51 | "tap": "0.4.11", 52 | "tape": "^4.6.0" 53 | }, 54 | "nyc": { 55 | "all": false, 56 | "include": [ 57 | "lib/**/*.js" 58 | ], 59 | "exclude": [ 60 | "coverage", 61 | "locales", 62 | "modules", 63 | "reports", 64 | "test", 65 | "node_modules" 66 | ], 67 | "reporter": [ 68 | "html", 69 | "lcov", 70 | "clover" 71 | ], 72 | "report-dir": "./reports/coverage" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /release/ReadMe.md: -------------------------------------------------------------------------------- 1 | compiled microservice binaries will populate here by default -------------------------------------------------------------------------------- /test/all-languages-tests.js: -------------------------------------------------------------------------------- 1 | // all-languages-tests.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | var async = require('async'); 6 | 7 | var microcule, handler, app, server, examples; 8 | 9 | microcule = require('../'); 10 | 11 | // Remark: babel and coffee-script are commented out since they aren't included in the package 12 | // Even as devDependencies they are too big 13 | // TODO: update tests to use local examples folder for hello world? 14 | // or should it also include microcule-examples echo tests? 15 | var languages = ['bash', 'gcc', 'coffee-script', /* 'babel', , */ 'smalltalk', /*'lua',*/ 'go', 'javascript', 'perl', 'php', 'python', /* 'python3', */ 'ruby', 'rust', 'r', 'scheme', 'tcl']; 16 | 17 | test('attempt to require microcule-examples module', function (t) { 18 | examples = require('microcule-examples'); 19 | t.equal(typeof examples.services, "object", "returned services object"); 20 | t.end(); 21 | }); 22 | 23 | test('check if examples are available for all languages', function (t) { 24 | languages.forEach(function (lang) { 25 | t.equal(true, Object.keys(examples.services).indexOf(lang + '-hello-world') > 0, 'found example service for ' + lang); 26 | }); 27 | t.end(); 28 | }); 29 | 30 | return; 31 | 32 | // 33 | // Remark: Travis-Ci is not able to easily support multiple language binaries in a single test 34 | // There is a solution available at: https://github.com/travis-ci/travis-ci/issues/4090, 35 | // but this will require a bit of tinkering 36 | // Until we have improved the .travis.yml file, these tests have been commented out 37 | // 38 | // 39 | // Note: The following tests should pass locally if you remove the return, 40 | // and you have every single target language binary installed locally 41 | // 42 | 43 | test('attempt to start server with handlers for all languages', function (t) { 44 | app = express(); 45 | languages.forEach(function (lang) { 46 | var service = examples.services[lang + '-hello-world']; 47 | var handler = microcule.plugins.spawn({ 48 | language: lang, 49 | code: service.code 50 | }); 51 | app.use('/' + lang, handler, function (req, res) { 52 | res.end(); 53 | }); 54 | t.equal(typeof handler, "function", "/" + lang + " HTTP endpoint added"); 55 | }); 56 | server = app.listen(3000, function () { 57 | t.end(); 58 | }); 59 | }); 60 | 61 | test('attempt to run hello world all languages', function (t) { 62 | 63 | var customResponses = { 64 | 'r': '[1] "hello world"\n' 65 | }; 66 | 67 | async.eachSeries(languages, function iter (lang, next) { 68 | request('http://localhost:3000/' + lang, function (err, res, body) { 69 | var customResponses = { 70 | "r": '[1] "hello world"\n' 71 | }; 72 | var noCarriageReturn = ["perl", "scheme", "php"]; 73 | if (typeof customResponses[lang] !== 'undefined') { 74 | t.equal(body, customResponses[lang], 'got correct response from ' + lang); 75 | next(); 76 | return; 77 | } 78 | var doCRLF = ["python", "python3"]; 79 | var crlf = (process.platform == 'win32')?'\r\n':'\n'; 80 | if (noCarriageReturn.indexOf(lang) !== -1) { 81 | t.equal(body, 'hello world', 'got correct response from ' + lang); 82 | } else if (doCRLF.indexOf(lang) !== -1) { 83 | t.equal(body, 'hello world'+crlf, 'got correct response from ' + lang); 84 | } else { 85 | if (noCarriageReturn.indexOf(lang) !== -1) { 86 | t.equal(body, 'hello world', 'got correct response from ' + lang); 87 | } else { 88 | t.equal(body, 'hello world\n', 'got correct response from ' + lang); 89 | } 90 | } 91 | next(); 92 | }); 93 | }, function complete (err) { 94 | t.end(); 95 | }); 96 | }); 97 | 98 | 99 | // TODO: request params test with JSON / language specific output 100 | 101 | test('attempt to end server', function (t) { 102 | server.close(function(){ 103 | t.ok(true, "ended server"); 104 | t.end(); 105 | }); 106 | }); -------------------------------------------------------------------------------- /test/basic-tests.js: -------------------------------------------------------------------------------- 1 | // basic-tests.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handler, app, server; 7 | 8 | test('attempt to require microcule', function (t) { 9 | microcule = require('../'); 10 | t.equal(typeof microcule, 'object', 'microcule module required'); 11 | t.end(); 12 | }); 13 | 14 | test('attempt to create microservice spawn handler', function (t) { 15 | handler = microcule.plugins.spawn({ 16 | language: "bash", 17 | code: 'echo "hello world"' 18 | }); 19 | t.equal(typeof handler, "function", "returned HTTP middleware function") 20 | t.end(); 21 | }); 22 | 23 | test('attempt to start simple http server with spawn handler', function (t) { 24 | app = express(); 25 | app.use(handler); 26 | // Required for non-js services ( or else response will not end ) 27 | app.use(function(req, res){ 28 | res.end(); 29 | }); 30 | server = app.listen(3000, function () { 31 | t.equal(typeof handler, "function", "created listening HTTP server") 32 | t.end(); 33 | }); 34 | }); 35 | 36 | test('attempt to send simple http request to running microservice', function (t) { 37 | request('http://localhost:3000/', function (err, res, body) { 38 | t.equal(body, 'hello world\n', 'got correct response'); 39 | t.end(); 40 | }) 41 | }); 42 | 43 | test('attempt to end server', function (t) { 44 | server.close(function(){ 45 | t.ok("server ended"); 46 | t.end(); 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/custom-headers-test.js: -------------------------------------------------------------------------------- 1 | // custom-headers-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | test('attempt to start simple http server with spawn handler', function (t) { 11 | app = express(); 12 | handler = microcule.plugins.spawn({ 13 | language: "javascript", 14 | code: function service (req, res) { 15 | res.setHeader('x-custom', 'foo') 16 | res.writeHead(404); 17 | res.end(); 18 | } 19 | }); 20 | app.use(handler, function (req, res) { 21 | res.end(); 22 | }); 23 | server = app.listen(3000, function () { 24 | t.equal(typeof handler, "function", "started HTTP microservice server"); 25 | t.end(); 26 | }); 27 | }); 28 | 29 | test('attempt to send JSON data to running microservice', function (t) { 30 | request({ 31 | uri: 'http://localhost:3000/', 32 | method: "POST" 33 | }, function (err, res, body) { 34 | t.equal(res.headers['x-custom'], 'foo') 35 | t.equal(res.statusCode, 404, 'got correct response'); 36 | t.end(); 37 | }) 38 | }); 39 | 40 | test('attempt to end server', function (t) { 41 | server.close(function(){ 42 | t.ok("server ended"); 43 | t.end(); 44 | }); 45 | }); -------------------------------------------------------------------------------- /test/fixtures/assets/file.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | line two -------------------------------------------------------------------------------- /test/fixtures/invalid-services/ReadMe.md: -------------------------------------------------------------------------------- 1 | Collection of invalid / erroring microservices. Useful for testing error conditions, can be put into unit tests. -------------------------------------------------------------------------------- /test/fixtures/invalid-services/missing-command.sh: -------------------------------------------------------------------------------- 1 | asdasd -------------------------------------------------------------------------------- /test/fixtures/invalid-services/missing-exports.js: -------------------------------------------------------------------------------- 1 | console.log("no functions!"); -------------------------------------------------------------------------------- /test/fixtures/invalid-services/never-responds.js: -------------------------------------------------------------------------------- 1 | module.exports = function testService (opts) { 2 | // does nothing...should trigger timeout 3 | }; -------------------------------------------------------------------------------- /test/fixtures/invalid-services/require-error.js: -------------------------------------------------------------------------------- 1 | var x = a.b; 2 | 3 | module.exports = function testService (opts) { 4 | // does nothing...should trigger timeout 5 | opts.res.end('end'); 6 | }; -------------------------------------------------------------------------------- /test/fixtures/invalid-services/syntax-error.js: -------------------------------------------------------------------------------- 1 | module.exports = function testService (opts) { 2 | // does nothing...should trigger timeout 3 | var x = a.b; 4 | opts.res.end('end'); 5 | }; -------------------------------------------------------------------------------- /test/fixtures/invalid-services/writes-bad-headers.js: -------------------------------------------------------------------------------- 1 | module.exports = function testService (req, res) { 2 | // does nothing...should trigger timeout 3 | res.writeHead("abc"); 4 | 5 | res.writeHead(Infinity); 6 | res.writeHead(NaN); 7 | 8 | res.writeHead(99); 9 | res.statusCode = 99 10 | 11 | res.writeHead(1000); 12 | res.statusCode = 1000; 13 | 14 | res.writeHead(2, { 15 | 123: 123, 16 | foo: undefined, 17 | _: new Date() 18 | }); 19 | 20 | res.writeHead(200, { 21 | 123: 123, 22 | foo: undefined, 23 | _: new Date() 24 | }); 25 | 26 | res.statusMessage("abc"); 27 | res.statusMessage = "abc"; 28 | 29 | res.end(); 30 | }; -------------------------------------------------------------------------------- /test/invalid-service-test.js: -------------------------------------------------------------------------------- 1 | // invalid-service-test.js 2 | // attempts to run several user-defined services which may error in unique ways 3 | var test = require("tape"); 4 | var express = require('express'); 5 | var request = require('request'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | var microcule, handlers = {}, app, server; 10 | 11 | test('attempt to require microcule', function (t) { 12 | microcule = require('../'); 13 | t.equal(typeof microcule, 'object', 'microcule module required'); 14 | t.end(); 15 | }); 16 | 17 | test('attempt to create multiple invalid spawn handlers', function (t) { 18 | 19 | handlers['missing-exports'] = microcule.plugins.spawn({ 20 | language: "javascript", 21 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/missing-exports.js').toString() 22 | }); 23 | 24 | handlers['never-responds'] = microcule.plugins.spawn({ 25 | language: "javascript", 26 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/never-responds.js').toString(), 27 | customTimeout: 1600 28 | }); 29 | 30 | handlers['require-error'] = microcule.plugins.spawn({ 31 | language: "javascript", 32 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/require-error.js').toString() 33 | }); 34 | 35 | handlers['syntax-error'] = microcule.plugins.spawn({ 36 | language: "javascript", 37 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/syntax-error.js').toString() 38 | }); 39 | 40 | handlers['writes-bad-headers'] = microcule.plugins.spawn({ 41 | language: "javascript", 42 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/writes-bad-headers.js').toString() 43 | }); 44 | 45 | handlers['missing-command'] = microcule.plugins.spawn({ 46 | language: "bash", 47 | redirectStderrToStdout: true, // shows error in response for non-zero exit codes 48 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/missing-command.sh').toString() 49 | }); 50 | 51 | handlers['missing-command-silent'] = microcule.plugins.spawn({ 52 | language: "bash", 53 | code: fs.readFileSync(__dirname + '/fixtures/invalid-services/missing-command.sh').toString() 54 | }); 55 | 56 | t.end(); 57 | }); 58 | 59 | test('attempt to start simple http server with multiple invalid services', function (t) { 60 | app = express(); 61 | 62 | app.use('/missing-exports', handlers['missing-exports']); 63 | app.use('/never-responds', handlers['never-responds']); 64 | app.use('/require-error', handlers['require-error']); 65 | app.use('/syntax-error', handlers['syntax-error']); 66 | app.use('/writes-bad-headers', handlers['writes-bad-headers']); 67 | app.use('/missing-command', handlers['missing-command']); 68 | app.use('/missing-command-silent', handlers['missing-command-silent']); 69 | 70 | // Required for non-js services ( or else response will not end ) 71 | app.use(function(req, res){ 72 | res.end(); 73 | }); 74 | 75 | server = app.listen(3000, function () { 76 | t.end(); 77 | }); 78 | }); 79 | 80 | test('attempt to send request to javascript missing-exports service', function (t) { 81 | request('http://localhost:3000/missing-exports', function (err, res, body) { 82 | t.equal(res.statusCode, 500); 83 | t.equal(body, 'service is undefined', 'got correct response'); 84 | t.end(); 85 | }) 86 | }); 87 | 88 | test('attempt to send request to javascript never-responds', function (t) { 89 | request('http://localhost:3000/never-responds', function (err, res, body) { 90 | // timeouts return 200 instead of 500 91 | // t.equal(res.statusCode, 500); 92 | t.equal(res.statusCode, 200); 93 | t.equal(body.substr(0, 7), 'Timeout', 'got timeout response'); 94 | t.end(); 95 | }) 96 | }); 97 | 98 | test('attempt to send request to javascript require-error', function (t) { 99 | request('http://localhost:3000/require-error', function (err, res, body) { 100 | t.equal(res.statusCode, 500); 101 | t.equal(body, 'a is not defined', 'got correct node error'); 102 | t.end(); 103 | }) 104 | }); 105 | 106 | test('attempt to send request to javascript writes-bad-headers', function (t) { 107 | request('http://localhost:3000/writes-bad-headers', function (err, res, body) { 108 | t.equal(res.statusCode, 200); 109 | t.equal(body, '', 'got correct node error'); 110 | t.end(); 111 | }) 112 | }); 113 | 114 | test('attempt to send request to bash - missing command', function (t) { 115 | request('http://localhost:3000/missing-command', function (err, res, body) { 116 | t.equal(res.statusCode, 500); 117 | // t.equal(body, path.resolve(__dirname + '/../bin/binaries/micro-bash') + ': line 19: asdasd: command not found\n', 'got correct bash error'); 118 | t.end(); 119 | }) 120 | }); 121 | 122 | test('attempt to send request to bash - missing command - silent stderr', function (t) { 123 | request('http://localhost:3000/missing-command-silent', function (err, res, body) { 124 | t.equal(res.statusCode, 500); 125 | t.equal(body, '', 'got correct empty error response'); 126 | t.end(); 127 | }) 128 | }); 129 | 130 | test('attempt to end server', function (t) { 131 | server.close(function(){ 132 | t.ok("server ended"); 133 | t.end(); 134 | }); 135 | }); -------------------------------------------------------------------------------- /test/plugin-test.js: -------------------------------------------------------------------------------- 1 | // plugin-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | var logger = microcule.plugins.logger; 11 | var mschema = microcule.plugins.mschema; 12 | var RateLimiter = microcule.plugins.RateLimiter; 13 | var spawn = microcule.plugins.spawn; 14 | var rateLimiter = new RateLimiter(); 15 | 16 | var handler = spawn({ 17 | language: "javascript", 18 | code: function service (req, res) { 19 | res.json(req.params); 20 | } 21 | }); 22 | 23 | test('attempt to start simple http server with some of the plugins spawn handler', function (t) { 24 | app = express(); 25 | app.use(logger()); 26 | app.use(mschema({ 27 | "hello": { 28 | "type": "string", 29 | "required": true 30 | } 31 | })); 32 | 33 | app.use(rateLimiter.middle({ 34 | maxLimit: 1000, 35 | maxConcurrency: 2 36 | })); 37 | 38 | rateLimiter.registerService({ 39 | owner: 'anonymous', 40 | name: '' 41 | }); 42 | 43 | app.use(handler, function (req, res) { 44 | res.end(); 45 | }); 46 | server = app.listen(3000, function () { 47 | t.equal(typeof handler, "function", "started HTTP microservice server"); 48 | t.end(); 49 | }); 50 | }); 51 | 52 | test('attempt to send simple http request to running microservice', function (t) { 53 | request({ 54 | uri: 'http://localhost:3000/', 55 | method: "GET", 56 | json: true 57 | }, function (err, res, body) { 58 | t.equal(typeof body, "object", 'got correct response'); 59 | //t.equal(body, "b", "echo'd back property") 60 | t.end(); 61 | }) 62 | }); 63 | 64 | test('attempt to send JSON data to running microservice', function (t) { 65 | request({ 66 | uri: 'http://localhost:3000/', 67 | method: "POST", 68 | json: { 69 | hello: "world" 70 | } 71 | }, function (err, res, body) { 72 | t.equal(typeof body, "object", 'got correct response'); 73 | t.equal(body.hello, "world", "echo'd back property") 74 | t.end(); 75 | }) 76 | }); 77 | 78 | test('attempt to end server', function (t) { 79 | server.close(function(){ 80 | t.ok("server ended"); 81 | t.end(); 82 | }); 83 | }); -------------------------------------------------------------------------------- /test/rate-limit-test.js: -------------------------------------------------------------------------------- 1 | // rate-limit-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | var logger = microcule.plugins.logger; 11 | var mschema = microcule.plugins.mschema; 12 | var RateLimiter = microcule.plugins.RateLimiter; 13 | var spawn = microcule.plugins.spawn; 14 | 15 | var handler = spawn({ 16 | language: "javascript", 17 | code: function service (req, res) { 18 | res.json(req.params); 19 | } 20 | }); 21 | 22 | var neverResponds = spawn({ 23 | language: "javascript", 24 | customTimeout: 100, 25 | code: function service (req, res) { 26 | // does nothing 27 | console.log('never responding') 28 | } 29 | }); 30 | 31 | var Store = require('../lib/plugins/Store'); 32 | var localStore = new Store('memory', 'Rate-Limiter'); 33 | 34 | test('attempt to start simple http server with rate limiter plugin', function (t) { 35 | app = express(); 36 | 37 | var rateLimiter = new RateLimiter({ 38 | provider: localStore 39 | }); 40 | 41 | app.use(rateLimiter.middle({ 42 | maxLimit: 3, 43 | maxConcurrency: 2 44 | })); 45 | 46 | app.use('/echo', handler, function (req, res) { 47 | res.end(); 48 | }); 49 | 50 | app.use('/neverResponds', neverResponds, function (req, res) { 51 | res.end(); 52 | }); 53 | 54 | rateLimiter.registerService({ 55 | owner: 'anonymous', 56 | name: 'echo' 57 | }); 58 | 59 | rateLimiter.registerService({ 60 | owner: 'anonymous', 61 | name: 'neverResponds' 62 | }); 63 | 64 | server = app.listen(3000, function () { 65 | t.equal(typeof handler, "function", "started HTTP microservice server"); 66 | t.end(); 67 | }); 68 | }); 69 | 70 | test('attempt to send simple http request to a registered microservice', function (t) { 71 | request({ 72 | uri: 'http://localhost:3000/echo', 73 | method: "GET", 74 | json: true 75 | }, function (err, res, body) { 76 | t.equal(res.statusCode, 200); 77 | t.equal(typeof body, "object", 'got correct response'); 78 | t.end(); 79 | }) 80 | }); 81 | 82 | test('attempt to send simple http request to an unregistered microservice', function (t) { 83 | request({ 84 | uri: 'http://localhost:3000/echo-unknown', 85 | method: "GET", 86 | json: true 87 | }, function (err, res, body) { 88 | t.equal(res.statusCode, 500); 89 | t.end(); 90 | }) 91 | }); 92 | 93 | test('check metrics for current user', function (t) { 94 | 95 | t.equal(localStore.services['/system/report'].totalHits, 1, 'correct total hits - system report') 96 | t.equal(localStore.services['/system/report'].running, 0, 'correct currently running- system report') 97 | 98 | t.equal(localStore.services['/anonymous/echo/report'].totalHits, 1, 'correct total hits - service report') 99 | t.equal(localStore.services['/anonymous/echo/report'].running, 0, 'correct currently running- service report') 100 | 101 | t.equal(localStore.services['/anonymous/report'].totalHits, 1, 'correct total hits - user report') 102 | t.equal(localStore.services['/anonymous/report'].running, 0, 'correct currently running- user report') 103 | 104 | t.end(); 105 | }); 106 | 107 | test('attempt to send simple http request to microservice that never responds', function (t) { 108 | request({ 109 | uri: 'http://localhost:3000/neverResponds', 110 | method: "GET", 111 | json: true 112 | }, function (err, res, body) { 113 | // t.equal(res.statusCode, 500); 114 | t.equal(res.statusCode, 200); 115 | t.end(); 116 | }) 117 | }); 118 | 119 | test('check metrics for current user', function (t) { 120 | 121 | t.equal(localStore.services['/system/report'].totalHits, 2, 'correct total hits - system report') 122 | t.equal(localStore.services['/system/report'].running, 0, 'correct currently running- system report') 123 | 124 | t.equal(localStore.services['/anonymous/neverResponds/report'].totalHits, 1, 'correct total hits - service report') 125 | t.equal(localStore.services['/anonymous/neverResponds/report'].running, 0, 'correct currently running- service report') 126 | 127 | t.equal(localStore.services['/anonymous/report'].totalHits, 2, 'correct total hits - user report') 128 | t.equal(localStore.services['/anonymous/report'].running, 0, 'correct currently running- user report') 129 | 130 | t.end(); 131 | }); 132 | 133 | test('attempt to send simple http request to a registered microservice', function (t) { 134 | request({ 135 | uri: 'http://localhost:3000/echo', 136 | method: "GET", 137 | json: true 138 | }, function (err, res, body) { 139 | t.equal(res.statusCode, 200); 140 | t.equal(res.headers['x-ratelimit-limit'], '3'); 141 | t.equal(res.headers['x-ratelimit-remaining'], '1'); 142 | t.equal(res.headers['x-ratelimit-running'], '0'); 143 | t.equal(typeof body, "object", 'got correct response'); 144 | t.end(); 145 | }) 146 | }); 147 | 148 | test('attempt to send simple http request to a registered microservice - rate limit exceeded', function (t) { 149 | request({ 150 | uri: 'http://localhost:3000/echo', 151 | method: "GET", 152 | json: true 153 | }, function (err, res, body) { 154 | t.equal(res.statusCode, 500); 155 | t.equal(res.headers['x-ratelimit-limit'], '3'); 156 | t.equal(res.headers['x-ratelimit-remaining'], '0'); 157 | // Currently can't see amount running header when total limit has been exceeded ( could be fixed later ) 158 | // t.equal(res.headers['x-ratelimit-running'], '0'); 159 | t.end(); 160 | }) 161 | }); 162 | 163 | test('attempt to end server', function (t) { 164 | server.close(function(){ 165 | t.ok("server ended"); 166 | t.end(); 167 | }); 168 | }); -------------------------------------------------------------------------------- /test/request-large-json-test.js: -------------------------------------------------------------------------------- 1 | // request-large-json-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handler, luaHandler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | test('attempt to start simple http server with spawn handler', function (t) { 11 | app = express(); 12 | handler = microcule.plugins.spawn({ 13 | language: "javascript", 14 | code: function service (req, res) { 15 | res.end('responded'); 16 | } 17 | }); 18 | app.use(handler, function (req, res) { 19 | res.end(); 20 | }); 21 | server = app.listen(3000, function () { 22 | t.equal(typeof handler, "function", "started HTTP microservice server"); 23 | t.end(); 24 | }); 25 | }); 26 | 27 | test('attempt to send large amount of JSON data to running microservice', function (t) { 28 | 29 | // create a large JSON object 30 | var obj = {}; 31 | for (var i = 0; i < 100; i++) { 32 | obj[i] = new Buffer(1000).toString() 33 | } 34 | 35 | request({ 36 | uri: 'http://localhost:3000/', 37 | method: "POST", 38 | json: obj 39 | }, function (err, res, body) { 40 | // console.log('bbb', body) 41 | t.equal(typeof body, "string", 'got correct response type'); 42 | t.equal(body, "responded\n", 'got correct response'); 43 | t.end(); 44 | }) 45 | }); 46 | 47 | test('attempt to end server', function (t) { 48 | server.close(function(){ 49 | t.ok("server ended"); 50 | t.end(); 51 | }); 52 | }); -------------------------------------------------------------------------------- /test/request-multipart-test.js: -------------------------------------------------------------------------------- 1 | // request-multipart-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var fs = require('fs'); 5 | var request = require('request'); 6 | var microcule, handler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | test('attempt to start simple http server with spawn handler', function (t) { 11 | app = express(); 12 | handler = microcule.plugins.spawn({ 13 | language: "javascript", 14 | code: function service (req, res, next) { 15 | console.log(req.params) 16 | req.params.my_file.pipe(res); 17 | // res.end(req.params) 18 | } 19 | }); 20 | app.use(handler, function(req, res){ 21 | res.end(); 22 | }); 23 | server = app.listen(3000, function () { 24 | t.equal(typeof handler, "function", "started HTTP microservice server"); 25 | t.end(); 26 | }); 27 | }); 28 | 29 | /* 30 | test('attempt to send simple http request to running microservice', function (t) { 31 | request('http://localhost:3000/', function (err, res, body) { 32 | t.equal(body, '{}\n', 'got correct response'); 33 | t.end(); 34 | }) 35 | }); 36 | */ 37 | 38 | test('attempt to send multipart form data to running microservice', function (t) { 39 | 40 | var formData = { 41 | // Pass a simple key-value pair 42 | my_field: 'my_value', 43 | // Pass data via Buffers 44 | my_buffer: new Buffer([1, 2, 3]), 45 | // Pass data via Streams 46 | my_file: fs.createReadStream(__dirname + '/fixtures/assets/file.txt'), 47 | // Pass multiple values /w an Array 48 | attachments: [ 49 | fs.createReadStream(__dirname + '/request-params-test.js'), 50 | fs.createReadStream(__dirname + '/basic-tests.js') 51 | ] 52 | /* 53 | , 54 | // Pass optional meta-data with an 'options' object with style: {value: DATA, options: OPTIONS} 55 | // Use case: for some types of streams, you'll need to provide "file"-related information manually. 56 | // See the `form-data` README for more information about options: https://github.com/form-data/form-data 57 | custom_file: { 58 | value: fs.createReadStream('/dev/urandom'), 59 | options: { 60 | filename: 'topsecret.jpg', 61 | contentType: 'image/jpeg' 62 | } 63 | } 64 | */ 65 | }; 66 | 67 | request.post({url:'http://localhost:3000/', formData: formData }, function optionalCallback(err, httpResponse, body) { 68 | t.error(err); 69 | t.equal(body, 'hello world\nline two'); 70 | t.ok(true, 'did not error on multipart upload'); 71 | t.end(); 72 | }); 73 | 74 | /* 75 | request({ 76 | uri: 'http://localhost:3000/', 77 | method: "POST", 78 | json: { 79 | a: "b" 80 | } 81 | }, function (err, res, body) { 82 | t.equal(typeof body, "object", 'got correct response'); 83 | t.equal(body.a, "b", "echo'd back property") 84 | }) 85 | */ 86 | }); 87 | 88 | test('attempt to end server', function (t) { 89 | server.close(function(){ 90 | t.ok("server ended"); 91 | t.end(); 92 | }); 93 | }); -------------------------------------------------------------------------------- /test/request-params-test.js: -------------------------------------------------------------------------------- 1 | // request-params-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var fs = require('fs'); 5 | var request = require('request'); 6 | var microcule, handler, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | test('attempt to start simple http server with spawn handler', function (t) { 11 | app = express(); 12 | handler = microcule.plugins.spawn({ 13 | language: "javascript", 14 | code: function service (service) { 15 | service.res.json(service.params); 16 | } 17 | }); 18 | app.use(handler, function (req, res) { 19 | res.end(); 20 | }); 21 | server = app.listen(3000, function () { 22 | t.equal(typeof handler, "function", "started HTTP microservice server"); 23 | t.end(); 24 | }); 25 | }); 26 | 27 | test('attempt to send simple http request to running microservice', function (t) { 28 | request('http://localhost:3000/', function (err, res, body) { 29 | t.equal(body, '{}\n', 'got correct response'); 30 | t.end(); 31 | }) 32 | }); 33 | 34 | test('attempt to send JSON data to running microservice', function (t) { 35 | request({ 36 | uri: 'http://localhost:3000/', 37 | method: "POST", 38 | json: { 39 | a: "b" 40 | } 41 | }, function (err, res, body) { 42 | t.equal(typeof body, "object", 'got correct response'); 43 | t.equal(body.a, "b", "echo'd back property") 44 | t.end(); 45 | }) 46 | }); 47 | 48 | test('attempt to send multipart form data to running microservice', function (t) { 49 | 50 | var formData = { 51 | // Pass a simple key-value pair 52 | my_field: 'my_value', 53 | // Pass data via Buffers 54 | my_buffer: new Buffer([1, 2, 3]), 55 | // Pass data via Streams 56 | my_file: fs.createReadStream(__dirname + '/request-params-test.js'), 57 | // Pass multiple values /w an Array 58 | attachments: [ 59 | fs.createReadStream(__dirname + '/request-params-test.js'), 60 | fs.createReadStream(__dirname + '/basic-tests.js') 61 | ] 62 | /* 63 | , 64 | // Pass optional meta-data with an 'options' object with style: {value: DATA, options: OPTIONS} 65 | // Use case: for some types of streams, you'll need to provide "file"-related information manually. 66 | // See the `form-data` README for more information about options: https://github.com/form-data/form-data 67 | custom_file: { 68 | value: fs.createReadStream('/dev/urandom'), 69 | options: { 70 | filename: 'topsecret.jpg', 71 | contentType: 'image/jpeg' 72 | } 73 | } 74 | */ 75 | }; 76 | 77 | request.post({url:'http://localhost:3000/', formData: formData }, function optionalCallback(err, httpResponse, body) { 78 | t.error(err); 79 | t.ok(true, 'did not error on multipart upload'); 80 | t.end(); 81 | }); 82 | 83 | }); 84 | 85 | test('attempt to end server', function (t) { 86 | server.close(function(){ 87 | t.ok("server ended"); 88 | t.end(); 89 | }); 90 | }); -------------------------------------------------------------------------------- /test/response-methods-test.js: -------------------------------------------------------------------------------- 1 | // response-methods-test.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var fs = require('fs'); 5 | var request = require('request'); 6 | var microcule, app, server; 7 | 8 | microcule = require('../'); 9 | 10 | test('attempt to start simple http server with spawn handler', function (t) { 11 | app = express(); 12 | 13 | var handler = microcule.plugins.spawn({ 14 | language: "javascript", 15 | code: function service (req, res) { 16 | res.statusCode = 404; 17 | res.end('ended'); 18 | } 19 | }); 20 | app.use('/res-statusCode', handler, function (req, res) { 21 | res.end(); 22 | }); 23 | 24 | var resStatus = microcule.plugins.spawn({ 25 | language: "javascript", 26 | code: function service (req, res) { 27 | res.status(403); 28 | res.end('ended'); 29 | } 30 | }); 31 | app.use('/res-status', resStatus, function (req, res) { 32 | res.end(); 33 | }); 34 | 35 | var resWriteHead = microcule.plugins.spawn({ 36 | language: "javascript", 37 | code: function service (req, res) { 38 | res.writeHead(500, { 'custom-header-x': 'custom-val-0' }); 39 | res.end('ended'); 40 | } 41 | }); 42 | app.use('/res-writeHead', resWriteHead, function (req, res) { 43 | res.end(); 44 | }); 45 | 46 | server = app.listen(3000, function () { 47 | t.equal(typeof handler, "function", "started HTTP microservice server"); 48 | t.end(); 49 | }); 50 | }); 51 | 52 | test('attempt to send simple http request to running microservice', function (t) { 53 | request('http://localhost:3000/res-statusCode', function (err, res, body) { 54 | t.equal(body, 'ended\n', 'got correct response'); 55 | t.equal(res.statusCode, 404) 56 | t.end(); 57 | }) 58 | }); 59 | 60 | test('attempt to send simple http request to running microservice', function (t) { 61 | request('http://localhost:3000/res-status', function (err, res, body) { 62 | t.equal(body, 'ended\n', 'got correct response'); 63 | t.equal(res.statusCode, 403) 64 | t.end(); 65 | }) 66 | }); 67 | 68 | test('attempt to send simple http request to running microservice', function (t) { 69 | request('http://localhost:3000/res-writeHead', function (err, res, body) { 70 | t.equal(body, 'ended\n', 'got correct response'); 71 | t.equal(res.statusCode, 500); 72 | t.equal(res.headers['custom-header-x'], 'custom-val-0'); 73 | t.end(); 74 | }) 75 | }); 76 | 77 | test('attempt to end server', function (t) { 78 | server.close(function(){ 79 | t.ok("server ended"); 80 | t.end(); 81 | }); 82 | }); -------------------------------------------------------------------------------- /test/service-as-middleware-tests.js: -------------------------------------------------------------------------------- 1 | // service-as-middleware-tests.js 2 | var test = require("tape"); 3 | var express = require('express'); 4 | var request = require('request'); 5 | 6 | var microcule, handlers = {}, app, server; 7 | 8 | test('attempt to require microcule', function (t) { 9 | microcule = require('../'); 10 | t.equal(typeof microcule, 'object', 'microcule module required'); 11 | t.end(); 12 | }); 13 | 14 | test('attempt to create a few chainable microservice spawn handlers', function (t) { 15 | 16 | handlers['basicAuth'] = microcule.plugins.spawn({ 17 | language: "javascript", 18 | chain: true, 19 | code: function (req, res, next) { 20 | var auth = require('basic-auth') 21 | var credentials = auth(req) 22 | if (!credentials || credentials.name !== 'admin' || credentials.pass !== 'password') { 23 | //res.statusCode(401); 24 | res.setHeader('WWW-Authenticate', 'Basic realm="examples"') 25 | res.writeHead(401); 26 | res.end('Access denied'); 27 | } else { 28 | next(); 29 | } 30 | } 31 | }); 32 | 33 | handlers['write-a'] = microcule.plugins.spawn({ 34 | language: "bash", 35 | code: 'echo "a"', 36 | chain: true 37 | }); 38 | handlers['write-b'] = microcule.plugins.spawn({ 39 | language: "javascript", 40 | chain: true, 41 | code: function (req, res, next) { 42 | res.write('b\n'); 43 | next(); // call next() to indicate this services is not going to explictly end the response 44 | } 45 | }); 46 | handlers['write-c'] = microcule.plugins.spawn({ 47 | language: "bash", 48 | code: 'echo "c"', 49 | chain: true 50 | }); 51 | t.end(); 52 | }); 53 | 54 | test('attempt to start simple http server with spawn handler', function (t) { 55 | app = express(); 56 | 57 | app.use([handlers['basicAuth'], handlers['write-a'], handlers['write-b'], handlers['write-c']], function (req, res) { 58 | console.log("No middlewares ended response, made it to end"); 59 | res.end('caught end') 60 | }); 61 | 62 | server = app.listen(3000, function () { 63 | t.end(); 64 | }); 65 | }); 66 | 67 | test('attempt to send simple http request to running microservice', function (t) { 68 | request('http://admin:password@localhost:3000/', function (err, res, body) { 69 | t.equal(body, 'a\nb\nc\ncaught end', 'got correct response'); 70 | t.end(); 71 | }) 72 | }); 73 | 74 | test('attempt to end server', function (t) { 75 | server.close(function(){ 76 | t.ok("server ended"); 77 | t.end(); 78 | }); 79 | }); -------------------------------------------------------------------------------- /tmp/ReadMe.md: -------------------------------------------------------------------------------- 1 | temporary microservice source code files will populate here by default --------------------------------------------------------------------------------