6 use Digest::SHA qw(sha256_hex);
10 ###--------------------------------------------------------------------------
13 sub merge_hash (\%%) {
14 my ($hashref, %defaults) = @_;
15 for my $k (keys %defaults)
16 { $hashref->{$k} = $defaults{$k} unless exists $hashref->{$k}; }
19 ###--------------------------------------------------------------------------
22 our $DSN = "dbi:Pg(pg_enable_utf8=>1):host=db";
24 our @BACKOFF = (0.1, 10, 1.5, 0.5, 2.0);
26 our $BASEURL = "http://odin.gg/";
27 our $STATIC = "http://odin.gg/";
29 our $SHORTURL_PATH = "u";
30 our $PASTEBIN_PATH = "p";
32 our $URLMAXLEN = 1024;
37 our $PASTEMAXLEN = 1024*1024;
39 our %COOKIE_DEFAULTS = (
46 our ($SCHEME, $DOMAIN, $BASEPATH) = $BASEURL =~ m!^([^:]+)://([^/]+)(/.*)$!;
47 merge_hash %COOKIE_DEFAULTS, -domain => $DOMAIN, -path => $BASEPATH;
48 merge_hash %COOKIE_DEFAULTS, -secure => undef if $SCHEME eq "https";
50 our $SHORTURL = "$BASEURL$SHORTURL_PATH";
51 our $PASTEBIN = "$BASEURL$PASTEBIN_PATH";
53 ###--------------------------------------------------------------------------
54 ### Miscellaneous utilities.
57 sub update_now () { $NOW = time; }
60 (our $PROG = $0) =~ s:^.*/::;
62 sub fail_cmdline ($$%) {
63 my ($msg, $label, %args) = @_;
64 print STDERR "$PROG: $msg\n";
68 our $FAILPROC = \&fail_cmdline;
71 my ($msg, $label, %args) = @_;
72 $FAILPROC->($msg, $label, %args);
75 sub set_mason_failproc ($) {
78 my ($msg, $label, %args) = @_;
80 $m->comp($label, %args);
91 sub print_columns (@) {
95 while (@col && $col[1] eq "") { splice @col, 0, 2; }
96 my ($wd, $v) = splice @col, 0, 2;
97 push @fmt, "%s"; push @val, $v;
99 my ($wd, $v) = splice @col, 0, 2;
100 push @fmt, "%-${wd}s";
103 printf join(" ", reverse @fmt) . "\n", reverse @val;
108 return $t == -1 ? "--" : strftime "%Y-%m-%d %H:%M:%S %z", localtime $t;
111 ###--------------------------------------------------------------------------
112 ### Database utilities.
116 my $db = DBI->connect_cached($DSN, undef, undef, {
122 my $drv = $db->{Driver}{Name};
124 $db->{private_odin_retry_p} = sub { $db->state =~ /^40[0P]01$/ };
125 } elsif ($drv eq "SQLite") {
126 $db->{private_odin_retry_p} = sub { $db->err == 5 };
128 $db->{private_odin_retry_p} = sub { 0 };
135 my ($body, $db) = @_;
139 my ($sleep, $maxsleep, $mult, $minvar, $maxvar) = @BACKOFF;
140 for (my $i = 0; $i < $RETRY; $i++) {
142 eval { @rv = $body->(); $db->commit; };
144 return @rv unless $exc;
145 my $retryp = $db->{private_odin_retry_p}();
146 eval { $db->rollback; };
147 die $exc unless $retryp;
148 my $t = $sleep * ($minvar + rand($maxvar - $minvar));
149 $sleep *= $mult; $sleep = $max if $sleep > $max;
150 select undef, undef, undef, $t;
155 sub insert_record ($$%) {
156 my ($db, $table, %fields) = @_;
160 for my $v (keys %fields) {
162 push @val, $fields{$v};
164 $db->do("INSERT INTO $table (" . join(", ", @var) . ")
165 VALUES (" . join(", ", map { "?" } @var) . ")", undef, @val);
168 ###--------------------------------------------------------------------------
169 ### Sequence numbers and tagging.
172 my ($db, $table) = @_;
173 my ($seq) = $db->selectrow_array("SELECT seq FROM $table");
174 die "no sequence number in $table" unless defined $seq;
175 $db->do("UPDATE $table SET seq = ?", undef, $seq + 1);
180 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
181 my $NALPHA = length $ALPHABET;
187 $tag .= substr($ALPHABET, $seq % $NALPHA, 1);
188 $seq = int $seq/$NALPHA;
193 ###--------------------------------------------------------------------------
197 sub fetch_cookies ($) {
201 my $cookies = $r->header_in("Cookie");
202 if (defined $cookies) {
203 for my $kv (split /;/, $cookies) {
204 my ($k, $v) = split /=/, $kv, 2;
205 $k =~ s/^\s*(|\S|\S.*\S)\s*$/$1/;
206 $v =~ s/^\s*(|\S|\S.*\S)\s*$/$1/;
208 $v =~ s/\%([0-9a-f][0-9a-f])/chr hex $1/eg;
214 sub bake_cookie ($$%) {
215 my ($r, $cookie, %attr) = @_;
216 merge_hash %attr, %COOKIE_DEFAULTS;
218 my $v = $attr{$_}; tr/_-/-/d;
219 defined $v ? "$_=$v" : $_
221 $r->headers_out->add("Set-Cookie", join "; ", $cookie, @attr);
226 return $ENV{PATH_INFO} // $r->path_info;
229 ###--------------------------------------------------------------------------
232 sub escapify ($$;$) {
233 my ($m, $s, $mode) = @_;
234 return $m->interp->apply_escapes($s, $mode // "h");
237 ###--------------------------------------------------------------------------
240 our ($WHO, $WHOSURE);
241 our ($WHOMATCH, $WHOCMP, $WHOPAT);
245 my $raddr = $ENV{REMOTE_ADDR} // $r->connection->remote_ip;
246 $WHO = ":NET-$raddr"; $WHOSURE = 0;
247 $WHOMATCH = "LIKE"; $WHOCMP = ":NET-\%"; $WHOPAT = qr/^:NET-/;
251 $WHO = $ENV{USERV_USER}
252 // ($< == $> && $ENV{USER})
253 // @{[getpwuid $<]}[0]
254 // die "nameless user";
255 $WHOMATCH = "="; $WHOCMP = $WHO; $WHOPAT = qr/^\Q$WHO\E$/;
260 open my $fh, "/dev/urandom" or die "open urandom: $!";
261 sysread $fh, my $rand, 16;
262 (my $edit = encode_base64 $rand) =~ tr:+/=\n:.-:d;
263 return $edit, sha256_hex $edit;
266 ###--------------------------------------------------------------------------
269 sub get_shorturl ($) {
273 my ($url) = $db->selectrow_array
274 ("SELECT url FROM odin_shorturl WHERE tag = ?", undef, $tag);
275 fail "tag `$tag' not found", ".notfound", tag => $tag unless defined $url;
279 sub valid_url_p ($) {
282 length $url < $URLMAXLEN &&
283 scalar grep { $url =~ /$_/ } @URLPAT;
286 sub new_shorturl ($) {
289 valid_url_p $url or fail "invalid url", ".badurl", u => $url;
294 ($tag) = $db->selectrow_array
295 ("SELECT tag FROM odin_shorturl WHERE owner $WHOMATCH ? AND url = ?",
296 undef, $WHOCMP, $url);
297 unless (defined $tag) {
298 $tag = encode_tag(next_seq($db, "odin_shorturl_seq"));
299 insert_record $db, "odin_shorturl",
300 tag => $tag, stamp => $NOW, owner => $WHO, url => $url;
306 sub check_shorturl_owner ($$) {
309 my ($owner) = $db->selectrow_array
310 ("SELECT owner FROM odin_shorturl WHERE tag = ?", undef, $tag);
311 fail "tag `$tag' not found", ".notfound", tag => $tag
312 unless defined $owner;
313 fail "not owner of `$tag'", ".notowner", tag => $tag
314 unless $owner =~ /$WHOPAT/;
317 sub update_shorturl ($$) {
318 my ($tag, $url) = @_;
322 check_shorturl_owner $db, $tag;
323 $db->do("UPDATE odin_shorturl SET url = ? WHERE tag = ?",
328 sub delete_shorturl (@) {
333 for my $tag (@tags) {
334 check_shorturl_owner $db, $tag;
335 $db->do("DELETE FROM odin_shorturl WHERE tag = ?", undef, $tag);
340 ###--------------------------------------------------------------------------
343 our %PASTEBIN_DEFAULTS = (
344 title => "(untitled)",
348 our @PASTEBIN_PROPS = keys %PASTEBIN_DEFAULTS;
349 our $PASTEBIN_PROPCOLS = join ", ", @PASTEBIN_PROPS;
350 our $PASTEBIN_PROPPLACES = join ", ", map "?", @PASTEBIN_PROPS;
352 sub new_pastebin (\%) {
356 my ($editkey, $hash) = new_editkey;
359 merge_hash %$new, %PASTEBIN_DEFAULTS;
361 $tag = encode_tag next_seq $db, "odin_pastebin_seq";
362 insert_record $db, "odin_pastebin",
363 tag => $tag, stamp => $NOW, edithash => $hash, owner => $WHO,
366 return $tag, $editkey;
369 sub get_pastebin ($$\%) {
370 my ($db, $tag, $props) = @_;
372 (my $owner, my $hash, @{$props}{@PASTEBIN_PROPS}) =
373 $db->selectrow_array("SELECT owner, edithash, $PASTEBIN_PROPCOLS
374 FROM odin_pastebin WHERE tag = ?",
376 fail "tag `$tag' not found", ".notfound", tag => $tag
377 unless defined $owner;
378 return $owner, $hash;
381 sub get_pastebin_check_owner ($$\%) {
382 my ($db, $tag, $props) = @_;
384 my ($owner, $hash) = get_pastebin $db, $tag, %$props;
385 fail "not owner of `$tag'", ".notowner", tag => $tag
386 unless $WHOSURE && $WHO eq $owner;
389 sub get_pastebin_check_editkey_or_owner ($$$\%) {
390 my ($db, $tag, $editkey, $props) = @_;
392 if (!defined $editkey) { get_pastebin_check_owner $db, $tag, %$props; }
394 my ($owner, $hash) = get_pastebin $db, $tag, %$props;
395 fail "incorrect edit key for `$tag'", ".badhash", tag => $tag
396 unless $hash eq sha256_hex $editkey;
400 sub rekey_pastebin ($) {
406 get_pastebin_check_owner $db, $tag, my %hunoz;
407 ($editkey, my $hash) = new_editkey;
408 $db->do("UPDATE odin_pastebin SET edithash = ? WHERE tag = ?",
414 sub claim_pastebin ($$) {
415 my ($tag, $editkey) = @_;
418 $WHOSURE or fail "you can't claim pastes", ".notsure";
420 get_pastebin_check_editkey_or_owner $db, $tag, $editkey, my %hunoz;
421 $db->do("UPDATE odin_pastebin SET owner = ? WHERE tag = ?",
426 sub update_pastebin ($$\%) {
427 my ($tag, $editkey, $new) = @_;
432 get_pastebin_check_editkey_or_owner $db, $tag, $editkey, my %old;
433 for my $p (@PASTEBIN_PROPS) {
434 if (!defined $new->{$p}) { $new->{$p} = $old{$p}; }
436 $db->do("UPDATE odin_pastebin SET $p = ? WHERE tag = ?",
437 undef, $new->{$p}, $tag)
438 unless $new->{$p} eq $old{$p};
446 sub delete_pastebin (@) {
451 (my $tag, my $editkey, @a) = @a;
452 get_pastebin_check_editkey_or_owner $db, $tag, $editkey, my %hunoz;
453 $db->do("DELETE FROM odin_pastebin WHERE tag = ?", undef, $tag);
458 sub tidy_pastebin_content ($) {
460 return undef unless defined $content;
461 $content =~ tr/\r//d;
462 $content =~ s/([^\n])\z/$1\n/;
463 length $content <= $PASTEMAXLEN or
464 fail "invalid paste content", ".badpaste";
468 ###--------------------------------------------------------------------------
469 ### Simple option parser.
471 package Odin::OptParse;
474 my ($cls, @args) = @_;
485 if (!length $me->{cur}) {
486 my $args = $me->{args};
487 if (!@$args) { return undef; }
488 elsif ($args->[0] =~ /^[^-]|^-$/) { return undef; }
489 elsif ($args->[0] eq "--") { shift @$args; return undef; }
490 $me->{cur} = substr shift @$args, 1;
492 my $o = $me->{opt} = substr $me->{cur}, 0, 1;
493 $me->{cur} = substr $me->{cur}, 1;
500 if (length $me->{cur}) { $a = $me->{cur}; $me->{cur} = ""; }
501 elsif (@{$me->{args}}) { $a = shift @{$me->{args}}; }
502 else { $a = undef; $me->err("option `-$me->{opt}' requires an argument"); }
506 sub rest { return @{$_[0]->{args}}; }
507 sub ok { return $_[0]->{ok}; }
508 sub bad { $_[0]->{ok} = 0; }
509 sub err { $_[0]->bad; print STDERR "$PROG: $_[1]\n"; }
510 sub unk { $_[0]->err("unknown option `-$_[0]->{opt}'"); }
512 ###----- That's all, folks --------------------------------------------------