my @tcpsockets; foreach my $service (@{$config->{external_network}->{service}}) { print '** Service at port ', $service->{port}, ' does ', $service->{usessl} ? '' : 'NOT', " use SSL/TLS\n"; foreach my $ip (@{$service->{bind_adresses}->{ip}}) { my $tcp = IO::Socket::IP->new( LocalHost => $ip, LocalPort => $service->{port}, Listen => 1, ReuseAddr => 1, Proto => 'tcp', ) or croak("Failed to bind: " . $ERRNO); #binmode($tcp, ':bytes'); push @tcpsockets, $tcp; print " Listening on ", $ip, ":, ", $service->{port}, "/tcp\n"; } } my $select = IO::Select->new(@tcpsockets); $self->{select} = $select; #### sub run($self) { while(1) { while((my @connections = $self->{select}->can_read)) { foreach my $connection (@connections) { my $client = $connection->accept; #print "**** Connection from ", $client->peerhost(), " \n"; if(defined($self->{debugip})) { my $peerhost = $client->peerhost(); if($peerhost ne $self->{debugip}) { $client->close; next; } } if($childcount >= $self->{config}->{max_childs}) { #print "Too many children already!\n"; $client->close; next; } my $childpid = fork(); if(!defined($childpid)) { #print "FORK FAILED!\n"; $client->close; next; } elsif($childpid == 0) { # Child $PROGRAM_NAME = $self->{ps_appname}; $self->handleClient($client); #print "Child PID $PID is done, exiting...\n"; $self->endprogram(); } else { # Parent $childcount++; next; } } } } print "run() loop finished.\n"; return; } #### ... if($usessl) { my $defaultdomain = $self->{config}->{sslconfig}->{ssldefaultdomain}; my $encrypted; my $ok = 0; eval { $encrypted = IO::Socket::SSL->start_SSL($client, SSL_server => 1, SSL_key_file=> $self->{config}->{sslconfig}->{ssldomains}->{$defaultdomain}->{sslkey}, SSL_cert_file=> $self->{config}->{sslconfig}->{ssldomains}->{$defaultdomain}->{sslcert}, SSL_cipher_list => $self->{config}->{sslconfig}->{sslciphers}, SSL_create_ctx_callback => sub { my $ctx = shift; #print STDERR "******************* CREATING NEW CONTEXT ********************\n"; # Enable workarounds for broken clients Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_ALL); # Disable session resumption completely Net::SSLeay::CTX_set_session_cache_mode($ctx, $SSL_SESS_CACHE_OFF); # Disable session tickets Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_NO_TICKET); # Load certificate chain my $defaultdomain = $self->{config}->{sslconfig}->{ssldefaultdomain}; Net::SSLeay::CTX_use_certificate_chain_file($ctx, $self->{config}->{sslconfig}->{ssldomains}->{$defaultdomain}->{sslcert}); # Check requested server name Net::SSLeay::CTX_set_tlsext_servername_callback($ctx, sub { my $ssl = shift; my $h = Net::SSLeay::get_servername($ssl); if(!defined($h)) { #print STDERR "SSL: No Hostname given during SSL setup\n"; return; } if(!defined($self->{config}->{sslconfig}->{ssldomains}->{$h})) { #print STDERR "SSL: Hostname $h not configured\n"; #print STDERR Dumper($self->{config}->{sslconfig}->{ssldomains}); return; } if(defined($self->{config}->{sslconfig}->{ssldomains}->{$h}->{internal_socket})) { # This SSL connection uses a different backend $selectedbackend = $self->{config}->{sslconfig}->{ssldomains}->{$h}->{internal_socket}; } if($h eq $self->{config}->{sslconfig}->{ssldefaultdomain}) { # Already the correct CTX setting, just return return; } #print STDERR "§§§§§§§§§§§§§§§§§§§§§§§ Requested Hostname: $h §§§\n"; my $newctx; if(defined($self->{config}->{sslconfig}->{ssldomains}->{$h}->{ctx})) { $newctx = $self->{config}->{sslconfig}->{ssldomains}->{$h}->{ctx}; } else { $newctx = Net::SSLeay::CTX_new or croak("Can't create new SSL CTX"); Net::SSLeay::CTX_set_cipher_list($newctx, $self->{config}->{sslconfig}->{sslciphers}); Net::SSLeay::set_cert_and_key($newctx, $self->{config}->{sslconfig}->{ssldomains}->{$h}->{sslcert}, $self->{config}->{sslconfig}->{ssldomains}->{$h}->{sslkey}) or croak("Can't set cert and key file"); Net::SSLeay::CTX_use_certificate_chain_file($newctx, $self->{config}->{sslconfig}->{ssldomains}->{$h}->{sslcert}); #print STDERR "Cert: ", $self->{config}->{sslconfig}->{ssldomains}->{$h}->{sslcert}, " Key: ", $self->{config}->{sslconfig}->{ssldomains}->{$h}->{sslkey}, "\n"; $self->{config}->{sslconfig}->{ssldomains}->{$h}->{ctx} = $newctx; } Net::SSLeay::set_SSL_CTX($ssl, $newctx); }); # Prepared/tested for future ALPN needs (e.g. HTTP/2) ## Advertise supported HTTP versions #Net::SSLeay::CTX_set_alpn_select_cb($ctx, ['http/1.1', 'http/2.0']); }, ); $ok = 1; }; if(!$ok) { print "EVAL ERROR: ", $EVAL_ERROR, "\n"; $self->endprogram(); } elsif(!$ok || !defined($encrypted) || !$encrypted) { print "startSSL failed: ", $SSL_ERROR, "\n"; $self->endprogram(); } } ... #### sub socketstart($self, $ua) { my $sysh = $self->{server}->{modules}->{$self->{systemsettings}}; my $upgrade = $ua->{headers}->{"Upgrade"}; my $seckey = $ua->{headers}->{"Sec-WebSocket-Key"}; my $protocol = $ua->{headers}->{"Sec-WebSocket-Protocol"}; my $version = $ua->{headers}->{"Sec-WebSocket-Version"}; if(!defined($upgrade) || !defined($seckey) || !defined($version)) { return (status => 400); # BAAAD Request! Sit! Stay! } ... $seckey .= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # RFC6455 GUID for Websockets $seckey = encode_base64(sha1($seckey), ''); my $proto = 'base64'; if($settings{binaryMode}) { $proto = 'binary'; } ... my %result = (status => 101, Upgrade => "websocket", Connection => "Upgrade", "Sec-WebSocket-Accept" => $seckey, "Sec-WebSocket-Protocol" => $proto, ); return %result; } #### sub sockethandler($self, $ua) { ... { local $INPUT_RECORD_SEPARATOR = undef; my $socketclosed = 0; $ua->{realsocket}->blocking(0); binmode($ua->{realsocket}, ':bytes'); my $starttime = time + 10; while(!$socketclosed) { my $workCount = 0; # Read data from websocket my $buf; eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval) local $SIG{ALRM} = sub{croak "alarm"}; alarm 0.5; my $status = sysread($ua->{realsocket}, $buf, $settings{chunk_size} * 2); if(!$ua->{realsocket}) { #if(0 && defined($status) && $status == 0) { if($self->{isDebugging}) { print STDERR "Websocket closed\n"; } $socketclosed = 1; last; } alarm 0; }; if(defined($buf) && length($buf)) { $frame->append($buf); $workCount++; } while (my $message = $frame->next_bytes) { $workCount++; my $realmsg; my $parseok = 0; eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval) $realmsg = decode_json($message); $parseok = 1; }; if(!$parseok || !defined($realmsg) || !defined($realmsg->{type})) { # Broken message next; } if($frame->opcode == 8) { print STDERR "Connection closed by Browser\n"; $socketclosed = 1; last; } if($realmsg->{type} eq 'PING') { $timeout = time + $settings{client_disconnect_timeout}; my %msg = ( type => 'PING', ); if(!$self->wsprint(\%msg)) { print STDERR "Write to socket failed, closing connection!\n"; $socketclosed = 1; last; } next; } else { if(!$self->wshandlemessage($realmsg)) { $socketclosed = 1; last; } } } # This is OUTSIDE the $frame->next_bytes loop, because a close event never returns a full frame # from WSockFrame if($frame->is_close) { print STDERR "CLOSE FRAME RECIEVED!\n"; $socketclosed = 1; if(!webPrint($ua->{realsocket}, $frame->new(buffer => 'data', type => 'close')->to_bytes)) { print STDERR "Write to socket failed, failed to properly close connection!\n"; } } ... if(!$workCount) { sleep($self->{sleeptime}); } if($timeout < time) { print STDERR "CLIENT TIMEOUT\n"; $socketclosed = 1; } } } ... return 1; } #### sub wsprint($self, $message) { ... my $frametype = 'text'; my $buffer = encode_json($message); my $framedata = $frame->new(buffer => $buffer, type => $frametype)->to_bytes; if(!webPrint($ua->{realsocket}, $framedata)) { print STDERR "Write to socket failed, closing connection!\n"; return 0; } return 1; } #### sub webPrint($ofh, @parts) { my $brokenpipe = 0; local $SIG{PIPE} = sub { $brokenpipe = 1;}; local $INPUT_RECORD_SEPARATOR = undef; binmode($ofh, ':bytes'); my $full; foreach my $npart (@parts) { if(!defined($npart)) { #print STDERR "Empty npart in Webprint!\n"; next; } if(is_utf8($npart)) { $full .= encode_utf8($npart); } else { $full .= $npart; } } my $shownlimitmessage = 0; my $timeoutthres = 20; # Need to be able to send at least one byte per 20 seconds # Output bandwidth-limited stuff, in as big chunks as possible if(!defined($full) || $full eq '') { return 1; } my $written = 0; my $timeout = time + $timeoutthres; $ERRNO = 0; my $needprintdone = 0; while(1) { eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval) $written = syswrite($ofh, $full); }; if($EVAL_ERROR) { print STDERR "Write error: $EVAL_ERROR\n"; return 0; } if(!defined($written)) { $written = 0; } last if($written == length($full)); #print STDERR "Sent $written bytes (", length($full) - $written, "remaining)\n"; if($!{EWOULDBLOCK} || $!{EAGAIN}) { ## no critic (Variables::ProhibitPunctuationVars) if(!$shownlimitmessage) { print STDERR "Rate limiting output\n"; $shownlimitmessage = 1; } $timeout = time + $timeoutthres; if(!$written) { sleep(0.01); } } elsif(0 && $brokenpipe) { print STDERR "webPrint write failure: SIGPIPE\n"; return 0; } elsif($ofh->error || $ERRNO ne '') { print STDERR "webPrint write failure: $ERRNO / ", $ofh->opened, " / ", $ofh->error, "\n"; return 0; } if($written) { $timeout = time + $timeoutthres; $full = substr($full, $written); $written = 0; next; } if($timeout < time) { print STDERR "***** webPrint TIMEOUT ****** $ERRNO\n"; return 0; } sleep(0.01); $needprintdone = 1; } if($needprintdone) { print STDERR "Webprint Done\n"; } return 1; }