2 # FlightGear METAR proxy server
3 # Melchior FRANZ (c) 2005, <mfranz # aon : at>, GPL V2
7 # 1) fill cache, for example with:
8 # $ metarproxy --download 3h
10 # 2) run proxy with FlightGear:
11 # $ metarproxy -c -v &
12 # $ fgfs --enable-real-weather-fetch --proxy=localhost:5509 --start-date-lat=2005:01:11:12:00:00
19 my $HOME = $ENV{'HOME'} || ".";
20 my $FG_HOME = $ENV{'FG_HOME'} || $HOME . "/.fgfs";
21 my $BASE = $FG_HOME . "/metar";
22 my $SERVER = "weather.noaa.gov";
24 my $PROXY = $ENV{'http_proxy'};
25 my $METAR_MAX_AGE = 250 * 60;
26 my $METAR_DEFAULT = "00000KT 15KM CLR 15/00 A3000";
27 my @COLOR = ("31;1", "31", "32", "", "36;1");
33 metarproxy [-v] [-b <path>] [-p <port>] [--serve]
34 metarproxy [-v] [-b <path>] [-y <proxy>] --download <list of: all|7|0-10|6h>
35 metarproxy [-v] [-b <path>] [-y <proxy>] --record [<list of station IDs>] [-f <path>]
36 metarproxy [-v] [-b <path>] --install <list of metar files>
41 -s|--serve start proxy server (default)
42 -p|--port set port (default: $PORT)
45 -d|--download <list of hours>
46 "all" ... whole day (24 files)
47 <number> ... this hour (example: 6)
48 <range> ... these hours (example: 2-5)
49 <period> ... last n hours (example: 3h)
50 -y|--proxy use proxy (default: off)
53 -i|--install <list of files to install>
56 -r|--record <list of METAR station IDs (ICAO)>
57 -f|--file <file containing list of station IDs>
58 -y|--proxy use proxy (default: off)
61 -b|--base set base directory (default: \$FG_HOME/metar)
62 -c|--color toggle color mode (default: off)
63 -v|--verbose increase verbosity level (default: off; maximum: -vvvv)
65 -q|--quiet only show error messages
67 -V|--version return version number
70 FG_HOME ... FlightGear home directory (default: \$HOME/.fgfs)
71 METARPROXY ... default options (e.g. export METARPROXY='-vv --color')
72 http_proxy ... system wide proxy setting (currently: '$PROXY')
75 \$ metarproxy -b\$HOME/.fgfs/metar --download 3h
76 \$ metarproxy --proxy=http://localhost:3128 --download all
77 \$ metarproxy --download 3h 7 21-23
78 \$ metarproxy --record -f/tmp/list LOWW LOWL
79 \$ metarproxy -b/var/tmp/metar --install /tmp/*Z.TXT
80 \$ metarproxy -p5600 & fgfs --proxy=localhost:5600 --enable-real-weather-fetch
81 \$ http_proxy= metarproxy --record LOXL
84 http://weather.noaa.gov/pub/data/observations/metar/{stations,cycles}/
85 ftp://weather.noaa.gov/data/observations/metar/{stations,cycles}/
94 my $VERBOSITY = $INFO;
101 # main =======================================================================
107 map { return $_ if defined $_ and $_ ne "" } @_;
112 unshift @ARGV, split /\s+/, $ENV{'METARPROXY'} if defined $ENV{'METARPROXY'};
118 } elsif (/^(-d|--download)$/) {
120 } elsif (/^(-i|--install)$/) {
122 } elsif (/^(-r|--record)$/) {
124 } elsif (/^(-s|--server?)$/) {
126 } elsif (/^(-b(.*)|--base(=(.*))?)/) {
127 my $path = &argument($2, $4);
128 defined $path or &fatal("-b|--base option lacks <path> argument");
129 $path =~ s/^~/$HOME/;
131 &log($BULK, "set option --base: '$BASE'");
132 } elsif (/^(-f(.*)|--file(=(.*))?)$/) {
133 my $file = &argument($2, $4);
134 defined $file or &fatal("-f|--file option lacks <path> argument");
135 &log($BULK, "set option --file: '$file'");
136 &read_icao_file($file);
137 } elsif (/^(-p(.*)|--port(=(.*))?)$/) {
138 $PORT = &argument($2, $4);
139 defined $PORT or &fatal("--port option lacks <port number> argument");
140 &log($BULK, "set option --port: '$PORT'");
141 } elsif (/^(-y(.*)|--proxy(=(.*))?)$/) {
142 $PROXY = &argument($2, $4);
143 defined $PROXY or &fatal("--proxy option lacks <host> definition");
144 &log($BULK, "set option --proxy: '$PROXY'");
145 } elsif (/^--verbose$/) {
147 } elsif (/^-(v+)$/) {
148 $VERBOSITY += length($1);
149 } elsif (/^(-q|--quiet)$/) {
151 } elsif (/^(-h|--help)$/) {
154 } elsif (/^(-V|--version)$/) {
155 ($_ = '$Revision$') =~ s/.*(\d+\.\d+).*/print "$1\n"/e;
157 } elsif (/^(-c|--color)$/) {
158 $USECOLOR = !$USECOLOR;
160 &fatal("unknown option $_");
170 undef $PROXY if $PROXY eq "";
171 my $mode = &parse_options();
174 -d $FG_HOME or mkdir $FG_HOME or &fatal("cannot create directory $FG_HOME ($!)");
175 -d $BASE or mkdir $BASE or &fatal("cannot create directory $BASE ($!)");
177 if (defined $PROXY) {
178 $PROXY =~ m|^(http://)?([a-zA-Z][a-zA-Z0-9-.]*):(\d+)/?| or &fatal("invalid proxy address: '$PROXY'");
179 ($PROXYHOST, $PROXYPORT) = ($2, $3);
185 } elsif ($mode == 2) {
187 } elsif ($mode == 3) {
189 } elsif ($mode == 4) {
190 &log($ERR, "ignoring command line args: " . (join ", ", @ITEMS)) if @ITEMS;
197 sub read_icao_file($)
200 $path =~ s/^\~/$HOME/;
202 if (!open(F, "<$path")) {
203 &log($ERR, "cannot open station list $path ($!)");
209 if (/^[A-Z][A-Z0-9]{3}$/) {
212 &log($ERR, "discarding suspicious station from $path: $_");
216 close F or &log($ERR, "cannot close station list $path ($!)");
220 # download ===================================================================
234 map { $h{$_} = 1 } (0 .. 23);
235 } elsif (/^(\d+)-(\d+)$/) {
236 map { $h{$_} = 1 } (&norm($1) .. &norm($2));
237 } elsif (/^(\d+)h$/) {
238 my $to = (gmtime(time))[2];
239 my $from = $to - &norm($1) + 1;
241 map { $h{$_} = 1 } ((24 + $from) .. 23);
244 map { $h{$_} = 1 } ($from .. $to);
245 } elsif (/^(\d+)$/) {
248 &log($ERR, "illegal download argument '$_' ignored");
251 @ITEMS = sort { $a <=> $b } keys %h;
252 @ITEMS or &fatal("nothing to download");
253 &log($INFO, "downloading: " . (join ", ", @ITEMS));
255 my $file = sprintf "/pub/data/observations/metar/cycles/%02dZ.TXT", $_;
256 &install_metar_http($SERVER, "80", $file);
262 # install ====================================================================
267 foreach my $file (@ITEMS) {
268 &log($INFO, "installing $file");
270 &log($ERR, "file $file doesn't exist");
273 if (!open (IN, "<$file")) {
274 &log($ERR, "cannot open $file ($!)");
278 &install_metar($_) foreach <IN>;
279 close IN or &log($ERR, "cannot close $file ($!)");
285 # install a METAR string KSFO under $FG_HOME/metar/2005-01-12/K/KS/KSFO
289 return unless $metar =~ /^(\d{4})\/(\d+)\/(\d+)\s(\d+):(\d+).*\015?\012([A-Z])([A-Z0-9])([A-Z0-9]{2})\s/s;
291 my $name = sprintf "$BASE/%04d-%02d-%02d", $1, $2, $3;
292 -d $name or mkdir $name or &fatal("cannot create directory $name ($!)");
294 -d $name or mkdir $name or &fatal("cannot create directory $name ($!)");
296 -d $name or mkdir $name or &fatal("cannot create directory $name ($!)");
300 if (open(F, "<$name")) {
303 if (m|^$1/$2/$3 $4:$5\s|s) {
308 close F or &log($ERR, "cannot close file $name ($!)");
309 return if defined $found;
311 &log($INFO, "writing to $name");
313 open(F, ">>$name") or &fatal("cannot append to file $name ($!)");
315 close F or &log($ERR, "cannot close file $name ($!)");
320 sub install_metar_http($$$)
322 my ($server, $port, $addr) = @_;
323 &log($INFO, "installing data from http://$server:$port$addr");
324 if (defined $PROXYHOST) {
325 &log($INFO, "via proxy http://$PROXYHOST:$PROXYPORT");
326 $addr = "http://$server" . $addr;
327 ($server, $port) = ($PROXYHOST, $PROXYPORT);
330 my $socket = IO::Socket::INET->new(Proto => "tcp", PeerAddr => $server, PeerPort => $port);
331 $socket or &fatal("cannot connect to http://$server:$port$addr/ ($!)");
332 $socket->autoflush(1);
333 my $get = "GET $addr HTTP/1.0";
334 print $socket "$get\015\012\015\012";
335 &log($DEBUG, ":$get:");
341 &log($DEBUG, "[$_]");
344 foreach (<$socket>) {
345 &install_metar("$_\n");
347 close($socket) or &log($ERR, "cannot close INET socket ($!)");
352 # record =====================================================================
357 @ITEMS or &fatal("no stations given");
360 # check for validity and remove duplicates
362 if (/^[A-Z][A-Z0-9]{3}$/) {
365 &log($ERR, "discarding invalid station '$_'");
368 @ITEMS = sort keys %h;
370 &log($INFO, "recording stations @ITEMS");
373 &install_metar_http($SERVER, "80", "/pub/data/observations/metar/stations/$_.TXT");
375 &log($INFO, "sleeping ...");
381 # serve ======================================================================
386 my $server = IO::Socket::INET->new(Proto => 'tcp', LocalPort => $PORT, Listen => SOMAXCONN, Reuse => 1);
387 $server or &fatal("cannot setup server ($!)");
388 &log($BULK, "server $0 accepting clients on port $PORT");
391 while (my $client = $server->accept()) {
392 $client->autoflush(1);
393 my $hostinfo = gethostbyaddr($client->peeraddr);
394 my $clientname = $hostinfo->name || $client->peerhost;
395 my $addr = inet_ntoa(inet_aton($clientname));
402 if (m|^GET\s+http://weather.noaa.gov/.*/([A-Z][A-Z0-9]{3}).TXT\s+HTTP/|) {
404 } elsif (/X-Time: (\d+)/) {
409 &log($INFO, "$_") if $VERBOSITY < $DEBUG;
413 if (defined $icao and defined $epoch) {
414 my ($min, $hour, $day, $mon, $year) = (gmtime($epoch))[1 .. 5];
417 &log($BULK, sprintf "client '$clientname' [$addr] requests data for station $icao "
418 . "at %04d/%02d/%02d %02d:%02d", $year, $mon, $day, $hour, $min);
420 my ($metar, $age) = &get_metar($icao, $epoch);
421 if (defined $metar) {
422 if ($age <= $METAR_MAX_AGE) {
423 &log($BULK, "found (" . int($age / 60) . " min old)");
425 $last_metar{$addr} = $metar;
426 $last_metar{$addr} =~ s/.*\015?\012[A-Z0-9]{4}\s+[0-9]{6}Z\s+//s;
427 &log($DEBUG, "setting default for [$addr] to '$last_metar{$addr}'");
428 $metar =~ s/\015?\012/\015\012/g;
430 &log($INFO, "found, but too old (" . int($age / 60) . " min)");
434 &log($WARN, "not found!");
437 if (!defined $metar) {
438 &log($INFO, "sending last successful data again");
439 $metar = sprintf "%04d/%02d/%02d %02d:%02d\015\012",
440 $year, $mon, $day, $hour, $min;
441 $metar .= sprintf "$icao %02d%02d%02dZ ", $day, $hour, $min;
442 $metar .= $last_metar{$addr} || $METAR_DEFAULT;
445 print $client "HTTP/1.0 200 OK\015\012";
446 print $client "Content-Type: text/plain\015\012"
447 . "X-MetarProxy: nasse Maus\015\012"
452 &log($WARN, "incomplete request");
454 &log($BULK, "closing connection");
463 my $rq_epoch = shift;
464 $icao =~ /^([A-Z])([A-Z0-9])([A-Z0-9]{2})$/;
469 my ($hour, $day, $mon, $year) = (gmtime($time))[2 .. 5];
470 my $name = sprintf "$BASE/%04d-%02d-%02d/$1/$1$2/$1$2$3", $year + 1900, $mon + 1, $day;
471 if (open (F, "<$name")) {
472 &log($BULK, "reading $name");
475 close F or &log($ERR, "cannot close file $name ($!)");
477 &log($BULK, "no file $name to read ($!)");
481 my @list; # "today" (and maybe "yesterday")
482 &scan_file($rq_epoch, \@list) and &scan_file($rq_epoch - 24 * 60 * 60, \@list);
487 /^(\d{4})\/(\d+)\/(\d+)\s(\d+):(\d+).*\015?\012$icao\s/s or next;
488 $epoch = timegm(0, $5, $4, $3, $2 - 1, $1 - 1900);
489 next if $epoch > $rq_epoch; # lies in the future
490 next if $rq_epoch - $epoch > $age; # older than previous entry
492 $age = $rq_epoch - $epoch;
494 return ($metar, $age);
498 # ==================================================================
503 &log($ERR, "$0: @_");
511 return if $v > $VERBOSITY;
513 print "\033[$COLOR[$v]m" if $USECOLOR;
515 print "\033[m" if $USECOLOR;