diff --git a/docs/source/14_faq.rst b/docs/source/14_faq.rst index 665bbe8..f0b8145 100644 --- a/docs/source/14_faq.rst +++ b/docs/source/14_faq.rst @@ -69,11 +69,11 @@ I would like to use my favorite web server instead of the one embedded into **RP Can you add login/password and authentication to RPI-Monitor web interface? - **No**. I'll never add such a feature to **RPi-Monitor**. Why? Just because it is - something complex to do in a clean and fully secured way. Some software are - already design to do the job and they do it well. + **Yes** but only using the basic-authentication challenge. Only a single username + and password is allowed, and be aware that if you are not in HTTPS, the username + and password may be collected by a malicious user. - A `solution is proposed in this documentation using nginx + An other `solution is proposed in this documentation using nginx <34_autentication.html#authentication-and-secure-access>`_ frontend. | diff --git a/docs/source/21_daemon.rst b/docs/source/21_daemon.rst index 3e955ab..ce6d6f8 100644 --- a/docs/source/21_daemon.rst +++ b/docs/source/21_daemon.rst @@ -58,6 +58,11 @@ daemon.logfile=/var/log/rpimonitor.log daemon.loglevel=0 Define log level (Default:0) +daemon.basicauth= + Define a username and a password to access the embedded web server. (Default: no authentication) + + .. important:: Syntax is ``username:password`` in plain text (note the semicolon). + SNMP configuration ------------------ ``snmpagent`` is defining SNMP behavior of ``rpimonitord``. diff --git a/docs/source/34_autentication.rst b/docs/source/34_autentication.rst index d97f33c..e1fff41 100644 --- a/docs/source/34_autentication.rst +++ b/docs/source/34_autentication.rst @@ -5,7 +5,9 @@ Authentication and secure access ================================ The purpose of **RPi-Monitor** is to... monitor. For different reason (security, -time, scope of project...) it do not add any authentication. +time, scope of project...) it do not add any high secure level authentication. + +A mecanism, based on the basic-auth challenge is provided, see the daemon configuration. Be aware this kind of authentication should be enclosed in an HTTPS connexion. We will see here how to configure a reverse proxy which will be in charge of user authentication and ssl connections. We will also configure a firewall to @@ -22,6 +24,7 @@ To install nginx execute the following command: Manage authentication --------------------- +Be sure to disable the internal basic-auth by commenting the line ``daemon.basicauth`` in the config file. To manage authentication we need to create a file gathering the username and passwords. The following script will help you to generate new users id diff --git a/src/etc/rpimonitor/daemon.conf b/src/etc/rpimonitor/daemon.conf index 77d24ca..5ae72a5 100644 --- a/src/etc/rpimonitor/daemon.conf +++ b/src/etc/rpimonitor/daemon.conf @@ -16,6 +16,7 @@ #daemon.datastore=/var/lib/rpimonitor #daemon.logfile=/var/log/rpimonitor.log #daemon.loglevel=0 +#daemon.basicauth=login:password ######################################################################## # SNMP configuration # Section 'snmpagent' is defining SNMP behavior of rpimonitord. diff --git a/src/etc/rpimonitor/template/raspbian.conf b/src/etc/rpimonitor/template/raspbian.conf index 4035974..f4adec7 100644 --- a/src/etc/rpimonitor/template/raspbian.conf +++ b/src/etc/rpimonitor/template/raspbian.conf @@ -26,3 +26,6 @@ include=/etc/rpimonitor/template/network.conf #include=/etc/rpimonitor/template/wlan.conf #include=/etc/rpimonitor/template/dht11.conf #include=/etc/rpimonitor/template/entropy.conf + +#include=/etc/rpimonitor/template/weather.conf + diff --git a/src/etc/rpimonitor/template/weather.conf b/src/etc/rpimonitor/template/weather.conf new file mode 100644 index 0000000..0fb452c --- /dev/null +++ b/src/etc/rpimonitor/template/weather.conf @@ -0,0 +1,147 @@ +######################################################################## +# Display current weather inforamte +# Page: 3 +# Information Status Statistics +# - location name - yes - no +# - weather icon - yes - no +# - temperature - yes - yes +# - feels like temp - yes - yes +# - air pressure - yes - yes +# - humidity - yes - yes +# - date of the report - yes - no +# - weather description - yes - no +# - wind speed - yes - yes +# - wind direction - yes - yes +# +# Setup : +# - Request an OpenWeatherMap API key by going to https://www.openweathermap.org > API . It is free. +# Or use your favorite search engine to search for an existing API key (hint : "site:github.com api.openweathermap.org/data appid=" ), replace XXXXXXXXXXXXXXX with your appid. +# - Modify the location by entering zip code and country code, or lat/long on the following Shell command. Also, +# set the appid with the API key. Modify the "lang" parameter and the "units" to fit your need. +# See OpenWeatherMap "Current Weather API" help : https://openweathermap.org/current +# +# To reduce number of API call, the Curl command is triggered only when the minute is a multiple +# of 5. You can adjust this by changine the modulo "%" in the Shell command. The result of the API call +# is stored in /tmp/weather.dat for data processing of the dynamic section. +######################################################################## +# Add new pages (number 3) +web.status.3.name=Weather +web.statistics.3.name=Weather + +# The API is called in a small Shell script, but only if the RPIMonitor loop is running +# in a multiple of 5 minute. JSON result is stored in a temporary file for further data processing. +dynamic.31.name=weather_curl +# By ZIP CODE and country. +#dynamic.31.source=/bin/bash -c "m=\$(expr \$(date '+%M') % 5); if [[ \$m == 0 ]]; then curl -s \"https://api.openweathermap.org/data/2.5/weather?units=metric&zip=73500,fr&appid=XXXXXXXXXXXXXXX&lang=en\" --output /tmp/weather.dat; fi" +# By latitude, longitude +dynamic.31.source=/bin/bash -c "m=\$(expr \$(date '+%M') % 1); if [[ \$m == 0 ]]; then curl -s \"https://api.openweathermap.org/data/2.5/weather?units=metric&lat=45,2262&lon=6,7441&appid=XXXXXXXXXXXXXXX&lang=en\" --output /tmp/weather.dat; fi" + +# Parsing the JSON result of the API. Because RPIMonitor do not support +# JSON natively, if the format of the result change, the parsing will fail. +dynamic.32.name=weather_location +dynamic.32.source=/tmp/weather.dat +dynamic.32.regexp="name":"(.*?)" +dynamic.32.postprocess=use HTML::Entities; use Encode;use utf8; encode_entities(Encode::decode('utf8', $1)); + +dynamic.33.name=weather_icon +dynamic.33.source=/tmp/weather.dat +dynamic.33.regexp="icon":"(.*?)" + +dynamic.34.name=weather_temp +dynamic.34.source=/tmp/weather.dat +dynamic.34.regexp="temp":(.*?), +dynamic.34.rrd=GAUGE +dynamic.34.interval=10 + +dynamic.35.name=weather_feel +dynamic.35.source=/tmp/weather.dat +dynamic.35.regexp="feels_like":(.*?), +dynamic.35.rrd=GAUGE + +dynamic.36.name=weather_pressure +dynamic.36.source=/tmp/weather.dat +dynamic.36.regexp="pressure":(.*?), +dynamic.36.rrd=GAUGE + +dynamic.37.name=weather_hum +dynamic.37.source=/tmp/weather.dat +dynamic.37.regexp="humidity":(.*?)} +dynamic.37.postprocess=$1+0 +dynamic.37.rrd=GAUGE + +dynamic.38.name=weather_date +dynamic.38.source=/tmp/weather.dat +dynamic.38.regexp="dt":(.*?)} +dynamic.38.postprocess=strftime("%Y-%m-%d %H:%M:%S", localtime($1)) + +dynamic.39.name=weather_desc +dynamic.39.source=/tmp/weather.dat +dynamic.39.regexp="description":"(.*?)" +dynamic.39.postprocess=use HTML::Entities; use Encode;use utf8; encode_entities(Encode::decode('utf8', $1)); + +# Sometime, the API return also the gust speed, after the "deg", but +# sometime not. Dont rely on it. +dynamic.40.name=weather_wind,weather_wind_deg +dynamic.40.source=/tmp/weather.dat +dynamic.40.regexp="speed":(.*?),"deg":(.*?)[},] +dynamic.40.rrd=GAUGE + +dynamic.41.name=weather_sunrise +dynamic.41.source=/tmp/weather.dat +dynamic.41.regexp="sunrise":(.*?), +# If the metric start with a 0, it is interpreted as an Octal value (0730 (1h30) = 472). Use "0 + ..." to convert it to an int +dynamic.41.postprocess=0 + strftime("%H%M", localtime($1)) +dynamic.41.rrd=GAUGE + +dynamic.42.name=weather_sunset +dynamic.42.source=/tmp/weather.dat +dynamic.42.regexp="sunset":(.*?), +dynamic.42.postprocess=0 + strftime("%H%M", localtime($1)) +dynamic.42.rrd=GAUGE + + +####################################### +# The web page with the current weather +web.status.3.content.1.name=Weather +web.status.3.content.1.icon=cpu_temp.png +web.status.3.content.1.line.1="" + data.weather_location + ", at " + data.weather_date +web.status.3.content.1.line.2="" + data.weather_desc+", "+data.weather_temp + "°C" +web.status.3.content.1.line.3="feels like " + data.weather_feel + "°C, wind "+data.weather_wind+" m/s, direction " + data.weather_wind_deg + "° " +web.status.3.content.1.line.4=JustGageBar("Humidity", "%", 0,data.weather_hum,100, 100,80,[ "#f0f000", "#00f000", "#0000f0" ]) + JustGageBar("Sun light", "HHMM", data.weather_sunrise, (100*new Date().getHours()) + new Date().getMinutes(), data.weather_sunset, 100,80,[ "#A0A0A0", "#ffc300", "#404040" ]) +web.status.3.content.1.line.5=" pressure "+data.weather_pressure+ " hPa" + + +####################################### +# The graphic page with history +web.statistics.3.content.1.name=Temperature +web.statistics.3.content.1.graph.1=weather_temp +web.statistics.3.content.1.graph.2=weather_feel +web.statistics.3.content.1.graph.3=weather_hum +web.statistics.3.content.1.graph_options.tooltipOpts={ content: "

%s

Value: %y.1
At: %x", dateFormat:"%y-%0m-%0d %H:%M" } +web.statistics.3.content.1.ds_graph_options.weather_temp.label=T° +web.statistics.3.content.1.ds_graph_options.weather_feel.label=Feels T° +web.statistics.3.content.1.ds_graph_options.weather_hum.label=Humidity % +web.statistics.3.content.1.ds_graph_options.weather_hum.yaxis=2 +web.statistics.3.content.1.ds_graph_options.weather_hum.shadowSize=0 +web.statistics.3.content.1.ds_graph_options.weather_hum.lines={ lineWidth:1 } +web.statistics.3.content.1.graph_options.y2axis={ position: "right", min: 0, max: 100 } + +web.statistics.3.content.2.name=Wind +web.statistics.3.content.2.graph.1=weather_wind +web.statistics.3.content.2.graph.2=weather_wind_deg +web.statistics.3.content.2.graph_options.tooltipOpts={ content: "

%s

Value: %y.1
At: %x", dateFormat:"%y-%0m-%0d %H:%M" } +web.statistics.3.content.2.ds_graph_options.weather_wind.label=m/s +web.statistics.3.content.2.ds_graph_options.weather_wind_deg.label=° direction +web.statistics.3.content.2.ds_graph_options.weather_wind_deg.yaxis=2 +web.statistics.3.content.2.graph_options.y2axis={ position: "right", min: 0, max: 360 } + +web.statistics.3.content.3.name=Pressure +web.statistics.3.content.3.graph.1=weather_pressure +web.statistics.3.content.3.graph_options.tooltipOpts={ content: "

%s

Value: %y
At: %x", dateFormat:"%y-%0m-%0d %H:%M" } +web.statistics.3.content.3.ds_graph_options.weather_pressure.label=hPa +web.statistics.3.content.3.graph_options.yaxis={min: 960, max: 1060 } + +web.statistics.3.content.4.name=Sun Light +web.statistics.3.content.4.graph.1=weather_sunrise +web.statistics.3.content.4.graph.2=weather_sunset +web.statistics.3.content.4.graph_options.tooltipOpts={ content: "

%s

Value: %y.0
At: %x", dateFormat:"%y-%0m-%0d %H:%M" } \ No newline at end of file diff --git a/src/usr/bin/rpimonitord b/src/usr/bin/rpimonitord index 5c7399b..a36a7f8 100755 --- a/src/usr/bin/rpimonitord +++ b/src/usr/bin/rpimonitord @@ -320,6 +320,7 @@ use IO::Handle; use HTTP::Daemon; use HTTP::Status; use JSON -convert_blessed_universally; +use MIME::Base64; #use Data::Dumper; sub new @@ -405,6 +406,38 @@ sub SendError return 1; } +sub SendChallenge +{ + my $this=shift; + $this->Debug(2,"Challenge auth"); + my $response = HTTP::Response->new( + 401 => 'Authorization Required', + [ 'WWW-Authenticate' => 'Basic realm="Identity"', + 'Content-Type' => "text/plain"], + "Authentication needed" + ); + $this->{'connection'}->send_response($response); + $this->{'connection'}->close(); +} + +sub CheckChallenge +{ + my $this = shift; + my $request = shift; + my $auth_received = shift; + my $configuration = shift; + my $auth = "Basic ".encode_base64($configuration->{'daemon'}->{'basicauth'}, ''); + if ($auth eq $auth_received) + { + return 1; + } + else + { + $this->Debug(1, "Bad authent provided"); + return 0; + } +} + sub DoGET { my $this = shift; @@ -586,8 +619,27 @@ sub Run for (;;){ while ( $this->{'connection'} = $this->{'server'}->accept) { while (my $request = $this->{'connection'}->get_request) { - my $method = "Do".$request->method(); - $this->can($method) and $this->$method($request,$configuration); + if ($configuration->{'daemon'}->{'basicauth'}) { + # Basic authen enabled + if ($request->header( 'Authorization' )) { + my $auth = $request->header( 'Authorization' ); + if ($this->CheckChallenge($request, $auth, $configuration)) { + my $method = "Do".$request->method(); + $this->can($method) and $this->$method($request,$configuration); + } + else { + $this->SendChallenge(); + } + } + else { + $this->SendChallenge(); + } + } + else { + # Basic auth not enabled + my $method = "Do".$request->method(); + $this->can($method) and $this->$method($request,$configuration); + } } $this->{'connection'}->close; undef($this->{'connection'});