#!/usr/bin/perl # Julianster, (C) 2001-2005 Julian Haight. GPL License, all rights reserved. # Version: 1.3 # WARNING, THIS SCRIPT MAY NOT BE SECURE - IT MAY EXPOSE READ ACCESS TO # FILES OUTSIDE THE SHARE FOLDER. Attempts have been made to defeat # this sort of attack against it, but they may not be 100% effective. # Furthermore, the script should be run as an under-privileged user (nobody). # BUGS: External programs (mpg123, bladeenc, tar) should be referenced # as constants rather than hard-coded. use strict; use CGI; use DB_File; use DBI; package julianster; my($base) = '/home/share/snd'; my($q) = new CGI; my($path, $dir, $action, %settings, %stats, $fh); my(%speeds) = ('192' => '192K/second or best, from local disk '. $base, '192slimp3' => '192K/second or best, through slimp3', '192http' => '192K/second or best, from webserver', '32' => '32K/second (modem speed), from webserver)', '64' => '64K/second (ISDN speed), from webserver)'); my(@sorder) = ('32', '64', '192', '192slimp3', '192http'); my($bwlimit) = 100000; # bytes/second when dling tar my($db) = "$base/julianster"; my($locked) = 0; my($body) = ''; julianster(); sub julianster { ($action) = $q->param('action') || 'show'; ($path) = $ENV{'PATH_INFO'}; ($settings{'speed'}) = $q->cookie('speed') || '192http'; ($settings{'mtype'}) = $q->cookie('mtype') || 'audio/x-mpegurl'; ($settings{'user'}) = $q->cookie('user'); ($dir) = "$base$path"; unless ($path) { my $host = $ENV{SERVER_NAME}; ## Port (from ENV or from HOST or 80) my $port = $ENV{SERVER_PORT} || $ENV{HTTP_PORT}; $port = (($host =~ s/:(\d+)$//) ? $1 : 80) unless (defined $port); ## For the URL my $domain = $port==80 ? $host : "$host:$port"; print "Location: http://$domain$ENV{'SCRIPT_NAME'}/\n\n"; # play (recursively or not) } elsif (($action eq 'play') || ($action eq 'p')) { play($path); # download a tarfile } elsif ($action eq 'download') { download(); # default actions, show directory } elsif (($action eq 'show') || ($action eq 'date')) { show($dir); # show settings form } elsif ($action eq 'settings') { showSettings(); # save settings with cookies, then show dir } elsif ($action eq 'savesettings') { saveSettings(); show($dir); # stream a song, downsampling } elsif ($action eq 'downsample32') { downSample(32); } elsif ($action eq 'downsample64') { downSample(64); # show yourself! } elsif ($action eq 'sourcecode') { sourceCode(); # Two random-play modes } elsif (($action eq 'random') || ($action eq 'truerandom') || ($action eq 'userrandom')) { playRandom(100); } elsif ($action eq 'search') { search(); # debugging function } elsif ($action eq 'listall') { listAll(); } elsif ($action eq 'editlist') { editList(); } elsif ($action eq 'truncate') { trunc(); } elsif ($action eq 'uploadform') { uploadForm(); } elsif ($action eq 'upload') { upload(); } elsif ($action eq 'slimp3set') { slimp3set(); } elsif ($action eq 'shim') { shim(); } else { print "Content-type:text/plain\n\nUnknown action:$action\n"; } } sub upload { my($name, $tmpname, $fh, $artist, $album, $dir, $tmpname, $file); $artist = $q->param('artist'); $album = $q->param('album'); $name = $q->param('file'); $tmpname = $q->tmpFileName($name), $name =~ m/([^\/\\\~\|]*)$/; # trim off all but filename part print "Content-type: text/html\n\n"; print 'Julianster - ' . $path . ' ' . $body . ' Julianster - Upload'; foreach $dir ("$base/UPLOAD", "$base/UPLOAD/$artist", "$base/UPLOAD/$artist/$album") { unless (-e $dir) { mkdir ($dir, 0777) || die "$dir $!"; } } $file = "$base/UPLOAD/$artist/$album/$name"; $fh = $q->upload('file'); open (OUT, ">$file") || die "$file $!"; while (read($fh, $_, 1024)) { print OUT $_; } close OUT; (@_) = (stat($file)); print '

Wrote ' . $file . ', size:' . $_[7] . '

Thanks!

Upload another? '; } sub uploadForm { print "Content-type: text/html\n\n"; print 'Julianster - ' . $path . ' ' . $body . ' Julianster - Upload

One file at a time - combine mp3s into archive if uploading many.
Artist:

Album:

Click button, find file, then click \'upload now\':

'; } sub lockClient { my($client) = @_; $client = '/tmp/' . crypt($client, 'xx') . '.lock'; if (-e $client) { return 0; } else { unless (open(LOCK, ">$client")) { # print STDERR "$! opening $client\n"; return 0; } close(LOCK); $locked = 1; } # print STDERR "client:$client\n"; return 1; } sub END { # print STDERR "end\n"; close (TAR); unlockClient($ENV{'REMOTE_ADDR'}); } sub sigPipe { print STDERR "sigpipe\n"; } sub unlockClient { my($client) = @_; if ($locked) { $client = '/tmp/' . crypt($client, 'xx') . '.lock'; if (-e $client) { unlink($client); } } } sub error { print 'Content-type: text/plain ' . $_[0] . ' '; } sub trunc { # name "truncate" reserved word my($user) = $settings{'user'}; my($id) = $q->param('id'); print 'Content-type: text/html Julianster - edit random play ' . $body . ' Julianster - edit random play - Return to Menu'; dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); $stats{"$user-max"} = $id; print "

Truncated random-play list to $id\n"; } sub editList { my(%stats, $max, $i, $f); my($user) = $settings{'user'}; print 'Content-type: text/html Julianster - edit random play ' . $body . ' Julianster - edit random play - Return to Menu'; dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); $max = $stats{"$user-max"}; for ($i=($max-100); $i < $max; $i++) { $f = $stats{$user . '-' . $i}; print '
[CUT] ' . $i . ' - '; modeLink($f); } dbmclose(%stats); } sub show { my($addhtml) = @_; my($file, $frontimg, $backimg, $listing); my(@files); print 'Expires: ' . CGI::expires("+1d") . ' Content-type: text/html Julianster - ' . $path . ' ' . $body . ' Julianster - ' . headList($path, $path) . '
Settings: ' . ($settings{'user'} || 'guest') . ', ' . $speeds{$settings{'speed'}} . ', ' . $settings{'mtype'} . '
'; if ((-e "$base/julianster-announce.html") && (open(ANNOUNCE, "$base/julianster-announce.html"))) { while () { print; } } print '
'; foreach $_ ('A'..'Z') { print ' ' . $_ . ''; } print ' '; rebuildFind(); # rebuild file DB if older than 1 week print '
Random Play . Weighted .'; if ($settings{'user'}) { print ' User Weighted .'; } print ' Newest . Search All Music . Upload
' . ($addhtml || 'One request at a time, please!'); (@files) = readDirSubs($dir); if (@files) { print '

Subdirectories: '; if ($path ne '/') { print '

Play All . Download All '; } print '

'; my($let, $last); foreach $file (sort mysort @files) { if ($path eq '/') { $let = uc(substr($file, 0, 1)); if (($let ne $last) && ($action ne 'date')) { $last = $let; print '

' . $let . ' (^)
'; } } print ' '; if (-e "$base$path$file/" . "Cover.jpg") { print ''; } else { print ''; } print 'View . Play . DL . '; print $file; if ($action eq 'date') { print sprintf(' (%.1f days old)', -M "$base$path$file"); } print "
\n"; } } (@files) = readDirFiles($dir); if (@files) { foreach $file (sort mysort @files) { if ($file =~ m/[\.\_]jpg$/) { if ($file =~ m/back/i) { $backimg = imgLink($path, $file); } else { if (!$frontimg || ($file =~ m/large/i)) { $frontimg = imgLink($path, $file); } elsif ($file =~ m/Cover\.jpg$/) { $frontimg = imgLink($path, $file); } } } elsif ($file =~ m/jpg$/) { } else { $listing .= '
Play . DL . ' . pretify($file); } } print '

' . $frontimg . '
' . $backimg . '
Files:
Play All . Download All
' . $listing; } print foot(); } sub pretify { my($name) = @_; $name =~ s/[-_]/ /g; $name =~ s/\.mp3//i; return $name; } sub imgLink { my($path, $file) = @_; return ''; } sub foot { return '

Julianster is (C) 2001, 2002 Julian Haight.
Released under the GPL (GNU Public License). All rights reserved. Perl Source Code
This page generated ' . CGI::expires('now') . '
'; } sub headList { my($dir, $where) = @_; my($file, $tmp); my($r) = ' TOP'; foreach $file (split('\/', $dir)) { if ($file) { $tmp .= "$file/"; if ("/$tmp" eq $where) { $r .= ' :: ' . $file . ''; } else { $r .= ' :: ' . $file . ''; } } } return $r; } sub download { $| = 1; my($name) = $path; my($size, $timer, $bytes, $chunk, $b); $name =~ s/(.*\/)([^\/]+)(\/)?$/$2/; $name .= '.tar'; chdir($base); $size = int($size); print STDERR "Sending $name ($size bytes)\n"; print "Content-Disposition: inline; filename=\"$name\"\n"; print "Content-type: application/octet-stream\n\n"; if (1==1) { open (TAR, "/bin/tar -c \".$path\" |") || die $!; $timer = time(); while ($chunk = read(TAR, $b, 1024)) { $bytes += $chunk; while (($ENV{'REMOTE_ADDR'} !~ m/^192\.168\./) && ($bytes > ((time() - $timer) * $bwlimit))) { sleep(1); } print $b; } if (!defined($chunk)) { print STDERR "Error reading from tar: $!\n"; } close (TAR) || die $!; } else { system('/bin/tar', '-c', ".$path"); } close STDOUT; } sub play { dbmopen(%stats, $db, 0644) || print STDERR ("Can't open $db $!"); if ($settings{'speed'} eq '192slimp3') { startSlimp3(); makePlayList($path); endSlimp3(); } else { print "Content-type: " . $settings{'mtype'} . "\n\n"; makePlayList($path); } dbmclose(%stats); } sub makePlayList { my($dir) = @_; my($file, $num, $choice); my($user) = $settings{'user'}; # collect stats on files we chose if (-d "$base$dir") { if ($action ne 'p') { # recurse only on "play" action foreach $file (sort mysort readDirSubs("$base$dir")) { makePlayList("$dir$file/"); } } foreach $file (sort mysort readDirFiles("$base$dir")) { $num = $stats{'max'}++; $stats{$num} = "$dir$file"; if ($user) { $num = $stats{"$user-max"}++; $stats{"$user-$num"} = "$dir$file"; } modeLink("$dir$file"); } } else { $num = $stats{'max'}++; $stats{$num} = $path; if ($user) { $num = $stats{"$user-max"}++; $stats{"$user-$num"} = "$dir$file"; } modeLink($path); } } sub playRandom { my($qty) = @_; my($i, $f, $max); my($user) = $settings{'user'}; if ($settings{'speed'} eq '192slimp3') { startSlimp3(); } else { print "Content-type: " . $settings{'mtype'} . "\n\n"; } dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); if ($action eq 'random') { $max = $stats{'max'}; } elsif ($user && ($action eq 'userrandom')) { $max = $stats{"$user-max"}; } else { $max = $stats{'maxf'}; } for ($i=0; $i < $qty; $i++) { if ($action eq 'random') { $f = $stats{(int(rand()*$max))}; } elsif ($user && ($action eq 'userrandom')) { $f = $stats{$user . '-' . (int(rand()*$max))}; } else { $f = $stats{('f' . int(rand()*$max))}; } if ($f =~ m/\.mp3$/i) { modeLink($f); } } if ($settings{'speed'} eq '192slimp3') { endSlimp3(); } dbmclose(%stats); } sub search { my($qry) = $q->param('q'); my(@files, $file, $dir, $olddir, $fname); print 'Expires: ' . CGI::expires("+1d") . "\n"; print "Content-type: text/html\n\n"; print 'Julianster - ' . $qry . ' search results ' . $body . ' Julianster - search "' . $qry . '" - Return to Menu

'; (@files) = findMatches($qry); if (@files) { foreach $file (sort mysort @files) { $file = CGI::escape($file); $file =~ s/\%2F/\//g; $dir = $file; $dir =~ s/^(.*\/)([^\/]*)$/$1/; $fname = $2; if ($dir ne $olddir) { $olddir = $dir; print '

' . headList($dir) . ' '; } print '
Play . DL ' . $fname; } } else { print 'Nothing found'; } print foot(); } sub highlight { my($text, $light, $font) = @_; my($result, $loc, $lloc); $loc = 0; $lloc = 0; while ($text) { $loc = index($text, $light); if ($loc >= 0) { $result .= (substr($text, 0, $loc) . '' . $light . ''); $text = substr($text, $loc + length($light)); } else { $result .= $text; $text = ''; } } return $result; } sub findMatches { my($qry) = @_; $qry = lc($qry); unless ($qry) { return (); } my($max, $f, $i, @res); dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); $max = $stats{'maxf'}; for ($i=0; $i < $max; $i++) { $f = $stats{('f' . int($i))}; if (index(lc($f), $qry) > 0) { push(@res, $f); } } dbmclose(%stats); return (@res); } sub modeLink { my($path) = @_; if (($settings{'speed'} eq '192') || ($settings{'speed'} eq '192slimp3')) { print "$base$path\n"; } else { $path = CGI::escape($path); $path =~ s/\%2F/\//g; if ($settings{'speed'} eq '192http') { print "http://$ENV{'HTTP_HOST'}/mp3s$path\n"; } elsif ($settings{'speed'} eq '32') { print ("http://$ENV{'HTTP_HOST'}$ENV{'SCRIPT_NAME'}$path?action=downsample32\n"); } elsif ($settings{'speed'} eq '64') { print ("http://$ENV{'HTTP_HOST'}$ENV{'SCRIPT_NAME'}$path?action=downsample64\n"); } else { print STDERR $settings{speed} . "\n"; } } } sub listAll { my($i, $max); print "Content-type: " . $settings{'mtype'} . "\n\n"; print join("\n", %ENV); return; dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); $max = $stats{'max'}; for ($i=0; $i < $max; $i++) { modeLink($stats{$i}); } $max = $stats{'maxf'}; for ($i=0; $i < $max; $i++) { modeLink($stats{'f' . $i}); } dbmclose(%stats); } sub rebuildFind { dbmopen(%stats, $db, 0644) || die("Can't open $db $!"); if ($stats{'fdate'} < (time() - (60*60*24*7))) { $| = 0; print "
Rebuilding full file list.\n"; $stats{'maxf'} = 0; $stats{'fdate'} = time(); makeFind(''); } else { print ' , ' . $stats{'maxf'} . ' files in database'; } dbmclose(%stats); } sub downSample { my($rate) = @_; # print "Content-type: text/plain\n\nrate:$rate file:$dir"; print "Content-type: audio/mpeg\n\n"; open(IN, "mpg123 -s -q \"$dir\" | /usr/local/bin/bladeenc -$rate -quiet -mono stdin stdout|") || die $!; while () { print; } close(IN); } sub makeFind { my($dir) = @_; my($max, $entry, @files); opendir(RD, "$base/$dir") || die "$base/$dir $!"; (@files) = readdir RD; closedir(RD); foreach $entry (@files) { if (($entry eq '.') || ($entry eq '..')) { } elsif (-d "$base$dir/$entry") { makeFind("$dir/$entry"); } else { $max = $stats{'maxf'}++; $stats{"f$max"} = "$dir/$entry"; } } } sub mysort { if ($action eq 'date') { return (-M "$base$path$a") <=> (-M "$base$path$b"); } else { return $a <=> $b || lc($a) cmp lc($b); } } sub readDirSubs { return readCacheDir($_[0], 'dir'); } sub readDirFiles { return readCacheDir($_[0], 'file'); } sub readCacheDir { my($dir, $what) = @_; my($entry, @vals); if ((!-e "$dir/.DIRCACHE") || (-M "$dir/.DIRCACHE" > 1)) { open(CACHE, ">$dir/.DIRCACHE"); opendir(RD, $dir) || die "$! $dir"; while ($entry = readdir(RD)) { if ($entry =~ m/^[\.\+]/) { } elsif (-d "$dir/$entry") { print CACHE "D$entry\n"; if ($what eq 'dir') { push(@vals, $entry); } } else { print CACHE "F$entry\n"; if ($what eq 'file') { push(@vals, $entry); } } } closedir(RD); close(CACHE); } else { open(CACHE, "$dir/.DIRCACHE") || die "Can't open cache dir $dir/.DIRCACHE for read: $!"; while ($entry = ) { $entry =~ m/(.)(.*)/; $entry = $2; if ($1 eq 'D') { if ($what eq 'dir') { push(@vals, $entry); } } else { if ($what eq 'file') { push(@vals, $entry); } } } close(CACHE); } return (@vals); } sub showSettings { print 'Content-type: text/html Julianster - settings ' . $body . '

Playback bandwidth and file location:
' . $q->radio_group(-name => 'speed', -values => \@sorder, -default => $settings{'speed'}, -labels => \%speeds, -linebreak => 'true') . '

Playlist MIME type (don\'t change unless you know what you\'re doing)

Your name (for personal random play)
(Reset random)
(requires cookies)


Edit random list '; } sub saveSettings { # print "Content-type: text/plain\n\n"; my($setting); foreach $setting ('speed', 'mtype', 'user') { if ($q->param($setting) ne $settings{$setting}) { print 'Set-Cookie: ' . $q->cookie(-name => $setting, -value => ($settings{$setting} = $q->param($setting)), -expires => '+1y', -path => '/') . "\n"; # print STDERR "setting cookie $setting\n"; } } if ($q->param('resetrand')) { dbmopen(%stats, $db, 0644) || print STDERR ("Can't open $db $!"); $stats{$q->param('user') . '-max'} = 0; dbmclose(%stats); } } sub startSlimp3 { open (OUT, '>/tmp/play.m3u') || die "/tmp/play.m3u $!"; $fh = select(OUT); } sub endSlimp3 { select($fh); close(OUT); print ('Content-type: text/html Julianster - Adding to slimp3 playlist ' . $body . ' Julianster - Adding to slimp3 playlist - Return to Menu '); } sub slimp3set { print 'Content-type: text/html Julianster -> Slimp3

'; } sub sourceCode { open (SC, $ENV{'SCRIPT_FILENAME'}) || die ("Cannot open SCRIPT_FILENAME $!"); print "Content-type: text/plain\n\n"; while () { print; } close SC; } sub shim { print 'Expires: ' . CGI::expires("+1y") . "\n"; print "Content-type: image/png\n\n"; print pack('c*', 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xa5, 0xd9, 0x9f, 0xdd, 0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82); } 1;