lufi/lib/Lufi/Controller/Files.pm

521 lines
21 KiB
Perl

# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
package Lufi::Controller::Files;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw(encode_json decode_json to_json true false);
use Mojo::Util qw(encode decode);
use Mojo::File;
use Lufi::DB::File;
use Lufi::DB::Slice;
use File::Spec::Functions;
use Number::Bytes::Human qw(format_bytes);
use Filesys::DfPortable;
use Crypt::SaltedHash;
sub files {
my $c = shift;
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
$c->render(template => 'files');
} else {
$c->redirect_to('login');
}
}
sub upload {
my $c = shift;
my $invitation;
my $token = $c->session->{guest_token};
$invitation = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated || $invitation) {
$c->inactivity_timeout(30000000);
$c->app->log->debug('Client connected');
$c->on(
message => sub {
my ($ws, $text) = @_;
my $invit = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;
my $begin = time;
my ($json) = split('XXMOJOXX', $text, 2);
$json = encode 'UTF-8', $json;
$text =~ s/^.*?XXMOJOXX/${json}XXMOJOXX/;
$json = decode_json $json;
$c->app->log->debug('Got message');
if (defined($json->{cancel}) && $json->{cancel}) {
my $f = Lufi::DB::File->new(app => $c->app)->from_short($json->{id});
if ($f && $f->mod_token && $f->mod_token eq $json->{mod_token}) {
$f = $f->delete();
return $ws->send(to_json(
{
action => 'cancel',
success => $f->deleted ? true : false,
msg => $f->deleted ? 'Lufi::DB::File->delete() was successfull' : 'Lufi::DB::File->delete() failed',
i => $json->{i}
}
));
} else {
return $ws->send(to_json(
{
action => 'cancel',
success => false,
msg => 'Lufi::DB::File not found or invalid mod_token',
i => $json->{i}
}
));
}
}
my $stop = 0;
# Check if stop_upload file is present
if ($c->stop_upload) {
$stop = 1;
return $ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Sorry, uploading is disabled.'),
sent_delay => $json->{delay},
i => $json->{i}
}
)));
}
# Check against max_size
if (defined $c->config('max_file_size')) {
if ($json->{size} > $c->config('max_file_size')) {
$stop = 1;
return $ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Your file is too big: %1 (maximum size allowed: %2)', format_bytes($json->{size}), format_bytes($c->config('max_file_size'))),
sent_delay => $json->{delay},
i => $json->{i}
}
)));
}
}
# Check that we have enough space (multiplying by 2 since it's encrypted, it takes more place that the original file)
# Only check if using filesystem, not Swift storage
if (!defined($c->config('swift')) && $json->{part} == 0 && ($json->{size} * 2) >= dfportable($c->config('upload_dir'))->{bavail}) {
$stop = 1;
return $ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('No enough space available on the server for this file (size: %1).', format_bytes($json->{size})),
sent_delay => $json->{delay},
i => $json->{i}
}
)));
}
# Check that the invitation is still valid, but only if it's the first chunk
# (i.e. a new file, we don't want to stop a current uploading)
if ($json->{part} == 0 && $invit && !$invit->is_valid()) {
$stop = 1;
$c->app->log->info(sprintf('Someone (%s) tried to use an expired or deleted invitation.', $invit->guest_mail));
$ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Sorry, your invitation has expired or has been deleted. Please contact %1 to have another invitation.', $invit->ldap_user_mail),
}
)));
}
unless ($stop) {
my $f;
if (defined($json->{id})) {
$f = Lufi::DB::File->new(app => $c->app)->from_short($json->{id});
} else {
my $delay;
unless (defined $json->{delay}) {
$json->{delay} = $c->max_delay;
}
if (defined $c->config('delay_for_size')) {
# Choose delay according to config
my $delays = $c->config('delay_for_size');
my @keys = sort {$b <=> $a} keys %{$delays};
for my $key (@keys) {
if ($json->{size} >= $key) {
$delay = ($json->{delay} < $delays->{$key}) ? $json->{delay} : $delays->{$key};
last;
}
}
}
# If the file size is lower than the lowest configured size or if there is no delay_for_size setting, we choose the configured max delay
unless (defined $delay) {
$delay = (($json->{delay} > 0 && $json->{delay} <= $c->max_delay) || $c->max_delay == 0) ? $json->{delay} : $c->max_delay;
}
# If we have a password
my $salted_pwd;
if ($c->config('allow_pwd_on_files') && defined($json->{file_pwd}) && $json->{file_pwd} ne '') {
my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-256', salt_len => 8);
$csh->add($json->{file_pwd});
$salted_pwd = $csh->generate();
}
my $creator = $c->ip;
# Authenticated user logging
if ((defined($c->config('ldap')) || defined($c->config('htpasswd'))) && !$invitation) {
$creator = sprintf('User: %s, IP: %s', $c->current_user->{username}, $creator);
}
# Guest user logging
if ($invitation) {
$creator = sprintf('User: %s, IP: %s', $invitation->guest_mail, $creator);
}
my $delete_at_first_view = ($json->{del_at_first_view}) ? 1 : 0;
$delete_at_first_view = 1 if $c->app->config('force_burn_after_reading');
$f = Lufi::DB::File->new(app => $c->app)->get_empty()
->created_by($creator)
->delete_at_first_view($delete_at_first_view)
->delete_at_day($delay)
->mediatype($json->{type})
->filename($json->{name})
->filesize($json->{size})
->nbslices($json->{total})
->mod_token($c->shortener($c->config('token_length')))
->passwd($salted_pwd)
->zipped($json->{zipped})
->write;
}
# This check is just in case we didn't succeed to find a corresponding record
# It normally can't happen
if (defined $f) {
# If we already have a part, it's a resend because the websocket has been broken
# In this case, we don't need to rewrite the file
unless ($f->slices->grep(sub { $_->j == $json->{part} })->size) {
# Create slice file
my $s = Lufi::DB::Slice->new(
app => $c->app,
short => $f->short,
j => $json->{part}
)->store($text);
push @{$f->slices}, $s;
$s->write;
if (($json->{part} + 1) == $json->{total}) {
$f->complete(1);
$f->created_at(time);
$f->write;
}
}
my $result = {
success => true,
i => $json->{i},
j => $json->{part},
parts => $json->{total},
short => $f->short,
name => $f->filename,
size => $f->filesize,
del_at_first_view => (($f->delete_at_first_view) ? true : false),
created_at => $f->created_at,
delay => $f->delete_at_day,
token => $f->mod_token,
sent_delay => $json->{delay},
duration => time - $begin
};
$ws->send(to_json($result));
} else {
$ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('The server was unable to find the file record to add your file part to. Please, contact the administrator.'),
sent_delay => $json->{delay},
i => $json->{i}
}
)));
}
}
}
);
$c->on(
finish => sub {
$c->app->log->debug('Client disconnected');
}
);
} else {
$c->on(
message => sub {
$c->app->log->info(sprintf('Someone unauthenticated tried to upload a file. IP: %s', $c->ip));
$c->finish;
}
);
}
}
sub download {
my $c = shift;
my $short = $c->param('short');
$c->inactivity_timeout(300000);
$c->app->log->debug('Client connected');
my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);
# Do we have a file?
if (defined $ldfile) {
# Is the file fully uploaded?
if ($ldfile->deleted
|| (
$ldfile->delete_at_day != 0
&& (
($ldfile->created_at + $ldfile->delete_at_day * 86400) < time()
)
)
) {
unless ($ldfile->deleted) {
$ldfile->delete;
}
$c->on(
message => sub {
my ($ws, $json) = @_;
$c->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Error: the file existed but was deleted.')
}
)));
}
);
} elsif (defined($ldfile->abuse)) {
my $abuse_msg = $c->l('This file has been deactivated by the admins. Contact them to know why.');
$abuse_msg = $c->app->config('abuse')->{$ldfile->abuse} if ($c->app->config('abuse') && $c->app->config('abuse')->{$ldfile->abuse});
$c->on(
message => sub {
my ($ws, $json) = @_;
$c->send(decode('UTF-8', encode_json(
{
success => false,
msg => $abuse_msg
}
)));
}
);
} elsif ($ldfile->complete) {
my $f = $ldfile;
$c->on(
message => sub {
my ($ws, $json) = @_;
$json = decode_json $json;
# Do we need a password?
my $valid = 1;
if ($c->config('allow_pwd_on_files') && defined($f->{passwd})) {
my $pwd = $json->{file_pwd};
$valid = Crypt::SaltedHash->validate($f->{passwd}, $json->{file_pwd}, 8);
}
if ($valid) {
if (defined($json->{part})) {
# Make $num an integer instead of a string
my $num = $json->{part} + 0;
# Get the slice
my $e = $f->slices->[$num];
my $text = $e->retrieve();
my ($json2) = split('XXMOJOXX', $text, 2);
$json2 = decode 'UTF-8', $json2;
$text =~ s/^.*?XXMOJOXX/${json2}XXMOJOXX/;
# Send the slice
$c->send($text);
} elsif (defined($json->{ended}) && $json->{ended}) {
$f->counter($f->counter + 1);
$f->last_access_at(time);
if ($f->delete_at_first_view) {
$f->delete;
} else {
$f->write;
}
}
} else {
$c->send(decode('UTF-8', encode_json(
{
msg => $c->l('Your password is not valid. Please refresh the page to retry.')
}
)));
}
}
);
$c->on(
finish => sub {
$c->app->log->debug('Client disconnected');
}
);
} else {
$c->on(
message => sub {
my ($ws, $json) = @_;
$c->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Error: the file has not been sent entirely.')
}
)));
}
);
}
} else {
$c->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Error: unable to find the file. Are you sure of your URL?')
}
)));
}
}
sub r {
my $c = shift;
my $short = $c->param('short');
my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);
if (defined $ldfile) {
return $c->render(
template => 'render',
f => $ldfile,
file_pwd => ($c->config('allow_pwd_on_files') && defined($ldfile->passwd))
);
} else {
return $c->render(
template => 'render',
msg => $c->l('Could not find the file. Are you sure of the URL?')
);
}
}
sub get_counter {
my $c = shift;
my $short = $c->param('short');
my $token = $c->param('token');
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);
if (defined $ldfile) {
if ($ldfile->mod_token eq $token) {
return $c->render(
json => {
success => true,
short => $short,
counter => $ldfile->counter,
deleted => ($ldfile->deleted) ? true : false
}
);
} else {
return $c->render(
json => {
success => false,
missing => false,
short => $short,
msg => $c->l('Unable to get counter for %1. The token is invalid.', $short)
}
);
}
} else {
return $c->render(
json => {
success => false,
missing => true,
short => $short,
msg => $c->l('Unable to get counter for %1. The file does not exists. It will be removed from your localStorage.', $short)
}
);
}
} else {
return $c->render(
json => {
success => false,
missing => false,
short => $short,
msg => $c->l('Unable to get counter for %1. You are not authenticated.', $short)
}
);
}
}
sub delete {
my $c = shift;
my $short = $c->param('short');
my $token = $c->param('token');
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
my $ldfile = Lufi::DB::File->new(app => $c->app)->from_short($short);
$ldfile = undef unless (defined($ldfile) && $ldfile->mod_token eq $token);
if (defined $ldfile) {
my $msg;
if ($ldfile->deleted) {
$msg = $c->l('The file has already been deleted');
} else {
$ldfile->delete;
$msg = $c->l('File deleted');
}
return $c->respond_to(
json => {
json => {
success => true,
msg => $msg
}
},
any => sub {
$c->render(
template => 'msg',
f => $ldfile,
msg => $msg
);
}
);
} else {
my $msg = $c->l('Could not find the file. Are you sure of the URL and the token?');
return $c->respond_to(
json => {
json => {
success => false,
msg => $msg
}
},
any => sub {
$c->render(
template => 'msg',
f => undef,
msg => $msg
);
}
);
}
} else {
my $msg = $c->l('Could not delete the file. You are not authenticated.');
return $c->respond_to(
json => {
json => {
success => false,
msg => $msg
}
},
any => sub {
$c->render(
template => 'msg',
f => undef,
msg => $msg
);
}
);
}
}
1;