unit class HTTP::Message; use HTTP::Header; use HTTP::MediaType; use Encode; has HTTP::Header $.header = HTTP::Header.new; has $.content is rw; has $.protocol is rw = 'HTTP/1.1'; has Bool $.binary = False; has Str @.text-types; my $CRLF = "\r\n"; method new($content?, *%fields) { my $header = HTTP::Header.new(|%fields); self.bless(:$header, :$content); } method add-content($content) { $.content ~= $content; } class X::Decoding is Exception { has HTTP::Message $.response; has Blob $.content; method message() { "Problem decoding content"; } } method content-type() returns Str { $!header.field('Content-Type').values[0] || ''; } has HTTP::MediaType $!media-type; method media-type() returns HTTP::MediaType { if not $!media-type.defined { if self.content-type() -> $ct { $!media-type = HTTP::MediaType.parse($ct); } } $!media-type; } # Don't want to put the heuristic in the HTTP::MediaType # Also moving this here makes it much more easy to test method charset() returns Str { if self.media-type -> $mt { $mt.charset || ( $mt.major-type eq 'text' ?? $mt.sub-type eq 'html' ?? 'utf-8' !! 'iso-8859-1' !! 'utf-8'); } else { # At this point we're probably screwed anyway 'iso-8859-1' } } # This is already a candidate for refactoring # Just want to get it working method is-text() returns Bool { my Bool $ret = do { if $!binary { False; } else { if self.media-type -> $mt { if $mt.type ~~ any(@!text-types) { True; } else { given $mt.major-type { when 'text' { True; } when any() { False; } when 'application' { given $mt.sub-type { when /xml|javascript|json/ { True; } default { False; } } } default { # Not sure about this True; } } } } else { # No content type, try and blow up True; } } } $ret; } method is-binary() returns Bool { !self.is-text; } method content-encoding() { $!header.field('Content-Encoding'); } class X::Deflate is Exception { has Str $.message; } method inflate-content() returns Blob { if self.content-encoding -> $v is copy { # This is a guess $v = 'zlib' if $v eq 'compress' ; $v = 'zlib' if $v eq 'deflate'; try require ::('Compress::Zlib'); if ::('Compress::Zlib::Stream') ~~ Failure { X::Deflate.new(message => "Please install 'Compress::Zlib' to uncompress '$v' encoded content").throw; } else { my $z = ::('Compress::Zlib::Stream').new( |{ $v => True }); $z.inflate($!content); } } else { $!content; } } method decoded-content(:$bin) { return $!content if $!content ~~ Str || $!content.bytes == 0; my $content = self.inflate-content; # [todo] # If charset is missing from Content-Type, then before defaulting # to anything it should attempt to extract it from $.content like (for HTML): # # my $decoded_content; if !$bin && self.is-text { my $charset = self.charset; $decoded_content = try { Encode::decode($charset, $content); } || try { $content.decode('iso-8859-1'); } || try { $content.unpack("A*") } || X::Decoding.new(content => $content, response => self).throw; } else { $decoded_content = $content; } $decoded_content } multi method field(Str $f) { $.header.field($f); } multi method field(*%fields) { $.header.field(|%fields); } method push-field(*%fields) { $.header.push-field(|%fields); } method remove-field(Str $field) { $.header.remove-field($field); } method clear { $.header.clear; $.content = ''; } method parse($raw_message) { my @lines = $raw_message.split(/$CRLF/); my ($first, $second, $third); ($first, $second, $third) = @lines.shift.split(/\s+/); if $third.index('/') { # is a request $.protocol = $third; } else { # is a response $.protocol = $first; } loop { last until @lines; my $line = @lines.shift; if $line { my ($k, $v) = $line.split(/\:\s*/, 2); if $k and $v { if $.header.field($k) { $.header.push-field: |($k => $v.split(',')>>.trim); } else { $.header.field: |($k => $v.split(',')>>.trim); } } } else { $.content = @lines.grep({ $_ }).join("\n"); last; } } self; } method Str($eol = "\n", :$debug, Bool :$bin) { my constant $max_size = 300; my $s = $.header.Str($eol); $s ~= $eol if $.content; # The :bin will be passed from the H::UA if not $bin { $s ~= $.content ~ $eol if $.content and !$debug; } if $.content and $debug { if $bin || self.is-binary { $s ~= $eol ~ "=Content size : " ~ $.content.elems ~ " bytes "; $s ~= "$eol ** Not showing binary content ** $eol"; } else { $s ~= $eol ~ "=Content size: "~$.content.Str.chars~" chars"; $s ~= "- Displaying only $max_size" if $.content.Str.chars > $max_size; $s ~= $eol ~ $.content.Str.substr(0, $max_size) ~ $eol; } } return $s; } =begin pod =head1 NAME HTTP::Message - class encapsulating HTTP message =head1 SYNOPSIS use HTTP::Message; my $raw_msg = "GET / HTTP/1.1\r\nHost: somehost\r\n\r\n"; my $mess = HTTP::Message.new.parse($raw_msg); say $mess; =head1 DESCRIPTION This module provides a bunch of methods to easily manage HTTP message. =head1 METHODS =head2 method new method new($content?, *%fields) A constructor, takes following parameters: =item content : content of the message (optional) =item fields : fields of the header section my $msg = HTTP::Message.new('content', :field); =head2 method add-content method add-content(HTTP::Message:, Str $content) Adds HTTP message content. It does not remove the existing value, it concats to the existing content. my $msg = HTTP::Message.new('content', :field); $msg.add-content: 's'; say $msg.content; # says 'contents' =head2 method decoded-content method decoded-content(HTTP::Message:) Returns decoded content of the message (using L module to decode). my $msg = HTTP::Message.new(); say $msg.decoded-content; =head2 method field multi method field(HTTP::Message:, Str $s) returns HTTP::Header::Field multi method field(HTTP::Message:, *%fields) See L. =head2 method init-field method init-field(HTTP::Message:, *%fields) See L. =head2 method push-field method push-field(HTTP::Message:, HTTP::Header::Field $field) See L. =head2 method remove-field method remove-field(HTTP::Message:, Str $field) See L. =head2 method clear method clear(HTTP::Message:) Removes the whole message, both header and content section. my $msg = HTTP::Message.new('content', :field); $msg.clear; say ~$msg; # says nothing =head2 method parse method parse(HTTP::Message:, Str $raw_message) returns HTTP::Message Parses the whole HTTP message. It takes the HTTP message (with \r\n as a line separator) and obtain the header and content section, creates a HTTP::Header object. my $msg = HTTP::Message.new.parse("GET / HTTP/1.1\r\nHost: example\r\ncontent\r\n"); say $msg.perl; =head2 method Str method Str(HTTP::Message:, Str $eol = "\n") returns Str Returns HTTP message in a readable form. =head1 SEE ALSO L, L, L =end pod