40 my $opt_ssl = 1; |
40 my $opt_ssl = 1; |
41 my $opt_ssl_cert = "crt.pem"; |
41 my $opt_ssl_cert = "crt.pem"; |
42 my $opt_ssl_key = "key.pem"; |
42 my $opt_ssl_key = "key.pem"; |
43 |
43 |
44 # these vars will be filled with the real dirs later |
44 # these vars will be filled with the real dirs later |
45 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ]; |
45 my $rundir = ["/var/run/$ME", "$ENV{HOME}/.$ME"]; |
46 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ]; |
46 my $logdir = ["/var/log/$ME", "$ENV{HOME}/.$ME"]; |
47 |
47 |
48 my $maxlogsize = 1_000_000; # ca 1 MByte |
48 my $maxlogsize = 1_000_000; # ca 1 MByte |
49 my $killtimeout = 3; |
49 my $killtimeout = 3; |
50 |
50 |
51 # these are refs to detect if they're converted already |
51 # these are refs to detect if they're converted already |
52 my $access = \"%s/access.log"; |
52 my $access = \"%s/access.log"; |
53 my $errors = \"%s/error.log"; |
53 my $errors = \"%s/error.log"; |
54 my $pidfile = \"%s/%s.%s.pid"; # %dir/%ip.%port |
54 my $pidfile = \"%s/%s.%s.pid"; # %dir/%ip.%port |
55 |
55 |
56 # remember the pid that is actually written to the pid file so we can ensure |
56 # remember the pid that is actually written to the pid file so we can ensure |
57 # that only the process with that pid is attempting to remove the pidfile at |
57 # that only the process with that pid is attempting to remove the pidfile at |
58 # exit |
58 # exit |
59 my $masterpid; |
59 my $masterpid; |
79 |
79 |
80 our (@ISA, $DEBUG); |
80 our (@ISA, $DEBUG); |
81 |
81 |
82 sub new { |
82 sub new { |
83 |
83 |
84 my $class = shift; |
84 my $class = shift; |
85 |
85 |
86 my %args = @_; |
86 my %args = @_; |
87 my $ssl = delete $args{SSL}; |
87 my $ssl = delete $args{SSL}; |
88 |
88 |
89 if ($ssl) { |
89 if ($ssl) { |
90 |
90 |
91 @ISA = qw(IO::Socket::SSL); |
91 @ISA = qw(IO::Socket::SSL); |
92 $IO::Socket::SSL::DEBUG = $DEBUG ? 3 : 0; |
92 $IO::Socket::SSL::DEBUG = $DEBUG ? 3 : 0; |
93 |
93 |
94 } else { |
94 } |
95 |
95 else { |
96 %args = _delete_ssl_args(%args); |
96 |
97 @ISA = qw(IO::Socket::INET); |
97 %args = _delete_ssl_args(%args); |
98 |
98 @ISA = qw(IO::Socket::INET); |
99 } |
99 |
100 |
100 } |
101 my $self; |
101 |
102 unless ($self = $class->SUPER::new(%args)) { |
102 my $self; |
103 |
103 unless ($self = $class->SUPER::new(%args)) { |
104 return; |
104 |
105 |
105 return; |
106 } |
106 |
107 |
107 } |
108 print "$class: $self created\n" if $DEBUG; |
108 |
109 |
109 print "$class: $self created\n" if $DEBUG; |
110 return $self; |
110 |
|
111 return $self; |
111 |
112 |
112 } |
113 } |
113 |
114 |
114 sub close { |
115 sub close { |
115 |
116 |
116 my $self = shift; |
117 my $self = shift; |
117 print "$self: closing\n" if $DEBUG; |
118 print "$self: closing\n" if $DEBUG; |
118 |
119 |
119 my %args = @_; |
120 my %args = @_; |
120 |
121 |
121 %args = _delete_ssl_args(%args) unless $self->_is_ssl; |
122 %args = _delete_ssl_args(%args) unless $self->_is_ssl; |
122 |
123 |
123 return $self->SUPER::close(%args); |
124 return $self->SUPER::close(%args); |
124 |
125 |
125 } |
126 } |
126 |
127 |
127 sub errstr { |
128 sub errstr { |
128 |
129 |
129 return IO::Socket::SSL::errstr if _is_ssl(); |
130 return IO::Socket::SSL::errstr if _is_ssl(); |
130 |
131 |
131 return $@; |
132 return $@; |
132 |
133 |
133 } |
134 } |
134 |
135 |
135 sub _delete_ssl_args { |
136 sub _delete_ssl_args { |
136 |
137 |
137 my %args = @_; |
138 my %args = @_; |
138 map { delete $args{$_} if /^SSL/; } keys %args; |
139 map { delete $args{$_} if /^SSL/; } keys %args; |
139 return %args; |
140 return %args; |
140 |
141 |
141 } |
142 } |
142 |
143 |
143 sub _is_ssl { |
144 sub _is_ssl { |
144 |
145 |
145 my $self = shift; |
146 my $self = shift; |
146 return $ISA[0] eq "IO::Socket::SSL"; |
147 return $ISA[0] eq "IO::Socket::SSL"; |
147 |
148 |
148 } |
149 } |
149 |
150 |
150 # authentication |
151 # authentication |
151 package Authen::hlog; |
152 package Authen::hlog; |
154 use Digest::SHA1 qw(sha1_base64); |
155 use Digest::SHA1 qw(sha1_base64); |
155 use MIME::Base64 qw(decode_base64); |
156 use MIME::Base64 qw(decode_base64); |
156 |
157 |
157 sub new { |
158 sub new { |
158 |
159 |
159 my $class = shift; |
160 my $class = shift; |
160 |
161 |
161 my $self = { @_ }; |
162 my $self = {@_}; |
162 |
163 |
163 die "At least one of 'filename' or 'authdata' parameters is required" unless $self->{filename} || $self->{authdata}; |
164 die "At least one of 'filename' or 'authdata' parameters is required" |
164 |
165 unless $self->{filename} || $self->{authdata}; |
165 bless $self, $class; |
166 |
166 $self->authdata if $self->{filename}; |
167 bless $self, $class; |
167 |
168 $self->authdata if $self->{filename}; |
168 return $self; |
169 |
|
170 return $self; |
169 |
171 |
170 } |
172 } |
171 |
173 |
172 sub verify_base64 { |
174 sub verify_base64 { |
173 |
175 |
174 my $self = shift; |
176 my $self = shift; |
175 return $self->verify(split /:/, decode_base64($_[0])); |
177 return $self->verify(split /:/, decode_base64($_[0])); |
176 |
178 |
177 } |
179 } |
178 |
|
179 |
180 |
180 sub verify { |
181 sub verify { |
181 |
182 |
182 my $self = shift; |
183 my $self = shift; |
183 |
184 |
184 my ($u, $p) = @_; |
185 my ($u, $p) = @_; |
185 |
186 |
186 my $hp = $self->{authdata}->{$u}; |
187 my $hp = $self->{authdata}->{$u}; |
187 |
188 |
188 # crypt? |
189 # crypt? |
189 if (length $hp == 13) { |
190 if (length $hp == 13) { |
190 return crypt ($p, $hp) eq $hp; |
191 return crypt($p, $hp) eq $hp; |
191 # apache md5? |
192 |
192 } elsif (length $hp == 37 && $hp =~ /^\$apr/) { |
193 # apache md5? |
193 return apache_md5_crypt ($p, $hp) eq $hp; |
194 } |
194 } elsif ($hp =~ s/^\{SHA\}//) { |
195 elsif (length $hp == 37 && $hp =~ /^\$apr/) { |
195 # remove trailing equality signs because sha1_base64 refuses to add any |
196 return apache_md5_crypt($p, $hp) eq $hp; |
196 $hp =~ s/=*$//; |
197 } |
197 return sha1_base64($p) eq $hp; |
198 elsif ($hp =~ s/^\{SHA\}//) { |
198 } else { |
199 |
199 warn "unknown hash format: >>>$hp<<<"; |
200 # remove trailing equality signs because sha1_base64 refuses to add any |
200 } |
201 $hp =~ s/=*$//; |
201 |
202 return sha1_base64($p) eq $hp; |
202 return; |
203 } |
|
204 else { |
|
205 warn "unknown hash format: >>>$hp<<<"; |
|
206 } |
|
207 |
|
208 return; |
203 |
209 |
204 } |
210 } |
205 |
211 |
206 sub authdata { |
212 sub authdata { |
207 |
213 |
208 my $self = shift; |
214 my $self = shift; |
209 |
215 |
210 my ($htpasswd) = @_ || $self->{filename} || die "Missing filename"; |
216 my ($htpasswd) = @_ || $self->{filename} || die "Missing filename"; |
211 |
217 |
212 $self->{authdata} = {}; |
218 $self->{authdata} = {}; |
213 |
219 |
214 open H, "<$htpasswd" or die "Cant open '<$htpasswd': $!"; |
220 open H, "<$htpasswd" or die "Cant open '<$htpasswd': $!"; |
215 while (my $line = <H>) { |
221 while (my $line = <H>) { |
216 |
222 |
217 chomp $line; |
223 chomp $line; |
218 # htpasswd lines may have more than 2 fields |
224 |
219 my ($u, $p) = split /:/, $line, 3; |
225 # htpasswd lines may have more than 2 fields |
220 |
226 my ($u, $p) = split /:/, $line, 3; |
221 unless ($u && $p) { |
227 |
222 warn "invalid htpasswd line in '$htpasswd' at line $."; |
228 unless ($u && $p) { |
223 next; |
229 warn "invalid htpasswd line in '$htpasswd' at line $."; |
224 } |
230 next; |
225 |
231 } |
226 warn "duplicate user '$u' in '$htpasswd' at line $. - overriding previous record" if exists $self->{authdata}->{$u}; |
232 |
227 $self->{authdata}->{$u} = $p; |
233 warn |
228 |
234 "duplicate user '$u' in '$htpasswd' at line $. - overriding previous record" |
229 } |
235 if exists $self->{authdata}->{$u}; |
230 |
236 $self->{authdata}->{$u} = $p; |
231 close H or warn "Cant close '<$htpasswd': $!"; |
237 |
232 |
238 } |
233 warn "no authentication data found" unless %{$self->{authdata}}; |
239 |
234 |
240 close H or warn "Cant close '<$htpasswd': $!"; |
235 return $self->{authdata}; |
241 |
236 |
242 warn "no authentication data found" unless %{ $self->{authdata} }; |
|
243 |
|
244 return $self->{authdata}; |
|
245 |
237 } |
246 } |
238 |
247 |
239 # back to main package |
248 # back to main package |
240 package main; |
249 package main; |
241 |
250 |
242 END { |
251 END { |
243 unlink $pidfile |
252 unlink $pidfile |
244 if defined $pidfile and not ref $pidfile and defined $masterpid and $masterpid == $$; |
253 if defined $pidfile |
|
254 and not ref $pidfile |
|
255 and defined $masterpid |
|
256 and $masterpid == $$; |
245 } |
257 } |
246 |
258 |
247 sub find_writable_dir(@); |
259 sub find_writable_dir(@); |
248 |
260 |
249 sub log_open($); |
261 sub log_open($); |
276 "ssl-key=s" => \$opt_ssl_key |
288 "ssl-key=s" => \$opt_ssl_key |
277 ) or pod2usage(); |
289 ) or pod2usage(); |
278 |
290 |
279 $IO::Socket::hlog::DEBUG = $opt_debug; |
291 $IO::Socket::hlog::DEBUG = $opt_debug; |
280 |
292 |
281 $authdata = new Authen::hlog(filename => $opt_htpasswd, realm => $opt_realm) if $opt_auth; |
293 $authdata = new Authen::hlog(filename => $opt_htpasswd, realm => $opt_realm) |
|
294 if $opt_auth; |
282 |
295 |
283 if (defined($logdir = find_writable_dir(@$logdir))) { |
296 if (defined($logdir = find_writable_dir(@$logdir))) { |
284 $access = sprintf $$access, $logdir; |
297 $access = sprintf $$access, $logdir; |
285 $errors = sprintf $$errors, $logdir; |
298 $errors = sprintf $$errors, $logdir; |
286 log_open($access); |
299 log_open($access); |
335 LocalPort => $opt_port, |
348 LocalPort => $opt_port, |
336 Proto => "tcp", |
349 Proto => "tcp", |
337 Listen => 1, |
350 Listen => 1, |
338 ReuseAddr => 1, |
351 ReuseAddr => 1, |
339 SSL => $opt_ssl, |
352 SSL => $opt_ssl, |
340 SSL_key_file => $opt_ssl_key, |
353 SSL_key_file => $opt_ssl_key, |
341 SSL_cert_file => $opt_ssl_cert, |
354 SSL_cert_file => $opt_ssl_cert, |
342 debug => $opt_debug |
355 debug => $opt_debug |
343 ) or die "Can't create listener socket: ", IO::Socket::hlog::errstr, "\n"; |
356 ) or die "Can't create listener socket: ", IO::Socket::hlog::errstr, "\n"; |
344 |
|
345 |
357 |
346 # go daemon |
358 # go daemon |
347 chdir("/") or die "Can't chdir to /: $!\n"; |
359 chdir("/") or die "Can't chdir to /: $!\n"; |
348 |
360 |
349 if ($opt_daemon) { |
361 if ($opt_daemon) { |
461 my $lines = (s/(\d+)$// ? $1 : $opt_lines); |
473 my $lines = (s/(\d+)$// ? $1 : $opt_lines); |
462 my $tag = (s/^\/*(\w+)// ? $1 : "default"); |
474 my $tag = (s/^\/*(\w+)// ? $1 : "default"); |
463 |
475 |
464 my $authorized; |
476 my $authorized; |
465 $authorized = 1 unless $opt_auth; |
477 $authorized = 1 unless $opt_auth; |
466 # read and verify (first) authentication header and discard any other headers |
478 |
467 while (<$client>) { |
479 # read and verify (first) authentication header and discard any other headers |
468 |
480 while (<$client>) { |
469 if (!defined $authorized && /^Authorization: Basic ([[:alnum:]+\/=]+)\r?$/) { |
481 |
470 $authorized = $authdata->verify_base64($1); |
482 if (!defined $authorized |
471 log_write("authentication failure from " . $client->peerhost) unless $authorized; |
483 && /^Authorization: Basic ([[:alnum:]+\/=]+)\r?$/) |
472 } |
484 { |
473 last if /^\s*$/; |
485 $authorized = $authdata->verify_base64($1); |
474 |
486 log_write("authentication failure from " . $client->peerhost) |
|
487 unless $authorized; |
|
488 } |
|
489 last if /^\s*$/; |
|
490 |
475 } |
491 } |
476 |
492 |
477 unless ($authorized) { |
493 unless ($authorized) { |
478 |
494 |
479 $client->print(http { |
495 $client->print( |
480 code => "401 Unauthorized", |
496 http { |
481 headers => { "WWW-Authenticate" => "Basic realm=\"$authdata->{realm}\"" } |
497 code => "401 Unauthorized", |
482 }, "not authorized"); |
498 headers => { |
483 return; |
499 "WWW-Authenticate" => "Basic realm=\"$authdata->{realm}\"" |
|
500 } |
|
501 }, |
|
502 "not authorized" |
|
503 ); |
|
504 return; |
484 |
505 |
485 } |
506 } |
486 |
507 |
487 if (not exists $FILE{$tag}) { |
508 if (not exists $FILE{$tag}) { |
488 $client->print(http "500 unknown file tag", |
509 $client->print(http "500 unknown file tag", |
549 |
570 |
550 my ($headers, $code, $date) = (''); |
571 my ($headers, $code, $date) = (''); |
551 |
572 |
552 if (ref $_[0] eq "HASH") { |
573 if (ref $_[0] eq "HASH") { |
553 |
574 |
554 my $h; |
575 my $h; |
555 ($code, $date, $h) = @{$_[0]}{'code', 'date', 'headers'}; |
576 ($code, $date, $h) = @{ $_[0] }{ 'code', 'date', 'headers' }; |
556 $headers = ( join "\n", map { "$_: $h->{$_}" } keys %{$h} ) . "\n" if defined $h; |
577 $headers = (join "\n", map { "$_: $h->{$_}" } keys %{$h}) . "\n" |
557 shift; |
578 if defined $h; |
558 |
579 shift; |
559 } else { |
580 |
560 |
581 } |
561 $code = shift; |
582 else { |
562 |
583 |
563 } |
584 $code = shift; |
564 |
585 |
|
586 } |
|
587 |
565 $date ||= date1123(); |
588 $date ||= date1123(); |
566 |
589 |
567 my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain"; |
590 my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain"; |
568 |
591 |
569 return <<__EOF, @_; |
592 return <<__EOF, @_; |
576 } |
599 } |
577 |
600 |
578 sub date1123(;$) { |
601 sub date1123(;$) { |
579 my @now = gmtime(@_ ? shift : time); |
602 my @now = gmtime(@_ ? shift : time); |
580 sprintf "%s, %2d %s %4d %02d:%02d:%02d GMT", |
603 sprintf "%s, %2d %s %4d %02d:%02d:%02d GMT", |
581 qw(Sun Mon Tue Wed Thu Fri Sat Sun) [ $now[6] ], |
604 qw(Sun Mon Tue Wed Thu Fri Sat Sun) [$now[6]], |
582 $now[3], |
605 $now[3], |
583 qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) [ $now[4] ], |
606 qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) [$now[4]], |
584 $now[5] + 1900, @now[ 2, 1, 0 ]; |
607 $now[5] + 1900, @now[2, 1, 0]; |
585 } |
608 } |
586 |
609 |
587 sub bad_request() { |
610 sub bad_request() { |
588 return <<'__EOF'; |
611 return <<'__EOF'; |
589 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> |
612 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> |