diff --git a/config/cfgroot/graphs.pl b/config/cfgroot/graphs.pl index e4c3613fb..beddff032 100644 --- a/config/cfgroot/graphs.pl +++ b/config/cfgroot/graphs.pl @@ -29,6 +29,12 @@ require '/var/ipfire/general-functions.pl'; require "${General::swroot}/lang.pl"; require "${General::swroot}/header.pl"; +# Graph image size in pixel +our %image_size = ('width' => 910, 'height' => 300); + +# List of all available time ranges +our @time_ranges = ("hour", "day", "week", "month", "year"); + my $ERROR; my @GRAPH_ARGS = ( @@ -48,8 +54,8 @@ my @GRAPH_ARGS = ( "-W www.ipfire.org", # Default size - "-w 910", - "-h 300", + "-w $image_size{'width'}", + "-h $image_size{'height'}", # Use alternative grid "--alt-y-grid", diff --git a/config/rootfiles/common/web-user-interface b/config/rootfiles/common/web-user-interface index 540bf1e4b..23e9f3e5e 100644 --- a/config/rootfiles/common/web-user-interface +++ b/config/rootfiles/common/web-user-interface @@ -20,6 +20,7 @@ srv/web/ipfire/cgi-bin/extrahd.cgi srv/web/ipfire/cgi-bin/fireinfo.cgi srv/web/ipfire/cgi-bin/firewall.cgi srv/web/ipfire/cgi-bin/fwhosts.cgi +srv/web/ipfire/cgi-bin/getrrdimage.cgi srv/web/ipfire/cgi-bin/gpl.cgi #srv/web/ipfire/cgi-bin/guardian.cgi srv/web/ipfire/cgi-bin/gui.cgi @@ -300,6 +301,7 @@ srv/web/ipfire/html/images/view-refresh.png srv/web/ipfire/html/images/wakeup.gif srv/web/ipfire/html/images/window-new.png srv/web/ipfire/html/include +srv/web/ipfire/html/include/rrdimage.js srv/web/ipfire/html/include/zoneconf.js srv/web/ipfire/html/index.cgi srv/web/ipfire/html/redirect-templates diff --git a/html/cgi-bin/getrrdimage.cgi b/html/cgi-bin/getrrdimage.cgi new file mode 100644 index 000000000..0caefe0ac --- /dev/null +++ b/html/cgi-bin/getrrdimage.cgi @@ -0,0 +1,245 @@ +#!/usr/bin/perl +############################################################################### +# # +# IPFire.org - A linux based firewall # +# Copyright (C) 2005-2021 IPFire Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +use strict; +use URI; +use GD; +use GD::Text::Wrap; +use experimental 'smartmatch'; + +# debugging +#use warnings; +#use CGI::Carp 'fatalsToBrowser'; + +require '/var/ipfire/general-functions.pl'; +require "${General::swroot}/lang.pl"; +require "${General::swroot}/header.pl"; +require "${General::swroot}/graphs.pl"; + +# List of graph origins that getrrdimage.cgi can process directly +# (unknown origins are forwarded to ensure compatibility) +my @supported_origins = ("entropy.cgi", "hardwaregraphs.cgi", "media.cgi", + "memory.cgi","netexternal.cgi", "netinternal.cgi", "netother.cgi", + "netovpnrw.cgi", "netovpnsrv.cgi", "qos.cgi", "system.cgi"); + +### Process GET parameters ### +# URL format: /?origin=[graph origin cgi]&graph=[graph name]&range=[time range] +my $uri = URI->new($ENV{'REQUEST_URI'}); +my %query = $uri->query_form; + +my $origin = lc $query{'origin'}; # lower case +my $graph = $query{'graph'}; +my $range = lc $query{'range'}; # lower case + +# Check parameters +unless(($origin =~ /^\w+?\.cgi$/) && ($graph =~ /^[\w-]+?$/) && ($range ~~ @Graphs::time_ranges)) { + # Send HTTP headers + _start_png_output(); + + _print_error("URL parameters missing or malformed."); + exit; +} + +# Unsupported graph origin: Redirect request to the CGI specified in the "origin" parameter +# This enables backwards compatibility with addons that use Graphs::makegraphbox to ouput their own graphs +unless($origin ~~ @supported_origins) { + # Rewrite to old URL format: /[graph origin cgi]?[graph name]?[time range] + my $location = "https://$ENV{'SERVER_NAME'}:$ENV{'SERVER_PORT'}/cgi-bin/${origin}?${graph}?${range}"; + + # Send HTTP redirect + print "Status: 302 Found\n"; + print "Location: $location\n"; + print "Content-type: text/html; charset=UTF-8\n"; + print "\n"; # End of HTTP headers + + print "Unsupported origin, request redirected to '$location'"; + exit; +} + +### Create graphs ### +# Send HTTP headers +_start_png_output(); + +# Graphs are first grouped by their origin. +# This is because some graph categories require special parameter handling. +my $graphstatus = ''; +if($origin eq "entropy.cgi") { ## entropy.cgi + $graphstatus = Graphs::updateentropygraph($range); +# ------ + +} elsif($origin eq "hardwaregraphs.cgi") { ## hardwaregraphs.cgi + if($graph eq "hwtemp") { + $graphstatus = Graphs::updatehwtempgraph($range); + } elsif($graph eq "hwfan") { + $graphstatus = Graphs::updatehwfangraph($range); + } elsif($graph eq "hwvolt") { + $graphstatus = Graphs::updatehwvoltgraph($range); + } elsif($graph eq "thermaltemp") { + $graphstatus = Graphs::updatethermaltempgraph($range); + } elsif($graph =~ "sd?") { + $graphstatus = Graphs::updatehddgraph($graph, $range); + } elsif($graph =~ "nvme?") { + $graphstatus = Graphs::updatehddgraph($graph, $range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} elsif($origin eq "media.cgi") { ## media.cgi + if ($graph =~ "sd?" || $graph =~ "mmcblk?" || $graph =~ "nvme?n?" || $graph =~ "xvd??" || $graph =~ "vd?" || $graph =~ "md*" ) { + $graphstatus = Graphs::updatediskgraph($graph, $range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} elsif($origin eq "memory.cgi") { ## memory.cgi + if($graph eq "memory") { + $graphstatus = Graphs::updatememorygraph($range); + } elsif($graph eq "swap") { + $graphstatus = Graphs::updateswapgraph($range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} elsif($origin eq "netexternal.cgi") { ## netexternal.cgi + $graphstatus = Graphs::updateifgraph($graph, $range); +# ------ + +} elsif($origin eq "netinternal.cgi") { ## netinternal.cgi + if ($graph =~ /wireless/){ + $graph =~ s/wireless//g; + $graphstatus = Graphs::updatewirelessgraph($graph, $range); + } else { + $graphstatus = Graphs::updateifgraph($graph, $range); + } +# ------ + +} elsif($origin eq "netother.cgi") { ## netother.cgi + if($graph eq "conntrack") { + $graphstatus = Graphs::updateconntrackgraph($range); + } elsif($graph eq "fwhits") { + $graphstatus = Graphs::updatefwhitsgraph($range); + } else { + $graphstatus = Graphs::updatepinggraph($graph, $range); + } +# ------ + +} elsif($origin eq "netovpnrw.cgi") { ## netovpnrw.cgi + if($graph ne "UNDEF") { + $graphstatus = Graphs::updatevpngraph($graph, $range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} elsif($origin eq "netovpnsrv.cgi") { ## netovpnsrv.cgi + if ($graph =~ /ipsec-/){ + $graph =~ s/ipsec-//g; + $graphstatus = Graphs::updateifgraph($graph, $range); + } else { + $graphstatus = Graphs::updatevpnn2ngraph($graph, $range); + } +# ------ + +} elsif($origin eq "qos.cgi") { ## qos.cgi + $graphstatus = Graphs::updateqosgraph($graph, $range); +# ------ + +} elsif($origin eq "services.cgi") { ## services.cgi + if($graph eq "processescpu") { + $graphstatus = Graphs::updateprocessescpugraph($range); + } elsif($graph eq "processesmemory") { + $graphstatus = Graphs::updateprocessesmemorygraph($range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} elsif($origin eq "system.cgi") { ## system.cgi + if($graph eq "cpu") { + $graphstatus = Graphs::updatecpugraph($range); + } elsif($graph eq "cpufreq") { + $graphstatus = Graphs::updatecpufreqgraph($range); + } elsif($graph eq "load") { + $graphstatus = Graphs::updateloadgraph($range); + } else { + $graphstatus = "Unknown graph name."; + } +# ------ + +} else { + $graphstatus = "Unknown graph origin."; +} + +### Print error message ### +# Add request parameters for debugging +if($graphstatus) { + $graphstatus = "$graphstatus\n($origin, $graph, $range)"; + _print_error($graphstatus); +} + +###--- Internal functions ---### + +# Send HTTP headers and switch to binary output +# (don't print any non-image data to STDOUT afterwards) +sub _start_png_output { + print "Cache-Control: no-cache, no-store\n"; + print "Content-Type: image/png\n"; + print "\n"; # End of HTTP headers + binmode(STDOUT); +} + +# Print error message to PNG output +sub _print_error { + my ($message) = @_; + $message = "- Error -\n \n$message"; + + # Create new image with the same size as a graph + my $img = GD::Image->new($Graphs::image_size{'width'}, $Graphs::image_size{'height'}); + $img->interlaced('true'); + + # Basic colors + my $color_background = $img->colorAllocate(255, 255, 255); + my $color_border = $img->colorAllocate(255, 0, 0); + my $color_text = $img->colorAllocate(0, 0, 0); + + # Background and border + $img->setThickness(2); + $img->filledRectangle(0, 0, $img->width, $img->height, $color_background); + $img->rectangle(10, 10, $img->width - 10, $img->height - 10, $color_border); + + # Draw message with line-wrap + my $textbox = GD::Text::Wrap->new($img, + text => $message, + width => ($img->width - 50), + color => $color_text, + align => 'center', + line_space => 5, + preserve_nl => 1 + ); + $textbox->set_font(gdLargeFont); + $textbox->draw(25, 25); + + # Get PNG output + print $img->png; +} diff --git a/html/html/include/rrdimage.js b/html/html/include/rrdimage.js new file mode 100644 index 000000000..e7ee4c769 --- /dev/null +++ b/html/html/include/rrdimage.js @@ -0,0 +1,122 @@ +/*############################################################################# +# # +# IPFire.org - A linux based firewall # +# Copyright (C) 2007-2021 IPFire Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +// "onclick" event handler for graph time range select button +// buttonObj: reference to the button +function rrdimage_selectRange(buttonObj) { + if(! (buttonObj && ('range' in buttonObj.dataset))) { + return; //required parameters are missing + } + + // Get selected time range from button + const range = buttonObj.dataset.range; + + // Get surrounding div box and select new range + let graphBox = $(buttonObj).closest('div'); + _rrdimg_setRange(graphBox, range); +} + +// Document loaded: Process all graphs, start reload timers +$(function() { + $('div.rrdimage').each(function() { + let graphBox = $(this); + _rrdimg_setRange(graphBox, graphBox.data('defaultRange'), true); + }); +}); + +//--- Internal functions --- + +// Set or update graph time range, start automatic reloading +// graphBox: jQuery object, reference to graph div box +// range: time range (day, hour, ...) +// initMode: don't immediately reload graph, but force timers and attributes update +function _rrdimg_setRange(graphBox, range, initMode = false) { + if(! ((graphBox instanceof jQuery) && (graphBox.length === 1))) { + return; //graphBox element missing + } + + // Check range parameter, default to "day" on error + if(! ["hour", "day", "week", "month", "year"].includes(range)) { + range = "day"; + } + + // Check if the time range is changed + if((graphBox.data('range') !== range) || initMode) { + graphBox.data('range', range); //Store new range + + // Update button highlighting + graphBox.find('button').removeClass('selected'); + graphBox.find(`button[data-range="${range}"]`).addClass('selected'); + } + + // Clear pending reload timer to prevent multiple image reloads + let timerId = graphBox.data('reloadTimer'); + if(timerId !== undefined) { + window.clearInterval(timerId); + graphBox.removeData('reloadTimer'); + } + + // Determine auto reload interval (in seconds), + // interval = 0 disables auto reloading by default + let interval = 0; + switch(range) { + case 'hour': + interval = 60; + break; + + case 'day': + case 'week': + interval = 300; + break; + } + + // Start reload timer and store reference + if(interval > 0) { + timerId = window.setInterval(function(graphRef) { + _rrdimg_reload(graphRef); + }, interval * 1000, graphBox); + graphBox.data('reloadTimer', timerId); + } + + // Always reload image unless disabled by init mode + if(! initMode) { + _rrdimg_reload(graphBox); + } +} + +// Reload graph image, add timestamp to prevent caching +// graphBox: jQuery object (graph element must be valid) +function _rrdimg_reload(graphBox) { + const origin = graphBox.data('origin'); + const graph = graphBox.data('graph'); + const timestamp = Date.now(); + + // Get user selected range or fall back to default + let range = graphBox.data('range'); + if(! range) { + range = graphBox.data('defaultRange'); + } + + // Generate new image URL with timestamp + const imageUrl = `/cgi-bin/getrrdimage.cgi?origin=${origin}&graph=${graph}&range=${range}×tamp=${timestamp}`; + + // Get graph image and set new URL + graphBox.children('img').first().attr('src', imageUrl); +}