Sometimes you have to allow the end user to provide some (server side) program code, but you don't want them to allow system access. This could be anything, from custom formatting stuff, smart contracts, whatever. The solution is usually a sandbox. Now, Perl itself is a little to powerful and flexible to allow you to do that somewhat safely, but you can use something like the Duktape javascript engine.
In our case, let's take a look at JavaScript::Duktape. One thing i wanted to implement is "simulated persistance", meaning the JavaScript would be coded as if it is kept in memory, yet the perl program can unload and load it whenever needed. For this, we will define a "memory" object, which the JavaScript can use to keep data in memory.
Our Javascript program looks like this:
function initMemory() { memory.counter = 0; } function incCounter(amount) { memory.counter = memory.counter + amount; } function printCounter() { log("Current count is " + memory.counter); }
To test this, let's write a small test program that uses PageCamel::Helpers::JavaScript. Don't worry about the "PageCamel" part, it's just a helper function in my framework and i didn't have the time to pull it out into a standalone thing. It's pretty much self contained though, code included in this post. Because the PageCamel framework requires a ReportingHandler, for simplicity reasons out test program will just bring it's own.
#!/usr/bin/env perl package ReportingHandler; use strict; use warnings; sub new { my ($proto, %config) = @_; my $class = ref($proto) || $proto; my $self = bless \%config, $class; return $self; } sub debuglog { my ($self, @debugtext) = @_; print "DEBUG: ", join(' ', @debugtext), "\n"; } package main; use strict; use warnings; use PageCamel::Helpers::JavaScript; use Data::Dumper; my $reph = ReportingHandler->new(); my $memory; #print Dumper($reph); my $jscode = qq{ function initMemory() { memory.counter = 0; } function incCounter(amount) { memory.counter = memory.counter + amount; } function printCounter() { log("Current count is " + memory.counter); } }; { # First run print "First run\n"; my $jsh = PageCamel::Helpers::JavaScript->new(reph => $reph, timeo +ut => 10, code => $jscode); $jsh->initMemory(); $jsh->call('printCounter'); $jsh->call('incCounter', 42); $jsh->call('printCounter'); $memory = $jsh->getMemory(); } print "Memory contents: ", $memory, "\n"; { # Second run print "Second run\n"; my $jsh = PageCamel::Helpers::JavaScript->new(reph => $reph, timeo +ut => 10, code => $jscode); $jsh->setMemory($memory); $jsh->call('printCounter'); $jsh->call('incCounter', 23); $jsh->call('printCounter'); }
Basically, in the "first run", it initializes a new instance of the script, increases the counter, reads out the memory into a variable, then throws away the handler. Then (in "second run"), makes a new instance, sets it to the previous memory content and does some more counter incring to show that persistant memory works.
And here is the PageCamel::Helpers::JavaScript module:
package PageCamel::Helpers::JavaScript; #---AUTOPRAGMASTART--- use 5.030; use strict; use warnings; use diagnostics; use mro 'c3'; use English; use Carp qw[carp croak confess cluck longmess shortmess]; our $VERSION = 4.0; use autodie qw( close ); use Array::Contains; use utf8; use Data::Dumper; use PageCamel::Helpers::UTF; #---AUTOPRAGMAEND--- BEGIN { mkdir '/tmp/pagecamel_helpers_javascript_inline'; $ENV{PERL_INLINE_DIRECTORY} = '/tmp/pagecamel_helpers_javascript_i +nline'; }; use JavaScript::Duktape; use JSON::XS; sub new { my ($class, %config) = @_; my $self = bless \%config, $class; if(!defined($self->{reph})) { croak('PageCamel::Helpers::JavaScript needs reph reporting han +dler'); } if(!defined($self->{timeout})) { croak('PageCamel::Helpers::JavaScript needs timeout (default t +imeout value)'); } my $js = JavaScript::Duktape->new(timeout => $self->{timeout}); $self->{js} = $js; $self->{js}->set('log' => sub { $self->_logfromjs($_[0]); }); $self->{js}->eval(qq{ var memory = new Object; function __encode(obj) { return JSON.stringify(obj); } function __decode(txt) { return JSON.parse(txt); } function __setmemory(txt) { memory = __decode(txt); } function __getmemory() { return __encode(memory); } function __getKeys(obj) { var keys = Object.keys(obj); return keys; } }); if(defined($self->{code})) { $self->load(); } return $self; } sub _logfromjs { my ($self, $text) = @_; $self->{reph}->debuglog($text); return; } sub load { my ($self, $code) = @_; if(defined($code)) { $self->{code} = $code; } $self->{js}->eval($self->{code}); return; } sub call { my ($self, $name, @arguments) = @_; my $func = $self->{js}->get_object($name); if(!defined($func)) { print STDERR "Function $func does not exist!\n"; return; } return $func->(@arguments); } sub registerCallback { my ($self, $name, $func) = @_; $self->{js}->set($name, $func); return; } sub encode { my ($self, $data) = @_; return encode_json $data; } sub decode { my ($self, $json) = @_; return decode_json $json; } sub toArray { my ($self, $object) = @_; my @arr; $object->forEach(sub { my ($value, $index, $ar) = @_; push @arr, $value; }); return @arr; } sub getKeys { my ($self, $object) = @_; my $rval = $self->call('__getKeys', $object); return $self->toArray($rval); } sub toHash { my ($self, $object) = @_; my @keys = $self->getKeys($object); my %hash; foreach my $key (@keys) { $hash{$key} = $object->$key; } return %hash; } sub setMemory { my ($self, $memory) = @_; $self->call('__setmemory', $memory); return; } sub getMemory { my ($self) = @_; return $self->call('__getmemory'); } sub initMemory { my ($self) = @_; $self->call('initMemory'); return; } 1;
On startup, this defines some additional JavaScript functions to handle the memory object. The rest is mostly wrapper functions to make life a bit easier.
Output of the program:
First run DEBUG: Current count is 0 DEBUG: Current count is 42 Memory contents: {"counter":42} Second run DEBUG: Current count is 42 DEBUG: Current count is 65
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: Running user-provided JavaScript code
by aitap (Curate) on Apr 17, 2022 at 09:00 UTC | |
by cavac (Prior) on Apr 19, 2022 at 07:59 UTC | |
by afoken (Chancellor) on Apr 21, 2022 at 07:23 UTC | |
by cavac (Prior) on Apr 21, 2022 at 09:32 UTC | |
Re: Running user-provided JavaScript code
by etj (Priest) on Apr 13, 2022 at 21:09 UTC | |
by cavac (Prior) on Apr 14, 2022 at 07:10 UTC |