diff --git a/.rubocop.yml b/.rubocop.yml index b1348c6..fb3a468 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,8 +4,5 @@ Metrics/BlockLength: Naming/AccessorMethodName: Enabled: false -Style/Documentation: - Enabled: false - Style/FrozenStringLiteralComment: Enabled: false diff --git a/Gemfile b/Gemfile index 6426c8f..b4ff83d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,9 @@ source 'https://rubygems.org' gem 'faraday', '~> 2.12', '>= 2.12.2' +gem 'json', '~> 2.9', '>= 2.9.1' +gem 'logger', '~> 1.6', '>= 1.6.4' +gem 'open3', '~> 0.2.1' gem 'rubocop', '~> 1.69', '>= 1.69.2', require: false +gem 'uri', '~> 1.0', '>= 1.0.2' +gem 'yaml', '~> 0.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 4e41c11..8066d9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,7 @@ GEM logger (1.6.4) net-http (0.6.0) uri + open3 (0.2.1) parallel (1.26.3) parser (3.3.6.0) ast (~> 2.4.1) @@ -37,6 +38,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.2) + yaml (0.4.0) PLATFORMS arm64-darwin-24 @@ -44,7 +46,12 @@ PLATFORMS DEPENDENCIES faraday (~> 2.12, >= 2.12.2) + json (~> 2.9, >= 2.9.1) + logger (~> 1.6, >= 1.6.4) + open3 (~> 0.2.1) rubocop (~> 1.69, >= 1.69.2) + uri (~> 1.0, >= 1.0.2) + yaml (~> 0.4.0) BUNDLED WITH 2.5.22 diff --git a/README.md b/README.md index e2251a4..7b339f7 100644 --- a/README.md +++ b/README.md @@ -5,31 +5,31 @@ A tool for keeping ProtonVPN, OPNsense, and qBittorrent forwarded ports in sync. > This is beta software. I'm not responsible for any issues you may encounter. ## Purpose -This tool helps automate port forwarding from ProtonVPN to qBittorrent via OPNsense. The tool polls ProtonVPN for the given forwarded port, checks the port set in OPNsense and qBittorrent, and updates it if necessary. You can ignore qBittorrent by using the `QBIT_SKIP` environment variable. +This tool helps automate port forwarding from ProtonVPN to qBittorrent via OPNsense. The tool polls ProtonVPN for the given forwarded port, checks the port set in OPNsense and qBittorrent, and updates it if necessary. -Version v0.5 and later allows you to skip qBittorrent and just sync Proton's forwarded port to OPNsense. +You can ignore qBittorrent by using the `QBIT_SKIP` environment variable. ## Installation I recommend using the provided Docker Compose file to simplify the set up of qbop. The Docker container is available here: https://github.com/clajiness/qbop/pkgs/container/qbop -#### Requirements +### Requirements * Docker Engine - https://docs.docker.com/engine/install/ * OPNsense - This is the tutorial I used to set up selective routing to ProtonVPN. https://docs.opnsense.org/manual/how-tos/wireguard-selective-routing.html * qBittorrent * ProtonVPN subscription -#### Config +### Config The given environment variables are required. 1. `LOOP_FREQ:` This value determines how often the script runs. The default value is 45 seconds. This probably shouldn't be changed. -2. `PROTON_GATEWAY:` The IP address of your ProtonVPN gateway. For example, `10.2.0.1`. -3. `OPN_INTERFACE_ADDR:` The IP address of your OPNsense interface. For example, `https://10.1.1.1`. -4. `OPN_API_KEY:` Your OPNsense API key - https://docs.opnsense.org/development/how-tos/api.html -5. `OPN_API_SECRET:` Your OPNsense API secret +2. `PROTON_GATEWAY:` Usually 10.2.0.1. Do not use http(s):// or a trailing slash. +3. `OPN_INTERFACE_ADDR:` OPNsense Interface Address. Requires http(s):// and no trailing slash. +4. `OPN_API_KEY:` OPNsense API Key - https://docs.opnsense.org/development/how-tos/api.html +5. `OPN_API_SECRET:` OPNsense API Secret 6. `OPN_PROTON_ALIAS_NAME:` The firewall alias that you use for ProtonVPN's forwarded port. For example, `proton_vpn_forwarded_port`. -7. `QBIT_SKIP:` [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not necessary. -8. `QBIT_ADDR:` The IP address of your qBittorrent app. For example, `http://10.1.1.100:8080`. -9. `QBIT_USER:` Your qBittorrent username -10. `QBIT_PASS:` Your qBittorrent password +7. `QBIT_SKIP:` [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not required. +8. `QBIT_ADDR:` The IP address of your qBittorrent app. Requires http(s):// and no trailing slash. +9. `QBIT_USER:` qBittorrent username +10. `QBIT_PASS:` qBittorrent password diff --git a/docker-compose.yml b/docker-compose.yml index 66093b4..2441e14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,25 +8,25 @@ services: - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro environment: - # This value determines how often the script runs. The default value is 45 seconds. + # This value determines how often the script runs. The default value is 45 seconds. - LOOP_FREQ= - # Usually 10.2.0.1. Do not use http(s):// or a trailing slash. + # Usually 10.2.0.1. Do not use http(s):// or a trailing slash. - PROTON_GATEWAY= - # OPNsense Interface Address. Requires http(s):// and no trailing slash. + # OPNsense Interface Address. Requires http(s):// and no trailing slash. - OPN_INTERFACE_ADDR= - # OPNsense API Key + # OPNsense API Key - OPN_API_KEY= - # OPNsense API Secret + # OPNsense API Secret - OPN_API_SECRET= - # The firewall alias that you use for ProtonVPN's forwarded port + # The firewall alias that you use for ProtonVPN's forwarded port - OPN_PROTON_ALIAS_NAME= - # [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not necessary. + # [true/false] Skip qBittorrent. If true, subsequent qBit environment variables are not required. - QBIT_SKIP= - # The IP address of your qBittorrent app. Requires http(s):// and no trailing slash. + # The IP address of your qBittorrent app. Requires http(s):// and no trailing slash. - QBIT_ADDR= - # Your qBittorrent username + # qBittorrent username - QBIT_USER= - # Your qBittorrent password + # qBittorrent password - QBIT_PASS= volumes: diff --git a/qbop.rb b/qbop.rb index f9f4c52..f03df07 100644 --- a/qbop.rb +++ b/qbop.rb @@ -1,53 +1,19 @@ require 'bundler/setup' Bundler.require(:default) -require 'json' -require 'yaml' -require 'logger' Dir['./service/*.rb'].sort.each { |file| require_relative file } -def parse_version - return unless File.exist?('version.yml') +helpers = Service::Helpers.new - YAML.safe_load(File.read('version.yml')) -end +# collect env variables in a config variable +config = helpers.env_variables -# get version number of qbop -script_version = parse_version['version'] +# collect version number of qbop in a script_version variable +script_version = helpers.version -# LOGGER +# set up logger @logger = Logger.new('log/qbop.log', 10, 1_024_000) @logger.info("starting qbop v#{script_version}") - -def exit_script - @logger.info("qbop completed at #{Time.now}") - @logger.info('----------') - @logger.close - exit -end -# ---------- - -# ENV -# get env -def parse_env # rubocop:disable Metrics/MethodLength - { - loop_freq: ENV['LOOP_FREQ'] || 45, - proton_gateway: ENV['PROTON_GATEWAY'], - opnsense_interface_addr: ENV['OPN_INTERFACE_ADDR'], - opnsense_api_key: ENV['OPN_API_KEY'], - opnsense_api_secret: ENV['OPN_API_SECRET'], - opnsense_alias_name: ENV['OPN_PROTON_ALIAS_NAME'], - qbit_skip: ENV['QBIT_SKIP'], - qbit_addr: ENV['QBIT_ADDR'], - qbit_user: ENV['QBIT_USER'], - qbit_pass: ENV['QBIT_PASS'] - } -end - -# parse config -config = parse_env - @logger.info('----------') -# ---------- # DO SOME WORK! @@ -65,10 +31,16 @@ def parse_env # rubocop:disable Metrics/MethodLength proton ||= Service::Proton.new # make natpmpc call to proton - proton_response = proton.proton_natpmpc(config[:proton_gateway]) + response = proton.proton_natpmpc(config[:proton_gateway]) + + # raise error if stderr is not empty + raise StandardError, response[:stderr].chomp unless response[:stderr].empty? + + # get proton response from standard output + proton_response = response[:stdout] # parse natpmpc response - forwarded_port = proton.parse_proton_response(proton_response) + forwarded_port = proton.parse_proton_response(proton_response.chomp) # sleep and restart loop if forwarded port isn't returned if forwarded_port.nil? @@ -204,7 +176,8 @@ def parse_env # rubocop:disable Metrics/MethodLength end # sleep before looping again - @logger.info("end of loop. sleeping for #{config[:loop_freq].to_i} seconds.") + @logger.info('end of loop') + @logger.info("sleeping for #{config[:loop_freq].to_i} seconds...") @logger.info('----------') sleep config[:loop_freq].to_i end diff --git a/service/helpers.rb b/service/helpers.rb new file mode 100644 index 0000000..090351e --- /dev/null +++ b/service/helpers.rb @@ -0,0 +1,30 @@ +module Service + # the Helpers class provides methods for setting environment variables and the script version + class Helpers + def initialize + @env_variables = env_variables + @version = version + end + + def env_variables # rubocop:disable Metrics/MethodLength + { + loop_freq: ENV['LOOP_FREQ'] || 45, + proton_gateway: ENV['PROTON_GATEWAY'], + opnsense_interface_addr: ENV['OPN_INTERFACE_ADDR'], + opnsense_api_key: ENV['OPN_API_KEY'], + opnsense_api_secret: ENV['OPN_API_SECRET'], + opnsense_alias_name: ENV['OPN_PROTON_ALIAS_NAME'], + qbit_skip: ENV['QBIT_SKIP'], + qbit_addr: ENV['QBIT_ADDR'], + qbit_user: ENV['QBIT_USER'], + qbit_pass: ENV['QBIT_PASS'] + } + end + + def version + return unless File.exist?('version.yml') + + YAML.safe_load(File.read('version.yml'))['version'] + end + end +end diff --git a/service/opnsense.rb b/service/opnsense.rb index 0575e57..88b9025 100644 --- a/service/opnsense.rb +++ b/service/opnsense.rb @@ -1,4 +1,5 @@ module Service + # the Opnsense class provides methods for getting and setting the OPNsense alias value for the Proton forwarded port class Opnsense def initialize(config) @config = config diff --git a/service/proton.rb b/service/proton.rb index d55bccc..b0c3c91 100644 --- a/service/proton.rb +++ b/service/proton.rb @@ -1,7 +1,12 @@ module Service + # the Proton class provides methods for returning the port that Proton has forwarded class Proton def proton_natpmpc(proton_gateway) - `timeout 5 natpmpc -a 1 0 udp 60 -g #{proton_gateway} && natpmpc -a 1 0 tcp 60 -g #{proton_gateway}` + stdout, stderr, status = Open3.capture3( + "timeout 5 natpmpc -a 1 0 udp 60 -g #{proton_gateway} && natpmpc -a 1 0 tcp 60 -g #{proton_gateway}" + ) + + { stdout: stdout, stderr: stderr, status: status } end def parse_proton_response(proton_response) diff --git a/service/qbit.rb b/service/qbit.rb index f4e50e1..1080aaf 100644 --- a/service/qbit.rb +++ b/service/qbit.rb @@ -1,4 +1,5 @@ module Service + # the Qbit class provides methods for getting and setting the qBit forwarded port class Qbit def initialize(config) @config = config @@ -8,14 +9,16 @@ def initialize(config) ) end - def qbt_auth_login + def qbt_auth_login # rubocop:disable Metrics/MethodLength response = @conn.post do |req| req.url "#{@config[:qbit_addr]}/api/v2/auth/login" req.headers = { 'Content-Type' => 'application/x-www-form-urlencoded' } - req.body = URI.encode_www_form({ - 'username': @config[:qbit_user], - 'password': @config[:qbit_pass] - }) + req.body = URI.encode_www_form( + { + 'username': @config[:qbit_user], + 'password': @config[:qbit_pass] + } + ) end response['set-cookie'].split(';')[0] diff --git a/version.yml b/version.yml index 7377a7b..76e0677 100644 --- a/version.yml +++ b/version.yml @@ -1,2 +1,2 @@ --- -version: 0.7.0 +version: 0.8.0