290 lines
7.7 KiB
Perl
290 lines
7.7 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Request::Plack
|
||
|
|
#
|
||
|
|
# Abstraction layer for using Plack's $env model to power Dreamwidth based
|
||
|
|
# systems.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# 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;
|