ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/lib/cf.pm
Revision: 1.23
Committed: Wed Jul 19 22:19:19 2006 UTC (17 years, 10 months ago) by root
Branch: MAIN
Changes since 1.22: +60 -4 lines
Log Message:
Implement initial Safe compartment code to execute map-script code in a controlled environment
(no looping sconstructs, no I/O, no access to the cf API or any loaded modules etc.)

Added register_script_fucntion to add functionality to the compartment.

File Contents

# Content
1 package cf;
2
3 use Symbol;
4 use List::Util;
5 use Storable;
6 use Opcode;
7 use Safe;
8 use Safe::Hole;
9
10 use Event;
11 $Event::Eval = 1; # no idea why this is required, but it is
12
13 use strict;
14
15 our %COMMAND;
16 our @EVENT;
17 our %PROP_TYPE;
18 our %PROP_IDX;
19
20 BEGIN {
21 @EVENT = map lc, @EVENT;
22
23 *CORE::GLOBAL::warn = sub {
24 my $msg = join "", @_;
25 $msg .= "\n"
26 unless $msg =~ /\n$/;
27
28 print STDERR "cfperl: $msg";
29 LOG llevError, "cfperl: $msg";
30 };
31 }
32
33 my %ignore_set = (MAP_PROP_PATH => 1); # I hate the plug-in api. Deeply!
34
35 # generate property mutators
36 sub prop_gen {
37 my ($prefix, $class) = @_;
38
39 no strict 'refs';
40
41 for my $prop (keys %PROP_TYPE) {
42 $prop =~ /^\Q$prefix\E_(.*$)/ or next;
43 my $sub = lc $1;
44
45 my $type = $PROP_TYPE{$prop};
46 my $idx = $PROP_IDX {$prop};
47
48 *{"$class\::get_$sub"} = *{"$class\::$sub"} = sub {
49 $_[0]->get_property ($type, $idx)
50 };
51
52 *{"$class\::set_$sub"} = sub {
53 $_[0]->set_property ($type, $idx, $_[1]);
54 } unless $ignore_set{$prop};
55 }
56 }
57
58 # auto-generate most of the API
59
60 prop_gen OBJECT_PROP => "cf::object";
61 # CFAPI_OBJECT_ANIMATION?
62 prop_gen PLAYER_PROP => "cf::object::player";
63
64 prop_gen MAP_PROP => "cf::map";
65 prop_gen ARCH_PROP => "cf::arch";
66
67 # guessed hierarchies
68
69 @cf::object::player::ISA = 'cf::object';
70 @cf::object::map::ISA = 'cf::object';
71
72 $Event::DIED = sub {
73 warn "error in event callback: @_";
74 };
75
76 my %ext_pkg;
77 my @exts;
78 my @hook;
79 my %command;
80 my %extcmd;
81
82 sub inject_event {
83 my $extension = shift;
84 my $event_code = shift;
85
86 my $cb = $hook[$event_code]{$extension}
87 or return;
88
89 &$cb
90 }
91
92 sub inject_global_event {
93 my $event = shift;
94
95 my $cb = $hook[$event]
96 or return;
97
98 List::Util::max map &$_, values %$cb
99 }
100
101 sub inject_command {
102 my ($name, $obj, $params) = @_;
103
104 for my $cmd (@{ $command{$name} }) {
105 $cmd->[1]->($obj, $params);
106 }
107
108 -1
109 }
110
111 sub register_command {
112 my ($name, $time, $cb) = @_;
113
114 my $caller = caller;
115 #warn "registering command '$name/$time' to '$caller'";
116
117 push @{ $command{$name} }, [$time, $cb, $caller];
118 $COMMAND{"$name\000"} = List::Util::max map $_->[0], @{ $command{$name} };
119 }
120
121 sub register_extcmd {
122 my ($name, $cb) = @_;
123
124 my $caller = caller;
125 #warn "registering extcmd '$name' to '$caller'";
126
127 $extcmd{$name} = [$cb, $caller];
128 }
129
130 sub register {
131 my ($base, $pkg) = @_;
132
133 for my $idx (0 .. $#EVENT) {
134 if (my $ref = $pkg->can ("on_$EVENT[$idx]")) {
135 #warn "registering $EVENT[$idx] hook to '$pkg'\n";
136 $hook[$idx]{$base} = $ref;
137 }
138 }
139 }
140
141 sub load_extension {
142 my ($path) = @_;
143
144 $path =~ /([^\/\\]+)\.ext$/ or die "$path";
145 my $base = $1;
146 my $pkg = $1;
147 $pkg =~ s/[^[:word:]]/_/g;
148 $pkg = "cf::ext::$pkg";
149
150 warn "loading '$path' into '$pkg'\n";
151
152 open my $fh, "<:utf8", $path
153 or die "$path: $!";
154
155 my $source =
156 "package $pkg; use strict; use utf8;\n"
157 . "#line 1 \"$path\"\n{\n"
158 . (do { local $/; <$fh> })
159 . "\n};\n1";
160
161 eval $source
162 or die "$path: $@";
163
164 push @exts, $pkg;
165 $ext_pkg{$base} = $pkg;
166
167 # no strict 'refs';
168 # @{"$pkg\::ISA"} = ext::;
169
170 register $base, $pkg;
171 }
172
173 sub unload_extension {
174 my ($pkg) = @_;
175
176 warn "removing extension $pkg\n";
177
178 if (my $cb = $pkg->can ("on_unload")) {
179 $cb->($pkg);
180 }
181
182 # remove hooks
183 for my $idx (0 .. $#EVENT) {
184 delete $hook[$idx]{$pkg};
185 }
186
187 # remove commands
188 for my $name (keys %command) {
189 my @cb = grep $_->[2] ne $pkg, @{ $command{$name} };
190
191 if (@cb) {
192 $command{$name} = \@cb;
193 $COMMAND{"$name\000"} = List::Util::max map $_->[0], @cb;
194 } else {
195 delete $command{$name};
196 delete $COMMAND{"$name\000"};
197 }
198 }
199
200 # remove extcmds
201 for my $name (grep $extcmd{$_}[1] eq $pkg, keys %extcmd) {
202 delete $extcmd{$name};
203 }
204
205 Symbol::delete_package $pkg;
206 }
207
208 sub load_extensions {
209 my $LIBDIR = maps_directory "perl";
210
211 for my $ext (<$LIBDIR/*.ext>) {
212 next unless -r $ext;
213 eval {
214 load_extension $ext;
215 1
216 } or warn "$ext not loaded: $@";
217 }
218 }
219
220 register_command "perl-reload", 0, sub {
221 my ($who, $arg) = @_;
222
223 if ($who->flag (FLAG_WIZ)) {
224 $who->message ("reloading...");
225
226 warn "reloading...\n";
227 eval {
228 $_->cancel for Event::all_watchers;
229
230 unload_extension $_ for @exts;
231 delete $INC{"cf.pm"};
232
233 # don't, removes xs symbols, too
234 #Symbol::delete_package __PACKAGE__;
235
236 require cf;
237 };
238 warn $@ if $@;
239 $who->message ($@) if $@;
240 warn "reloaded\n";
241
242 $who->message ("reloaded");
243 } else {
244 $who->message ("Intruder Alert!");
245 }
246 };
247
248 #############################################################################
249 # extcmd framework, basically convert ext <id> <pkg> arg1 args
250 # into pkg::->on_extcmd_arg1 (...) while shortcutting a few
251
252 sub on_extcmd {
253 my ($pl, $buf) = @_;
254
255 my ($type) = $buf =~ s/^(\S+) // ? $1 : "";
256
257 $extcmd{$type}[0]->($pl, $buf)
258 if $extcmd{$type};
259 }
260
261 #############################################################################
262 # load/save/clean perl data associated with a map
263
264 *on_mapclean = sub {
265 my ($map) = @_;
266
267 my $path = $map->tmpname;
268 defined $path or return;
269
270 unlink "$path.cfperl";
271 };
272
273 *on_mapin =
274 *on_mapload = sub {
275 my ($map) = @_;
276
277 my $path = $map->tmpname;
278 $path = $map->path unless defined $path;
279
280 open my $fh, "<:raw", "$path.cfperl"
281 or return; # no perl data
282
283 my $data = Storable::thaw do { local $/; <$fh> };
284
285 $data->{version} <= 1
286 or return; # too new
287
288 $map->_set_obs ($data->{obs});
289 };
290
291 *on_mapout = sub {
292 my ($map) = @_;
293
294 my $path = $map->tmpname;
295 $path = $map->path unless defined $path;
296
297 my $obs = $map->_get_obs;
298
299 if (defined $obs) {
300 open my $fh, ">:raw", "$path.cfperl"
301 or die "$path.cfperl: $!";
302
303 stat $path;
304
305 print $fh Storable::nfreeze {
306 size => (stat _)[7],
307 time => (stat _)[9],
308 version => 1,
309 obs => $obs,
310 };
311
312 chmod SAVE_MODE, "$path.cfperl"; # very racy, but cf-compatible *g*
313 } else {
314 unlink "$path.cfperl";
315 }
316 };
317
318 #############################################################################
319 # load/save perl data associated with player->ob objects
320
321 *on_player_load = sub {
322 my ($ob, $path) = @_;
323
324 if (open my $fh, "<:raw", "$path.cfperl") {
325
326 #d##TODO#remove
327
328 my $data = Storable::thaw do { local $/; <$fh> };
329
330 $data->{version} <= 1
331 or return; # too new
332
333 %$ob = %{$data->{ob}};
334 return;
335 }
336
337 for my $o ($ob, $ob->inv) {
338 if (my $value = $o->get_ob_key_value ("_perl_data")) {
339 $o->set_ob_key_value ("_perl_data");
340
341 %$o = %{ Storable::thaw pack "H*", $value };
342 }
343 }
344 };
345
346 *on_player_save = sub {
347 my ($ob, $path) = @_;
348
349 $_->set_ob_key_value (_perl_data => unpack "H*", Storable::nfreeze $_)
350 for grep %$_, $ob, $ob->inv;
351
352 unlink "$path.cfperl";#d##TODO#remove
353 };
354
355 #############################################################################
356 # core extensions - in perl
357
358 =item cf::player::exists $login
359
360 Returns true when the given account exists.
361
362 =cut
363
364 sub cf::player::exists($) {
365 cf::player::find $_[0]
366 or -f sprintf "%s/%s/%s/%s.pl", cf::localdir, cf::playerdir, ($_[0]) x 2;
367 }
368
369 # rough implementation of a future "reply" method that works
370 # with dialog boxes.
371 sub cf::object::player::reply($$$;$) {
372 my ($self, $npc, $msg, $flags) = @_;
373
374 $flags = cf::NDI_BROWN | cf::NDI_UNIQUE unless @_ >= 4;
375
376 $msg = $npc->name . " says: $msg" if $npc;
377
378 $self->message ($msg, $flags);
379 }
380
381 #############################################################################
382 # map scripting support
383
384 our $safe = new Safe "ext";
385 our $safe_hole = new Safe::Hole;
386
387 $SIG{FPE} = 'IGNORE';
388
389 $safe->permit_only (Opcode::opset qw(:base_core :base_mem :base_orig :base_math sort time));
390
391 # here we would export the classes and methods available to script code
392 #@ext::cf::object::player::ISA = @cf::object::player::ISA;
393 #@ext::cf::object::map::ISA = @cf::object::map::ISA;
394
395 sub safe_eval($;@) {
396 my ($code, %vars) = @_;
397
398 my $qcode = $code;
399 $qcode =~ s/"/‟/g; # not allowed in #line filenames
400 $qcode =~ s/\n/\\n/g;
401
402 local $_;
403 local @ext::cf::_safe_eval_args = values %vars;
404
405 $safe->reval (
406 "do {\n"
407 . "my (" . (join ",", map "\$$_", keys %vars) . ") = \@cf::_safe_eval_args;\n"
408 . "#line 0 \"{$qcode}\"\n"
409 . $code
410 . "\n}"
411 )
412 }
413
414 sub register_script_function {
415 my ($fun, $cb) = @_;
416
417 no strict 'refs';
418 *{"ext::$fun"} = $safe_hole->wrap ($cb);
419 }
420
421 #############################################################################
422 # initialisation
423
424 register "<global>", __PACKAGE__;
425
426 unshift @INC, maps_directory "perl";
427
428 load_extensions;
429
430 1
431