source: gutenbach/debian/lib/sipbmp3-filter @ b281379

debianmacno-cupsnodebathenaweb
Last change on this file since b281379 was b281379, checked in by jtwang <jtwang>, 21 years ago

Correct security problems with $uri being passed on the command lines
to w3m, mpg123, and ogg123, all of which via /bin/sh, and thus subject
to shell expansion, especially of shell metacharacters.

Replace them with open("-|") fork-exec machinery. Note that this
eliminates a previously required temporary file for mpg123/ogg123
output, but at the cost of some code-complexity. Perhaps the common
parts of the mpg123/ogg123 invokation should be factored out.

(log message from jhawk)

  • Property mode set to 100755
File size: 16.7 KB
Line 
1#!/usr/athena/bin/perl
2# Play the data on STDIN as an audio file
3#
4# $Id: sipbmp3-filter,v 1.9 2003-10-22 20:27:44 jtwang Exp $
5# $Source: /tmp/tmp.UFBNno9997/RCS/sipbmp3-filter,v $
6#
7# TODO
8# ----
9# Make this structured code. It's a mess.
10# Repeat what we just played for EXT files too
11# Support HTTP Auth on ogg streams
12# License, cleanup and package
13#
14# Jered Floyd <jered@mit.edu> takes very little credit for this code
15
16use Getopt::Std;
17
18# Get the MP3Info module from this directory, because I suck.
19unshift(@INC, "/usr/local/bin");
20require MP3::Info;
21import MP3::Info;
22&use_winamp_genres();
23
24print STDERR "STDERR FROM SPOOL FILTER\n";
25
26# set real uid to be effective uid
27$< = $>;
28
29# Attach necessary lockers
30system("/bin/athena/attach -h -n -q infoagents sipb outland consult 2>&1 > /dev/null");
31
32# Select the correct output device and set the volume
33system("/usr/local/bin/audio_setdevice -out headphones 100 2>&1 </dev/null > /dev/null");
34
35# The command line we get from lpd is (no spaces between options and args):
36#  -C lpr -C class
37#  -A LPRng internal identifier
38#  -H originating host
39#  -J lpr -J jobname (default: list of files)
40#  -L lpr -U username
41#  -P logname
42#  -Q queuename (lpr -Q)
43#  -a printcap af (accounting file name)
44#  -d printcap sd entry (spool dir)
45#  -e print job data file name (currently being processed)
46#  -h print job originiating host (same as -H)
47#  -j job number in spool queue
48#  -k print job control file name
49#  -l printcap pl (page length)
50#  -n user name (same as -L)
51#  -s printcap sf (status file)
52#  -w printcap pw (page width)
53#  -x printcap px (page x dimension)
54#  -y printcap py (page y dimension)
55# accounting file name
56
57# All the filter_options from lpd
58getopt('ACFHJLPQRZacdefhijklnprswxy', \%opts);
59
60# Status messages at start of playback
61open(ZEPHYR, '|/usr/athena/bin/zwrite -d -n -c sipb-auto -i sipbmp3@xcb -s "SIPB LPR music spooler"');
62print(ZEPHYR "User $opts{'n'} on host $opts{'H'} is playing:\n");
63
64# So, the file we're currently processing is "-d/-e".
65# Do some magic to make sure it's an MP3, and get the important bits
66# to zephyr out.
67open(DATAFILE, "$opts{'d'}/$opts{'e'}");
68sysread(DATAFILE, $magic, 3);
69close(DATAFILE);
70
71# MPEG header is beshort &0xffe0
72# meditate upon the header:
73($magic0, $magic1, @magic2) = unpack("C3", $magic);
74if ((($magic0 & 0xff) == 0xff) &&
75    (($magic1 & 0xe0) == 0xe0)) {
76  # MPEG audio file, we like it
77  # Fall through
78} elsif ($magic eq "ID3") {
79  # ID3 v2 does sketchy things with "garbage" beginning with ID3 at file start.
80  # MPEG audio file with ID3 tag, we like it
81  # Fall through
82} elsif ($magic eq "EXT") {
83  # This is an external stream reference (a jered-special)
84  &play_external_reference(\%opts, $magic);
85  exit 0;
86} elsif ($magic eq "htt") {
87  # This is an external stream reference (a jered-special)
88  &play_external_reference(\%opts, $magic);
89  exit 0;
90} elsif ($magic eq "Ogg") {
91  # Ogg-Vorbis audio file
92  &play_ogg_audio(\%opts);
93  exit 0;
94} else {
95    # add more cases later, whine for now
96    printf(ZEPHYR "I don't think this is an MPEG audio file... %02x %02x\n",
97           $magic0, $magic1);
98    print ZEPHYR "I'm going to try playing it as one anyway.\n";
99}
100# Default
101&play_mpeg_audio(\%opts);
102
103
104# Play an external stream reference
105sub play_external_reference {
106    # Retrieve those command line opts.
107    my %opts = %{shift(@_)};
108    my $magic = shift(@_);
109
110    # External references are *not* playlists; they only support
111    # a single URL in order to be fair.  A reference is structured as
112    # follows:
113    #
114    # EXT <format>\n
115    # <URI>[ <user:pass>]\n
116    # Descriptive text of the file that will be\n
117    # played; this may span multiple lines until the\n
118    # EOF
119    #
120    # Where <URI> is a valid URI for the stream, and <format> is a
121    # recognized format name (currently 'MP3' or 'OGG'). <user:pass>
122    # is an optional user and password pair for HTTP Basic Auth.
123    my $format, $uri, $userpass;
124
125    if ("$magic" eq "EXT") {
126      if (<STDIN> =~ /^EXT (.*)$/) {
127        # Found the header
128        $format = $1;
129      } else {
130        print ZEPHYR "Couldn't read EXT header\n";
131        close(ZEPHYR);
132      }
133 
134      if (<STDIN> =~ /^(\S*)\s*(.*)$/) {
135        # Found the URI (and optionally user:pass)
136        $uri = $1;
137        if ($2) {
138        $userpass = $2;
139        }
140      } else {
141        print ZEPHYR "Couldn't read URI for external reference\n";
142        close(ZEPHYR);
143      }
144  } elsif ("$magic" eq "htt") {
145      if (<STDIN> =~ /^(\S*)\s*(.*)$/) {
146          $uri=$1;
147          my $pid = open(W3M, "-|");
148          unless (defined $pid) {
149              print ZEPHYR "Couldn't fork: $!\n";
150              close(ZEPHYR);
151              return;
152          }
153
154          if ($pid) { #parent
155              $contenttype="unknown";
156              while (<W3M>) {
157                  if ($_ =~ /^Content-Type:\s(\S*)/) {
158                      $contenttype=$1;
159                  }
160              }
161              if ($contenttype eq "audio/mpeg") { $format="MP3" }
162              elsif ($contenttype eq "application/x-ogg") { $format="OGG" }
163              elsif ($contenttype eq "application/ogg") { $format="OGG" }
164              else {
165                  print ZEPHYR "Unknown Content-Type $contenttype for URI $uri\n";
166                  close(ZEPHYR);
167              }
168              close(W3M) || print ZEPHYR "w3m exited $?\n";
169          }
170          else { # child
171              exec("/mit/sipb/bin/w3m","-dump_head",$uri) || die "Couldn't exec";
172          }
173      } else {
174        print ZEPHYR "Couldn't read URI for external reference\n";
175        close(ZEPHYR);
176    }
177  } else {
178      print ZEPHYR "Unknown syntax in play_external_reference(): $magic\n";
179      close(ZEPHYR);
180  }
181
182    # Echo the rest to the zephyr
183    while (<STDIN>) {
184      print ZEPHYR $_;
185    }
186    print ZEPHYR "\n";
187
188    # Play the thing the right way
189    if (($format eq "MP3") ||
190        ($format eq "mp3")) {
191      print ZEPHYR "Playing MP3 audio stream...\n";
192      close(ZEPHYR);
193      &play_mpeg_stream($uri, $userpass);
194    } elsif (($format eq "OGG") ||
195             ($format eq "ogg")) {
196      print ZEPHYR "Playing OggVorbis audio stream...\n";
197      close(ZEPHYR);
198      &play_ogg_stream($uri, $userpass);
199    } else {
200      print ZEPHYR "Unrecognized stream format: $format\n";
201      close(ZEPHYR);
202    }
203}
204
205
206# Play an MPEG audio stream
207# play_mpeg_stream(URI, userpass)
208sub play_mpeg_stream {
209  my $uri = shift(@_);
210  my $userpass = shift(@_);
211
212  # Add the user:pass argument if is was given to us
213  my $up = '';
214  if ($userpass) {
215    $up = '-u ' . $userpass;
216  }
217
218  system("ps ax | grep ogg123 | awk '{print $2}' | xargs kill -9");
219  system("ps ax | grep mpg123 | awk '{print $2}' | xargs kill -9");
220  system("chmod a+rw /dev/audio");
221
222  # Prepare to write status:
223  open(ZEPHYR, '|/usr/athena/bin/zwrite -d -n -c sipb-auto -i sipbmp3@xcb -s "SIPB LPR music spooler"');
224     
225  # fork for mpg123
226  my $pid = open(MP3STATUS, "-|");
227  unless (defined $pid) {
228      print ZEPHYR "Couldn't fork: $!\n";
229      close(ZEPHYR);
230      return;
231  }
232 
233  if ($pid) { #parent
234      # Check if there were any errors
235      if ($_ = <MP3STATUS>) {
236          print ZEPHYR "Playback completed with the following errors:\n";
237          print ZEPHYR $_;
238          while (<MP3STATUS>) {
239              print ZEPHYR $_;
240          }
241      } else {
242          print ZEPHYR "Playback completed successfully.\n";
243      }
244      close(MP3STATUS) || print ZEPHYR "mpg123 exited $?\n";
245     
246      close(ZEPHYR);
247  }
248  else { # child
249      # redirect STDERR to STDOUT
250      open STDERR, '>&STDOUT';
251      exec("/mit/infoagents/bin/mpg123","-b16384","-q",$up,$uri) or die "Couldn't exec";
252  }
253}
254
255# ID3 comments often have useless crap because tools like iTunes were
256# written by drooling idiots
257sub filter_comment {
258  my $comment = shift(@_);
259
260  if ($comment =~ /^engiTunes_CDDB/) {
261    return undef;
262  }
263  return $comment;
264}
265
266
267# Play an MPEG audio file
268sub play_mpeg_audio {
269    # Retrieve those command line opts.
270    my %opts = %{shift(@_)};
271
272    my %MPEGModes = ( 0 => "stereo",
273                      1 => "joint-stereo",
274                      2 => "dual-channel",
275                      3 => "single-channel");
276
277    # If it's an MP3 file, try to extract useful data
278    my $tag = get_mp3tag("$opts{'d'}/$opts{'e'}");
279    if (!$tag) {
280        print ZEPHYR "No ID3 tag found\n";
281        my @fnamearray = split /,/, $opts{'J'};
282        foreach $fname (@fnamearray) {
283            print ZEPHYR "Filename: $fname\n";
284        }
285#           print ZEPHYR "Filename: $opts{'J'}\n\n";
286    } else {
287      print ZEPHYR "Title          : $tag->{TITLE}\n";
288      print ZEPHYR "Artist         : $tag->{ARTIST}\n";
289      print ZEPHYR "Album          : $tag->{ALBUM}\n";
290      if ($tag->{TRACKNUM} =~ /(.*)\/.*/) {
291          $tag->{TRACKNUM} = $1;
292      }
293      if ($tag->{TRACKNUM}) {
294          print ZEPHYR "Track #        : $tag->{TRACKNUM}\n";
295      }
296      if ($tag->{YEAR}) {
297        print ZEPHYR "Year           : $tag->{YEAR}\n";
298      }
299      if ($tag->{GENRE}) {
300        # iTunes (?) does something weird with genres, leaving them
301        # as the string "(##)" where ## is the genre type. Let's deal
302        # with this.
303        if ($tag->{GENRE} =~ /^\((\d*)\)$/) {
304          $tag->{GENRE} = $MP3::Info::mp3_genres[$1];
305        }
306        if ($tag->{GENRE} =~ /^(\d*)$/) {
307          $tag->{GENRE} = $MP3::Info::mp3_genres[$1];
308        }
309        print ZEPHYR "Genre          : $tag->{GENRE}\n";
310      }
311      if (ref $tag->{COMMENT} eq 'ARRAY') {
312        foreach $index (0 .. $#{$tag->{COMMENT}}) {
313          if ($comment = filter_comment(@{$tag->{COMMENT}}[$index])) {
314            print ZEPHYR "Comment        : $comment\n";
315          }
316        }
317      } elsif ($tag->{COMMENT}) {
318        if ($comment = filter_comment($tag->{COMMENT})) {
319          print ZEPHYR "Comment        : $comment\n";
320        }
321      }
322
323      # Maybe get some extended ID3v2 info?
324      my $v2tag = get_mp3tag("$opts{'d'}/$opts{'e'}", 2, 1);
325      %lessinfo = %$v2tag;
326      foreach $dtag (keys %MP3::Info::v2_to_v1_names) {
327        delete $lessinfo{$dtag};
328      }
329      delete $lessinfo{'GENRE'};
330      # Delete annoying useless crap
331      my @bad_tags = ('GEO', 'GEOB', # General encapsulated object
332                      'PIC', 'APIC', # Attached picture.
333                      );
334      foreach $dtag (@bad_tags) {
335          delete $lessinfo{$dtag};
336      }
337      while (($key,$val) = each %lessinfo) {
338        printf ZEPHYR "%-15s: %s\n", $MP3::Info::v2_tag_names{$key}, $val;
339      }
340      print ZEPHYR "\n";
341    }
342
343    my $info = get_mp3info("$opts{'d'}/$opts{'e'}");
344    if (!$info) {
345        print ZEPHYR "No MPEG header found\n";
346    } else {
347        print ZEPHYR "MPEG $info->{VERSION} layer $info->{LAYER}, ";
348        if ($info->{VBR}) {
349            print ZEPHYR "VBR ";
350        }
351        print ZEPHYR "$info->{BITRATE} kbit/s, $info->{FREQUENCY} kHz ";
352        print ZEPHYR $MPEGModes{$info->{STEREO}};
353        print ZEPHYR "\n\n";
354        printf ZEPHYR "Track length: %d:%02ds\n", $info->{MM}, $info->{SS};
355    }
356    close(ZEPHYR);
357
358    # Play the file
359    # mpg123 is a crock.  If you don't give it -q, it needs to be on a pty
360    # or it SEGVs. Really.
361
362    system("chmod a+rw /dev/audio");
363    system("ps ax | grep ogg123 | awk '{print $2}' | xargs kill -9");
364    system("ps ax | grep mpg123 | awk '{print $2}' | xargs kill -9");
365    system("/mit/infoagents/bin/mpg123 -b 16384 -q - >/tmp/mpg123.out 2>&1");
366
367    # Done. Status:
368    open(ZEPHYR, '|/usr/athena/bin/zwrite -d -n -c sipb-auto -i sipbmp3@xcb -s "SIPB LPR music spooler"');
369
370    # Check if there were any errors
371    open(MP3STATUS, "/tmp/mpg123.out");
372    if ($_ = <MP3STATUS>) {
373        print ZEPHYR "Playback completed with the following errors:\n";
374        print ZEPHYR $_;
375        while (<MP3STATUS>) {
376            print ZEPHYR $_;
377        }
378    } else {
379        print ZEPHYR "Playback completed successfully.\n";
380        # Repeat tag data for those playing at home
381        if (!$tag) {
382          print ZEPHYR "No ID3 tag found\n";
383          print ZEPHYR "Filename: $opts{'J'}\n\n";
384        } else {
385          print ZEPHYR "Title          : $tag->{TITLE}\n";
386          print ZEPHYR "Artist         : $tag->{ARTIST}\n";
387          print ZEPHYR "Album          : $tag->{ALBUM}\n";
388          if ($tag->{TRACKNUM} =~ /(.*)\/.*/) {
389              $tag->{TRACKNUM} = $1;
390          }
391          if ($tag->{TRACKNUM}) {
392              print ZEPHYR "Track #        : $tag->{TRACKNUM}\n";
393          }
394          if ($tag->{YEAR}) {
395            print ZEPHYR "Year           : $tag->{YEAR}\n";
396          }
397          if ($tag->{GENRE}) {
398              print ZEPHYR "Genre          : $tag->{GENRE}\n";
399          }
400          if (ref $tag->{COMMENT} eq 'ARRAY') {
401            foreach $index (0 .. $#{$tag->{COMMENT}}) {
402              if ($comment = filter_comment(@{$tag->{COMMENT}}[$index])) {
403                print ZEPHYR "Comment        : $comment\n";
404              }
405            }
406          } elsif ($tag->{COMMENT}) {
407            if ($comment = filter_comment($tag->{COMMENT})) {
408              print ZEPHYR "Comment        : $comment\n";
409            }
410          }
411
412          # others
413          while (($key,$val) = each %lessinfo) {
414            printf ZEPHYR "%-15s: %s\n", $MP3::Info::v2_tag_names{$key}, $val;
415          }
416        }
417    }
418    close(MP3STATUS);
419    unlink(MP3STATUS);
420
421    close(ZEPHYR);
422}
423
424# Play an OggVorbis audio stream (doesn't support auth!)
425# play_ogg_stream(URI, userpass)
426sub play_ogg_stream {
427  my $uri = shift(@_);
428  my $userpass = shift(@_);
429
430  system("chmod a+rw /dev/audio");
431  system("ps -aef | grep ogg123 | awk '{print $2}' | xargs kill -9");
432  system("ps -aef | grep mpg123 | awk '{print $2}' | xargs kill -9");
433
434  # Prepare to write status:
435  open(ZEPHYR, '|/usr/athena/bin/zwrite -d -n -c sipb-auto -i sipbmp3@xcb -s "SIPB LPR music spooler"');
436
437  # fork for ogg123
438  my $pid = open(OGGSTATUS, "-|");
439  unless (defined $pid) {
440      print ZEPHYR "Couldn't fork: $!\n";
441      close(ZEPHYR);
442      return;
443  }
444  if ($pid) { # parent
445      # Check if there were any errors
446      if ($_ = <OGGSTATUS>) {
447          print ZEPHYR "Playback completed with the following errors:\n";
448          print ZEPHYR $_;
449          while (<OGGSTATUS>) {
450              print ZEPHYR $_;
451          }
452      } else {
453          print ZEPHYR "Playback completed successfully.\n";
454      }
455      close(OGGSTATUS) || print ZEPHYR "ogg123 exited $?\n";
456           
457      close(ZEPHYR);
458  }
459  else { #child
460      # redirect STDERR to STDOUT
461      open STDERR, '>&STDOUT';
462      exec("/mit/sipb/bin/ogg123","-b40000","-dau","-q","-f","-",$uri);
463  }
464}
465
466# Play an OggVorbis audio file
467sub play_ogg_audio {
468  # Retrieve those command line opts.
469  my %opts = %{shift(@_)};
470
471  # Get ogginfo stuff
472  open(OGGINFO, "/mit/sipb/bin/ogginfo $opts{'d'}/$opts{'e'}|");
473  while (<OGGINFO>) {
474    if (/(.*)=(.*)/) {
475      $ogginfo{lc("$1")} = $2;
476    }
477  }
478  close(OGGINFO);
479
480  # If there's no title, print the filename
481  if (!$ogginfo{'title'}) {
482    print ZEPHYR "No ogginfo data found\n";
483    print ZEPHYR "Filename: $opts{'J'}\n";
484  } else {
485    print ZEPHYR "Title          : $ogginfo{'title'}\n";
486    print ZEPHYR "Artist         : $ogginfo{'artist'}\n";
487    print ZEPHYR "Album          : $ogginfo{'album'}\n";
488    print ZEPHYR "Track #        : $ogginfo{'tracknumber'}\n";
489
490    # others
491    %lessinfo = %ogginfo;
492    delete $lessinfo{'filename'};
493    delete $lessinfo{'title'};
494    delete $lessinfo{'artist'};
495    delete $lessinfo{'album'};
496    delete $lessinfo{'tracknumber'};
497    delete $lessinfo{'header_integrity'};
498    delete $lessinfo{'stream_integrity'};
499    delete $lessinfo{'file_truncated'};
500    delete $lessinfo{'version'};
501    delete $lessinfo{'channels'};
502    delete $lessinfo{'bitrate_upper'};
503    delete $lessinfo{'bitrate_nominal'};
504    delete $lessinfo{'bitrate_lower'};
505    delete $lessinfo{'bitrate_average'};
506    delete $lessinfo{'length'};
507    delete $lessinfo{'playtime'};
508    delete $lessinfo{'kbps'};
509    delete $lessinfo{'time'};
510    delete $lessinfo{'rg_radio'};
511    delete $lessinfo{'rg_audiophile'};
512    delete $lessinfo{'rg_peak'};
513    delete $lessinfo{'replaygain_album_peak'};
514    delete $lessinfo{'replaygain_track_peak'};
515    delete $lessinfo{'replaygain_album_gain'};
516    delete $lessinfo{'replaygain_track_gain'};
517
518    while (($key,$val) = each %lessinfo) {
519      printf ZEPHYR "%-15s: %s\n", $key, $val;
520    }
521  }
522
523  printf ZEPHYR "\nOgg Vorbis, average %.1f kbit/s, %d channels\n\n",
524    $ogginfo{'bitrate_average'}/1024, $ogginfo{'channels'};
525  print ZEPHYR "Track length: $ogginfo{'playtime'}s\n";
526  close(ZEPHYR);
527
528  # Play the file
529
530  system("chmod a+rw /dev/audio");
531  system("ps -aef | grep ogg123 | awk '{print $2}' | xargs kill -9");
532  system("ps -aef | grep mpg123 | awk '{print $2}' | xargs kill -9");
533  system("/mit/sipb/bin/ogg123 -b 40000 -dau -q -f - - 2> /tmp/ogg123.out | audioplay");
534
535  # Done. Status:
536  open(ZEPHYR, '|/usr/athena/bin/zwrite -d -n -c sipb-auto -i sipbmp3@xcb -s "SIPB LPR music spooler"');
537
538  # Check if there were any errors
539  open(OGGSTATUS, "/tmp/ogg123.out");
540  if ($_ = <OGGSTATUS>) {
541    print ZEPHYR "Playback completed with the following errors:\n";
542    print ZEPHYR $_;
543    while (<OGGSTATUS>) {
544      print ZEPHYR $_;
545    }
546  } else {
547    print ZEPHYR "Playback completed successfully.\n";
548    # Repeat tag data for those playing at home
549    if (!$ogginfo{'title'}) {
550      print ZEPHYR "No ogginfo data found\n";
551      print ZEPHYR "Filename: $opts{'J'}\n\n";
552    } else {
553      print ZEPHYR "Title          : $ogginfo{'title'}\n";
554      print ZEPHYR "Artist         : $ogginfo{'artist'}\n";
555      print ZEPHYR "Album          : $ogginfo{'album'}\n";
556      print ZEPHYR "Track #        : $ogginfo{'tracknumber'}\n";
557
558      # others
559      while (($key,$val) = each %lessinfo) {
560        printf ZEPHYR "%-15s: %s\n", $key, $val;
561      }
562    }
563  }
564
565  close(OGGSTATUS);
566  unlink(OGGSTATUS);
567
568  close(ZEPHYR);
569}
Note: See TracBrowser for help on using the repository browser.