├── .gitignore ├── Makefile ├── README.md ├── TODO.md ├── priv ├── base.js ├── cert.js ├── console.js ├── context.js ├── dns.js ├── http.js └── ws.js ├── rebar.config ├── rebar3 ├── src ├── erlang_v8_lib.app.src ├── erlang_v8_lib.erl ├── erlang_v8_lib_app.erl ├── erlang_v8_lib_bg_procs.erl ├── erlang_v8_lib_pool.erl ├── erlang_v8_lib_run.erl ├── erlang_v8_lib_sup.erl ├── erlang_v8_lib_test.erl ├── erlang_v8_lib_utils.erl ├── erlang_v8_lib_vm_sup.erl ├── erlang_v8_lib_worker.erl └── handlers │ ├── erlang_v8_cert.erl │ ├── erlang_v8_dns.erl │ ├── erlang_v8_http.erl │ ├── erlang_v8_http_taser.erl │ ├── erlang_v8_stdout_console.erl │ └── erlang_v8_ws.erl └── test ├── cert_SUITE.erl ├── dns_SUITE.erl ├── generic_SUITE.erl ├── http_SUITE.erl ├── pool_SUITE.erl └── ws_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.beam 4 | deps/* 5 | ebin/* 6 | erl_crash.dump 7 | /log 8 | /logs 9 | /.rebar 10 | 11 | /_build 12 | /_checkouts 13 | /.rebar3 14 | /rebar.lock 15 | /.erlang.mk 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR := "./rebar3" 2 | 3 | .PHONY: build test deps rel shell 4 | 5 | all: deps build 6 | 7 | build: 8 | $(REBAR) compile 9 | 10 | test: 11 | $(REBAR) ct 12 | 13 | deps: 14 | $(REBAR) get-deps 15 | 16 | shell: 17 | $(REBAR) shell 18 | 19 | rel: test 20 | $(REBAR) release 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `erlang_v8_lib` 2 | 3 | An opinionated JavaScript framework built on top of `erlang_v8`. 4 | `erlang_v8_lib` is an Erlang application that includes a small framework to 5 | simplify the task of adding functionality to the scripting environment. 6 | 7 | The app adds three major components to accomplish this: 8 | 9 | - A v8 worker pool 10 | - A simple module system that makes it easy to connect JavaScript and Erlang. 11 | - A few batteries included modules 12 | 13 | ## Getting started 14 | 15 | Compile and start a shell: 16 | 17 | $ make 18 | $ make shell 19 | 20 | Ensure that all required applications have been started, start the lib 21 | supervisor (note that this is a separate and mandatory step. the reasoning 22 | will be outlined below) and run some code: 23 | 24 | 1> application:ensure_all_started(erlang_v8_lib). 25 | {ok, [...]} 26 | 2> erlang_v8_lib_sup:start_link(). 27 | {ok,<0.69.0>} 28 | 3> erlang_v8_lib:run(<<"process.return(1 + 1)">>). 29 | {ok,2} 30 | 31 | ## Registering handlers 32 | 33 | A handler is an erlang module that exposes a `run/2` function that is 34 | registered with an identifier: 35 | 36 | -module(my_handler). 37 | 38 | -export([run/2]). 39 | 40 | run(_Args, _HandlerContext) -> 41 | {ok, <<"hello">>}. 42 | 43 | Registering the module as `{<<"myhandler">>, my_handler}` (more docs inc) will 44 | allow you to invoke the handler from JavaScript: 45 | 46 | external.run('myhandler').then(function(value) { 47 | process.return(value); 48 | }); 49 | 50 | The above will match `'myhandler'` with the handler registered as 51 | `<<"myhandler">>`, call `run/1` without arguments, fulfill the promise with 52 | the value `'hello'` and "return" (see below) the value to the calling process. 53 | 54 | ## JavaScript API 55 | 56 | The application comes with a few pre-built modules. 57 | 58 | ### `process` 59 | 60 | The `process` module does cool stuff. 61 | 62 | #### `process.return(value)` 63 | 64 | Return a value to the calling Erlang process. Execution in the current scope 65 | proceeds in the VM, but no external events occur after the call to return. 66 | 67 | {ok, 1} = erlang_v8_lib:run(<<"process.return(1);">>). 68 | 69 | ### `http` 70 | 71 | Simple HTTP api. 72 | 73 | #### `http.request(method, url, [options])` 74 | 75 | Make a HTTP request to `url`. 76 | 77 | Example: 78 | 79 | {ok, _Body} = erlang_v8_lib:run(<<" 80 | http.request('get', 'http://httpbin.org/get') 81 | .then((resp) => resp.body()) 82 | .then((body) => process.return(body)); 83 | }); 84 | ">>). 85 | 86 | Valid `options` attributes: 87 | 88 | - `payload`: Data to be sent (String) 89 | - `headers`: Headers to send (Object) 90 | 91 | The function returns a promise. The resolve function takes one argument: 92 | `response`. The response object has the following attributes and methods: 93 | 94 | - `response.json()`: Convert payload to a JavaScript structure (Promise). 95 | - `response.text()`: Return payload as text (Promise). 96 | 97 | #### `http.get(url, [options])` 98 | 99 | Shorthand for `http.request('get', url, [options])`. 100 | 101 | #### `http.post(url, [options])` 102 | 103 | Shorthand for `http.request('post', url, [options])`. 104 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | x custom handlers 2 | x pool 3 | x (and fix erlang v8) 4 | x reset vm 5 | x exit/escape vm 6 | x tests 7 | - error handling (timeout etc) 8 | - http module fixes (headers, request/4 etc) 9 | - freeze modules 10 | - limit sizes 11 | -------------------------------------------------------------------------------- /priv/base.js: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/a/18391400 2 | if (!('toJSON' in Error.prototype)) 3 | Object.defineProperty(Error.prototype, 'toJSON', { 4 | value: function () { 5 | var alt = {}; 6 | 7 | Object.getOwnPropertyNames(this).forEach(function (key) { 8 | alt[key] = this[key]; 9 | }, this); 10 | 11 | return alt; 12 | }, 13 | configurable: true, 14 | writable: true 15 | }); 16 | 17 | var __internal = { 18 | actions: [], 19 | data: [], 20 | promises: {}, 21 | context: {}, 22 | setContext: function(context) { 23 | __internal.context = context || {}; 24 | }, 25 | handleExternal: function(status, ref, value) { 26 | __internal.actions = []; 27 | if (status && ref) { 28 | var promise = __internal.promises[ref]; 29 | 30 | if (status === 'success') { 31 | promise.resolve(value); 32 | } else if (status === 'error') { 33 | promise.reject(value); 34 | } 35 | } 36 | return __internal.actions; 37 | } 38 | }; 39 | 40 | var external = { 41 | run: function(command, args) { 42 | var ref = String(Math.random()); 43 | 44 | var p = new Promise(function(resolve, reject) { 45 | __internal.promises[ref] = { 46 | resolve: resolve, 47 | reject: reject 48 | }; 49 | __internal.actions.push(['external', command, ref, args]); 50 | }); 51 | 52 | return p; 53 | } 54 | }; 55 | 56 | var process = { 57 | return: function(value) { 58 | __internal.actions.push(['return', value]); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /priv/cert.js: -------------------------------------------------------------------------------- 1 | var cert = { 2 | validity: function(hostname, port) { 3 | return external.run('cert', ['validity', hostname, port || 443]); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /priv/console.js: -------------------------------------------------------------------------------- 1 | var console = { 2 | log: function(msg) { 3 | external.run('console', ['log', msg]); 4 | }, 5 | warn: function(msg) { 6 | external.run('console', ['warn', msg]); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /priv/context.js: -------------------------------------------------------------------------------- 1 | var Context = { 2 | get: function() { 3 | return __internal.context; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /priv/dns.js: -------------------------------------------------------------------------------- 1 | var dns = { 2 | resolve: function(hostname, type) { 3 | return external.run('dns', ['resolve', hostname, type || 'A']); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /priv/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = (function() { 4 | /** 5 | * Supported HTTP request methods. 6 | * 7 | * @enum {string} 8 | */ 9 | var methods = { 10 | DELETE: 'DELETE', 11 | GET: 'GET', 12 | HEAD: 'HEAD', 13 | PATCH: 'PATCH', 14 | POST: 'POST', 15 | PUT: 'PUT' 16 | }; 17 | 18 | /** 19 | * Takes an object and converts its key value pairs into a query string. 20 | * 21 | * @param {obect} params 22 | * @return {string} 23 | */ 24 | function formatQueryParams(params) { 25 | var prefix = '?'; 26 | var qs = ''; 27 | for (var k in params) { 28 | if (params.hasOwnProperty(k)) { 29 | qs += prefix + k.toString() + '=' + params[k].toString(); 30 | prefix = '&'; 31 | } 32 | } 33 | return qs; 34 | } 35 | 36 | /** 37 | * Makes a request. 38 | * 39 | * @param {string} method The HTTP "verb" to use. 40 | * @param {string} url The remote url to make the request to. 41 | * @param {object} config Configuration 42 | */ 43 | function request(method, url, config) { 44 | config = config || {}; 45 | 46 | var body = config.body || ''; 47 | var headers = config.headers || {}; 48 | var queryParams = config.queryParams || {}; 49 | var followRedirect = config.followRedirect || false; 50 | 51 | url += formatQueryParams(queryParams); 52 | 53 | return external.run('http', [ 54 | String(url), 55 | String(method).toUpperCase(), 56 | Object(headers), 57 | String(body), 58 | Boolean(followRedirect) 59 | ]); 60 | }; 61 | 62 | function __resolve_promise(status, ref, resp) { 63 | var body = resp.body; 64 | delete resp.body; 65 | 66 | try { 67 | resp.headers = JSON.parse(resp.headers); 68 | } catch(e) { 69 | resp.headers = {}; 70 | } 71 | 72 | resp.text = function() { 73 | var p = new Promise(function(resolve, reject) { 74 | resolve(body); 75 | }); 76 | return p; 77 | }; 78 | 79 | resp.json = function() { 80 | var p = new Promise(function(resolve, reject) { 81 | try { 82 | var json = JSON.parse(body); 83 | resolve(json); 84 | } catch(e) { 85 | reject(e); 86 | } 87 | }); 88 | return p; 89 | }; 90 | 91 | resp.blob = function() { 92 | var p = new Promise(function(resolve, reject) { 93 | reject('Blobs are not supported yet.'); 94 | }); 95 | return p; 96 | }; 97 | 98 | resp.arrayBuffer = function() { 99 | var p = new Promise(function(resolve, reject) { 100 | reject('ArrayBuffers are not supported yet.'); 101 | }); 102 | return p; 103 | }; 104 | 105 | return __internal.handleExternal(status, ref, resp); 106 | }; 107 | 108 | /** 109 | * Expose methods on the http object. 110 | */ 111 | return { 112 | request: request, 113 | get: request.bind({}, methods.GET), 114 | delete: request.bind({}, methods.DELETE), 115 | head: request.bind({}, methods.HEAD), 116 | patch: request.bind({}, methods.PATCH), 117 | post: request.bind({}, methods.POST), 118 | put: request.bind({}, methods.PUT), 119 | __resolve_promise: __resolve_promise 120 | }; 121 | })(); 122 | -------------------------------------------------------------------------------- /priv/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ws = (function() { 4 | function open(url, options) { 5 | options = options || {}; 6 | var headers = options.headers || {}; 7 | var subprotocols = options.subprotocols || []; 8 | 9 | if (!Array.isArray(subprotocols)) { 10 | subprotocols = [String(subprotocols)]; 11 | } else { 12 | subprotocols = subprotocols.map((v) => String(v)); 13 | } 14 | 15 | return external.run('ws', [ 16 | 'connect', 17 | String(url), 18 | Object(headers), 19 | subprotocols 20 | ]); 21 | }; 22 | 23 | function __resolve_conn_promise(status, ref, socket) { 24 | var conn = { 25 | receive: function() { 26 | return external.run('ws', ['receive', socket]); 27 | }, 28 | send: function(data) { 29 | return external.run('ws', ['send', socket, String(data)]); 30 | }, 31 | close: function() { 32 | return external.run('ws', ['close', socket]); 33 | } 34 | }; 35 | 36 | return __internal.handleExternal(status, ref, conn); 37 | }; 38 | 39 | return { 40 | open: open, 41 | __resolve_conn_promise: __resolve_conn_promise 42 | }; 43 | })(); 44 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {erlang_v8, {git, "https://github.com/strange/erlang-v8", {branch, master}}}, 3 | {taser, {git, "https://github.com/trelltech/taser.git"}}, 4 | {oath, {git, "https://github.com/strange/oath.git"}}, 5 | hackney, 6 | {gun, {git, "https://github.com/ninenines/gun/", {tag, "1.0.0-pre.4"}}}, 7 | lager 8 | ]}. 9 | 10 | {profiles, [ 11 | {test, [ 12 | {deps, [ 13 | {hemlock, {git, "https://github.com/trelltech/hemlock.git"}} 14 | ]} 15 | ]} 16 | ]}. 17 | 18 | {erl_opts, [{parse_transform, lager_transform}]}. 19 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strange/erlang_v8_lib/fe9a3e6c1eb87c3d8a0df61ff6410cc1adb0c773/rebar3 -------------------------------------------------------------------------------- /src/erlang_v8_lib.app.src: -------------------------------------------------------------------------------- 1 | {application, erlang_v8_lib, [ 2 | {description, ""}, 3 | {vsn, "rolling"}, 4 | {id, "git"}, 5 | {modules, [ 6 | erlang_v8_http, 7 | erlang_v8_http_taser, 8 | erlang_v8_ws, 9 | erlang_v8_lib, 10 | erlang_v8_lib_app, 11 | erlang_v8_lib_bg_procs, 12 | erlang_v8_lib_pool, 13 | erlang_v8_lib_run, 14 | erlang_v8_lib_sup, 15 | erlang_v8_lib_test, 16 | erlang_v8_lib_utils, 17 | erlang_v8_lib_vm_sup, 18 | erlang_v8_lib_worker, 19 | erlang_v8_stdout_console 20 | ]}, 21 | {registered, [erlang_v8_lib_sup]}, 22 | {applications, [kernel,stdlib,erlang_v8,hackney,taser,gun,oath,lager]}, 23 | {mod, {erlang_v8_lib_app, []}}, 24 | {env, [ 25 | {core, [ 26 | {erlang_v8_lib, "base.js"} 27 | ]}, 28 | {modules, [ 29 | {erlang_v8_lib, "http.js"}, 30 | {erlang_v8_lib, "ws.js"}, 31 | {erlang_v8_lib, "console.js"}, 32 | {erlang_v8_lib, "cert.js"}, 33 | {erlang_v8_lib, "dns.js"}, 34 | {erlang_v8_lib, "context.js"} 35 | ]}, 36 | {handlers, [ 37 | {<<"console">>, erlang_v8_stdout_console}, 38 | {<<"cert">>, erlang_v8_cert}, 39 | {<<"ws">>, erlang_v8_ws}, 40 | {<<"dns">>, erlang_v8_dns}, 41 | {<<"http">>, erlang_v8_http_taser} 42 | ]} 43 | ]} 44 | ]}. 45 | -------------------------------------------------------------------------------- /src/erlang_v8_lib.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib). 2 | 3 | -export([run/1]). 4 | -export([run/2]). 5 | 6 | run(Source) -> 7 | run(Source, #{}). 8 | 9 | run(Source, Opts) when is_binary(Source) -> 10 | run([{eval, Source}], Opts); 11 | 12 | run(Instructions, Opts) when is_map(Opts) -> 13 | erlang_v8_lib_run:run(Instructions, Opts). 14 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_app.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_app). 2 | 3 | -behaviour(application). 4 | 5 | -export([start/2]). 6 | -export([stop/1]). 7 | 8 | -behaviour(supervisor). 9 | 10 | -export([start_link/0]). 11 | -export([init/1]). 12 | 13 | start(_Type, _Args) -> 14 | erlang_v8_lib_app:start_link(). 15 | 16 | stop(_State) -> 17 | ok. 18 | 19 | start_link() -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 21 | 22 | init([]) -> 23 | SupFlags = #{ 24 | strategy => one_for_one, 25 | intensity => 3, 26 | period => 10 27 | }, 28 | ChildSpecs = [#{ 29 | id => erlang_v8_lib_vm_sup, 30 | start => {erlang_v8_lib_vm_sup, start_link, []}, 31 | type => supervisor 32 | }], 33 | {ok, {SupFlags, ChildSpecs}}. 34 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_bg_procs.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_bg_procs). 2 | 3 | -export([start_link/0]). 4 | 5 | -export([connect/0]). 6 | -export([disconnect/0]). 7 | -export([add/1]). 8 | -export([get/1]). 9 | -export([remove/1]). 10 | 11 | -export([init/1]). 12 | -export([handle_call/3]). 13 | -export([handle_cast/2]). 14 | -export([handle_info/2]). 15 | -export([terminate/2]). 16 | -export([code_change/3]). 17 | 18 | %% External API 19 | 20 | start_link() -> 21 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 22 | 23 | connect() -> 24 | gen_server:call(?MODULE, {connect, self()}). 25 | 26 | disconnect() -> 27 | gen_server:call(?MODULE, {disconnect, self()}). 28 | 29 | add(Proc) -> 30 | gen_server:call(?MODULE, {add, self(), Proc}). 31 | 32 | get(Ref) -> 33 | gen_server:call(?MODULE, {get, self(), Ref}). 34 | 35 | remove(Ref) -> 36 | gen_server:call(?MODULE, {remove, self(), Ref}). 37 | 38 | %% Callbacks 39 | 40 | init([]) -> 41 | lager:info("Background process monitor started."), 42 | ets:new(?MODULE, [ordered_set, named_table]), 43 | {ok, []}. 44 | 45 | handle_call({add, Pid, Proc}, _From, State) -> 46 | Ref = base64:encode(crypto:strong_rand_bytes(32)), 47 | Procs = ets:lookup_element(?MODULE, Pid, 3), 48 | ets:update_element(?MODULE, Pid, {3, Procs#{ Ref => Proc }}), 49 | {reply, {ok, Ref}, State}; 50 | 51 | handle_call({get, Pid, Ref}, _From, State) -> 52 | case ets:lookup_element(?MODULE, Pid, 3) of 53 | #{ Ref := Proc } -> 54 | {reply, {ok, Proc}, State}; 55 | _ -> 56 | {reply, {error, not_found}, State} 57 | end; 58 | 59 | handle_call({remove, Pid, Ref}, _From, State) -> 60 | case ets:lookup_element(?MODULE, Pid, 3) of 61 | #{ Ref := _Proc } = Procs -> 62 | NewProcs = maps:remove(Ref, Procs), 63 | ets:update_element(?MODULE, Pid, {3, NewProcs}), 64 | {reply, ok, State}; 65 | _ -> 66 | {reply, {error, not_found}, State} 67 | end; 68 | 69 | handle_call({connect, Pid}, _From, State) -> 70 | MRef = erlang:monitor(process, Pid), 71 | ets:insert(?MODULE, {Pid, MRef, #{}}), 72 | {reply, ok, State}; 73 | 74 | handle_call({disconnect, Pid}, _From, State) -> 75 | [{_Pid, MRef, Procs}] = ets:lookup(?MODULE, Pid), 76 | exit_all_procs(Procs, normal), 77 | true = ets:delete(?MODULE, Pid), 78 | true = erlang:demonitor(MRef, [flush]), 79 | {reply, ok, State}; 80 | 81 | handle_call(_Message, _From, State) -> 82 | {reply, ok, State}. 83 | 84 | handle_cast(_Message, State) -> 85 | {noreply, State}. 86 | 87 | handle_info({'DOWN', MRef, process, Pid, Reason}, State) -> 88 | [{_Pid, MRef, Procs}] = ets:lookup(?MODULE, Pid), 89 | true = ets:delete(?MODULE, Pid), 90 | exit_all_procs(Procs, Reason), 91 | {noreply, State}; 92 | 93 | handle_info(Msg, State) -> 94 | lager:info("Other: ~p", [Msg]), 95 | {noreply, State}. 96 | 97 | terminate(_Reason, _State) -> 98 | ok. 99 | 100 | code_change(_OldVersion, State, _Extra) -> 101 | {ok, State}. 102 | 103 | %% Internal API 104 | 105 | exit_all_procs(Procs, Reason) -> 106 | [exit(Pid, Reason) || Pid <- maps:values(Procs)], 107 | ok. 108 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_pool.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_pool). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/1]). 6 | 7 | -export([claim/0]). 8 | -export([release/1]). 9 | -export([eval/2]). 10 | -export([call/3]). 11 | 12 | -export([init/1]). 13 | -export([handle_call/3]). 14 | -export([handle_cast/2]). 15 | -export([handle_info/2]). 16 | -export([terminate/2]). 17 | -export([code_change/3]). 18 | 19 | -record(state, { 20 | max_contexts, 21 | handlers, 22 | nvms, 23 | vms 24 | }). 25 | 26 | start_link(Opts) -> 27 | gen_server:start_link({local, ?MODULE}, ?MODULE, [Opts], []). 28 | 29 | claim() -> 30 | {ok, VM, Ref, Handlers} = gen_server:call(?MODULE, {claim, self()}, 10000), 31 | {ok, Context} = erlang_v8_vm:create_context(VM), 32 | ets:insert(?MODULE, {self(), VM, Context, Ref}), 33 | {ok, {VM, Context, Handlers}}. 34 | 35 | release({VM, Context, _Handlers}) -> 36 | gen_server:call(?MODULE, {release, VM, Context}, 30000). 37 | 38 | eval({VM, Context, _Handlers}, Source) -> 39 | erlang_v8:eval(VM, Context, Source). 40 | 41 | call({VM, Context, _Handlers}, Fun, Args) -> 42 | erlang_v8:call(VM, Context, Fun, Args). 43 | 44 | init([Opts]) -> 45 | ets:new(?MODULE, [ 46 | duplicate_bag, 47 | named_table, 48 | public, 49 | {write_concurrency, true} 50 | ]), 51 | 52 | {ok, NVMs, Files, Handlers} = parse_opts(Opts), 53 | 54 | Args = [{file, File} || File <- Files], 55 | VMs = lists:map(fun(_) -> 56 | {ok, VM} = erlang_v8_lib_vm_sup:start_child(Args), 57 | VM 58 | end, lists:seq(1, NVMs)), 59 | 60 | {ok, #state{vms = VMs, handlers = Handlers, nvms = length(VMs)}}. 61 | 62 | handle_call({claim, Pid}, _From, #state{vms = VMs, nvms = NVMs, 63 | handlers = Handlers} = State) -> 64 | Ref = erlang:monitor(process, Pid), 65 | VM = random_vm(VMs, NVMs, Pid), 66 | {reply, {ok, VM, Ref, Handlers}, State}; 67 | 68 | handle_call({release, VM, Context}, _From, State) -> 69 | Pattern = {'_', VM, Context, '_'}, 70 | case ets:match_object(?MODULE, Pattern) of 71 | [{_Pid, _VM, _Context, Ref}] -> 72 | ok = erlang_v8_vm:destroy_context(VM, Context), 73 | true = erlang:demonitor(Ref, [flush]), 74 | true = ets:match_delete(?MODULE, Pattern), 75 | {reply, ok, State}; 76 | [] -> 77 | {reply, {error, invalid_worker}, State} 78 | end; 79 | 80 | handle_call(_Request, _From, State) -> 81 | {reply, ok, State}. 82 | 83 | handle_cast(_Msg, State) -> 84 | {noreply, State}. 85 | 86 | handle_info({'DOWN', Ref, process, Pid, _Reason}, State) -> 87 | Pattern = {Pid, '_', '_', Ref}, 88 | case ets:match_object(?MODULE, Pattern) of 89 | [{Pid, VM, Context, Ref}] -> 90 | _ = erlang_v8_vm:destroy_context(VM, Context), 91 | true = ets:match_delete(?MODULE, Pattern), 92 | {noreply, State}; 93 | [] -> 94 | {noreply, State} 95 | end; 96 | 97 | handle_info(_Info, State) -> 98 | {noreply, State}. 99 | 100 | terminate(_Reason, _State) -> 101 | ok. 102 | 103 | code_change(_OldVsn, State, _Extra) -> 104 | {ok, State}. 105 | 106 | %% Internal 107 | 108 | parse_opts(Opts) -> 109 | VMs = maps:get(vms, Opts, 5), 110 | 111 | ExtraModules = maps:get(extra_modules, Opts, []), 112 | Modules = maps:get(modules, Opts, 113 | application:get_env(erlang_v8_lib, modules, [])), 114 | Core = application:get_env(erlang_v8_lib, core, []), 115 | 116 | Files = [begin 117 | Path = priv_dir(Appname), 118 | filename:join(Path, Filename) 119 | end || {Appname, Filename} <- Core ++ Modules ++ ExtraModules], 120 | 121 | DefaultHandlers = application:get_env(erlang_v8_lib, handlers, []), 122 | ExtraHandlers = maps:get(extra_handlers, Opts, []), 123 | HandlerList = erlang_v8_lib_utils:extend(1, DefaultHandlers, ExtraHandlers), 124 | Handlers = maps:from_list(HandlerList), 125 | 126 | {ok, VMs, Files, Handlers}. 127 | 128 | random_vm(VMs, NVMs, Pid) -> 129 | Rand = erlang:phash2(Pid, NVMs) + 1, 130 | %% Rand = random:uniform(NVMs), 131 | lists:nth(Rand, VMs). 132 | 133 | priv_dir(Appname) -> 134 | case code:priv_dir(Appname) of 135 | {error, bad_name} -> 136 | %% The app has probably not been loaded properly yet. Attempt to 137 | %% achieve the same effect by loading the priv dir relative to a 138 | %% module with the same name as the app. This is a terrible idea 139 | %% for several reasons, but the system will be replaced soon 140 | %% anyway. 141 | Ebin = filename:dirname(code:which(Appname)), 142 | filename:join(filename:dirname(Ebin), "priv"); 143 | Name -> 144 | Name 145 | end. 146 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_run.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_run). 2 | 3 | -export([run/2]). 4 | 5 | run(Instructions, Opts) -> 6 | HandlerContext = maps:get(handler_context, Opts, #{}), 7 | {ok, Worker} = erlang_v8_lib_pool:claim(), 8 | ok = erlang_v8_lib_bg_procs:connect(), 9 | R = run(Worker, Instructions, HandlerContext), 10 | %% ok = erlang_v8_lib_bg_procs:disconnect(), 11 | _ = erlang_v8_lib_pool:release(Worker), 12 | R. 13 | 14 | run(Worker, [Instruction|Instructions], HandlerContext) -> 15 | case unwind(Worker, [Instruction], HandlerContext) of 16 | {error, Reason} -> 17 | {error, Reason}; 18 | Other when length(Instructions) =:= 0 -> 19 | Other; 20 | _Other -> 21 | run(Worker, Instructions, HandlerContext) 22 | end. 23 | 24 | unwind(_Worker, [], _HandlerContext) -> 25 | ok; 26 | 27 | unwind(Worker, [{context, Context}], _HandlerContext) -> 28 | case erlang_v8_lib_pool:call(Worker, 29 | <<"__internal.setContext">>, [Context]) of 30 | {error, Reason} -> 31 | {error, Reason}; 32 | {ok, undefined} -> 33 | ok 34 | end; 35 | 36 | unwind(Worker, [{call, Fun, Args}], HandlerContext) -> 37 | {ok, []} = erlang_v8_lib_pool:eval(Worker, <<"__internal.actions = [];">>), 38 | case erlang_v8_lib_pool:call(Worker, Fun, Args) of 39 | {error, Reason} -> 40 | {error, Reason}; 41 | {ok, undefined} -> 42 | case erlang_v8_lib_pool:eval(Worker, <<"__internal.actions;">>) of 43 | {ok, Actions} -> 44 | unwind(Worker, Actions, HandlerContext); 45 | {error, Reason} -> 46 | {error, Reason} 47 | end; 48 | {ok, Value} -> 49 | %% TODO: What about returned values? Treat as a regular return? 50 | {ok, jsx:decode(jsx:encode(Value), [return_maps])} 51 | end; 52 | 53 | unwind(Worker, [{eval, Source}], HandlerContext) -> 54 | case erlang_v8_lib_pool:eval(Worker, <<" 55 | __internal.actions = []; 56 | ", Source/binary, " 57 | __internal.actions; 58 | ">>) of 59 | {ok, Actions} -> 60 | unwind(Worker, Actions, HandlerContext); 61 | {error, Reason} -> 62 | {error, Reason} 63 | end; 64 | 65 | unwind(_Worker, [[<<"return">>, Value]|_], _HandlerContext) -> 66 | {ok, jsx:decode(jsx:encode(Value), [return_maps])}; 67 | 68 | unwind(Worker, [[<<"external">>, HandlerIdentifier, Ref, Args]|T], 69 | HandlerContext) -> 70 | Actions = dispatch_external(Worker, Ref, Args, HandlerIdentifier, 71 | HandlerContext), 72 | unwind(Worker, Actions ++ T, HandlerContext); 73 | 74 | unwind(Worker, [[resolve_in_js, Status, Ref, Fun, Args]|T], HandlerContext) -> 75 | {ok, Actions} = erlang_v8_lib_pool:call(Worker, Fun, [Status, Ref, Args]), 76 | unwind(Worker, Actions ++ T, HandlerContext); 77 | 78 | unwind(Worker, [[callback, Status, Ref, Args]|T], HandlerContext) -> 79 | Fun = <<"__internal.handleExternal">>, 80 | {ok, Actions} = erlang_v8_lib_pool:call(Worker, Fun, [Status, Ref, Args]), 81 | unwind(Worker, Actions ++ T, HandlerContext); 82 | 83 | unwind(Worker, [Action|T], HandlerContext) -> 84 | lager:error("Unknown instruction: ~p", [Action]), 85 | unwind(Worker, T, HandlerContext). 86 | 87 | dispatch_external({_, _, Handlers}, Ref, Args, HandlerIdentifier, 88 | HandlerContext) -> 89 | case maps:get(HandlerIdentifier, Handlers, undefined) of 90 | undefined -> 91 | [[callback, <<"error">>, Ref, <<"Invalid external handler.">>]]; 92 | HandlerMod -> 93 | case HandlerMod:run(Args, HandlerContext) of 94 | {resolve_in_js, Fun, Response} -> 95 | [[resolve_in_js, <<"success">>, Ref, Fun, Response]]; 96 | {ok, Response} -> 97 | [[callback, <<"success">>, Ref, Response]]; 98 | ok -> 99 | [[callback, <<"success">>, Ref, <<>>]]; 100 | {error, Reason} when is_binary(Reason); is_atom(Reason) -> 101 | [[callback, <<"error">>, Ref, Reason]]; 102 | {error, Reason} -> 103 | lager:error("Unknown dispatch error: ~p", [Reason]), 104 | [[callback, <<"error">>, Ref, <<"Unknown error.">>]] 105 | end 106 | end. 107 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_sup.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0]). 6 | -export([start_link/1]). 7 | -export([init/1]). 8 | 9 | -define(CHILD(I, Type, Args), 10 | {I, {I, start_link, Args}, permanent, 5000, Type, [I]}). 11 | 12 | start_link() -> 13 | start_link(#{}). 14 | 15 | start_link(Opts) -> 16 | supervisor:start_link({local, ?MODULE}, ?MODULE, [Opts]). 17 | 18 | init([Opts]) -> 19 | SupFlags = #{ 20 | strategy => one_for_one, 21 | intensity => 3, 22 | period => 10 23 | }, 24 | ChildSpecs = [#{ 25 | id => erlang_v8_lib_pool, 26 | start => {erlang_v8_lib_pool, start_link, [Opts]} 27 | }, #{ 28 | id => erlang_v8_bg_procs, 29 | start => {erlang_v8_lib_bg_procs, start_link, []} 30 | }], 31 | {ok, {SupFlags, ChildSpecs}}. 32 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_test.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_test). 2 | 3 | -export([n/0]). 4 | -export([n/1]). 5 | 6 | -export([perf/0]). 7 | -export([perf/1]). 8 | -export([run_perf/0]). 9 | 10 | n() -> n(100). 11 | n(N) -> 12 | application:ensure_all_started(erlang_v8_lib), 13 | R = [eralng_v8_lib:run(<<"var x = 1; x">>) || _ <- lists:seq(1, N)], 14 | io:format("Ran ~p tests: ~p~n", [N, R]), 15 | timer:sleep(500), 16 | n(N). 17 | 18 | perf() -> perf(100). 19 | 20 | perf(N) -> 21 | application:ensure_all_started(erlang_v8_lib), 22 | test_avg(?MODULE, run_perf, [], N). 23 | 24 | run_perf() -> 25 | [erlang_v8_lib:run(<<"var x = 1; x">>) || _ <- lists:seq(1, 100)]. 26 | 27 | test_avg(M, F, A, N) when N > 0 -> 28 | L = test_loop(M, F, A, N, []), 29 | Length = length(L), 30 | Min = lists:min(L), 31 | Max = lists:max(L), 32 | Med = lists:nth(round((Length / 2)), lists:sort(L)), 33 | Avg = round(lists:foldl(fun(X, Sum) -> X + Sum end, 0, L) / Length), 34 | io:format("Range: ~b - ~b mics~n" 35 | "Median: ~b mics~n" 36 | "Average: ~b mics~n", 37 | [Min, Max, Med, Avg]), 38 | Med. 39 | 40 | test_loop(_M, _F, _A, 0, List) -> 41 | List; 42 | test_loop(M, F, A, N, List) -> 43 | {T, _Result} = timer:tc(M, F, A), 44 | test_loop(M, F, A, N - 1, [T|List]). 45 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_utils.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_utils). 2 | 3 | -export([extend/3]). 4 | 5 | extend(N, L1, L2) -> 6 | compact(lists:keysort(N, L1 ++ L2), []). 7 | 8 | compact([], Acc) -> 9 | lists:reverse(Acc); 10 | compact([{K,_}, {K,V}| Rest], Acc) -> 11 | compact([{K,V} |Rest],Acc); 12 | compact([X|Rest], Acc) -> 13 | compact(Rest,[X|Acc]). 14 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_vm_sup.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_vm_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0]). 6 | -export([start_child/1]). 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 11 | 12 | start_child(Args) -> 13 | supervisor:start_child(?MODULE, [Args]). 14 | 15 | init(_Args) -> 16 | SupFlags = #{ 17 | strategy => simple_one_for_one, 18 | intensity => 2, 19 | period => 30 20 | }, 21 | ChildSpecs = [#{ 22 | id => erlang_v8_vm, 23 | start => {erlang_v8_vm, start_link, []}, 24 | restart => transient 25 | }], 26 | {ok, {SupFlags, ChildSpecs}}. 27 | -------------------------------------------------------------------------------- /src/erlang_v8_lib_worker.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_lib_worker). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/2]). 6 | -export([run/3]). 7 | 8 | -export([init/1]). 9 | -export([handle_call/3]). 10 | -export([handle_cast/2]). 11 | -export([handle_info/2]). 12 | -export([terminate/2]). 13 | -export([code_change/3]). 14 | 15 | -record(state, {vm, handlers}). 16 | 17 | start_link(Files, Handlers) -> 18 | gen_server:start_link(?MODULE, [Files, Handlers], []). 19 | 20 | run({VM, Context}, Instructions, Opts) -> 21 | gen_server:call(VM, {run, {Context, Instructions, Opts}}). 22 | 23 | %% Callbacks 24 | 25 | init([Files, Handlers]) -> 26 | {ok, VM} = erlang_v8:start_vm([{file, File} || File <- Files]), 27 | {ok, #state{vm = VM, handlers = Handlers}}. 28 | 29 | handle_call({run, Context, Instructions, Opts}, _From, 30 | #state{vm = VM, handlers = Handlers} = State) -> 31 | HandlerState = maps:get(handlers, Opts, []), 32 | Reply = run({VM, Context}, Instructions, Handlers, HandlerState), 33 | {reply, Reply, State}; 34 | 35 | handle_call(_Request, _From, State) -> 36 | {reply, ok, State}. 37 | 38 | handle_cast(_Msg, State) -> 39 | {noreply, State}. 40 | 41 | handle_info(_Info, State) -> 42 | {noreply, State}. 43 | 44 | terminate(_Reason, _State) -> 45 | ok. 46 | 47 | code_change(_OldVsn, State, _Extra) -> 48 | {ok, State}. 49 | 50 | %% Internal 51 | 52 | run(VM, [Instruction|Instructions], Handlers, HandlerContext) -> 53 | case unwind(VM, [Instruction], Handlers, HandlerContext) of 54 | {error, Reason} -> 55 | {error, Reason}; 56 | Other when length(Instructions) =:= 0 -> 57 | Other; 58 | _Other -> 59 | run(VM, Instructions, Handlers, HandlerContext) 60 | end. 61 | 62 | unwind(_VM, [], _Handlers, _HandlerContext) -> 63 | ok; 64 | 65 | unwind(VM, [{context, Context}], _Handlers, _HandlerContext) -> 66 | case erlang_v8_lib_pool:call(VM, <<"__internal.setContext">>, [Context]) of 67 | {error, Reason} -> 68 | {error, Reason}; 69 | {ok, undefined} -> 70 | ok 71 | end; 72 | 73 | unwind(VM, [{call, Fun, Args}], Handlers, HandlerContext) -> 74 | {ok, []} = erlang_v8_lib_pool:eval(VM, <<"__internal.actions = [];">>), 75 | case erlang_v8_lib_pool:call(VM, Fun, Args) of 76 | {error, Reason} -> 77 | {error, Reason}; 78 | {ok, undefined} -> 79 | case erlang_v8_lib_pool:eval(VM, <<"__internal.actions;">>) of 80 | {ok, Actions} -> 81 | unwind(VM, Actions, Handlers, HandlerContext); 82 | {error, Reason} -> 83 | {error, Reason} 84 | end; 85 | {ok, Value} -> 86 | %% TODO: What about returned values? Treat as a regular return? 87 | {ok, jsx:decode(jsx:encode(Value), [return_maps])} 88 | end; 89 | 90 | unwind(VM, [{eval, Source}], Handlers, HandlerContext) -> 91 | case erlang_v8_lib_pool:eval(VM, <<" 92 | __internal.actions = []; 93 | ", Source/binary, " 94 | __internal.actions; 95 | ">>) of 96 | {ok, Actions} -> 97 | unwind(VM, Actions, Handlers, HandlerContext); 98 | {error, Reason} -> 99 | {error, Reason} 100 | end; 101 | 102 | unwind(_VM, [[<<"return">>, Value]|_], _Handlers, _HandlerContext) -> 103 | {ok, jsx:decode(jsx:encode(Value), [return_maps])}; 104 | 105 | unwind(VM, [Action|T], Handlers, HandlerContext) -> 106 | NewActions = case Action of 107 | [<<"external">>, HandlerIdentifier, Ref, Args] -> 108 | dispatch_external(HandlerIdentifier, Ref, Args, Handlers, 109 | HandlerContext); 110 | [callback, Status, Ref, Args] -> 111 | {ok, Actions} = erlang_v8_lib_pool:call(VM, 112 | <<"__internal.handleExternal">>, 113 | [Status, Ref, Args]), 114 | Actions; 115 | Other -> 116 | io:format("Other: ~p~n", [Other]), 117 | [] 118 | end, 119 | unwind(VM, NewActions ++ T, Handlers, HandlerContext). 120 | 121 | dispatch_external(HandlerIdentifier, Ref, Args, Handlers, HandlerContext) -> 122 | case maps:get(HandlerIdentifier, Handlers, undefined) of 123 | undefined -> 124 | [[callback, <<"error">>, Ref, <<"Invalid external handler.">>]]; 125 | HandlerMod -> 126 | case HandlerMod:run(Args, HandlerContext) of 127 | {ok, Response} -> 128 | [[callback, <<"success">>, Ref, Response]]; 129 | ok -> 130 | [[callback, <<"success">>, Ref, <<>>]]; 131 | {error, Reason} when is_binary(Reason); is_atom(Reason) -> 132 | [[callback, <<"error">>, Ref, Reason]]; 133 | {error, _Reason} -> 134 | [[callback, <<"error">>, Ref, <<"Unknown error.">>]] 135 | end 136 | end. 137 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_cert.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_cert). 2 | 3 | -export([run/2]). 4 | 5 | -include_lib("public_key/include/public_key.hrl"). 6 | 7 | run([<<"validity">>, Hostname, Port], _HandlerContext) -> 8 | application:ensure_all_started(ssl), 9 | case ssl:connect(binary_to_list(Hostname), Port, [], 10000) of 10 | {ok, Socket} -> 11 | case ssl:peercert(Socket) of 12 | {ok, Cert} -> 13 | OTPCert = public_key:pkix_decode_cert(Cert, otp), 14 | TBSCert = OTPCert#'OTPCertificate'.tbsCertificate, 15 | 16 | {'Validity', NotBefore, NotAfter} 17 | = TBSCert#'OTPTBSCertificate'.validity, 18 | 19 | UTime = calendar:universal_time(), 20 | Now = calendar:datetime_to_gregorian_seconds(UTime), 21 | 22 | NotBeforeSeconds 23 | = pubkey_cert:time_str_2_gregorian_sec(NotBefore) - Now, 24 | NotAfterSeconds 25 | = pubkey_cert:time_str_2_gregorian_sec(NotAfter) - Now, 26 | 27 | {ok, #{ 28 | 'validInSeconds' => NotBeforeSeconds, 29 | 'invalidInSeconds' => NotAfterSeconds 30 | }}; 31 | {error, no_peercert} -> 32 | {error, <<"No peer certificate.">>} 33 | end; 34 | {error, {tls_alert, "record overflow"}} -> 35 | {error, <<"Invalid TLS host and port combination.">>}; 36 | Error = {error, _} -> 37 | Error 38 | end. 39 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_dns.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_dns). 2 | 3 | -export([run/2]). 4 | 5 | -include_lib("kernel/src/inet_dns.hrl"). 6 | 7 | %% TODO: Add options for alt nameservers and timeout 8 | 9 | run([<<"resolve">>, Hostname, Type], _HandlerContext) -> 10 | Type0 = get_type(Type), 11 | Hostname0 = binary_to_list(Hostname), 12 | Now = erlang:timestamp(), 13 | case inet_res:resolve(Hostname0, in, Type0) of 14 | {ok, #dns_rec{ anlist = Answers }} -> 15 | Time = timer:now_diff(erlang:timestamp(), Now) / 1000, 16 | Answers0 = [{TTL, Data} || 17 | #dns_rr{ ttl = TTL, data = Data, class = in, type = 18 | AnType } 19 | <- Answers, AnType =:= Type0], 20 | {ok, #{ time => Time, answers => format_answers(Type0, Answers0) }}; 21 | {error, {Error, _Msg}} -> 22 | handle_error(Error); 23 | {error, Error} -> 24 | handle_error(Error) 25 | end. 26 | 27 | handle_error(nxdomain) -> 28 | {error, <<"Invalid domain.">>}; 29 | handle_error(timeout) -> 30 | {error, <<"Timeout.">>}; 31 | handle_error(servfail) -> 32 | {error, <<"Server failed.">>}; 33 | handle_error(refused) -> 34 | {error, <<"Connection refused.">>}; 35 | handle_error(Other) -> 36 | io:format("Other error: ~p~n", [Other]), 37 | {error, <<"Unknown DNS error.">>}. 38 | 39 | get_type(Type) when is_binary(Type) -> 40 | get_type(string:to_upper(binary_to_list(Type))); 41 | 42 | get_type("A") -> a; 43 | get_type("AAAA") -> aaaa; 44 | get_type("CNAME") -> cname; 45 | get_type("MX") -> mx; 46 | get_type("SRV") -> srv; 47 | get_type("PTR") -> ptr; 48 | get_type("TXT") -> txt; 49 | get_type("NS") -> ns; 50 | get_type("SOA") -> soa; 51 | get_type("NAPTR") -> naptr; 52 | get_type(_) -> a. 53 | 54 | format_answers(soa, Answers) -> 55 | [#{ ttl => TTL, primary => format_value(Primary), 56 | rname => format_value(RName), revision => Revision, 57 | refresh => Refresh, retry => Retry, expiration => Expiration, 58 | min_ttl => MinTTL } 59 | || {TTL, {Primary, RName, Revision, Refresh, Retry, Expiration, MinTTL}} <- Answers]; 60 | format_answers(mx, Answers) -> 61 | [#{ ttl => TTL, exchange => Exchange, priority => Priority } 62 | || {TTL, {Priority, Exchange}} <- Answers]; 63 | format_answers(_Type, Answers) -> 64 | [#{ ttl => TTL, value => format_value(Data) } || {TTL, Data} <- Answers]. 65 | 66 | format_value(Value) when is_tuple(Value) -> 67 | list_to_binary(inet:ntoa(Value)); 68 | format_value(Value) -> 69 | list_to_binary(Value). 70 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_http.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_http). 2 | 3 | -export([run/2]). 4 | 5 | -define(RESOLVE_FUN, <<"http.__resolve_promise">>). 6 | 7 | run([URL, Method, Headers, Payload, FollowRedirect], HandlerContext) -> 8 | validate_args(#{ 9 | url => URL, 10 | method => Method, 11 | headers => Headers, 12 | payload => Payload, 13 | follow_redirect => FollowRedirect 14 | }, HandlerContext). 15 | 16 | validate_args(Config, HandlerContext) -> 17 | case oath:validate(Config, map, #{ rules => [ 18 | {url, url, #{ default_to_http => true }}, 19 | {method, binary, #{}}, 20 | {headers, map, #{ required => false, default => #{} }}, 21 | {payload, binary, #{ required => false, default => <<>> }}, 22 | {follow_redirect, any, #{ required => false, default => false, 23 | in => [true, false] }} 24 | ]}) of 25 | {ok, ValidConfig} -> 26 | validate_headers(ValidConfig, HandlerContext); 27 | {error, #{ url := Reason }} -> 28 | {error, Reason}; 29 | {error, _Errors} -> 30 | {error, <<"Invalid arguments">>} 31 | end. 32 | 33 | validate_headers(#{ headers := Headers } = Config, HandlerContext) -> 34 | ValidHeaders = clean_headers(Headers), 35 | UpdatedHeaders = [{<<"Connection">>, <<"close">>}|ValidHeaders], 36 | validate_method(Config#{ headers => UpdatedHeaders }, HandlerContext). 37 | 38 | validate_method(#{ method := Method } = Config, HandlerContext) -> 39 | ValidMethod = clean_method(Method), 40 | perform_request(Config#{ method => ValidMethod }, HandlerContext). 41 | 42 | perform_request(#{ url := URL, headers := Headers, payload := Payload, 43 | method := Method, follow_redirect := FollowRedirect }, 44 | _HandlerContext) -> 45 | Opts = [ 46 | {connect_timeout, 6000}, 47 | {recv_timeout, 6000}, 48 | {follow_redirect, FollowRedirect} 49 | ], 50 | Now = erlang:timestamp(), 51 | case catch hackney:request(Method, URL, Headers, Payload, Opts) of 52 | {ok, Code, RespHeaders, ClientRef} -> 53 | Time = timer:now_diff(erlang:timestamp(), Now) / 1000, 54 | case hackney:body(ClientRef) of 55 | {ok, Body} -> 56 | {resolve_in_js, ?RESOLVE_FUN, 57 | #{ code => Code, body => Body, time => Time, 58 | headers => jsx:encode(RespHeaders) }}; 59 | {error, Error} -> 60 | lager:info("Error reading body! ~p", [Error]), 61 | {error, <<"Error reading body.">>} 62 | end; 63 | {ok, Code, _RespHeaders} -> 64 | Time = timer:now_diff(erlang:timestamp(), Now) / 1000, 65 | {resolve_in_js, ?RESOLVE_FUN, #{ code => Code, time => Time }}; 66 | {error, nxdomain} -> 67 | {error, <<"Invalid domain.">>}; 68 | {error, closed} -> 69 | {error, <<"HTTP Socket closed.">>}; 70 | {error, timeout} -> 71 | {error, <<"HTTP request timed out">>}; 72 | {error, connect_timeout} -> 73 | {error, <<"HTTP connection timed out">>}; 74 | {error, ehostunreach} -> 75 | {error, <<"HTTP host not reachable">>}; 76 | {error, enetunreach} -> 77 | {error, <<"Network is not reachable">>}; 78 | {error, econnrefused} -> 79 | {error, <<"HTTP connection refused.">>}; 80 | Other -> 81 | io:format("Unspecified HTTP error: ~p~n", [Other]), 82 | {error, <<"Unspecified HTTP error.">>} 83 | end. 84 | 85 | clean_method(<<"POST">>) -> post; 86 | clean_method(<<"PUT">>) -> put; 87 | clean_method(<<"GET">>) -> get; 88 | clean_method(<<"DELETE">>) -> delete; 89 | clean_method(<<"HEAD">>) -> head; 90 | clean_method(_Other) -> get. 91 | 92 | clean_headers(Headers) when is_map(Headers) -> 93 | case jsx:decode(jsx:encode(Headers)) of 94 | [{}] -> []; 95 | NewHeaders -> NewHeaders 96 | end; 97 | clean_headers(_) -> 98 | []. 99 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_http_taser.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_http_taser). 2 | 3 | -export([run/2]). 4 | 5 | -define(RESOLVE_FUN, <<"http.__resolve_promise">>). 6 | 7 | run([URL, Method, Headers, Payload, FollowRedirect], HandlerContext) -> 8 | validate_args(#{ 9 | url => URL, 10 | method => Method, 11 | headers => Headers, 12 | payload => Payload, 13 | follow_redirect => FollowRedirect 14 | }, HandlerContext). 15 | 16 | validate_args(Config, HandlerContext) -> 17 | case oath:validate(Config, map, #{ rules => [ 18 | {url, url, #{ default_to_http => true }}, 19 | {method, binary, #{}}, 20 | {headers, map, #{ required => false, default => #{} }}, 21 | {payload, binary, #{ required => false, default => <<>> }}, 22 | {follow_redirect, any, #{ required => false, default => false, 23 | in => [true, false] }} 24 | ]}) of 25 | {ok, ValidConfig} -> 26 | validate_headers(ValidConfig, HandlerContext); 27 | {error, #{ url := Reason }} -> 28 | {error, Reason}; 29 | {error, _Errors} -> 30 | {error, <<"Invalid arguments">>} 31 | end. 32 | 33 | validate_headers(#{ headers := Headers } = Config, HandlerContext) -> 34 | ValidHeaders = clean_headers(Headers), 35 | validate_method(Config#{ headers => ValidHeaders }, HandlerContext). 36 | 37 | validate_method(#{ method := Method } = Config, HandlerContext) -> 38 | ValidMethod = clean_method(Method), 39 | perform_request(Config#{ method => ValidMethod }, HandlerContext). 40 | 41 | perform_request(#{ url := URL, headers := Headers, payload := Payload, 42 | method := Method, follow_redirect := FollowRedirect }, 43 | _HandlerContext) -> 44 | Opts = #{ 45 | connect_timeout => 6000, 46 | response_timeout => 10000, 47 | data => Payload, 48 | follow_redirects => FollowRedirect 49 | }, 50 | Now = erlang:timestamp(), 51 | case catch taser:request(Method, URL, Headers, Opts) of 52 | {ok, Code, RespHeaders, Body} -> 53 | Time = timer:now_diff(erlang:timestamp(), Now) / 1000, 54 | Response = #{ code => Code, body => Body, time => Time, 55 | headers => jsx:encode(RespHeaders) }, 56 | {resolve_in_js, ?RESOLVE_FUN, Response}; 57 | {error, nxdomain} -> 58 | {error, <<"Invalid domain.">>}; 59 | {error, closed} -> 60 | {error, <<"HTTP Socket closed.">>}; 61 | {error, timeout} -> 62 | {error, <<"HTTP request timed out">>}; 63 | {error, connect_timeout} -> 64 | {error, <<"HTTP connection timed out">>}; 65 | {error, response_timeout} -> 66 | {error, <<"HTTP server did not respond in time">>}; 67 | {error, body_timeout} -> 68 | {error, <<"HTTP server did not send entire body in time">>}; 69 | {error, ehostunreach} -> 70 | {error, <<"HTTP host not reachable">>}; 71 | {error, enetunreach} -> 72 | {error, <<"Network is not reachable">>}; 73 | {error, econnrefused} -> 74 | {error, <<"HTTP connection refused.">>}; 75 | Other -> 76 | lager:error("Unspecified HTTP error (~p): ~p", [URL, Other]), 77 | {error, <<"Unspecified HTTP error.">>} 78 | end. 79 | 80 | clean_method(<<"POST">>) -> post; 81 | clean_method(<<"PUT">>) -> put; 82 | clean_method(<<"GET">>) -> get; 83 | clean_method(<<"DELETE">>) -> delete; 84 | clean_method(<<"HEAD">>) -> head; 85 | clean_method(_Other) -> get. 86 | 87 | clean_headers(Headers) when is_map(Headers) -> 88 | case jsx:decode(jsx:encode(Headers)) of 89 | [{}] -> []; 90 | NewHeaders -> NewHeaders 91 | end; 92 | clean_headers(_) -> 93 | []. 94 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_stdout_console.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_stdout_console). 2 | 3 | -export([run/2]). 4 | 5 | run([Level, Message], _HandlerContext) -> 6 | io:format(standard_error, "[Log(~p)]: ~p~n", [Level, Message]), 7 | ok. 8 | -------------------------------------------------------------------------------- /src/handlers/erlang_v8_ws.erl: -------------------------------------------------------------------------------- 1 | -module(erlang_v8_ws). 2 | 3 | -export([run/2]). 4 | 5 | -define(DEFAULT_RECV_TIMEOUT, 5000). 6 | -define(RESOLVE_CONN_FUN, <<"ws.__resolve_conn_promise">>). 7 | 8 | run([<<"connect">>, URL, Headers, Subprotocols], HandlerContext) -> 9 | validate_args(#{ 10 | url => URL, 11 | headers => Headers, 12 | subprotocols => Subprotocols 13 | }, HandlerContext); 14 | 15 | run([<<"send">>, Ref, Data], _HandlerContext) -> 16 | case erlang_v8_lib_bg_procs:get(Ref) of 17 | {error, not_found} -> 18 | {error, <<"No connection.">>}; 19 | {ok, Pid} -> 20 | Pid ! {send, Data}, 21 | {ok, <<"Data sent.">>} 22 | end; 23 | 24 | run([<<"close">>, Ref], _HandlerContext) -> 25 | case erlang_v8_lib_bg_procs:get(Ref) of 26 | {error, not_found} -> 27 | {error, <<"No connection.">>}; 28 | {ok, Pid} -> 29 | Pid ! close, 30 | receive 31 | ws_closed -> 32 | erlang_v8_lib_bg_procs:remove(Ref), 33 | {ok, <<"Socket closed.">>} 34 | end 35 | end; 36 | 37 | run([<<"receive">>, Ref], HandlerContext) -> 38 | Timeout = maps:get(ws_recv_timeout, HandlerContext, ?DEFAULT_RECV_TIMEOUT), 39 | case erlang_v8_lib_bg_procs:get(Ref) of 40 | {error, not_found} -> 41 | {error, <<"No connection.">>}; 42 | {ok, Pid} -> 43 | Pid ! read, 44 | receive 45 | {ws_frame, Frame} -> 46 | {ok, Frame}; 47 | ws_closed -> 48 | {error, <<"Socket closed recv.">>} 49 | after Timeout -> 50 | {error, <<"Receive timeout reached.">>} 51 | end 52 | end. 53 | 54 | validate_args(Config, HandlerContext) -> 55 | case oath:validate(Config, map, #{ rules => [ 56 | {url, string, #{ default_to_http => true }}, 57 | {subprotocols, list, #{ default => []}}, 58 | {headers, map, #{ required => false, default => #{} }} 59 | ]}) of 60 | {ok, ValidConfig} -> 61 | validate_headers(ValidConfig, HandlerContext); 62 | {error, #{ url := Reason }} -> 63 | {error, Reason}; 64 | {error, _Errors} -> 65 | {error, <<"Invalid arguments">>} 66 | end. 67 | 68 | validate_headers(#{ headers := Headers } = Config, HandlerContext) -> 69 | ValidHeaders = clean_headers(Headers), 70 | inject_subprotocols(Config#{ headers => ValidHeaders }, HandlerContext). 71 | 72 | inject_subprotocols(#{ headers := Headers, 73 | subprotocols := Subprotocols } = Config, 74 | HandlerContext) -> 75 | SubprotocolHeaders = [{<<"sec-websocket-protocol">>, P} 76 | || P <- Subprotocols], 77 | validate_url(Config#{ headers => Headers ++ SubprotocolHeaders }, 78 | HandlerContext). 79 | 80 | validate_url(#{ url := URL, headers := Headers } = _Config, _HandlerContext) -> 81 | case clean_url(URL) of 82 | {ok, Transport, Hostname, Port, Path} -> 83 | case connect(Transport, Hostname, Port, Path, Headers) of 84 | {ok, Pid} -> 85 | {ok, ConnRef} = erlang_v8_lib_bg_procs:add(Pid), 86 | {resolve_in_js, ?RESOLVE_CONN_FUN, ConnRef}; 87 | {error, Reason} -> 88 | {error, Reason} 89 | end; 90 | {error, _} -> 91 | {error, <<"Invalid URI.">>} 92 | end. 93 | 94 | clean_url(URL) -> 95 | Opts = [{scheme_defaults, [{ws, 80}, {wss, 443}]}], 96 | case http_uri:parse(URL, Opts) of 97 | {error, no_scheme} -> 98 | clean_url("ws://" ++ URL); 99 | {ok, {ws, _, Host, Port, Path, Query}} -> 100 | {ok, tcp, Host, Port, Path ++ Query}; 101 | {ok, {wss, _, Host, Port, Path, Query}} -> 102 | {ok, ssl, Host, Port, Path ++ Query}; 103 | {error, Reason} -> 104 | {error, Reason} 105 | end. 106 | 107 | clean_headers(Headers) when is_map(Headers) -> 108 | case jsx:decode(jsx:encode(Headers)) of 109 | [{}] -> []; 110 | NewHeaders -> NewHeaders 111 | end; 112 | clean_headers(_) -> 113 | []. 114 | 115 | %% WS connection stuff 116 | 117 | connect(Transport, Hostname, Port, Path, Headers) -> 118 | Parent = self(), 119 | Pid = spawn(fun() -> 120 | link(Parent), 121 | process_flag(trap_exit, true), 122 | connect(Parent, Transport, Hostname, Port, Path, Headers) 123 | end), 124 | receive 125 | ok -> 126 | {ok, Pid}; 127 | {error, Reason} -> 128 | {error, Reason} 129 | end. 130 | 131 | connect(Parent, Transport, Hostname, Port, Path, Headers) -> 132 | case gun:open(Hostname, Port, #{ retry => 0, transport => Transport }) of 133 | {ok, Pid} -> 134 | case gun:await_up(Pid) of 135 | {ok, http} -> 136 | gun:ws_upgrade(Pid, Path, Headers, #{ compress => true }), 137 | receive 138 | {gun_ws_upgrade, Pid, ok, _} -> 139 | Parent ! ok, 140 | loop(Pid, Parent); 141 | Msg -> 142 | lager:error("Websocket upgrade error: ~p", [Msg]), 143 | Parent ! {error, <<"WebSocket upgrade failed<.">>} 144 | end; 145 | {error, _Reason} -> 146 | gun:shutdown(Pid), 147 | Parent ! {error, <<"Unable to connect.">>} 148 | end; 149 | {error, _Reason} -> 150 | Parent ! {error, <<"Unable to connect.">>} 151 | end. 152 | 153 | loop(Pid, Parent) -> 154 | loop(Pid, Parent, [], 0). 155 | 156 | loop(Pid, Parent, Buf, Waiting) -> 157 | receive 158 | {gun_ws, Pid, close} -> 159 | gun:ws_send(Pid, close), 160 | loop(Pid, Parent, Buf, Waiting); 161 | {gun_ws, Pid, {close, Code, _}} -> 162 | gun:ws_send(Pid, {close, Code, <<>>}), 163 | loop(Pid, Parent, Buf, Waiting); 164 | {gun_ws, Pid, {text, Frame}} when Waiting > 0 -> 165 | Parent ! {ws_frame, Frame}, 166 | loop(Pid, Parent, Buf, Waiting - 1); 167 | {gun_ws, Pid, {text, Frame}} -> 168 | loop(Pid, Parent, Buf ++ [Frame], Waiting); 169 | {gun_down, Pid, ws, _, _, _} -> 170 | close(Pid, Parent); 171 | {send, Frame} -> 172 | gun:ws_send(Pid, {text, Frame}), 173 | loop(Pid, Parent, Buf, Waiting); 174 | read when length(Buf) > 0 -> 175 | [Frame|Rest] = Buf, 176 | Parent ! {ws_frame, Frame}, 177 | loop(Pid, Parent, Rest, Waiting); 178 | read -> 179 | loop(Pid, Parent, Buf, Waiting + 1); 180 | close -> 181 | close(Pid, Parent); 182 | {'EXIT', _Pid, _Reason} -> 183 | close(Pid, Parent); 184 | Other -> 185 | lager:error("Unexpected ws message ~p", [Other]), 186 | close(Pid, Parent) 187 | end. 188 | 189 | close(Pid, Parent) -> 190 | Parent ! ws_closed, 191 | gun:shutdown(Pid). 192 | -------------------------------------------------------------------------------- /test/cert_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(cert_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([success/1]). 12 | -export([invalid_domain/1]). 13 | -export([invalid_port/1]). 14 | 15 | %% Callbacks 16 | 17 | all() -> 18 | [ 19 | success, 20 | invalid_domain 21 | %% TODO: Invalid port test fails due to bug in current version of Arch. 22 | %% invalid_port 23 | ]. 24 | 25 | init_per_suite(Config) -> 26 | application:ensure_all_started(erlang_v8_lib), 27 | Config. 28 | 29 | end_per_suite(Config) -> 30 | Config. 31 | 32 | init_per_testcase(_Case, Config) -> 33 | {ok, Pid} = erlang_v8_lib_sup:start_link(), 34 | [{pid, Pid}|Config]. 35 | 36 | end_per_testcase(_Case, Config) -> 37 | Pid = proplists:get_value(pid, Config), 38 | exit(Pid, normal), 39 | ok. 40 | 41 | %% Tests 42 | 43 | success(_Config) -> 44 | {ok, #{ <<"validInSeconds">> := _ }} = erlang_v8_lib:run(<<" 45 | cert.validity('google.com', 443) 46 | .then((x) => process.return(x)); 47 | ">>), 48 | ok. 49 | 50 | invalid_domain(_Config) -> 51 | {ok, <<"nxdomain">>} = erlang_v8_lib:run(<<" 52 | cert.validity('imadomainthatdoesnotexistwitharandomxxx.com', 443) 53 | .catch((x) => process.return(x)); 54 | ">>), 55 | ok. 56 | 57 | invalid_port(_Config) -> 58 | {ok, <<"Invalid TLS", _/binary>>} = erlang_v8_lib:run(<<" 59 | cert.validity('github.com', 80) 60 | .catch((x) => process.return(x)); 61 | ">>), 62 | ok. 63 | -------------------------------------------------------------------------------- /test/dns_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(dns_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([generic/1]). 12 | 13 | %% Callbacks 14 | 15 | all() -> 16 | [ 17 | generic 18 | ]. 19 | 20 | init_per_suite(Config) -> 21 | application:ensure_all_started(erlang_v8_lib), 22 | Config. 23 | 24 | end_per_suite(Config) -> 25 | Config. 26 | 27 | init_per_testcase(_Case, Config) -> 28 | {ok, Pid} = erlang_v8_lib_sup:start_link(), 29 | [{pid, Pid}|Config]. 30 | 31 | end_per_testcase(_Case, Config) -> 32 | Pid = proplists:get_value(pid, Config), 33 | exit(Pid, normal), 34 | ok. 35 | 36 | %% Tests 37 | 38 | generic(_Config) -> 39 | {ok, #{ <<"answers">> := [#{ <<"value">> := _ }]}} = erlang_v8_lib:run(<<" 40 | dns.resolve('google.com', 'a') 41 | .then((x) => process.return(x)) 42 | .catch((x) => process.return(x)); 43 | ">>), 44 | {ok, #{ <<"answers">> := [#{ <<"value">> := _ }]}} = erlang_v8_lib:run(<<" 45 | dns.resolve('google.com', 'aaaa') 46 | .then((x) => process.return(x)) 47 | .catch((x) => process.return(x)); 48 | ">>), 49 | {ok, #{ <<"answers">> := [#{ <<"value">> := _ }]}} = erlang_v8_lib:run(<<" 50 | dns.resolve('www.facebook.com', 'cname') 51 | .then((x) => process.return(x)) 52 | .catch((x) => process.return(x)); 53 | ">>), 54 | {ok, #{ <<"answers">> := [#{ <<"value">> := _ }]}} = erlang_v8_lib:run(<<" 55 | dns.resolve('facebook.com', 'txt') 56 | .then((x) => process.return(x)) 57 | .catch((x) => process.return(x)); 58 | ">>), 59 | {ok, #{ <<"answers">> := [#{ <<"value">> := _ }|_]}} = erlang_v8_lib:run(<<" 60 | dns.resolve('facebook.com', 'ns') 61 | .then((x) => process.return(x)) 62 | .catch((x) => process.return(x)); 63 | ">>), 64 | {ok, #{ <<"answers">> := []}} = erlang_v8_lib:run(<<" 65 | dns.resolve('facebook.com', 'naptr') 66 | .then((x) => process.return(x)) 67 | .catch((x) => process.return(x)); 68 | ">>), 69 | {ok, #{ <<"answers">> := [#{ <<"primary">> := _ }|_]}} = erlang_v8_lib:run(<<" 70 | dns.resolve('facebook.com', 'soa') 71 | .then((x) => process.return(x)) 72 | .catch((x) => process.return(x)); 73 | ">>), 74 | {ok, #{ <<"answers">> := [#{ <<"exchange">> := _, <<"priority">> := _, <<"ttl">> := _ }|_]}} = erlang_v8_lib:run(<<" 75 | dns.resolve('google.com', 'mx') 76 | .then((x) => process.return(x)) 77 | .catch((x) => process.return(x)); 78 | ">>), 79 | ok. 80 | -------------------------------------------------------------------------------- /test/generic_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(generic_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([exceptions/1]). 12 | -export([console_log/1]). 13 | -export([instructions/1]). 14 | -export([return/1]). 15 | -export([manual_release/1]). 16 | -export([automatic_release/1]). 17 | -export([context/1]). 18 | 19 | %% Callbacks 20 | 21 | all() -> 22 | [ 23 | %% console_log, 24 | %% instructions, 25 | exceptions, 26 | manual_release, 27 | automatic_release 28 | %% context, 29 | %% return 30 | ]. 31 | 32 | init_per_suite(Config) -> 33 | application:ensure_all_started(erlang_v8_lib), 34 | Config. 35 | 36 | end_per_suite(Config) -> 37 | Config. 38 | 39 | init_per_testcase(_Case, Config) -> 40 | {ok, Pid} = erlang_v8_lib_sup:start_link(), 41 | [{pid, Pid}|Config]. 42 | 43 | end_per_testcase(_Case, Config) -> 44 | Pid = proplists:get_value(pid, Config), 45 | exit(Pid, normal), 46 | ok. 47 | 48 | %% Tests 49 | 50 | exceptions(_Config) -> 51 | {ok, #{ <<"message">> := <<"ERROR">> }} = erlang_v8_lib:run(<<" 52 | var p = new Promise(function(resolve, reject) { 53 | throw new Error('ERROR'); 54 | }); 55 | p.catch(function(err) { 56 | process.return(err); 57 | }); 58 | ">>), 59 | ok. 60 | 61 | console_log(_Config) -> 62 | ok = erlang_v8_lib:run(<<"console.log('test');">>), 63 | ok. 64 | 65 | instructions(_Config) -> 66 | erlang_v8_lib_sup:start_link(), 67 | {ok, 1} = erlang_v8_lib:run([ 68 | {eval, <<"function lol() { process.return(1); }">>}, 69 | {call, <<"lol">>, []} 70 | ]), 71 | ok. 72 | 73 | context(_Config) -> 74 | erlang_v8_lib_sup:start_link(), 75 | {ok, <<"abc">>} = erlang_v8_lib:run([ 76 | {context, #{ type => <<"abc">> }}, 77 | {eval, <<"process.return(Context.get().type);">>} 78 | ]), 79 | ok. 80 | 81 | return(_Config) -> 82 | erlang_v8_lib_sup:start_link(), 83 | {ok, 1} = erlang_v8_lib:run(<<"process.return(1);">>), 84 | ok. 85 | 86 | automatic_release(_Config) -> 87 | Parent = self(), 88 | Pid = spawn(fun() -> 89 | {ok, Worker} = erlang_v8_lib_pool:claim(), 90 | Parent ! Worker, 91 | receive never -> ok end 92 | end), 93 | 94 | Worker = receive Response -> Response end, 95 | 96 | exit(Pid, kill), 97 | timer:sleep(1), 98 | 99 | {error, invalid_worker} = erlang_v8_lib_pool:release(Worker), 100 | 101 | ok. 102 | 103 | manual_release(_Config) -> 104 | Parent = self(), 105 | 106 | _Pid = spawn(fun() -> 107 | {ok, Worker} = erlang_v8_lib_pool:claim(), 108 | Parent ! Worker, 109 | receive never -> ok end 110 | end), 111 | 112 | Worker = receive Response -> Response end, 113 | timer:sleep(1), 114 | ok = erlang_v8_lib_pool:release(Worker), 115 | 116 | ok. 117 | -------------------------------------------------------------------------------- /test/http_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(http_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([get/1]). 12 | -export([post/1]). 13 | -export([put/1]). 14 | -export([delete/1]). 15 | -export([head/1]). 16 | 17 | -export([https/1]). 18 | -export([arguments/1]). 19 | -export([headers/1]). 20 | -export([redirect/1]). 21 | -export([timeout/1]). 22 | 23 | %% Callbacks 24 | 25 | all() -> 26 | [ 27 | %% timeout, 28 | arguments, 29 | get, 30 | headers, 31 | post, 32 | put, 33 | delete, 34 | head, 35 | redirect 36 | %% https 37 | ]. 38 | 39 | init_per_suite(Config) -> 40 | application:ensure_all_started(erlang_v8_lib), 41 | application:ensure_all_started(hemlock), 42 | Config. 43 | 44 | end_per_suite(Config) -> 45 | application:stop(hemlock), 46 | Config. 47 | 48 | init_per_testcase(_Case, Config) -> 49 | {ok, Pid} = erlang_v8_lib_sup:start_link(), 50 | [{pid, Pid}|Config]. 51 | 52 | end_per_testcase(_Case, Config) -> 53 | Pid = proplists:get_value(pid, Config), 54 | exit(Pid, normal), 55 | ok. 56 | 57 | %% Tests 58 | 59 | timeout(_Config) -> 60 | {ok, <<"HTTP server did not respond in time">>} = erlang_v8_lib:run(<<" 61 | http.get('http://127.0.0.1:5000/timeout/15') 62 | .then((resp) => resp.json()) 63 | .then((json) => process.return(json)) 64 | .catch((error) => process.return(error)); 65 | ">>), 66 | ok. 67 | 68 | get(_Config) -> 69 | {ok, #{ <<"query">> := #{ <<"test">> := <<"fest">> } }} = erlang_v8_lib:run(<<" 70 | http.get('http://127.0.0.1:5000/get?test=fest').then(function(resp) { 71 | return resp.json(); 72 | }).then(function(json) { 73 | process.return(json); 74 | }).catch(function(err) { 75 | process.return(err); 76 | }); 77 | ">>), 78 | 79 | {ok, Data1} = erlang_v8_lib:run(<<" 80 | http.get('http://127.0.0.1:5000/get?test=fest').then((resp) => { 81 | return resp.json(); 82 | }).then(function(json) { 83 | process.return(json); 84 | }); 85 | ">>), 86 | #{ <<"query">> := #{ <<"test">> := <<"fest">> } } = Data1, 87 | 88 | ok. 89 | 90 | post(_Config) -> 91 | {ok, Data0} = erlang_v8_lib:run(<<" 92 | http.post('http://127.0.0.1:5000/post', { body: 'hello' }).then((resp) => { 93 | return resp.json(); 94 | }).then(function(json) { 95 | process.return(json); 96 | }); 97 | ">>), 98 | #{ <<"data">> := <<"hello">> } = Data0, 99 | 100 | ok. 101 | 102 | put(_Config) -> 103 | {ok, Data0} = erlang_v8_lib:run(<<" 104 | http.put('http://127.0.0.1:5000/put', { body: 'hello' }).then((resp) => { 105 | return resp.json(); 106 | }).then(function(json) { 107 | process.return(json); 108 | }); 109 | ">>), 110 | #{ <<"data">> := <<"hello">> } = Data0, 111 | ok. 112 | 113 | delete(_Config) -> 114 | {ok, Data0} = erlang_v8_lib:run(<<" 115 | http.delete('http://127.0.0.1:5000/delete', { body: 'hello' }).then((resp) => { 116 | return resp.json(); 117 | }).then(function(json) { 118 | process.return(json); 119 | }); 120 | ">>), 121 | #{ <<"data">> := <<"hello">> } = Data0, 122 | ok. 123 | 124 | head(_Config) -> 125 | {ok, Data0} = erlang_v8_lib:run(<<" 126 | http.head('http://127.0.0.1:5000/head').then(function(resp) { 127 | process.return(resp); 128 | }); 129 | ">>), 130 | #{<<"code">> := 200 } = Data0, 131 | 132 | ok. 133 | 134 | headers(_Config) -> 135 | {ok, #{ <<"headers">> := #{ <<"header">> := <<"ok">> }}} = erlang_v8_lib:run(<<" 136 | http.get('http://127.0.0.1:5000/get', { headers: { 'header': 'ok' } }) 137 | .then((resp) => resp.json()) 138 | .then((json) => process.return(json)) 139 | .catch((error) => process.return(error)); 140 | ">>), 141 | 142 | {ok, #{ <<"headers">> := #{ <<"header">> := <<"1">> }}} = erlang_v8_lib:run(<<" 143 | http.get('http://127.0.0.1:5000/get', { headers: { 'header': '1' } }) 144 | .then((resp) => resp.json()) 145 | .then((json) => process.return(json)) 146 | .catch((error) => process.return(error)); 147 | ">>), 148 | 149 | {ok, #{ <<"headers">> := #{ <<"1">> := <<"header">> }}} = erlang_v8_lib:run(<<" 150 | http.get('http://127.0.0.1:5000/get', { headers: { 1: 'header' } }) 151 | .then((resp) => resp.json()) 152 | .then((json) => process.return(json)) 153 | .catch((error) => process.return(error)); 154 | ">>), 155 | 156 | {ok, #{ <<"content-type">> := <<"application/json">> }} = erlang_v8_lib:run(<<" 157 | http.get('http://127.0.0.1:5000/get') 158 | .then((resp) => process.return(resp.headers)) 159 | .catch((error) => process.return(error)); 160 | ">>), 161 | 162 | ok. 163 | 164 | arguments(_Config) -> 165 | %% {ok, <<"invalid_url">>} = erlang_v8_lib:run(<<" 166 | %% http.get(1).catch((error) => process.return(error)); 167 | %% ">>), 168 | ok. 169 | 170 | redirect(_Config) -> 171 | {ok, 302} = erlang_v8_lib:run(<<" 172 | http.get('http://127.0.0.1:5000/redirect/1') 173 | .then((resp) => process.return(resp.code)); 174 | ">>), 175 | 176 | {ok, 200} = erlang_v8_lib:run(<<" 177 | http.get('http://127.0.0.1:5000/redirect/1', { followRedirect: true }) 178 | .then((resp) => process.return(resp.code)); 179 | ">>), 180 | 181 | ok. 182 | 183 | https(_Config) -> 184 | {ok, Data0} = erlang_v8_lib:run(<<" 185 | http.get('https://127.0.0.1:5000/get', { 186 | body: { test: 'fest' } 187 | }).then(function(data) { 188 | process.return(data); 189 | }).catch(function(err) { 190 | process.return(err); 191 | }); 192 | ">>), 193 | #{ <<"body">> := Body0 } = Data0, 194 | #{ <<"query">> := #{ <<"test">> := <<"fest">> } } = 195 | jsx:decode(Body0, [return_maps]), 196 | 197 | ok. 198 | -------------------------------------------------------------------------------- /test/pool_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(pool_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([empty/1]). 12 | 13 | %% Callbacks 14 | 15 | all() -> 16 | [ 17 | empty 18 | ]. 19 | 20 | init_per_suite(Config) -> 21 | application:ensure_all_started(erlang_v8_lib), 22 | Config. 23 | 24 | end_per_suite(Config) -> 25 | Config. 26 | 27 | init_per_testcase(_Case, Config) -> 28 | {ok, Pid} = erlang_v8_lib_sup:start_link(#{ vms => 1 }), 29 | [{pid, Pid}|Config]. 30 | 31 | end_per_testcase(_Case, Config) -> 32 | Pid = proplists:get_value(pid, Config), 33 | exit(Pid, normal), 34 | ok. 35 | 36 | %% Tests 37 | 38 | empty(_Config) -> 39 | ok = erlang_v8_lib:run(<<"">>), 40 | ok. 41 | -------------------------------------------------------------------------------- /test/ws_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ws_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([all/0]). 6 | -export([init_per_suite/1]). 7 | -export([end_per_suite/1]). 8 | -export([init_per_testcase/2]). 9 | -export([end_per_testcase/2]). 10 | 11 | -export([simple/1]). 12 | -export([subprotocol/1]). 13 | -export([close/1]). 14 | 15 | %% Callbacks 16 | 17 | all() -> 18 | [ 19 | simple, 20 | subprotocol, 21 | close 22 | ]. 23 | 24 | init_per_suite(Config) -> 25 | application:ensure_all_started(erlang_v8_lib), 26 | application:ensure_all_started(hemlock), 27 | Config. 28 | 29 | end_per_suite(Config) -> 30 | application:stop(hemlock), 31 | Config. 32 | 33 | init_per_testcase(_Case, Config) -> 34 | {ok, Pid} = erlang_v8_lib_sup:start_link(), 35 | [{pid, Pid}|Config]. 36 | 37 | end_per_testcase(_Case, Config) -> 38 | Pid = proplists:get_value(pid, Config), 39 | exit(Pid, normal), 40 | ok. 41 | 42 | %% Tests 43 | 44 | simple(_Config) -> 45 | {ok, #{ <<"data">> := <<"data">> }} = erlang_v8_lib:run(<<" 46 | ws.open('ws://127.0.0.1:5000/ws') 47 | .then((conn) => { 48 | conn.send('data'); 49 | return conn.receive(); 50 | }) 51 | .then((data) => process.return(JSON.parse(data))) 52 | .catch((error) => process.return(error)); 53 | ">>), 54 | 55 | ok. 56 | 57 | subprotocol(_Config) -> 58 | {ok, #{ <<"data">> := <<"data">> }} = erlang_v8_lib:run(<<" 59 | ws.open('ws://127.0.0.1:5000/ws', { subprotocols: ['lol', 1] }) 60 | .then((conn) => { 61 | conn.send('data'); 62 | return conn.receive(); 63 | }) 64 | .then((data) => process.return(JSON.parse(data))) 65 | .catch((error) => process.return(error)); 66 | ">>), 67 | 68 | ok. 69 | 70 | 71 | close(_Config) -> 72 | {ok, <<"No connection.">>} = erlang_v8_lib:run(<<" 73 | ws.open('ws://127.0.0.1:5000/ws') 74 | .then((conn) => { 75 | conn.close(); 76 | return conn.send('test'); 77 | }) 78 | .catch((error) => process.return(error)); 79 | ">>), 80 | 81 | ok. 82 | --------------------------------------------------------------------------------