Skip to content
Snippets Groups Projects
Verified Commit d982d2c3 authored by Andrej Ramašeuski's avatar Andrej Ramašeuski
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #673 passed
Showing
with 677 additions and 0 deletions
_work
docker-compose.yaml
Dockerfile
_work
docker-compose.yaml
image: docker:19.03.1
variables:
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
services:
- docker:19.03.1-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
FROM debian:buster-slim
RUN apt-get update && apt-get install -y \
cpanminus \
wget \
make \
libxml2-dev \
libcrypt-openssl-rsa-perl \
libcrypt-openssl-x509-perl \
libdata-guid-perl \
libdbd-pg-perl \
libdbd-sqlite3-perl \
libdbi-perl \
libio-socket-ssl-perl \
libmodule-build-perl \
libnet-oauth2-perl \
libnet-ssleay-perl \
libyaml-libyaml-perl
RUN cpanm \
Data::Random \
DBIx::Class \
Mojolicious \
Mojo::Pg \
Mojo::JWT \
Mojolicious::Plugin::Authentication \
Mojolicious::Plugin::Authorization
ADD . /opt/PiTube
USER nobody
EXPOSE 3000
WORKDIR /opt/PiTube
CMD /opt/PiTube/script/pitube daemon
package PiTube;
use Mojo::Base 'Mojolicious';
use Mojo::Home;
use Mojo::Pg;
use Net::OAuth2::Profile::WebServer;
use Mojolicious::Plugin::Authentication;
use Mojolicious::Plugin::Authorization;
use PiTube::Schema;
sub startup {
my $self = shift;
$self->plugin('PiTube::Helpers::OAuth2');
$self->secrets([$ENV{SECRET}]);
my $home = Mojo::Home->new()->detect;
my $pg = Mojo::Pg->new
->dsn($ENV{DB_DSN})
->username($ENV{DB_USERNAME})
->password($ENV{DB_PASSWORD})
;
$pg->migrations->from_file("$home/sql/migrations.sql");
$pg->migrations->migrate();
# Spojeni s databazi
my $schema = PiTube::Schema->connect( {
dsn => $ENV{DB_DSN},
user => $ENV{DB_USERNAME},
password => $ENV{DB_PASSWORD},
AutoCommit => 1,
quote_char => '"',
name_sep => '.',
pg_enable_utf8 => 1,
on_connect_do => [
"set timezone to 'Europe/Prague'",
],
});
$self->helper( schema => sub { return $schema; } );
$self->plugin('authentication', {
autoload_user => 1,
load_user => sub {
my $c = shift;
return $c->schema->resultset('User')->find({
uuid => $c->session->{oauth}{sub}
});
},
validate_user => sub {
my $c = shift;
return undef if ! $c->session->{oauth};
return $c->session->{oauth}{sub};
},
});
$self->plugin('Authorization' => {
is_role => sub {
my ($c, $role, $extradata) = @_;
return 0 if ! $c->session->{oauth};
my $client = 'pitube';
if ( $role =~ s/\@(.+)$// ) {
$client = $1;
}
return 0 if ! $c->session->{oauth}{resource_access}{$client};
my %client_roles = map { $_ => 1 }
@{ $c->session->{oauth}{resource_access}{$client}{roles} };
return exists $client_roles{$role} ? 1 : 0;
},
user_privs => sub {},
has_priv => sub {},
user_role => sub {},
fail_render => { status => 401, json => {} },
});
my $r = $self->routes;
$r->get('/')->to(cb => sub { shift->render('index'); });
$r->get('/streams')->to('Stream#list');
$r->get('/oauth2')->to('OAuth2#callback');
$r->get('/logout')->to('OAuth2#do_logout');
$r->post('/callback')->to('NginxRTMP#callback');
$r->get('/play/:key')->to('Stream#player');
$r->get('/hls/*')->to('Stream#hls');
}
1;
package PiTube::Controller::NginxRTMP;
use Mojo::Base 'Mojolicious::Controller';
sub callback {
my $c = shift;
if ( $c->param('call') =~ /publish/ ) {
# stream
my $stream = $c->schema->resultset('Stream')->find({
key => $c->param('name')
});
$c->render( status => 404, text => '' ), return if ! $stream;
# kontrola tokenu
my $user = $c->schema->resultset('User')->find({
token => $c->param('token')
});
$c->render( status => 403, text => ''), return if ! $user;
# aktualizace stavu streamu
$stream->update({
publish_last => '\now()',
publish_user_id => $user->id,
publish_time => ( $c->param('time') // 0 ),
});
}
$c->render( status => 204, text => '' );
}
1;
package PiTube::Controller::OAuth2;
use Mojo::Base 'Mojolicious::Controller';
use YAML;
sub callback {
my $c = shift;
my $token = $c->oauth2->get_access_token($c->param("code"));
# TODO: ERROR HANDLING
$c->session->{refresh_token} = $token->refresh_token;
my $claims = $c->oauth_claims( $token->access_token );
my $user = $c->schema->resultset('User')->update_or_create({
uuid => $claims->{sub},
name => $claims->{name},
username => $claims->{preferred_username},
},
{ key => 'uuid', }
);
$user->set_token;
$c->session->{oauth} = $claims;
$c->authenticate();
$c->redirect_to('/');
}
sub do_logout { # nesmi se jmenovat logout - rekurze
my $c = shift;
$c->logout;
delete $c->session->{oauth};
$c->redirect_to('/');
}
1;
package PiTube::Controller::Stream;
use Mojo::Base 'Mojolicious::Controller';
use constant CONTENT_TYPE => {
m3u8 => 'application/vnd.apple.mpegurl',
ts => 'video/mp2t',
};
sub list {
my $c = shift;
my $cond = {
is_active => 't'
};
my $streams = $c->schema->resultset('Stream_view')->search(
$cond,
{ order_by => 'name' }
);
$c->stash->{streams} = $streams;
}
sub player {
my $c = shift;
# stream
my $stream = $c->schema->resultset('Stream')->find({
key => $c->stash->{key}
});
if ( ! $stream ) {
$c->render('stream/404');
return;
}
$c->stash->{stream} = $stream;
if ( ! $stream->is_granted($c) ) {
$c->render('stream/403');
return;
}
$c->render();
}
sub hls {
my $c = shift;
if ( $c->req->url !~ /^\/hls\/([a-z0-9\-]+)(_\w+)?(\/\w+)?\.(m3u8|ts)$/i ) {
$c->render( status => 404, text => '');
return;
}
my ($type, $file) = ($4, $c->req->url);
if ( ! -f $file ) {
$c->render( status => 404, text => '' );
return;
}
if ( $type eq 'ts' ) { # manifesty necheckujeme
my $stream = $c->schema->resultset('Stream')->find({ key => $1 });
$c->render( status => 404, text => '' ), return if ! $stream;
if ( ! $stream->is_granted($c) ) {
$c->render( status => 403, text => '');
return;
}
}
$c->res->headers->content_type(CONTENT_TYPE->{$type});
$c->res->headers->cache_control('max-age=0, no-cache');
$c->res->headers->access_control_allow_origin('*');
$c->reply->file($file);
}
1;
package PiTube::Helpers::OAuth2;
use strict;
use warnings;
use constant KEY_FORMAT => "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----";
use base 'Mojolicious::Plugin';
use Mojo::JWT;
use Mojo::UserAgent;
sub register {
my ($class, $self) = @_;
my ($jwt, $discovered);
my $ua = Mojo::UserAgent->new();
# get public key
my $tx = $ua->get( $ENV{OAUTH_REALM_URL} );
my $res = $tx->result;
if ($res->is_success) {
$jwt = Mojo::JWT->new(
public => sprintf( KEY_FORMAT, $res->json->{public_key})
);
}
# get urls
$tx = $ua->get( $ENV{OAUTH_REALM_URL} . '/.well-known/openid-configuration');
$res = $tx->result;
if ($res->is_success) {
$discovered = $res->json;
}
my $oauth2 = Net::OAuth2::Profile::WebServer->new(
name => 'SSO',
scope => 'profile',
client_id => $ENV{OAUTH_CLIENT_ID},
client_secret => $ENV{OAUTH_CLIENT_SECRET},
authorize_url => $discovered->{authorization_endpoint},
access_token_url => $discovered->{token_endpoint},
redirect_uri => $ENV{BASE_URL} . '/oauth2',
);
$self->helper( oauth2 => sub { return $oauth2; } );
$self->helper( oauth_claims => sub {
my $c = shift;
my $token = shift // return undef;
return undef if ! $jwt;
my $claims;
eval { $claims = $jwt->decode( $token ); };
if ( $@ ) {
$c->app->log->warn( $@ );
return undef;
}
if ( Mojo::JWT->now() > $claims->{exp} ) {
$c->app->log->warn( 'Token expire' );
return undef;
}
return $claims;
});
}
1;
__END__
package PiTube::Schema;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
our $VERSION = 1;
__PACKAGE__->load_namespaces;
1;
package PiTube::Schema::Result::ACL;
use strict;
use warnings;
use base 'DBIx::Class::Core';
our $VERSION = 1;
__PACKAGE__->table('acl');
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
sequence => 'uid_seq'
},
qw(
stream_id
class
value
),
);
__PACKAGE__->set_primary_key('id');
1;
package PiTube::Schema::Result::Stream;
use strict;
use warnings;
use base 'DBIx::Class::Core';
our $VERSION = 1;
__PACKAGE__->table('streams');
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
sequence => 'uid_seq'
},
qw(
is_active
is_public
key
name
publish_last
publish_user_id
publish_time
),
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->add_unique_constraint(
'key' => [qw(key)]
);
__PACKAGE__->has_many(
acls => 'PiTube::Schema::Result::ACL',
{
'foreign.stream_id' => 'self.id',
},
);
sub is_granted {
my $self = shift;
my $c = shift;
return 1 if $self->is_public;
my $granted = 0;
ACL:
foreach my $acl ( $self->acls ) {
if ( $acl->class eq 'all' ) {
$granted = 1;
}
elsif ( $acl->class eq 'role') {
$granted = 1 if $c->is( $acl->value );
}
elsif ( $acl->class eq 'user' ) {
$granted = 1 if $acl->value eq $c->current_user->uuid;
}
last ACL if $granted;
}
return $granted;
}
1;
package PiTube::Schema::Result::Stream_view;
use strict;
use warnings;
use base 'PiTube::Schema::Result::Stream';
our $VERSION = 1;
__PACKAGE__->table('streams_view');
__PACKAGE__->add_columns(qw(
publish_last_age
publish_user_name
));
sub is_live {
my $self = shift;
my $max_age = shift // 60;
return 0 if ! $self->publish_last_age;
return $self->publish_last_age < $max_age;
}
1;
package PiTube::Schema::Result::User;
use strict;
use warnings;
use base 'DBIx::Class::Core';
use Data::Random qw(rand_chars);
our $VERSION = 1;
__PACKAGE__->table('users');
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
sequence => 'uid_seq'
},
qw(
uuid
username
name
token
),
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->add_unique_constraint(
'uuid' => [qw(uuid)]
);
__PACKAGE__->add_unique_constraint(
'token' => [qw(token)]
);
sub set_token {
my $self = shift;
my $new = shift;
return if $self->token and not $new;
my $token = rand_chars( set => 'alphanumeric', size => 32 );
$self->update({
token => $token
});
}
1;
daemon off;
error_log /dev/stdout info;
events {
worker_connections 1024;
}
rtmp {
server {
listen ${RTMP_PORT};
chunk_size 4000;
application stream {
live on;
on_publish http://pitube:3000/callback;
on_publish_done http://pitube:3000/callback;
on_update http://pitube:3000/callback;
notify_update_timeout 15s;
# -c:a libfdk_aac -b:a 128k -c:v libx264 -b:v 1000k -f flv -g 30 -r 30 -s 854x480 -preset superfast -profile:v baseline
# rtmp://localhost:1935/hls/$name_480p1128kbs
# -c:a libfdk_aac -b:a 128k -c:v libx264 -b:v 750k -f flv -g 30 -r 30 -s 640x360 -preset superfast -profile:v baseline
# rtmp://localhost:1935/hls/$name_360p878kbs
# -c:a libfdk_aac -b:a 128k -c:v libx264 -b:v 400k -f flv -g 30 -r 30 -s 426x240 -preset superfast -profile:v baseline
# rtmp://localhost:1935/hls/$name_240p528kbs
exec ffmpeg -i rtmp://localhost:1935/stream/$name
-c:a libfdk_aac -b:a 128k -c:v libx264 -b:v 2500k -f flv -g 30 -r 30 -s 1280x720 -preset superfast -profile:v baseline
rtmp://localhost:1935/hls/$name_720p2628kbs
-c:a libfdk_aac -b:a 64k -c:v libx264 -b:v 200k -f flv -g 15 -r 15 -s 426x240 -preset superfast -profile:v baseline
rtmp://localhost:1935/hls/$name_240p264kbs;
}
application hls {
live on;
hls on;
hls_fragment_naming system;
hls_fragment 5;
hls_playlist_length 10;
hls_path /tmp/hls;
hls_nested on;
hls_variant _720p2628kbs BANDWIDTH=2628000,RESOLUTION=1280x720;
# hls_variant _480p1128kbs BANDWIDTH=1128000,RESOLUTION=854x480;
# hls_variant _360p878kbs BANDWIDTH=878000,RESOLUTION=640x360;
# hls_variant _240p528kbs BANDWIDTH=528000,RESOLUTION=426x240;
hls_variant _240p264kbs BANDWIDTH=264000,RESOLUTION=426x240;
}
}
}
http {
access_log /dev/stdout combined;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
server {
listen ${HTTP_PORT};
location / {
proxy_pass http://pitube:3000/;
add_header Access-Control-Allow-Origin *;
}
location /hls {
alias /tmp/hls;
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
}
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet static/stat.xsl;
}
location /static {
alias /www/static;
}
location = /crossdomain.xml {
root /www/static;
default_type text/xml;
expires 24h;
}
}
}
This diff is collapsed.
public/img/poster.jpg

722 KiB

<!DOCTYPE html>
<html>
<head>
<title>Welcome to the Mojolicious real-time web framework!</title>
</head>
<body>
<h2>Welcome to the Mojolicious real-time web framework!</h2>
This is the static document "public/index.html",
<a href="/">click here</a> to get back to the start.
</body>
</html>
Source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment