Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 78 additions & 75 deletions lib/Renderer.pm
Original file line number Diff line number Diff line change
@@ -1,49 +1,35 @@
package Renderer;
use Mojo::Base 'Mojolicious';

BEGIN {
use Mojo::File;
$main::libname = Mojo::File::curfile->dirname;

# RENDER_ROOT is required for initializing conf files.
$ENV{RENDER_ROOT} = $main::libname->dirname
unless (defined($ENV{RENDER_ROOT}));

# PG_ROOT is required for PG/lib/PGEnvironment.pm
$ENV{PG_ROOT} = $main::libname . '/PG';
use Mojo::File;
use Env qw(RENDER_ROOT PG_ROOT baseURL);
use Date::Format;

# Used for reconstructing library paths from sym-links.
$ENV{OPL_DIRECTORY} = "$ENV{RENDER_ROOT}/webwork-open-problem-library";

$ENV{MOJO_CONFIG} =
(-r "$ENV{RENDER_ROOT}/renderer.conf")
? "$ENV{RENDER_ROOT}/renderer.conf"
: "$ENV{RENDER_ROOT}/renderer.conf.dist";
$ENV{MOJO_LOG_LEVEL} = 'debug';
BEGIN {
# RENDER_ROOT and PG_ROOT are required for the WeBWorK::PG::Environment.
$RENDER_ROOT = Mojo::File::curfile->dirname->dirname;
$PG_ROOT = Mojo::File::curfile->dirname->child('PG');
}

use lib "$main::libname";
print "using root directory: $ENV{RENDER_ROOT}\n";

use Renderer::Model::Problem;
use Renderer::Controller::IO;
use WeBWorK::FormatRenderedProblem;

sub startup {
my $self = shift;

# Merge environment variables with config file
$self->plugin('Config');
$self->plugin('TagHelpers');
$self->secrets($self->config('secrets'));
for (qw(problemJWTsecret webworkJWTsecret baseURL formURL SITE_HOST STRICT_JWT)) {
$ENV{$_} //= $self->config($_);
}

sanitizeHostURLs();
$self->sanitizeHostURLs;

# This is also required for the WeBWorK::PG::Environment, but is not needed at compile time.
$baseURL = $self->config->{baseURL};

print "Renderer is based at $main::basehref\n";
print "Problem attempts will be sent to $main::formURL\n";
say 'Renderer is based at ' . $self->defaults->{baseHREF};
say 'Problem attempts will be sent to ' . $self->defaults->{formURL};

$self->plugin('Renderer::Plugin::Assets');

# Handle optional CORS settings
if (my $CORS_ORIGIN = $self->config('CORS_ORIGIN')) {
Expand All @@ -63,42 +49,62 @@ sub startup {

# Logging
if ($ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'production') {
my $logPath = "$ENV{RENDER_ROOT}/logs/error.log";
print "[LOGS] Running in production mode, logging to $logPath\n";
my $logPath = $self->home->child('logs', 'error.log');
say "[LOGS] Running in production mode, logging to $logPath";
$self->log(Mojo::Log->new(
path => $logPath,
level => ($ENV{MOJO_LOG_LEVEL} || 'warn')
));
}

if ($self->config('INTERACTION_LOG')) {
my $interactionLogPath = "$ENV{RENDER_ROOT}/logs/interactions.log";
print "[LOGS] Saving interactions to $interactionLogPath\n";
my $interactionLogPath = $self->home->child('logs', 'interactions.log');
say "[LOGS] Saving interactions to $interactionLogPath";
my $resultsLog = Mojo::Log->new(path => $interactionLogPath, level => 'info');
$resultsLog->format(sub {
my ($time, $level, @lines) = @_;
my $start = shift(@lines);
my $msg = join ", ", @lines;
return sprintf "%s, %s, %s\n", $start, $time - $start, $msg;
return sprintf "%s, %s, %s\n", $start, $time - $start, join(', ', @lines);
});
$self->helper(logAttempt => sub { shift; $resultsLog->info(@_); });
}

my $resourceUsageLog = Mojo::Log->new(path => $self->home->child('logs', 'resource_usage.log'));
$resourceUsageLog->format(sub {
my ($time, $level, @lines) = @_;
return '[' . time2str('%a %b %d %H:%M:%S %Y', time) . '] ' . join(', ', @lines) . "\n";
});
$self->helper(resourceUsageLog => sub { shift; return $resourceUsageLog->info(@_); });

# Models
$self->helper(newProblem => sub { shift; Renderer::Model::Problem->new(@_) });
$self->helper(newProblem => sub { my ($c, $args) = @_; Renderer::Model::Problem->new($c, $args) });

# Helpers
$self->helper(format => sub { WeBWorK::FormatRenderedProblem::formatRenderedProblem(@_) });
$self->helper(validateRequest => sub { Renderer::Controller::IO::validate(@_) });
$self->helper(parseRequest => sub { Renderer::Controller::Render::parseRequest(@_) });
$self->helper(croak => sub { Renderer::Controller::Render::croak(@_) });
$self->helper(logID => sub { shift->req->request_id });
$self->helper(exception => sub { Renderer(@_) });
$self->helper(
format => sub {
my ($c, $rh_result) = @_;
WeBWorK::FormatRenderedProblem::formatRenderedProblem($c, $rh_result);
}
);
$self->helper(validateRequest => sub { my ($c, $options) = @_; Renderer::Controller::IO::validate($c, $options) });
$self->helper(parseRequest => sub { my $c = shift; Renderer::Controller::Render::parseRequest($c) });
$self->helper(
croak => sub {
my ($c, $exception, $depth) = @_;
Renderer::Controller::Render::croak($c, $exception, $depth);
}
);
$self->helper(logID => sub { my $c = shift; $c->req->request_id });
$self->helper(
exception => sub {
my ($c, $message, $status, %data) = @_;
Renderer::Controller::Render::exception($c, $message, $status, %data);
}
);

# Routes
# baseURL sets the root at which the renderer is listening,
# and is used in Environment for pg_root_url
my $r = $self->routes->under($ENV{baseURL});
# baseURL is the root at which the renderer is listening.
my $r = $self->routes->under($self->config->{baseURL});

$r->any('/render-api')->to('render#problem');
$r->any('/render-ptx')->to('render#render_ptx');
Expand All @@ -108,10 +114,12 @@ sub startup {
supplementalRoutes($r) if ($self->mode eq 'development' || $self->config('FULL_APP_INSECURE'));

# Static file routes
$r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file');
$r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file');
$r->any('/pg_files/*static')->to('StaticFiles#pg_file');
$r->any('/*static')->to('StaticFiles#public_file');
$r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file')->name('capaFile');
$r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file')->name('pgTempFile');
$r->any('/pg_files/*static')->to('StaticFiles#pg_file')->name('pgFile');
$r->any('/*static')->to('StaticFiles#public_file')->name('publicFile');

return;
}

sub supplementalRoutes {
Expand Down Expand Up @@ -156,45 +164,40 @@ sub timeout {
}

sub sanitizeHostURLs {
$ENV{SITE_HOST} =~ s!/$!!;

# set an absolute base href for asset urls under iframe embedding
if ($ENV{baseURL} =~ m!^https?://!) {
my $self = shift;

# this should only be used by MITM sites when proxying renderer assets
my $baseURL = $ENV{baseURL} =~ m!/$! ? $ENV{baseURL} : "$ENV{baseURL}/";
$main::basehref = Mojo::URL->new($baseURL);
$self->config->{SITE_HOST} =~ s!/$!!;

# do NOT use the proxy address in our router!
$ENV{baseURL} = '';
} elsif ($ENV{baseURL} =~ m!\S!) {
# Set an absolute base href for asset urls under iframe embedding.
if ($self->config->{baseURL} =~ m!^https?://!) {
# This should only be used by MITM sites when proxying renderer assets.
my $baseURL = $self->config->{baseURL} =~ m!/$! ? $self->config->{baseURL} : $self->config->{baseURL} . '/';
$self->defaults->{baseHREF} = Mojo::URL->new($baseURL);

# ENV{baseURL} is used to build routes, so configure as "/extension"
$ENV{baseURL} = "/$ENV{baseURL}";
warn "*** [CONFIG] baseURL should not end in a slash\n"
if $ENV{baseURL} =~ s!/$!!;
warn "*** [CONFIG] baseURL should begin with a slash\n"
unless $ENV{baseURL} =~ s!^//!/!;
# Do NOT use the proxy address for the router!
$self->config->{baseURL} = '';
} elsif ($self->config->{baseURL} =~ m!\S!) {
# Ensure baseURL starts with a slash but doesn't end with a slash.
$self->config->{baseURL} = '/' . $self->config->{baseURL} unless $self->config->{baseURL} =~ m!^/!;
$self->config->{baseURL} =~ s!/$!!;

# base href must end in a slash when not hosting at the root
$main::basehref =
Mojo::URL->new($ENV{SITE_HOST})->path("$ENV{baseURL}/");
# base href must end in a slash when not hosting at the root.
$self->defaults->{baseHREF} = Mojo::URL->new($self->config->{SITE_HOST})->path($self->config->{baseURL} . '/');
} else {
# no proxy and service is hosted at the root of SITE_HOST
$main::basehref = Mojo::URL->new($ENV{SITE_HOST});
$self->defaults->{baseHREF} = Mojo::URL->new($self->config->{SITE_HOST});
}

if ($ENV{formURL} =~ m!\S!) {

if ($self->config->{formURL} =~ m!\S!) {
# this should only be used by MITM
$main::formURL = Mojo::URL->new($ENV{formURL});
$self->defaults->{formURL} = Mojo::URL->new($self->config->{formURL});
die '*** [CONFIG] if provided, formURL must be absolute'
unless $main::formURL->is_abs;
unless $self->defaults->{formURL}->is_abs;
} else {
# if using MITM proxy base href + renderer api not at SITE_HOST root
# provide form url as absolute SITE_HOST/extension/render-api
$main::formURL =
Mojo::URL->new($ENV{SITE_HOST})->path("$ENV{baseURL}/render-api");
$self->defaults->{formURL} =
Mojo::URL->new($self->config->{SITE_HOST})->path($self->config->{baseURL} . '/render-api');
}
}

Expand Down
15 changes: 7 additions & 8 deletions lib/Renderer/Controller/IO.pm
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package Renderer::Controller::IO;
use Mojo::Base -async_await;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Base 'Mojolicious::Controller', -async_await;

use File::Spec::Functions qw(splitdir);
use File::Find qw(find);
use MIME::Base64 qw(decode_base64);
Expand Down Expand Up @@ -37,7 +37,7 @@ sub raw {
return unless $validatedInput;

my $file_path = $validatedInput->{sourceFilePath};
my $problem = $c->newProblem({ log => $c->log, read_path => $file_path });
my $problem = $c->newProblem({ read_path => $file_path });
$problem->{action} = 'fetch source';
return $c->exception($problem->{_message}, $problem->{status})
unless $problem->success();
Expand All @@ -58,7 +58,6 @@ async sub writer {
return unless $validatedInput;

my $problem = $c->newProblem({
log => $c->log,
write_path => $validatedInput->{writeFilePath},
problem_contents => $validatedInput->{problemSource}
});
Expand Down Expand Up @@ -362,7 +361,7 @@ async sub findNewVersion {
my $avoidProblems = {};
$c->render_later;
for my $seed (@avoidSeeds) {
my $problem = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $seed });
my $problem = $c->newProblem({ read_path => $filePath, random_seed => $seed });
my $renderedProblem = await $problem->render({});
next unless ($problem->success());
$avoidProblems->{$seed} = decode_json($renderedProblem);
Expand All @@ -375,7 +374,7 @@ async sub findNewVersion {
$newSeed = 1 + int rand(999999);
} until (!exists($avoidProblems->{$newSeed}));

my $newProblemObj = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $newSeed });
my $newProblemObj = $c->newProblem({ read_path => $filePath, random_seed => $newSeed });
my $newProblemJson = await $newProblemObj->render({});
next unless ($newProblemObj->success());
$newProblem = decode_json($newProblemJson);
Expand Down Expand Up @@ -453,7 +452,7 @@ async sub findUniqueSeeds {
do {
$newSeed = 1 + int rand(999999);
} until (!exists($triedSeeds->{$newSeed}));
my $newProblemObj = $c->newProblem({ log => $c->log, read_path => $filePath, random_seed => $newSeed });
my $newProblemObj = $c->newProblem({ read_path => $filePath, random_seed => $newSeed });
my $newProblemJson = await $newProblemObj->render({});
next unless ($newProblemObj->success());
$newProblem = decode_json($newProblemJson);
Expand Down Expand Up @@ -519,7 +518,7 @@ async sub setTags {
# the same holds for keywords
$incomingTags->{keywords} = [ $incomingTags->{keywords} ] unless (ref($incomingTags->{keywords}) =~ /ARRAY/);

my $problem = $c->newProblem({ log => $c->log, read_path => $incomingTags->{file} });
my $problem = $c->newProblem({ read_path => $incomingTags->{file} });

# wrap the get/update/write tags in a promise
my $tags = WeBWorK::Utils::Tags->new($incomingTags->{file});
Expand Down
28 changes: 14 additions & 14 deletions lib/Renderer/Controller/Render.pm
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ sub parseRequest {
// '' =~ s!^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*$!$1!r;
$originIP ||= $c->tx->remote_address || 'unknown-origin';

if ($ENV{STRICT_JWT} && !(defined $params{problemJWT} || defined $params{sessionJWT})) {
return $c->exception('Not allowed to request problems with raw data.', 403);
if ($c->config->{STRICT_JWT} && !(defined $params{problemJWT} || defined $params{sessionJWT})) {
$c->exception('Not allowed to request problems with raw data.', 403);
return;
}

# protect against DOM manipulation
Expand All @@ -41,8 +42,8 @@ sub parseRequest {
eval {
$claims = decode_jwt(
token => $sessionJWT,
key => $ENV{webworkJWTsecret},
verify_iss => $ENV{SITE_HOST},
key => $c->config->{webworkJWTsecret},
verify_iss => $c->config->{SITE_HOST},
);
1;
} or do {
Expand All @@ -65,8 +66,8 @@ sub parseRequest {
eval {
$claims = decode_jwt(
token => $problemJWT,
key => $ENV{problemJWTsecret},
verify_aud => $ENV{SITE_HOST},
key => $c->config->{problemJWTsecret},
verify_aud => $c->config->{SITE_HOST},
);
1;
} or do {
Expand All @@ -78,12 +79,12 @@ sub parseRequest {
@params{ keys %$claims } = values %$claims;
} elsif ($params{outputFormat} ne 'ptx') {
# if no JWT is provided, create one (unless this is a pretext request)
$params{aud} = $ENV{SITE_HOST};
$params{aud} = $c->config->{SITE_HOST};
$params{isInstructor} //= 0;
$params{sessionID} ||= time;
my $req_jwt = encode_jwt(
payload => \%params,
key => $ENV{problemJWTsecret},
key => $c->config->{problemJWTsecret},
alg => 'PBES2-HS512+A256KW',
enc => 'A256GCM',
auto_iat => 1
Expand Down Expand Up @@ -156,7 +157,6 @@ async sub problem {
}

my $problem = $c->newProblem({
log => $c->log,
read_path => $file_path,
random_seed => $random_seed,
problem_contents => $problem_contents
Expand Down Expand Up @@ -232,7 +232,7 @@ async sub sendAnswerJWT {
message => 'initial message'
};
my $header = {
Origin => $ENV{SITE_HOST},
Origin => $c->config->{SITE_HOST},
'Content-Type' => 'text/plain',
};

Expand Down Expand Up @@ -304,10 +304,10 @@ sub jweFromRequest {
my $c = shift;
my $inputs_ref = $c->parseRequest;
return unless $inputs_ref;
$inputs_ref->{aud} = $ENV{SITE_HOST};
$inputs_ref->{aud} = $c->config->{SITE_HOST};
my $req_jwt = encode_jwt(
payload => $inputs_ref,
key => $ENV{problemJWTsecret},
key => $c->config->{problemJWTsecret},
alg => 'PBES2-HS512+A256KW',
enc => 'A256GCM',
auto_iat => 1
Expand All @@ -319,10 +319,10 @@ sub jwtFromRequest {
my $c = shift;
my $inputs_ref = $c->parseRequest;
return unless $inputs_ref;
$inputs_ref->{aud} = $ENV{SITE_HOST};
$inputs_ref->{aud} = $c->config->{SITE_HOST};
my $req_jwt = encode_jwt(
payload => $inputs_ref,
key => $ENV{problemJWTsecret},
key => $c->config->{problemJWTsecret},
alg => 'HS256',
auto_iat => 1
);
Expand Down
Loading