CGI.pm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. # Poor Man's CGI Module for Perl
  2. #
  3. # (c) 2002--2011 Martin Mares <mj@ucw.cz>
  4. # Slightly modified by Tomas Valla <tom@ucw.cz>
  5. #
  6. # This software may be freely distributed and used according to the terms
  7. # of the GNU Lesser General Public License.
  8. package UCW::CGI;
  9. # First of all, set up error handling, so that even errors during parsing
  10. # will be reported properly.
  11. # Variables to be set by the calling module:
  12. # $UCW::CGI::error_mail mail address of the script admin (optional)
  13. # (this one has to be set in the BEGIN block!)
  14. # $UCW::CGI::error_hook function to be called for reporting errors
  15. my $error_reported;
  16. my $exit_code;
  17. my $debug = 0;
  18. sub report_bug($)
  19. {
  20. if (!defined $error_reported) {
  21. $error_reported = 1;
  22. print STDERR $_[0];
  23. if (defined($UCW::CGI::error_hook)) {
  24. &$UCW::CGI::error_hook($_[0]);
  25. } else {
  26. print "Content-Type: text/plain\n\n";
  27. print "Internal bug:\n";
  28. print $_[0], "\n";
  29. print "Please notify $UCW::CGI::error_mail\n" if defined $UCW::CGI::error_mail;
  30. }
  31. }
  32. die;
  33. }
  34. BEGIN {
  35. $SIG{__DIE__} = sub { report_bug($_[0]); };
  36. $SIG{__WARN__} = sub { report_bug("WARNING: " . $_[0]); };
  37. $exit_code = 0;
  38. }
  39. END {
  40. $? = $exit_code;
  41. }
  42. use strict;
  43. use warnings;
  44. require Exporter;
  45. our $VERSION = 1.0;
  46. our @ISA = qw(Exporter);
  47. our @EXPORT = qw(&html_escape &url_escape &url_deescape &url_param_escape &url_param_deescape &self_ref &self_form &http_get);
  48. our @EXPORT_OK = qw();
  49. our $utf8_mode = 0;
  50. sub http_error($;@) {
  51. my $err = shift @_;
  52. print join("\n", "Status: $err", "Content-Type: text/plain", @_, "", $err, "");
  53. exit;
  54. }
  55. ### Escaping ###
  56. sub url_escape($) {
  57. my $x = shift @_;
  58. utf8::encode($x) if $utf8_mode;
  59. $x =~ s/([^-\$_.!*'(),0-9A-Za-z\x80-\xff])/"%".unpack('H2',$1)/ge;
  60. utf8::decode($x) if $utf8_mode;
  61. return $x;
  62. }
  63. sub url_deescape($) {
  64. my $x = shift @_;
  65. utf8::encode($x) if $utf8_mode;
  66. $x =~ s/%(..)/pack("H2",$1)/ge;
  67. utf8::decode($x) if $utf8_mode;
  68. return $x;
  69. }
  70. sub url_param_escape($) {
  71. my $x = shift @_;
  72. $x = url_escape($x);
  73. $x =~ s/%20/+/g;
  74. return $x;
  75. }
  76. sub url_param_deescape($) {
  77. my $x = shift @_;
  78. $x =~ s/\+/ /g;
  79. return url_deescape($x);
  80. }
  81. sub html_escape($) {
  82. my $x = shift @_;
  83. $x =~ s/&/&amp;/g;
  84. $x =~ s/</&lt;/g;
  85. $x =~ s/>/&gt;/g;
  86. $x =~ s/"/&quot;/g;
  87. $x =~ s/'/&#39;/g;
  88. return $x;
  89. }
  90. ### Analysing RFC 822 Style Headers ###
  91. sub rfc822_prepare($) {
  92. my $x = shift @_;
  93. # Convert all %'s and backslash escapes to %xx escapes
  94. $x =~ s/%/%25/g;
  95. $x =~ s/\\(.)/"%".unpack("H2",$1)/ge;
  96. # Remove all comments, beware, they can be nested (unterminated comments are closed at EOL automatically)
  97. while ($x =~ s/^(("[^"]*"|[^"(])*(\([^)]*)*)(\([^()]*(\)|$))/$1 /) { }
  98. # Remove quotes and escape dangerous characters inside (again closing at the end automatically)
  99. $x =~ s{"([^"]*)("|$)}{my $z=$1; $z =~ s/([^0-9a-zA-Z%_-])/"%".unpack("H2",$1)/ge; $z;}ge;
  100. # All control characters are properly escaped, tokens are clearly visible.
  101. # Finally remove all unnecessary spaces.
  102. $x =~ s/\s+/ /g;
  103. $x =~ s/(^ | $)//g;
  104. $x =~ s{\s*([()<>@,;:\\"/\[\]?=])\s*}{$1}g;
  105. return $x;
  106. }
  107. sub rfc822_deescape($) {
  108. my $x = shift @_;
  109. return url_deescape($x);
  110. }
  111. ### Reading of HTTP headers ###
  112. sub http_get($) {
  113. my $h = shift @_;
  114. $h =~ tr/a-z-/A-Z_/;
  115. return $ENV{"HTTP_$h"} // $ENV{"$h"};
  116. }
  117. ### Parsing of Arguments ###
  118. my $main_arg_table;
  119. my %raw_args;
  120. sub parse_raw_args_ll($$) {
  121. my ($arg, $s) = @_;
  122. $s =~ s/\r\n/\n/g;
  123. $s =~ s/\r/\n/g;
  124. utf8::decode($s) if $utf8_mode;
  125. push @{$raw_args{$arg}}, $s;
  126. }
  127. sub parse_raw_args($) {
  128. my ($s) = @_;
  129. $s =~ s/\s+//;
  130. for $_ (split /[&:]/, $s) {
  131. (/^([^=]+)=(.*)$/) or next;
  132. my $arg = $1;
  133. $_ = $2;
  134. s/\+/ /g;
  135. s/%(..)/pack("H2",$1)/eg;
  136. parse_raw_args_ll($arg, $_);
  137. }
  138. }
  139. sub parse_multipart_form_data();
  140. sub init_args() {
  141. if (!defined $ENV{"GATEWAY_INTERFACE"}) {
  142. print STDERR "Must be called as a CGI script.\n";
  143. $exit_code = 1;
  144. exit;
  145. }
  146. my $method = $ENV{"REQUEST_METHOD"};
  147. if (my $qs = $ENV{"QUERY_STRING"}) {
  148. parse_raw_args($qs);
  149. }
  150. if ($method eq "GET" || $method eq "HEAD") {
  151. } elsif ($method eq "POST") {
  152. my $content_type = $ENV{"CONTENT_TYPE"} // "";
  153. if ($content_type =~ /^application\/x-www-form-urlencoded\b/i) {
  154. while (<STDIN>) {
  155. chomp;
  156. parse_raw_args($_);
  157. }
  158. } elsif ($content_type =~ /^multipart\/form-data\b/i) {
  159. parse_multipart_form_data();
  160. } else {
  161. http_error "415 Unsupported Media Type";
  162. exit;
  163. }
  164. } else {
  165. http_error "405 Method Not Allowed", "Allow: GET, HEAD, PUT";
  166. }
  167. }
  168. sub parse_args($) { # CAVEAT: attached files must be defined in the main arg table
  169. my $args = shift @_;
  170. if (!$main_arg_table) {
  171. $main_arg_table = $args;
  172. init_args();
  173. }
  174. for my $a (values %$args) {
  175. my $r = ref($a->{'var'});
  176. defined($a->{'default'}) or $a->{'default'}="";
  177. if ($r eq 'SCALAR') {
  178. ${$a->{'var'}} = $a->{'default'};
  179. } elsif ($r eq 'ARRAY') {
  180. @{$a->{'var'}} = ();
  181. }
  182. }
  183. for my $arg (keys %$args) {
  184. my $a = $args->{$arg};
  185. defined($raw_args{$arg}) or next;
  186. for (@{$raw_args{$arg}}) {
  187. $a->{'multiline'} or s/(\n|\t)/ /g;
  188. s/^\s+//;
  189. s/\s+$//;
  190. if (my $rx = $a->{'check'}) {
  191. if (!/^$rx$/) { $_ = $a->{'default'}; }
  192. }
  193. my $v = $a->{'var'};
  194. my $r = ref($v);
  195. if ($r eq 'SCALAR') {
  196. $$v = $_;
  197. } elsif ($r eq 'ARRAY') {
  198. push @$v, $_;
  199. }
  200. }
  201. }
  202. }
  203. ### Parsing Multipart Form Data ###
  204. my $boundary;
  205. my $boundary_len;
  206. my $mp_buffer;
  207. my $mp_buffer_i;
  208. my $mp_buffer_boundary;
  209. my $mp_eof;
  210. sub refill_mp_data($) {
  211. my ($more) = @_;
  212. if ($mp_buffer_boundary >= $mp_buffer_i) {
  213. return $mp_buffer_boundary - $mp_buffer_i;
  214. } elsif ($mp_buffer_i + $more <= length($mp_buffer) - $boundary_len) {
  215. return $more;
  216. } else {
  217. if ($mp_buffer_i) {
  218. $mp_buffer = substr($mp_buffer, $mp_buffer_i);
  219. $mp_buffer_i = 0;
  220. }
  221. while ($mp_buffer_i + $more > length($mp_buffer) - $boundary_len) {
  222. last if $mp_eof;
  223. my $data;
  224. my $n = read(STDIN, $data, 2048);
  225. if ($n > 0) {
  226. $mp_buffer .= $data;
  227. } else {
  228. $mp_eof = 1;
  229. }
  230. }
  231. $mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
  232. if ($mp_buffer_boundary >= 0) {
  233. return $mp_buffer_boundary;
  234. } elsif ($mp_eof) {
  235. return length($mp_buffer);
  236. } else {
  237. return length($mp_buffer) - $boundary_len;
  238. }
  239. }
  240. }
  241. sub get_mp_line($) {
  242. my ($allow_empty) = @_;
  243. my $n = refill_mp_data(1024);
  244. my $i = index($mp_buffer, "\r\n", $mp_buffer_i);
  245. if ($i >= $mp_buffer_i && $i < $mp_buffer_i + $n - 1) {
  246. my $s = substr($mp_buffer, $mp_buffer_i, $i - $mp_buffer_i);
  247. $mp_buffer_i = $i + 2;
  248. return $s;
  249. } elsif ($allow_empty) {
  250. if ($n) { # An incomplete line
  251. my $s = substr($mp_buffer, $mp_buffer_i, $n);
  252. $mp_buffer_i += $n;
  253. return $s;
  254. } else { # No more lines
  255. return undef;
  256. }
  257. } else {
  258. http_error "400 Bad Request: Premature end of multipart POST data";
  259. }
  260. }
  261. sub skip_mp_boundary() {
  262. if ($mp_buffer_boundary != $mp_buffer_i) {
  263. http_error "400 Bad Request: Premature end of multipart POST data";
  264. }
  265. $mp_buffer_boundary = -1;
  266. $mp_buffer_i += 2;
  267. my $b = get_mp_line(0);
  268. print STDERR "SEP $b\n" if $debug;
  269. $mp_buffer_boundary = index($mp_buffer, $boundary, $mp_buffer_i);
  270. if (substr("\r\n$b", 0, $boundary_len) eq "$boundary--") {
  271. return 0;
  272. } else {
  273. return 1;
  274. }
  275. }
  276. sub parse_mp_header() {
  277. my $h = {};
  278. my $last;
  279. while ((my $l = get_mp_line(0)) ne "") {
  280. print STDERR "HH $l\n" if $debug;
  281. if (my ($name, $value) = ($l =~ /([A-Za-z0-9-]+)\s*:\s*(.*)/)) {
  282. $name =~ tr/A-Z/a-z/;
  283. $h->{$name} = $value;
  284. $last = $name;
  285. } elsif ($l =~ /^\s+/ && $last) {
  286. $h->{$last} .= $l;
  287. } else {
  288. $last = undef;
  289. }
  290. }
  291. foreach my $n (keys %$h) {
  292. $h->{$n} = rfc822_prepare($h->{$n});
  293. print STDERR "H $n: $h->{$n}\n" if $debug;
  294. }
  295. return (keys %$h) ? $h : undef;
  296. }
  297. sub parse_multipart_form_data() {
  298. # First of all, find the boundary string
  299. my $ct = rfc822_prepare($ENV{"CONTENT_TYPE"});
  300. if (!(($boundary) = ($ct =~ /^.*;\s*boundary=([^; ]+)/))) {
  301. http_error "400 Bad Request: Multipart content with no boundary string received";
  302. }
  303. $boundary = rfc822_deescape($boundary);
  304. print STDERR "BOUNDARY IS $boundary\n" if $debug;
  305. # BUG: IE 3.01 on Macintosh forgets to add the "--" at the start of the boundary string
  306. # as the MIME specs preach. Workaround borrowed from CGI.pm in Perl distribution.
  307. my $agent = http_get("User-Agent") // "";
  308. $boundary = "--$boundary" unless $agent =~ /MSIE\s+3\.0[12];\s*Mac/;
  309. $boundary = "\r\n$boundary";
  310. $boundary_len = length($boundary) + 2;
  311. # Check upload size in advance
  312. if (my $size = http_get("Content-Length")) {
  313. my $max_allowed = 0;
  314. foreach my $a (values %$main_arg_table) {
  315. $max_allowed += $a->{"maxsize"} || 65536;
  316. }
  317. if ($size > $max_allowed) {
  318. http_error "413 Request Entity Too Large";
  319. }
  320. }
  321. # Initialize our buffering mechanism and part splitter
  322. $mp_buffer = "\r\n";
  323. $mp_buffer_i = 0;
  324. $mp_buffer_boundary = -1;
  325. $mp_eof = 0;
  326. # Skip garbage before the 1st part
  327. while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
  328. skip_mp_boundary() || return;
  329. # Process individual parts
  330. do { PART: {
  331. print STDERR "NEXT PART\n" if $debug;
  332. my $h = parse_mp_header();
  333. my ($field, $cdisp, $a);
  334. if ($h &&
  335. ($cdisp = $h->{"content-disposition"}) &&
  336. $cdisp =~ /^form-data/ &&
  337. (($field) = ($cdisp =~ /;name=([^;]+)/)) &&
  338. ($a = $main_arg_table->{"$field"})) {
  339. print STDERR "FIELD $field\n" if $debug;
  340. if (defined $h->{"content-transfer-encoding"}) {
  341. http_error "400 Bad Request: Unexpected Content-Transfer-Encoding";
  342. }
  343. if (defined $a->{"var"}) {
  344. while (defined (my $l = get_mp_line(1))) {
  345. print STDERR "VALUE $l\n" if $debug;
  346. parse_raw_args_ll($field, $l);
  347. }
  348. next PART;
  349. } elsif (defined $a->{"file"}) {
  350. require File::Temp;
  351. require IO::Handle;
  352. my $max_size = $a->{"maxsize"} || 1048576;
  353. my @tmpargs = (undef, UNLINK => 1);
  354. push @tmpargs, DIR => $a->{"tmpdir"} if defined $a->{"tmpdir"};
  355. my ($fh, $fn) = File::Temp::tempfile(@tmpargs);
  356. print STDERR "FILE UPLOAD to $fn\n" if $debug;
  357. ${$a->{"file"}} = $fn;
  358. ${$a->{"fh"}} = $fh if defined $a->{"fh"};
  359. my $total_size = 0;
  360. while (my $i = refill_mp_data(4096)) {
  361. print $fh substr($mp_buffer, $mp_buffer_i, $i);
  362. $mp_buffer_i += $i;
  363. $total_size += $i;
  364. if ($total_size > $max_size) { http_error "413 Request Entity Too Large"; }
  365. }
  366. $fh->flush(); # Don't close the handle, the file would disappear otherwise
  367. next PART;
  368. }
  369. }
  370. print STDERR "SKIPPING\n" if $debug;
  371. while (my $i = refill_mp_data(256)) { $mp_buffer_i += $i; }
  372. } } while (skip_mp_boundary());
  373. }
  374. ### Generating Self-ref URL's ###
  375. sub make_out_args(@) { # Usage: make_out_args([arg_table, ...] name => value, ...)
  376. my @arg_tables = ( $main_arg_table );
  377. while (@_ && ref($_[0]) eq 'HASH') {
  378. push @arg_tables, shift @_;
  379. }
  380. my %overrides = @_;
  381. my $out = {};
  382. for my $table (@arg_tables) {
  383. for my $name (keys %$table) {
  384. my $arg = $table->{$name};
  385. defined($arg->{'var'}) || next;
  386. defined($arg->{'pass'}) && !$arg->{'pass'} && !exists $overrides{$name} && next;
  387. defined $arg->{'default'} or $arg->{'default'} = "";
  388. my $value;
  389. if (!defined($value = $overrides{$name})) {
  390. if (exists $overrides{$name}) {
  391. $value = $arg->{'default'};
  392. } else {
  393. $value = ${$arg->{'var'}};
  394. defined $value or $value = $arg->{'default'};
  395. }
  396. }
  397. if ($value ne $arg->{'default'}) {
  398. $out->{$name} = $value;
  399. }
  400. }
  401. }
  402. return $out;
  403. }
  404. sub self_ref(@) {
  405. my $out = make_out_args(@_);
  406. return "?" . join(':', map { "$_=" . url_param_escape($out->{$_}) } sort keys %$out);
  407. }
  408. sub self_form(@) {
  409. my $out = make_out_args(@_);
  410. return join('', map { "<input type=hidden name=$_ value='" . html_escape($out->{$_}) . "'>\n" } sort keys %$out);
  411. }
  412. ### Cookies
  413. sub set_cookie($$@) {
  414. #
  415. # Unfortunately, the support for the new cookie standard (RFC 2965) among
  416. # web browsers is still very scarce, so we are still using the old Netscape
  417. # specification.
  418. #
  419. # Usage: set_cookie(name, value, option => value...), where options are:
  420. #
  421. # max-age maximal age in seconds
  422. # domain domain name scope
  423. # path path name scope
  424. # secure if present, cookie applies only to SSL connections
  425. # (in this case, the value should be undefined)
  426. # discard if present with any value, the cookie is discarded
  427. #
  428. my $key = shift @_;
  429. my $value = shift @_;
  430. my %other = @_;
  431. if (exists $other{'discard'}) {
  432. delete $other{'discard'};
  433. $other{'max-age'} = 0;
  434. }
  435. if (defined(my $age = $other{'max-age'})) {
  436. delete $other{'max-age'};
  437. my $exp = ($age ? (time + $age) : 0);
  438. # Avoid problems with locales
  439. my ($S,$M,$H,$d,$m,$y,$wd) = gmtime $exp;
  440. my @wdays = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
  441. my @mons = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' );
  442. $other{'expires'} = sprintf("%s, %02d-%s-%d %02d:%02d:%02d GMT",
  443. $wdays[$wd], $d, $mons[$m], $y+1900, $H, $M, $S);
  444. }
  445. print "Set-Cookie: $key=", url_escape($value);
  446. foreach my $k (keys %other) {
  447. print "; $k";
  448. print "=", $other{$k} if defined $other{$k};
  449. }
  450. print "\n";
  451. }
  452. sub parse_cookies() {
  453. my $h = http_get("Cookie") or return ();
  454. my @cook = ();
  455. foreach my $x (split /;\s*/, $h) {
  456. my ($k,$v) = split /=/, $x;
  457. $v = url_deescape($v) if defined $v;
  458. push @cook, $k => $v;
  459. }
  460. return @cook;
  461. }
  462. 1; # OK