| 1 | package HTTP::Headers::Fast; |
|---|
| 2 | use strict; |
|---|
| 3 | use warnings; |
|---|
| 4 | use 5.00800; |
|---|
| 5 | our $VERSION = '0.01'; |
|---|
| 6 | use Carp (); |
|---|
| 7 | use base qw/HTTP::Headers/; |
|---|
| 8 | |
|---|
| 9 | our $TRANSLATE_UNDERSCORE = 1; |
|---|
| 10 | |
|---|
| 11 | # "Good Practice" order of HTTP message headers: |
|---|
| 12 | # - General-Headers |
|---|
| 13 | # - Request-Headers |
|---|
| 14 | # - Response-Headers |
|---|
| 15 | # - Entity-Headers |
|---|
| 16 | |
|---|
| 17 | # yappo says "Readonly sucks". |
|---|
| 18 | my $OP_GET = 0; |
|---|
| 19 | my $OP_SET = 1; |
|---|
| 20 | my $OP_INIT = 2; |
|---|
| 21 | |
|---|
| 22 | my @general_headers = qw( |
|---|
| 23 | Cache-Control Connection Date Pragma Trailer Transfer-Encoding Upgrade |
|---|
| 24 | Via Warning |
|---|
| 25 | ); |
|---|
| 26 | |
|---|
| 27 | my @request_headers = qw( |
|---|
| 28 | Accept Accept-Charset Accept-Encoding Accept-Language |
|---|
| 29 | Authorization Expect From Host |
|---|
| 30 | If-Match If-Modified-Since If-None-Match If-Range If-Unmodified-Since |
|---|
| 31 | Max-Forwards Proxy-Authorization Range Referer TE User-Agent |
|---|
| 32 | ); |
|---|
| 33 | |
|---|
| 34 | my @response_headers = qw( |
|---|
| 35 | Accept-Ranges Age ETag Location Proxy-Authenticate Retry-After Server |
|---|
| 36 | Vary WWW-Authenticate |
|---|
| 37 | ); |
|---|
| 38 | |
|---|
| 39 | my @entity_headers = qw( |
|---|
| 40 | Allow Content-Encoding Content-Language Content-Length Content-Location |
|---|
| 41 | Content-MD5 Content-Range Content-Type Expires Last-Modified |
|---|
| 42 | ); |
|---|
| 43 | |
|---|
| 44 | my %entity_header = map { lc($_) => 1 } @entity_headers; |
|---|
| 45 | |
|---|
| 46 | my @header_order = |
|---|
| 47 | ( @general_headers, @request_headers, @response_headers, @entity_headers, ); |
|---|
| 48 | |
|---|
| 49 | # Make alternative representations of @header_order. This is used |
|---|
| 50 | # for sorting and case matching. |
|---|
| 51 | my %header_order; |
|---|
| 52 | my %standard_case; |
|---|
| 53 | |
|---|
| 54 | { |
|---|
| 55 | my $i = 0; |
|---|
| 56 | for (@header_order) { |
|---|
| 57 | my $lc = lc $_; |
|---|
| 58 | $header_order{$lc} = ++$i; |
|---|
| 59 | $standard_case{$lc} = $_; |
|---|
| 60 | } |
|---|
| 61 | } |
|---|
| 62 | |
|---|
| 63 | sub new { |
|---|
| 64 | my ($class) = shift; |
|---|
| 65 | my $self = bless {}, $class; |
|---|
| 66 | $self->header(@_) if @_; # set up initial headers |
|---|
| 67 | $self; |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | sub header { |
|---|
| 71 | my $self = shift; |
|---|
| 72 | Carp::croak('Usage: $h->header($field, ...)') unless @_; |
|---|
| 73 | my (@old); |
|---|
| 74 | my %seen; |
|---|
| 75 | while (@_) { |
|---|
| 76 | my $field = shift; |
|---|
| 77 | my $method; |
|---|
| 78 | if (@_) { |
|---|
| 79 | if ( $seen{ lc $field }++ ) { |
|---|
| 80 | $method = '_header_push'; |
|---|
| 81 | } else { |
|---|
| 82 | $method = '_header_set'; |
|---|
| 83 | } |
|---|
| 84 | } else { |
|---|
| 85 | $method = '_header_get'; |
|---|
| 86 | } |
|---|
| 87 | |
|---|
| 88 | @old = $self->$method($field, shift); |
|---|
| 89 | } |
|---|
| 90 | return @old if wantarray; |
|---|
| 91 | return $old[0] if @old <= 1; |
|---|
| 92 | join( ", ", @old ); |
|---|
| 93 | } |
|---|
| 94 | |
|---|
| 95 | sub clear { |
|---|
| 96 | my $self = shift; |
|---|
| 97 | %$self = (); |
|---|
| 98 | } |
|---|
| 99 | |
|---|
| 100 | sub push_header { |
|---|
| 101 | my $self = shift; |
|---|
| 102 | |
|---|
| 103 | if (@_ == 2) { |
|---|
| 104 | $self->_header_push_no_return( @_ ); |
|---|
| 105 | } else { |
|---|
| 106 | while (@_) { |
|---|
| 107 | $self->_header_push_no_return( splice( @_, 0, 2 ) ); |
|---|
| 108 | } |
|---|
| 109 | } |
|---|
| 110 | } |
|---|
| 111 | |
|---|
| 112 | sub init_header { |
|---|
| 113 | Carp::croak('Usage: $h->init_header($field, $val)') if @_ != 3; |
|---|
| 114 | shift->_header( @_, $OP_INIT ); |
|---|
| 115 | } |
|---|
| 116 | |
|---|
| 117 | sub remove_header { |
|---|
| 118 | my ( $self, @fields ) = @_; |
|---|
| 119 | my $field; |
|---|
| 120 | my @values; |
|---|
| 121 | for my $field (@fields) { |
|---|
| 122 | $field =~ tr/_/-/ if $field !~ /^:/ && $TRANSLATE_UNDERSCORE; |
|---|
| 123 | my $v = delete $self->{ lc $field }; |
|---|
| 124 | push( @values, ref($v) eq 'ARRAY' ? @$v : $v ) if defined $v; |
|---|
| 125 | } |
|---|
| 126 | return @values; |
|---|
| 127 | } |
|---|
| 128 | |
|---|
| 129 | sub remove_content_headers { |
|---|
| 130 | my $self = shift; |
|---|
| 131 | unless ( defined(wantarray) ) { |
|---|
| 132 | |
|---|
| 133 | # fast branch that does not create return object |
|---|
| 134 | delete @$self{ grep $entity_header{$_} || /^content-/, keys %$self }; |
|---|
| 135 | return; |
|---|
| 136 | } |
|---|
| 137 | |
|---|
| 138 | my $c = ref($self)->new; |
|---|
| 139 | for my $f ( grep $entity_header{$_} || /^content-/, keys %$self ) { |
|---|
| 140 | $c->{$f} = delete $self->{$f}; |
|---|
| 141 | } |
|---|
| 142 | $c; |
|---|
| 143 | } |
|---|
| 144 | |
|---|
| 145 | sub _standardize_field_name { |
|---|
| 146 | my $field = shift; |
|---|
| 147 | $field =~ tr/_/-/ if $TRANSLATE_UNDERSCORE; |
|---|
| 148 | my $old = $field; |
|---|
| 149 | $field = lc $field; |
|---|
| 150 | unless ( defined $standard_case{$field} ) { |
|---|
| 151 | # generate a %standard_case entry for this field |
|---|
| 152 | $old =~ s/\b(\w)/\u$1/g; |
|---|
| 153 | $standard_case{$field} = $old; |
|---|
| 154 | } |
|---|
| 155 | return $field; |
|---|
| 156 | } |
|---|
| 157 | |
|---|
| 158 | sub _header_get { |
|---|
| 159 | my ($self, $field) = @_; |
|---|
| 160 | |
|---|
| 161 | $field = _standardize_field_name($field) unless $field =~ /^:/; |
|---|
| 162 | |
|---|
| 163 | my $h = $self->{$field}; |
|---|
| 164 | my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () ); |
|---|
| 165 | return @old; |
|---|
| 166 | } |
|---|
| 167 | |
|---|
| 168 | sub _header_set { |
|---|
| 169 | my ($self, $field, $val) = @_; |
|---|
| 170 | |
|---|
| 171 | $field = _standardize_field_name($field) unless $field =~ /^:/; |
|---|
| 172 | |
|---|
| 173 | my $h = $self->{$field}; |
|---|
| 174 | my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () ); |
|---|
| 175 | if ( defined($val) ) { |
|---|
| 176 | my @new; |
|---|
| 177 | if ( ref($val) ne 'ARRAY' ) { |
|---|
| 178 | push( @new, $val ); |
|---|
| 179 | } |
|---|
| 180 | else { |
|---|
| 181 | push( @new, @$val ); |
|---|
| 182 | } |
|---|
| 183 | $self->{$field} = @new > 1 ? \@new : $new[0]; |
|---|
| 184 | } else { |
|---|
| 185 | delete $self->{$field}; |
|---|
| 186 | } |
|---|
| 187 | return @old; |
|---|
| 188 | } |
|---|
| 189 | |
|---|
| 190 | sub _header_push_no_return { |
|---|
| 191 | my ($self, $field, $val) = @_; |
|---|
| 192 | |
|---|
| 193 | $field = _standardize_field_name($field) unless $field =~ /^:/; |
|---|
| 194 | |
|---|
| 195 | my $h = $self->{$field}; |
|---|
| 196 | if (ref($h) eq 'ARRAY') { |
|---|
| 197 | push @$h, ref $val ne 'ARRAY' ? $val : @$val; |
|---|
| 198 | } elsif (defined $h) { |
|---|
| 199 | $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ]; |
|---|
| 200 | } else { |
|---|
| 201 | $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val; |
|---|
| 202 | } |
|---|
| 203 | } |
|---|
| 204 | |
|---|
| 205 | sub _header_push { |
|---|
| 206 | my ($self, $field, $val) = @_; |
|---|
| 207 | |
|---|
| 208 | $field = _standardize_field_name($field) unless $field =~ /^:/; |
|---|
| 209 | |
|---|
| 210 | my $h = $self->{$field}; |
|---|
| 211 | if (ref($h) eq 'ARRAY') { |
|---|
| 212 | my @old = @$h; |
|---|
| 213 | push @$h, ref $val ne 'ARRAY' ? $val : @$val; |
|---|
| 214 | return @old; |
|---|
| 215 | } elsif (defined $h) { |
|---|
| 216 | $self->{$field} = [$h, ref $val ne 'ARRAY' ? $val : @$val ]; |
|---|
| 217 | return ($h); |
|---|
| 218 | } else { |
|---|
| 219 | $self->{$field} = ref $val ne 'ARRAY' ? $val : @$val; |
|---|
| 220 | return (); |
|---|
| 221 | } |
|---|
| 222 | } |
|---|
| 223 | |
|---|
| 224 | sub _header { |
|---|
| 225 | my ($self, $field, $val, $op) = @_; |
|---|
| 226 | |
|---|
| 227 | $field = _standardize_field_name($field) unless $field =~ /^:/; |
|---|
| 228 | |
|---|
| 229 | $op ||= defined($val) ? $OP_SET : $OP_GET; |
|---|
| 230 | |
|---|
| 231 | my $h = $self->{$field}; |
|---|
| 232 | my @old = ref($h) eq 'ARRAY' ? @$h : ( defined($h) ? ($h) : () ); |
|---|
| 233 | |
|---|
| 234 | unless ( $op == $OP_GET || ( $op == $OP_INIT && @old ) ) { |
|---|
| 235 | if ( defined($val) ) { |
|---|
| 236 | my @new = ( $op == $OP_PUSH ) ? @old : (); |
|---|
| 237 | if ( ref($val) ne 'ARRAY' ) { |
|---|
| 238 | push( @new, $val ); |
|---|
| 239 | } |
|---|
| 240 | else { |
|---|
| 241 | push( @new, @$val ); |
|---|
| 242 | } |
|---|
| 243 | $self->{$field} = @new > 1 ? \@new : $new[0]; |
|---|
| 244 | } |
|---|
| 245 | elsif ( $op != $OP_PUSH ) { |
|---|
| 246 | delete $self->{$field}; |
|---|
| 247 | } |
|---|
| 248 | } |
|---|
| 249 | @old; |
|---|
| 250 | } |
|---|
| 251 | |
|---|
| 252 | sub _sorted_field_names { |
|---|
| 253 | my $self = shift; |
|---|
| 254 | return sort { |
|---|
| 255 | ( $header_order{$a} || 999 ) <=> ( $header_order{$b} || 999 ) |
|---|
| 256 | || $a cmp $b |
|---|
| 257 | } keys %$self; |
|---|
| 258 | } |
|---|
| 259 | |
|---|
| 260 | sub header_field_names { |
|---|
| 261 | my $self = shift; |
|---|
| 262 | return map $standard_case{$_} || $_, $self->_sorted_field_names |
|---|
| 263 | if wantarray; |
|---|
| 264 | return keys %$self; |
|---|
| 265 | } |
|---|
| 266 | |
|---|
| 267 | sub scan { |
|---|
| 268 | my ( $self, $sub ) = @_; |
|---|
| 269 | my @sorted = $self->_sorted_field_names; |
|---|
| 270 | for my $key ( @sorted ) { |
|---|
| 271 | next if index($key, '_') == 0; |
|---|
| 272 | my $vals = $self->{$key}; |
|---|
| 273 | if ( ref($vals) eq 'ARRAY' ) { |
|---|
| 274 | for my $val (@$vals) { |
|---|
| 275 | $sub->( $standard_case{$key} || $key, $val ); |
|---|
| 276 | } |
|---|
| 277 | } |
|---|
| 278 | else { |
|---|
| 279 | $sub->( $standard_case{$key} || $key, $vals ); |
|---|
| 280 | } |
|---|
| 281 | } |
|---|
| 282 | } |
|---|
| 283 | |
|---|
| 284 | sub as_string { |
|---|
| 285 | my ( $self, $endl ) = @_; |
|---|
| 286 | $endl = "\n" unless defined $endl; |
|---|
| 287 | |
|---|
| 288 | my @result = (); |
|---|
| 289 | my $process_newline = sub { |
|---|
| 290 | local $_ = shift; |
|---|
| 291 | # must handle header values with embedded newlines with care |
|---|
| 292 | s/\s+$//; # trailing newlines and space must go |
|---|
| 293 | s/\n\n+/\n/g; # no empty lines |
|---|
| 294 | s/\n([^\040\t])/\n $1/g; # intial space for continuation |
|---|
| 295 | s/\n/$endl/g; # substitute with requested line ending |
|---|
| 296 | $_; |
|---|
| 297 | }; |
|---|
| 298 | my @sorted = $self->_sorted_field_names; |
|---|
| 299 | for my $key ( @sorted ) { |
|---|
| 300 | next if index($key, '_') == 0; |
|---|
| 301 | my $vals = $self->{$key}; |
|---|
| 302 | if ( ref($vals) eq 'ARRAY' ) { |
|---|
| 303 | for my $val (@$vals) { |
|---|
| 304 | my $field = $standard_case{$key} || $key; |
|---|
| 305 | $field =~ s/^://; |
|---|
| 306 | if ( index($val, "\n") >= 0 ) { |
|---|
| 307 | $val = $process_newline->($val); |
|---|
| 308 | } |
|---|
| 309 | push @result, $field . ': ' . $val; |
|---|
| 310 | } |
|---|
| 311 | } else { |
|---|
| 312 | my $field = $standard_case{$key} || $key; |
|---|
| 313 | $field =~ s/^://; |
|---|
| 314 | if ( index($vals, "\n") >= 0 ) { |
|---|
| 315 | $vals = $process_newline->($vals); |
|---|
| 316 | } |
|---|
| 317 | push @result, $field . ': ' . $vals; |
|---|
| 318 | } |
|---|
| 319 | } |
|---|
| 320 | |
|---|
| 321 | join( $endl, @result, '' ); |
|---|
| 322 | } |
|---|
| 323 | |
|---|
| 324 | if ( eval { require Storable; 1 } ) { |
|---|
| 325 | *clone = \&Storable::dclone; |
|---|
| 326 | } |
|---|
| 327 | else { |
|---|
| 328 | *clone = sub { |
|---|
| 329 | my $self = shift; |
|---|
| 330 | my $clone = new HTTP::Headers::Fast; |
|---|
| 331 | $self->scan( sub { $clone->push_header(@_); } ); |
|---|
| 332 | $clone; |
|---|
| 333 | }; |
|---|
| 334 | } |
|---|
| 335 | |
|---|
| 336 | sub _date_header { |
|---|
| 337 | require HTTP::Date; |
|---|
| 338 | my ( $self, $header, $time ) = @_; |
|---|
| 339 | my ($old) = $self->_header_get($header); |
|---|
| 340 | if ( defined $time ) { |
|---|
| 341 | $self->_header_set( $header, HTTP::Date::time2str($time) ); |
|---|
| 342 | } |
|---|
| 343 | $old =~ s/;.*// if defined($old); |
|---|
| 344 | HTTP::Date::str2time($old); |
|---|
| 345 | } |
|---|
| 346 | |
|---|
| 347 | sub date { shift->_date_header( 'Date', @_ ); } |
|---|
| 348 | sub expires { shift->_date_header( 'Expires', @_ ); } |
|---|
| 349 | sub if_modified_since { shift->_date_header( 'If-Modified-Since', @_ ); } |
|---|
| 350 | sub if_unmodified_since { shift->_date_header( 'If-Unmodified-Since', @_ ); } |
|---|
| 351 | sub last_modified { shift->_date_header( 'Last-Modified', @_ ); } |
|---|
| 352 | |
|---|
| 353 | # This is used as a private LWP extension. The Client-Date header is |
|---|
| 354 | # added as a timestamp to a response when it has been received. |
|---|
| 355 | sub client_date { shift->_date_header( 'Client-Date', @_ ); } |
|---|
| 356 | |
|---|
| 357 | # The retry_after field is dual format (can also be a expressed as |
|---|
| 358 | # number of seconds from now), so we don't provide an easy way to |
|---|
| 359 | # access it until we have know how both these interfaces can be |
|---|
| 360 | # addressed. One possibility is to return a negative value for |
|---|
| 361 | # relative seconds and a positive value for epoch based time values. |
|---|
| 362 | #sub retry_after { shift->_date_header('Retry-After', @_); } |
|---|
| 363 | |
|---|
| 364 | sub content_type { |
|---|
| 365 | my $self = shift; |
|---|
| 366 | my $ct = $self->{'content-type'}; |
|---|
| 367 | $self->{'content-type'} = shift if @_; |
|---|
| 368 | $ct = $ct->[0] if ref($ct) eq 'ARRAY'; |
|---|
| 369 | return '' unless defined($ct) && length($ct); |
|---|
| 370 | my @ct = split( /;\s*/, $ct, 2 ); |
|---|
| 371 | for ( $ct[0] ) { |
|---|
| 372 | s/\s+//g; |
|---|
| 373 | $_ = lc($_); |
|---|
| 374 | } |
|---|
| 375 | wantarray ? @ct : $ct[0]; |
|---|
| 376 | } |
|---|
| 377 | |
|---|
| 378 | sub content_is_html { |
|---|
| 379 | my $self = shift; |
|---|
| 380 | return $self->content_type eq 'text/html' || $self->content_is_xhtml; |
|---|
| 381 | } |
|---|
| 382 | |
|---|
| 383 | sub content_is_xhtml { |
|---|
| 384 | my $ct = shift->content_type; |
|---|
| 385 | return $ct eq "application/xhtml+xml" |
|---|
| 386 | || $ct eq "application/vnd.wap.xhtml+xml"; |
|---|
| 387 | } |
|---|
| 388 | |
|---|
| 389 | sub content_is_xml { |
|---|
| 390 | my $ct = shift->content_type; |
|---|
| 391 | return 1 if $ct eq "text/xml"; |
|---|
| 392 | return 1 if $ct eq "application/xml"; |
|---|
| 393 | return 1 if $ct =~ /\+xml$/; |
|---|
| 394 | return 0; |
|---|
| 395 | } |
|---|
| 396 | |
|---|
| 397 | sub referer { |
|---|
| 398 | my $self = shift; |
|---|
| 399 | if ( @_ && $_[0] =~ /#/ ) { |
|---|
| 400 | |
|---|
| 401 | # Strip fragment per RFC 2616, section 14.36. |
|---|
| 402 | my $uri = shift; |
|---|
| 403 | if ( ref($uri) ) { |
|---|
| 404 | $uri = $uri->clone; |
|---|
| 405 | $uri->fragment(undef); |
|---|
| 406 | } |
|---|
| 407 | else { |
|---|
| 408 | $uri =~ s/\#.*//; |
|---|
| 409 | } |
|---|
| 410 | unshift @_, $uri; |
|---|
| 411 | } |
|---|
| 412 | ( $self->_header( 'Referer', @_ ) )[0]; |
|---|
| 413 | } |
|---|
| 414 | *referrer = \&referer; # on tchrist's request |
|---|
| 415 | |
|---|
| 416 | for my $key (qw/content-length content-language content-encoding title user-agent server from warnings www-authenticate authorization proxy-authenticate proxy-authorization/) { |
|---|
| 417 | no strict 'refs'; |
|---|
| 418 | (my $meth = $key) =~ s/-/_/g; |
|---|
| 419 | *{$meth} = sub { |
|---|
| 420 | my $self = shift; |
|---|
| 421 | if (@_) { |
|---|
| 422 | ( $self->_header_set( $key, @_ ) )[0] |
|---|
| 423 | } else { |
|---|
| 424 | ( $self->_header_get($key) )[0]; |
|---|
| 425 | } |
|---|
| 426 | }; |
|---|
| 427 | } |
|---|
| 428 | |
|---|
| 429 | sub authorization_basic { shift->_basic_auth( "Authorization", @_ ) } |
|---|
| 430 | sub proxy_authorization_basic { |
|---|
| 431 | shift->_basic_auth( "Proxy-Authorization", @_ ); |
|---|
| 432 | } |
|---|
| 433 | |
|---|
| 434 | sub _basic_auth { |
|---|
| 435 | require MIME::Base64; |
|---|
| 436 | my ( $self, $h, $user, $passwd ) = @_; |
|---|
| 437 | my ($old) = $self->_header($h); |
|---|
| 438 | if ( defined $user ) { |
|---|
| 439 | Carp::croak("Basic authorization user name can't contain ':'") |
|---|
| 440 | if $user =~ /:/; |
|---|
| 441 | $passwd = '' unless defined $passwd; |
|---|
| 442 | $self->_header( |
|---|
| 443 | $h => 'Basic ' . MIME::Base64::encode( "$user:$passwd", '' ) ); |
|---|
| 444 | } |
|---|
| 445 | if ( defined $old && $old =~ s/^\s*Basic\s+// ) { |
|---|
| 446 | my $val = MIME::Base64::decode($old); |
|---|
| 447 | return $val unless wantarray; |
|---|
| 448 | return split( /:/, $val, 2 ); |
|---|
| 449 | } |
|---|
| 450 | return; |
|---|
| 451 | } |
|---|
| 452 | |
|---|
| 453 | 1; |
|---|
| 454 | __END__ |
|---|
| 455 | |
|---|
| 456 | =encoding utf8 |
|---|
| 457 | |
|---|
| 458 | =head1 NAME |
|---|
| 459 | |
|---|
| 460 | HTTP::Headers::Fast - |
|---|
| 461 | |
|---|
| 462 | =head1 SYNOPSIS |
|---|
| 463 | |
|---|
| 464 | use HTTP::Headers::Fast; |
|---|
| 465 | |
|---|
| 466 | =head1 DESCRIPTION |
|---|
| 467 | |
|---|
| 468 | HTTP::Headers::Fast is |
|---|
| 469 | |
|---|
| 470 | =head1 AUTHOR |
|---|
| 471 | |
|---|
| 472 | Tokuhiro Matsuno E<lt>tokuhirom@gmail.comE<gt> |
|---|
| 473 | |
|---|
| 474 | =head1 SEE ALSO |
|---|
| 475 | |
|---|
| 476 | =head1 LICENSE |
|---|
| 477 | |
|---|
| 478 | This library is free software; you can redistribute it and/or modify |
|---|
| 479 | it under the same terms as Perl itself. |
|---|
| 480 | |
|---|
| 481 | =cut |
|---|