#!/usr/bin/perl # # DW::Request::Plack # # Abstraction layer for using Plack's $env model to power Dreamwidth based # systems. # # Authors: # Mark Smith # # Copyright (c) 2021 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. # package DW::Request::Plack; use strict; use v5.10; use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); use DW::Request::Base; use base 'DW::Request::Base'; use HTTP::Date (); use Plack::Request; use Plack::Response; use URI; use fields ( 'env', 'req', 'req_addr', 'res', 'res_body', 'res_length', 'notes', 'pnotes' ); $DW::Request::PLACK_AVAILABLE = 1; BEGIN { # Do initialization for pass-throughs that will go to the Plack::Request # object inside foreach my $method (qw/ method query_string /) { no strict 'refs'; *{"DW::Request::Plack::$method"} = sub { my DW::Request::Plack $self = shift; return $self->{req}->$method(@_); }; } } # uri: return just the path component, matching Apache's $r->uri behavior. # Plack::Request->uri returns the full URL which breaks code expecting a path. sub uri { my DW::Request::Plack $self = $_[0]; return $self->{req}->path_info || '/'; } # creates a new DW::Request object, based on what type of server environment we # are running under sub new { my DW::Request::Plack $self = $_[0]; my $plack_env = $_[1]; # Create self if needed $self = fields::new($self) unless ref $self; $self->SUPER::new; # Convert PSGI $env to Plack::Request and store for pass-thru usage $self->{env} = $plack_env; $self->{req} = Plack::Request->new($plack_env); $self->{req_addr} = undef; $self->{res} = Plack::Response->new; $self->{res_body} = undef; # or scalar $self->{res_length} = 0; # now stick ourselves as the primary request ... unless ($DW::Request::cur_req) { $DW::Request::determined = 1; $DW::Request::cur_req = $self; } return $self; } # response methods to update the response we're going to send sub header_out { my DW::Request::Plack $self = $_[0]; return $self->{res}->header( $_[1] ) if scalar @_ == 2; return $self->{res}->header( $_[1] => $_[2] ); } # In Plack there's no distinction between error and normal headers *err_header_out = \&header_out; # _add variants append instead of replacing (needed for Set-Cookie) sub header_out_add { my DW::Request::Plack $self = $_[0]; $self->{res}->headers->push_header( $_[1] => $_[2] ); } *err_header_out_add = \&header_out_add; sub set_last_modified { my DW::Request::Plack $self = $_[0]; my $mtime = $_[1]; $self->{res}->headers->header( 'Last-Modified' => HTTP::Date::time2str($mtime) ); } sub meets_conditions { my DW::Request::Plack $self = $_[0]; my $ims = $self->{req}->header('If-Modified-Since'); return 0 unless $ims; # 0 = OK, proceed normally my $lm = $self->{res}->headers->header('Last-Modified'); return 0 unless $lm; my $ims_time = HTTP::Date::str2time($ims); my $lm_time = HTTP::Date::str2time($lm); return 0 unless defined $ims_time && defined $lm_time; # If not modified, return 304 return 304 if $lm_time <= $ims_time; return 0; } # incoming headers, from request sub header_in { my DW::Request::Plack $self = $_[0]; return $self->{req}->header( $_[1] ) if scalar @_ == 2; return $self->{req}->header( $_[1] => $_[2] ); } # return all request headers as a flat list of (key, value) pairs, # matching DW::Request::Apache2::headers_in behavior sub headers_in { my DW::Request::Plack $self = $_[0]; my @headers; $self->{req}->headers->scan( sub { push @headers, @_ } ); return @headers; } # get client address; allow overriding it because we need to set it in some # cases when we're dealing with proxies sub address { my DW::Request::Plack $self = $_[0]; return $self->{req_addr} // $self->{req}->address if scalar @_ == 1; return $self->{req_addr} = $_[1]; } # return host sub host { my DW::Request::Plack $self = $_[0]; return $self->header_in('Host'); } # set the status sub status { my DW::Request::Plack $self = $_[0]; $self->{res}->status( $_[1] ) if defined $_[1]; return $self->{res}->status; } # set or get the status line (e.g. "200 OK") sub status_line { my DW::Request::Plack $self = $_[0]; if ( scalar @_ == 2 ) { my ($status) = $_[1] =~ m/^(\d+)/; $self->{res}->status($status); } return $self->{res}->status; } # append to the body sub print { my DW::Request::Plack $self = $_[0]; push @{ $self->{res_body} ||= [] }, $_[1]; $self->{res_length} += length( $_[1] ); } # flatten out the body and return the response sub res { my DW::Request::Plack $self = $_[0]; if ( defined $self->{res_body} ) { $self->{res}->body( $self->{res_body} ); $self->{res}->content_length( $self->{res_length} ); } return $self->{res}->finalize; } # return path sub path { my DW::Request::Plack $self = $_[0]; return $self->{req}->path; } # query parameters sub query_parameters { my DW::Request::Plack $self = $_[0]; return $self->{req}->query_parameters; } # return a new response that is a redirect sub redirect { my DW::Request::Plack $self = $_[0]; # Use a 303 because we want to be explicit that when we do a redirect we expect # the user-agent to switch to a GET; this is an old assumption baked into the LJ/DW # code now made explicit here. # # Set status and Location on the existing response object so that any cookies # or headers already set (e.g. login session cookies) are preserved in the # redirect response. $self->{res}->status(303); $self->{res}->header( 'Location' => $_[1] ); $self->{res_body} = undef; $self->{res_length} = 0; return $self->res; } # assemble a URL for something sub uri_for { my DW::Request::Plack $self = $_[0]; my ( $path, $args ) = ( $_[1], $_[2] ); my $uri = $self->{req}->base; $uri->path( $uri->path . $path ); $uri->query_form(@$args) if %$args; return $uri; } # content_type: getter reads from request, setter sets on response sub content_type { my DW::Request::Plack $self = $_[0]; if ( scalar @_ >= 2 ) { return $self->{res}->content_type( $_[1] ); } return $self->{req}->content_type; } # content: return raw request body sub content { my DW::Request::Plack $self = $_[0]; return $self->{req}->content; } # pnote: per-request notes hash (used by routing) sub pnote { my DW::Request::Plack $self = $_[0]; if ( scalar(@_) == 2 ) { return $self->{pnotes}->{ $_[1] }; } else { return $self->{pnotes}->{ $_[1] } = $_[2]; } } # note: per-request notes hash (separate from pnotes) sub note { my DW::Request::Plack $self = $_[0]; if ( scalar(@_) == 2 ) { return $self->{notes}->{ $_[1] }; } else { return $self->{notes}->{ $_[1] } = $_[2]; } } # no_cache: set cache-control headers to prevent caching sub no_cache { my DW::Request::Plack $self = $_[0]; $self->{res}->header( 'Cache-Control' => 'no-cache, no-store, must-revalidate' ); $self->{res}->header( 'Pragma' => 'no-cache' ); $self->{res}->header( 'Expires' => '0' ); } # get_remote_ip: return the client IP address sub get_remote_ip { my DW::Request::Plack $self = $_[0]; return $self->address; } # Some things we need to pass to our base class # *call_response_handler = \&DW::Request::call_response_handler; 1;