1
0
mirror of https://github.com/cmur2/openvpn-status-web.git synced 2025-09-28 21:52:05 +02:00

56 Commits

Author SHA1 Message Date
cn
109b27fecc release: 3.0.1
- use webrick gem which contains fixes against [CVE-2020-25613](https://www.ruby-lang.org/en/news/2020/09/29/http-request-smuggling-cve-2020-25613/)
2020-10-03 11:04:48 +02:00
cn
b44517ec21 gems: update webrick to version 1.6.1
- explicitly use webrick gem version with patch against CVE-2020-25613
- https://www.ruby-lang.org/en/news/2020/09/29/http-request-smuggling-cve-2020-25613/
2020-10-03 11:03:01 +02:00
depfu[bot]
520da15739 gems: update rubocop to version 0.92.0 2020-09-26 12:40:07 +02:00
depfu[bot]
9b3a13abef gems: update rubocop to version 0.91.0 2020-09-16 09:39:25 +02:00
depfu[bot]
b81d61b8d7 gems: update rubocop to version 0.90.0 (#16)
Update rubocop to version 0.90.0 (#16)

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
2020-09-02 10:35:06 +02:00
cn
147a905bfc gem: fix solargraph typecheck problem in CI + remove downpin 2020-08-14 17:39:02 +02:00
cn
eeed0fa089 gems: downpin solargraph
- releases newer than 0.39.12 fail on Travis CI although they work locally with RVM Ruby 2.7.1
2020-08-14 14:57:08 +02:00
cn
586ae8ae0f gem: add editorconfig 2020-08-14 14:07:47 +02:00
depfu[bot]
075ec1e548 gems: update rubocop to version 0.89.0
Update rubocop to version 0.89.0 (#15)
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
2020-08-07 08:32:29 +02:00
cn
64ec5e1e56 release: 3.0.0
- drop Ruby 2.3 and 2.4 compatibility
- add Dockerfile
2020-07-14 20:08:06 +02:00
depfu[bot]
7597c17e93 gem: upgrade rubocop to version 0.88.0
Update rubocop to version 0.88.0 (#13)

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: Christian Nicolai <cn@mycrobase.de>
2020-07-14 20:06:06 +02:00
ac647a4f21 gem: bump minimum required Ruby version to Ruby 2.5 (#14) 2020-07-14 19:57:25 +02:00
cn
78f45f5080 docs: add example Dockerfile 2020-07-13 10:39:30 +02:00
9aeb1a9ad5 docs: fix typos 2020-07-13 10:33:54 +02:00
88c22a6504 docs: fix Travis CI build badge 2020-07-10 23:09:34 +02:00
depfu[bot]
289535d753 gems: update bundler-audit to version 0.7.0.1
(#9)
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
2020-06-14 12:42:05 +02:00
depfu[bot]
8d42bdafc1 gems: update rubocop to version 0.81.0
(#3)
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
2020-04-02 13:51:52 +02:00
cn
f8666fdfc2 release: 2.1.0 2020-03-07 01:32:22 +01:00
cn
41f5d4ed62 gem: adopt frozen string literals 2020-03-07 01:30:57 +01:00
cn
92ee01e5de gem: port fixes from Sorbet from dyndnsd 2020-03-07 01:29:12 +01:00
cn
bf3ba8f7cd gem: refactor gemspec, exclude tests from gem, move binaries to ./exe
- based on recommendations of https://piotrmurach.com/articles/writing-a-ruby-gem-specification/
2020-03-07 01:28:17 +01:00
cn
140c60c753 gem: add rubocop and fix style 2020-03-02 01:57:58 +01:00
cn
d959e7fe62 gem: fix gemspec using rubocop hints
- especially the wrong homepage URL
2020-03-01 22:13:23 +01:00
cn
4cde78fe96 gem: add solargraph support 2020-03-01 22:12:38 +01:00
23dd4aa63f docs: add depfu to README 2020-02-28 21:40:09 +01:00
1547db7bc6 travis: fix badge image URL 2020-02-28 13:31:36 +01:00
b1aa38059f travis: add Ruby 2.7 2020-02-28 13:30:49 +01:00
17054794da travis: fix build config validation problems
- https://docs.travis-ci.com/user/reference/overview/#deprecated-virtualization-environments
2020-02-28 13:25:02 +01:00
cn
ab81c8975e gem: use bundler-audit 2019-12-18 20:20:14 +01:00
cn
1804693c15 travis: add Ruby 2.6 2019-01-04 18:32:32 +01:00
cn
cdc20e8042 Bump version 2018-11-05 09:12:20 +01:00
cn
998f9e683c spec: use new rspec expect syntax 2018-11-02 10:16:28 +01:00
cn
cf69d6417d gem: upgrade to Rack 2.0, loosen version constraints by dropping old rubies 2018-11-02 10:16:18 +01:00
cn
cb1d029326 travis: update rubies 2018-11-02 10:11:26 +01:00
cn
563fb5743b Bump version 2013-10-08 13:23:22 +02:00
cn
a852aa4b4d React to SIGTERM 2013-10-08 13:23:02 +02:00
cn
d5f8d66422 Bump version 2013-10-07 22:27:18 +02:00
cn
f873d8176e Update README 2013-07-19 09:32:37 +02:00
cn
c14d59e0bf Require better_errors only in development 2013-07-18 23:39:59 +02:00
cn
a78a178150 Bump version 2013-05-03 22:26:53 +02:00
cn
e0c3073d82 Allow dropping privs 2013-05-03 22:26:07 +02:00
cn
c885e875ad Use date formatting in UI 2013-05-03 22:23:56 +02:00
cn
f2794ccea4 Add tests and bug fixed parser 2013-05-03 22:23:45 +02:00
cn
457aec64db Add some parser tests 2013-05-03 21:38:10 +02:00
cn
468e002162 Update README 2013-05-03 21:27:13 +02:00
cn
35b5be15a4 Don't use 1.8.7 2013-05-03 21:17:27 +02:00
cn
c51968618d Show multiple vpns 2013-05-03 21:12:06 +02:00
cn
a1a6b33902 Refactor V2 and V3 parser into one 2013-05-03 20:16:17 +02:00
cn
33013c56f3 Add support for multiple status-versions 2013-05-03 20:09:46 +02:00
cn
ed42fc9f30 Use better_errors 2013-05-03 19:49:09 +02:00
cn
635e562a3d First step towards multiple status files 2013-05-03 18:47:32 +02:00
cn
438931f8a6 Use a status object 2013-05-03 18:30:34 +02:00
cn
cd6e41fcfa Refactoring 2013-05-03 18:20:07 +02:00
cn
87d9ea302f Update README 2013-05-03 16:12:11 +02:00
cn
d6a7b8c0ee Use config file 2013-05-03 16:06:30 +02:00
cn
addc1cf45a Update desc 2013-05-03 15:39:50 +02:00
25 changed files with 726 additions and 182 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store .DS_Store
*.lock *.lock
pkg/* pkg/*
.yardoc

88
.rubocop.yml Normal file
View File

@@ -0,0 +1,88 @@
AllCops:
TargetRubyVersion: '2.5'
NewCops: enable
Layout/EmptyLineAfterGuardClause:
Enabled: false
# allows nicer usage of private_class_method
Layout/EmptyLinesAroundArguments:
Enabled: false
Layout/HashAlignment:
Enabled: false
Layout/LeadingEmptyLines:
Enabled: false
Layout/LineLength:
Max: 200
Layout/SpaceInsideHashLiteralBraces:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/BlockLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Naming/MethodParameterName:
Enabled: false
Naming/MemoizedInstanceVariableName:
Enabled: false
Style/AccessorGrouping:
Enabled: false
Style/ConditionalAssignment:
Enabled: false
Style/Documentation:
Enabled: false
Style/FormatStringToken:
Enabled: false
Style/GuardClause:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/IdenticalConditionalBranches:
Enabled: false
Style/InverseMethods:
Enabled: false
Style/NegatedIf:
Enabled: false
Style/RescueModifier:
Enabled: false
Style/Semicolon:
AllowAsExpressionSeparator: true
Style/SymbolArray:
Enabled: false

16
.solargraph.yml Normal file
View File

@@ -0,0 +1,16 @@
---
include:
- "**/*.rb"
- "bin/openvpn-status-web"
exclude:
- spec/**/*
- test/**/*
- vendor/**/*
- ".bundle/**/*"
require: []
domains: []
reporters:
- rubocop
- require_not_found
require_paths: []
max_files: 5000

View File

@@ -1,9 +1,10 @@
---
os: linux
language: ruby language: ruby
rvm: rvm:
- 2.0.0 - 2.7
- 1.9.3 - 2.6
- 1.8.7 - 2.5
gemfile: script:
- Gemfile - bundle exec rake travis

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec

View File

@@ -1,17 +1,68 @@
# openvpn-status-web # openvpn-status-web
Small (another word for naive in this case, it's simple and serves my needs) [rack](http://rack.github.com/) app [![Build Status](https://travis-ci.org/cmur2/openvpn-status-web.svg?branch=master)](https://travis-ci.org/cmur2/openvpn-status-web) [![Depfu](https://badges.depfu.com/badges/c264e2f70f2a19c43f880ddcb4a12ba8/overview.svg)](https://depfu.com/github/cmur2/openvpn-status-web?project_id=6194)
providing the information the [OpenVPN](http://openvpn.net/index.php/open-source.html) server collects in it's status file
especially including a list of currently connected clients (common name, remote address, traffic, ...). ## Description
It comes with a Debian 6 compatible init.d file.
Small (another word for naive in this case, it's simple and serves my needs) [Rack](http://rack.github.com/) application providing the information an [OpenVPN](http://openvpn.net/index.php/open-source.html) server collects in it's status file especially including a list of currently connected clients (common name, remote address, traffic, ...).
It lacks: It lacks:
* authentication * caching (parses file on each request, page does auto-refresh every minute as OpenVPN updates the status file these often by default)
* caching (parses file on each request, page does auto-refresh every minute as OpenVPN updates the status file these often)
* management interface support * management interface support
* *possibly more...* * *possibly more...*
## Usage
Install the gem:
gem install openvpn-status-web
Create a configuration file in YAML format somewhere:
```yaml
# listen address and port
host: "0.0.0.0"
port: "8080"
# optional: drop priviliges in case you want to but you should give this user at least read access on the log files
user: "nobody"
group: "nogroup"
# logfile is optional, logs to STDOUT else
logfile: "openvpn-status-web.log"
# hash with each VPNs display name for humans as key and further config as value
vpns:
My Small VPN:
# the status file path and status file format version are required
version: 1
status_file: "/var/log/openvpn-status.log"
My Other VPN:
version: 3
status_file: "/var/log/other-openvpn-status.log"
```
Your OpenVPN configuration should contain something like this:
```
# ...snip...
status /var/log/openvpn-status.log
status-version 1
# ...snip...
```
For more information about OpenVPN status file and version, see their [man page](https://community.openvpn.net/openvpn/wiki/Openvpn23ManPage). openvpn-status-web is able to parse all versions from 1 to 3.
## Advanced topics
### Authentication
If the information exposed is important to you serve it via the VPN or use a webserver as a proxy to handle SSL and/or HTTP authentication.
### Startup
There is a [Dockerfile](docs/Dockerfile) that can be used to build a Docker image for running openvpn-status-web.
The [Debian 6 init script](docs/debian-init-openvpn-status-web) assumes that openvpn-status-web is installed into the system ruby (no RVM support) and the config.yaml is at `/opt/openvpn-status-web/config.yaml`. Modify to your needs.
## License ## License
openvpn-statsu-web is licensed under the Apache License, Version 2.0. See LICENSE for more information. openvpn-status-web is licensed under the Apache License, Version 2.0. See LICENSE for more information.

View File

@@ -1,6 +1,24 @@
# frozen_string_literal: true
require 'bundler/gem_tasks' require 'bundler/gem_tasks'
require 'rspec/core/rake_task' require 'rspec/core/rake_task'
require 'rubocop/rake_task'
require 'bundler/audit/task'
RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new
Bundler::Audit::Task.new
task :default => :spec desc 'Should be run by developer once to prepare initial solargraph usage (fill caches etc.)'
task :'solargraph:init' do
sh 'solargraph download-core'
end
desc 'Run experimental solargraph type checker'
task :'solargraph:tc' do
sh 'solargraph typecheck'
end
task default: [:rubocop, :spec, 'bundle:audit']
task travis: [:default, :'solargraph:init', :'solargraph:tc']

15
docs/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM alpine:3.12
EXPOSE 8080
ENV VERSION=3.0.1
RUN apk --no-cache add openssl ca-certificates && \
apk --no-cache add ruby ruby-etc ruby-webrick && \
apk --no-cache add --virtual .build-deps ruby-dev build-base tzdata && \
gem install --no-document openvpn-status-web -v ${VERSION} && \
# set timezone to Berlin
cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime && \
apk del .build-deps
ENTRYPOINT ["openvpn-status-web", "/etc/openvpn-status-web/config.yml"]

View File

@@ -0,0 +1,40 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: openvpn-status-web
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Handle openvpn-status-web gem
### END INIT INFO
# using the system ruby's gem binaries directory
DAEMON="/var/lib/gems/1.8/bin/openvpn-status-web"
CONFIG_FILE="/opt/openvpn-status-web/config.yaml"
DAEMON_OPTS="$CONFIG_FILE"
test -x $DAEMON || exit 0
. /lib/lsb/init-functions
case "$1" in
start)
log_daemon_msg "Starting openvpn-web-status" "openvpn-web-status"
start-stop-daemon --start --quiet --oknodo --make-pidfile --pidfile "/var/run/openvpn-web-status.pid" --background --exec $DAEMON -- $DAEMON_OPTS
;;
stop)
log_daemon_msg "Stopping openvpn-web-status" "openvpn-web-status"
start-stop-daemon --stop --quiet --oknodo --pidfile "/var/run/openvpn-web-status.pid"
;;
restart|force-reload)
log_daemon_msg "Restarting openvpn-web-status" "openvpn-web-status"
start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile "/var/run/openvpn-web-status.pid"
start-stop-daemon --start --quiet --oknodo --make-pidfile --pidfile "/var/run/openvpn-web-status.pid" --background --exec $DAEMON -- $DAEMON_OPTS
;;
*)
echo "Usage: $0 {start|stop|restart|force-reload}" >&2
exit 1
;;
esac

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'openvpn-status-web' require 'openvpn-status-web'

View File

@@ -1,48 +0,0 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: openvpn-status-web
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Handle openvpn-status-web
### END INIT INFO
# your ruby interpreter
DAEMON="/usr/bin/ruby"
# some unique name identifying your VPN
VPN_NAME="vpn.example.org"
# path to the OpenVPN status log file
STATUS_PATH="/var/log/openvpn-status.log"
# host and port for this daemon to listen on
HOST="127.0.0.1"
PORT="3000"
DAEMON_OPTS="/opt/openvpn-status-web/status.rb $VPN_NAME $STATUS_PATH $HOST $PORT"
test -x $DAEMON || exit 0
. /lib/lsb/init-functions
case "$1" in
start)
log_daemon_msg "Starting openvpn-web-status for $VPN_NAME" "openvpn-web-status"
start-stop-daemon --start --quiet --oknodo --make-pidfile --pidfile "/var/run/$VPN_NAME.pid" --background --exec $DAEMON -- $DAEMON_OPTS
;;
stop)
log_daemon_msg "Stopping openvpn-web-status for $VPN_NAME" "openvpn-web-status"
start-stop-daemon --stop --quiet --oknodo --pidfile "/var/run/$VPN_NAME.pid"
;;
restart|force-reload)
log_daemon_msg "Restarting openvpn-web-status for $VPN_NAME" "openvpn-web-status"
start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile "/var/run/$VPN_NAME.pid"
start-stop-daemon --start --quiet --oknodo --make-pidfile --pidfile "/var/run/$VPN_NAME.pid" --background --exec $DAEMON -- $DAEMON_OPTS
;;
*)
echo "Usage: $0 {start|stop|restart|force-reload}" >&2
exit 1
;;
esac

134
lib/openvpn-status-web.rb Normal file → Executable file
View File

@@ -1,96 +1,148 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
require 'date'
require 'etc' require 'etc'
require 'logger' require 'logger'
require 'ipaddr' require 'ipaddr'
require 'yaml' require 'yaml'
require 'rack' require 'rack'
require 'erb'
require 'metriks' require 'metriks'
require 'better_errors' if ENV['RACK_ENV'] == 'development'
require 'openvpn-status-web/status'
require 'openvpn-status-web/parser/v1'
require 'openvpn-status-web/parser/v2'
require 'openvpn-status-web/parser/v3'
require 'openvpn-status-web/int_patch' require 'openvpn-status-web/int_patch'
require 'openvpn-status-web/version' require 'openvpn-status-web/version'
module OpenVPNStatusWeb module OpenVPNStatusWeb
# @return [Logger]
def self.logger def self.logger
@logger @logger
end end
# @param logger [Logger]
# @return [Logger]
def self.logger=(logger) def self.logger=(logger)
@logger = logger @logger = logger
end end
class LogFormatter class LogFormatter
def call(lvl, time, progname, msg) # @param lvl [Object]
"[%s] %-5s %s\n" % [Time.now.strftime('%Y-%m-%d %H:%M:%S'), lvl, msg.to_s] # @param _time [DateTime]
# @param _progname [String]
# @param msg [Object]
# @return [String]
def call(lvl, _time, _progname, msg)
format("[%s] %-5s %s\n", Time.now.strftime('%Y-%m-%d %H:%M:%S'), lvl, msg.to_s)
end end
end end
class Daemon class Daemon
def initialize(name, file) def initialize(vpns)
@name = name @vpns = vpns
@file = file
@main_tmpl = read_template(File.join(File.dirname(__FILE__), 'openvpn-status-web/main.html.erb'))
end end
def call(env) def call(env)
main_tmpl = read_template(File.join(File.dirname(__FILE__), 'openvpn-status-web/main.html.erb')) return [405, {'Content-Type' => 'text/plain'}, ['Method Not Allowed']] if env['REQUEST_METHOD'] != 'GET'
return [404, {'Content-Type' => 'text/plain'}, ['Not Found']] if env['PATH_INFO'] != '/'
# variables for template # variables for template
name = @name vpns = @vpns
client_list, routing_table, global_stats = read_status_log(@file) stati = {}
@vpns.each do |name, config|
html = main_tmpl.result(binding) stati[name] = parse_status_log(config)
[200, {"Content-Type" => "text/html"}, [html]] end
# eval
html = @main_tmpl.result(binding)
[200, {'Content-Type' => 'text/html'}, [html]]
end end
def read_template(file) def read_template(file)
text = File.open(file, 'rb') do |f| f.read end text = File.read(file, mode: 'rb')
ERB.new(text) ERB.new(text)
end end
def read_status_log(file)
text = File.open(file, 'rb') do |f| f.read end
current_section = :none def parse_status_log(vpn)
client_list = [] text = File.read(vpn['status_file'], mode: 'rb')
routing_table = []
global_stats = []
text.lines.each do |line| case vpn['version']
(current_section = :cl; next) if line == "OpenVPN CLIENT LIST\n" when 1
(current_section = :rt; next) if line == "ROUTING TABLE\n" OpenVPNStatusWeb::Parser::V1.new.parse_status_log(text)
(current_section = :gs; next) if line == "GLOBAL STATS\n" when 2
(current_section = :end; next) if line == "END\n" OpenVPNStatusWeb::Parser::V2.new.parse_status_log(text)
when 3
case current_section OpenVPNStatusWeb::Parser::V3.new.parse_status_log(text)
when :cl then client_list << line.strip.split(',') else
when :rt then routing_table << line.strip.split(',') raise "No suitable parser for status-version #{vpn['version']}"
when :gs then global_stats << line.strip.split(',')
end
end end
[client_list[2..-1], routing_table[1..-1], global_stats]
end end
# @return [void]
def self.run! def self.run!
if ARGV.length != 4 if ARGV.length != 1
puts "Usage: openvpn-status-web vpn-name status-log listen-host listen-port" puts 'Usage: openvpn-status-web config_file'
exit 1 exit 1
end end
OpenVPNStatusWeb.logger = Logger.new(STDOUT) config_file = ARGV[0]
OpenVPNStatusWeb.logger.progname = "openvpn-status-web" if !File.file?(config_file)
puts 'Config file not found!'
exit 1
end
puts "openvpn-status-web version #{OpenVPNStatusWeb::VERSION}"
puts "Using config file #{config_file}"
config = YAML.safe_load(File.read(config_file, mode: 'r'))
if config['logfile']
OpenVPNStatusWeb.logger = Logger.new(config['logfile'])
else
OpenVPNStatusWeb.logger = Logger.new($stdout)
end
OpenVPNStatusWeb.logger.progname = 'openvpn-status-web'
OpenVPNStatusWeb.logger.formatter = LogFormatter.new OpenVPNStatusWeb.logger.formatter = LogFormatter.new
OpenVPNStatusWeb.logger.info "Starting..." OpenVPNStatusWeb.logger.info 'Starting...'
# drop priviliges as soon as possible
# NOTE: first change group than user
if config['group']
group = Etc.getgrnam(config['group'])
Process::Sys.setgid(group.gid) if group
end
if config['user']
user = Etc.getpwnam(config['user'])
Process::Sys.setuid(user.uid) if user
end
# configure rack
app = Daemon.new(config['vpns'])
if ENV['RACK_ENV'] == 'development'
app = BetterErrors::Middleware.new(app)
BetterErrors.application_root = File.expand_path(__dir__)
end
Signal.trap('INT') do Signal.trap('INT') do
OpenVPNStatusWeb.logger.info "Quitting..." OpenVPNStatusWeb.logger.info 'Quitting...'
Rack::Handler::WEBrick.shutdown
end
Signal.trap('TERM') do
OpenVPNStatusWeb.logger.info 'Quitting...'
Rack::Handler::WEBrick.shutdown Rack::Handler::WEBrick.shutdown
end end
app = Daemon.new(ARGV[0], ARGV[1]) Rack::Handler::WEBrick.run app, Host: config['host'], Port: config['port']
Rack::Handler::WEBrick.run app, :Host => ARGV[2], :Port => ARGV[3]
end end
end end
end end

View File

@@ -1,16 +1,17 @@
# frozen_string_literal: true
class Integer class Integer
def as_bytes def as_bytes
return "1 Byte" if self == 1 return '1 Byte' if self == 1
label = ["Bytes", "KiB", "MiB", "GiB", "TiB"] label = %w[Bytes KiB MiB GiB TiB]
i = 0 i = 0
num = self.to_f num = to_f
while num >= 1024 do while num >= 1024
num = num / 1024 num /= 1024
i += 1 i += 1
end end
"#{format('%.2f', num)} #{label[i]}" "#{format('%.2f', num)} #{label[i]}"
end end
end end

View File

@@ -42,67 +42,70 @@ thead {
</head> </head>
<body> <body>
<h1>OpenVPN Status for <%= name %></h1> <% vpns.each do |name,config| %>
<% status = stati[name] %>
<h1>OpenVPN Status for <%= name %></h1>
<h2>Client List</h2> <h2>Client List</h2>
<div> <div>
<table> <table>
<thead> <thead>
<td class="first">Common Name</td> <td class="first">Common Name</td>
<td class="middle">Real Address</td> <td class="middle">Real Address</td>
<td class="middle">Data Received</td> <td class="middle">Data Received</td>
<td class="middle">Data Sent</td> <td class="middle">Data Sent</td>
<td class="last">Connected Since</td> <td class="last">Connected Since</td>
</thead> </thead>
<tbody> <tbody>
<% client_list.each do |client| %> <% status.client_list.each do |client| %>
<tr> <tr>
<td class="first"><%= client[0] %></td> <td class="first"><%= client[0] %></td>
<td class="middle"><%= client[1] %></td> <td class="middle"><%= client[1] %></td>
<td class="middle"><%= client[2].to_i.as_bytes %></td> <td class="middle"><%= client[2].to_i.as_bytes %></td>
<td class="middle"><%= client[3].to_i.as_bytes %></td> <td class="middle"><%= client[3].to_i.as_bytes %></td>
<td class="last"><%= client[4] %></td> <td class="last"><%= client[4].strftime('%-d.%-m.%Y %H:%M:%S') %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>
<h2>Routing Table</h2> <h2>Routing Table</h2>
<div> <div>
<table> <table>
<thead> <thead>
<td class="first">Virtual Address</td> <td class="first">Virtual Address</td>
<td class="middle">Common Name</td> <td class="middle">Common Name</td>
<td class="middle">Real Address</td> <td class="middle">Real Address</td>
<td class="last">Last Ref</td> <td class="last">Last Ref</td>
</thead> </thead>
<tbody> <tbody>
<% routing_table.each do |e| %> <% status.routing_table.each do |route| %>
<tr> <tr>
<td class="first"><%= e[0] %></td> <td class="first"><%= route[0] %></td>
<td class="middle"><%= e[1] %></td> <td class="middle"><%= route[1] %></td>
<td class="middle"><%= e[2] %></td> <td class="middle"><%= route[2] %></td>
<td class="last"><%= e[3] %></td> <td class="last"><%= route[3].strftime('%-d.%-m.%Y %H:%M:%S') %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>
<h2>Global Stats</h2> <h2>Global Stats</h2>
<div> <div>
<table> <table>
<tbody> <tbody>
<% global_stats.each do |e| %> <% status.global_stats.each do |global| %>
<tr> <tr>
<td><%= e[0] %>:</td> <td><%= global[0] %>:</td>
<td><%= e[1] %></td> <td><%= global[1] %></td>
</tr> </tr>
<% end %>
</tbody>
</table>
</div>
<% end %> <% end %>
</tbody>
</table>
</div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
module OpenVPNStatusWeb
module Parser
class ModernStateless
def self.parse_status_log(text, sep)
status = Status.new
status.client_list = []
status.routing_table = []
status.global_stats = []
text.lines.each do |line|
parts = line.strip.split(sep)
status.client_list << parse_client(parts[1..5]) if parts[0] == 'CLIENT_LIST'
status.routing_table << parse_route(parts[1..4]) if parts[0] == 'ROUTING_TABLE'
status.global_stats << parse_global(parts[1..2]) if parts[0] == 'GLOBAL_STATS'
end
status
end
private_class_method def self.parse_client(client)
client[2] = client[2].to_i
client[3] = client[3].to_i
client[4] = DateTime.strptime(client[4], '%a %b %d %k:%M:%S %Y')
client
end
private_class_method def self.parse_route(route)
route[3] = DateTime.strptime(route[3], '%a %b %d %k:%M:%S %Y')
route
end
private_class_method def self.parse_global(global)
global[1] = global[1].to_i
global
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module OpenVPNStatusWeb
module Parser
class V1
def parse_status_log(text)
current_section = :none
client_list = []
routing_table = []
global_stats = []
text.lines.each do |line|
(current_section = :cl; next) if line == "OpenVPN CLIENT LIST\n"
(current_section = :rt; next) if line == "ROUTING TABLE\n"
(current_section = :gs; next) if line == "GLOBAL STATS\n"
(current_section = :end; next) if line == "END\n"
case current_section
when :cl
client_list << line.strip.split(',')
when :rt
routing_table << line.strip.split(',')
when :gs
global_stats << line.strip.split(',')
end
end
status = Status.new
status.client_list = client_list[2..-1].map { |client| parse_client(client) }
status.routing_table = routing_table[1..-1].map { |route| parse_route(route) }
status.global_stats = global_stats.map { |global| parse_global(global) }
status
end
private
def parse_client(client)
client[2] = client[2].to_i
client[3] = client[3].to_i
client[4] = DateTime.strptime(client[4], '%a %b %d %k:%M:%S %Y')
client
end
def parse_route(route)
route[3] = DateTime.strptime(route[3], '%a %b %d %k:%M:%S %Y')
route
end
def parse_global(global)
global[1] = global[1].to_i
global
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
require_relative 'modern_stateless'
module OpenVPNStatusWeb
module Parser
class V2
def parse_status_log(text)
OpenVPNStatusWeb::Parser::ModernStateless.parse_status_log(text, ',')
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
require_relative 'modern_stateless'
module OpenVPNStatusWeb
module Parser
class V3
def parse_status_log(text)
OpenVPNStatusWeb::Parser::ModernStateless.parse_status_log(text, "\t")
end
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module OpenVPNStatusWeb
class Status
attr_accessor :client_list
attr_accessor :routing_table
attr_accessor :global_stats
end
end

View File

@@ -1,4 +1,5 @@
# frozen_string_literal: true
module OpenVPNStatusWeb module OpenVPNStatusWeb
VERSION = "0.0.1" VERSION = '3.0.1'
end end

View File

@@ -1,31 +1,42 @@
# frozen_string_literal: true
$:.push File.expand_path("../lib", __FILE__) require_relative 'lib/openvpn-status-web/version'
require 'openvpn-status-web/version'
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = 'openvpn-status-web' s.name = 'openvpn-status-web'
s.version = OpenVPNStatusWeb::VERSION s.version = OpenVPNStatusWeb::VERSION
s.summary = 'openvpn-status-web' s.summary = 'openvpn-status-web'
s.description = 'Small Rack application that parses and serves the OpenVPN status file.' s.description = 'Small Rack (Ruby) application serving OpenVPN status file.'
s.author = 'Christian Nicolai' s.author = 'Christian Nicolai'
s.email = 'chrnicolai@gmail.com'
s.license = 'Apache License Version 2.0'
s.homepage = 'https://github.com/cmur2/openvpn-status-web'
s.files = `git ls-files`.split($/) s.homepage = 'https://github.com/cmur2/openvpn-status-web'
s.test_files = s.files.grep(%r{^(test|spec|features)/}) s.license = 'Apache-2.0'
s.metadata = {
'bug_tracker_uri' => "#{s.homepage}/issues",
'source_code_uri' => s.homepage
}
s.files = `git ls-files -z`.split("\x0").select do |f|
f.match(%r{^(init.d|lib)/})
end
s.require_paths = ['lib'] s.require_paths = ['lib']
s.bindir = 'exe'
s.executables = ['openvpn-status-web'] s.executables = ['openvpn-status-web']
s.extra_rdoc_files = Dir['README.md', 'LICENSE']
s.required_ruby_version = '>= 2.5'
s.add_runtime_dependency 'rack'
s.add_runtime_dependency 'json'
s.add_runtime_dependency 'metriks' s.add_runtime_dependency 'metriks'
s.add_runtime_dependency 'rack', '~> 2.0'
s.add_runtime_dependency 'webrick', '>= 1.6.1'
s.add_development_dependency 'bundler', '~> 1.3' s.add_development_dependency 'better_errors'
s.add_development_dependency 'binding_of_caller'
s.add_development_dependency 'bundler'
s.add_development_dependency 'bundler-audit', '~> 0.7.0'
s.add_development_dependency 'rack-test'
s.add_development_dependency 'rake' s.add_development_dependency 'rake'
s.add_development_dependency 'rspec' s.add_development_dependency 'rspec'
s.add_development_dependency 'rack-test' s.add_development_dependency 'rubocop', '~> 0.92.0'
s.add_development_dependency 'solargraph'
end end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
require_relative '../spec_helper'
describe OpenVPNStatusWeb::Parser::ModernStateless do
{
2 => status_v2,
3 => status_v3
}.each do |version, status|
context "for status-version #{version}" do
context 'for client list' do
it 'parses common names' do
expect(status.client_list.map { |client| client[0] }).to eq(%w[foo bar])
end
it 'parses real addresses' do
expect(status.client_list.map { |client| client[1] }).to eq(['1.2.3.4:1234', '1.2.3.5:1235'])
end
it 'parses received bytes' do
expect(status.client_list.map { |client| client[2] }).to eq([11_811_160_064, 512])
end
it 'parses sent bytes' do
expect(status.client_list.map { |client| client[3] }).to eq([4_194_304, 2048])
end
it 'parses connected since date' do
expect(status.client_list.map { |client| client[4] }).to eq(
[
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0)
]
)
end
end
context 'for routing table' do
it 'parses virtual addresses' do
expect(status.routing_table.map { |route| route[0] }).to eq(['192.168.0.0/24', '192.168.66.2', '192.168.66.3', '2001:db8:0:0::1000'])
end
it 'parses common names' do
expect(status.routing_table.map { |route| route[1] }).to eq(%w[foo bar foo bar])
end
it 'parses real addresses' do
expect(status.routing_table.map { |route| route[2] }).to eq(['1.2.3.4:1234', '1.2.3.5:1235', '1.2.3.4:1234', '1.2.3.5:1235'])
end
it 'parses last ref date' do
expect(status.routing_table.map { |route| route[3] }).to eq(
[
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0),
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0)
]
)
end
end
it 'parses global stats' do
expect(status.global_stats.size).to eq(1)
expect(status.global_stats.first).to eq(['Max bcast/mcast queue length', 42])
end
end
end
end

63
spec/parser/v1_spec.rb Normal file
View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require_relative '../spec_helper'
describe OpenVPNStatusWeb::Parser::V1 do
def status
status_v1
end
context 'for client list' do
it 'parses common names' do
expect(status.client_list.map { |client| client[0] }).to eq(%w[foo bar])
end
it 'parses real addresses' do
expect(status.client_list.map { |client| client[1] }).to eq(['1.2.3.4:1234', '1.2.3.5:1235'])
end
it 'parses received bytes' do
expect(status.client_list.map { |client| client[2] }).to eq([11_811_160_064, 512])
end
it 'parses sent bytes' do
expect(status.client_list.map { |client| client[3] }).to eq([4_194_304, 2048])
end
it 'parses connected since date' do
expect(status.client_list.map { |client| client[4] }).to eq(
[
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0)
]
)
end
end
context 'for routing table' do
it 'parses virtual addresses' do
expect(status.routing_table.map { |route| route[0] }).to eq(['192.168.0.0/24', '192.168.66.2', '192.168.66.3', '2001:db8:0:0::1000'])
end
it 'parses common names' do
expect(status.routing_table.map { |route| route[1] }).to eq(%w[foo bar foo bar])
end
it 'parses real addresses' do
expect(status.routing_table.map { |route| route[2] }).to eq(['1.2.3.4:1234', '1.2.3.5:1235', '1.2.3.4:1234', '1.2.3.5:1235'])
end
it 'parses last ref date' do
expect(status.routing_table.map { |route| route[3] }).to eq(
[
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0),
DateTime.new(2012, 1, 1, 23, 42, 0), DateTime.new(2012, 1, 1, 23, 42, 0)
]
)
end
end
it 'parses global stats' do
expect(status.global_stats.size).to eq(1)
expect(status.global_stats.first).to eq(['Max bcast/mcast queue length', 42])
end
end

22
spec/spec_helper.rb Normal file
View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rubygems'
require 'bundler/setup'
require 'rack/test'
require 'openvpn-status-web'
def status_v1
text = File.open('examples/status.v1', 'rb', &:read)
OpenVPNStatusWeb::Parser::V1.new.parse_status_log text
end
def status_v2
text = File.open('examples/status.v2', 'rb', &:read)
OpenVPNStatusWeb::Parser::V2.new.parse_status_log text
end
def status_v3
text = File.open('examples/status.v3', 'rb', &:read)
OpenVPNStatusWeb::Parser::V3.new.parse_status_log text
end