securevpn-xyz-http-hooks 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +39 -0
  5. data/LICENSE +19 -0
  6. data/README.md +22 -0
  7. data/bin/openvpn-activate +6 -0
  8. data/bin/openvpn-authenticate +8 -0
  9. data/bin/openvpn-connect +6 -0
  10. data/bin/openvpn-disconnect +7 -0
  11. data/lib/api/activation.rb +37 -0
  12. data/lib/api/authentication.rb +23 -0
  13. data/lib/api/billing.rb +55 -0
  14. data/lib/api/connect.rb +21 -0
  15. data/lib/api/connection.rb +70 -0
  16. data/lib/api/disconnect.rb +21 -0
  17. data/lib/exceptions/option_not_found.rb +2 -0
  18. data/lib/openvpn-http-hooks.rb +19 -0
  19. data/lib/openvpn_password_authenticator.rb +18 -0
  20. data/lib/option/base.rb +30 -0
  21. data/lib/option/i2p.rb +28 -0
  22. data/lib/option/proxy.rb +66 -0
  23. data/lib/option/repository.rb +18 -0
  24. data/lib/signer.rb +7 -0
  25. data/lib/system/openvpn_status.rb +19 -0
  26. data/lib/system/openvpn_status_log_parser.rb +30 -0
  27. data/lib/system/openvpn_status_log_reader.rb +29 -0
  28. data/openvpn-http-hooks.gemspec +16 -0
  29. data/spec/fixtures/active_connections.txt +10 -0
  30. data/spec/fixtures/multiple_connections.txt +11 -0
  31. data/spec/lib/api/activation_spec.rb +30 -0
  32. data/spec/lib/api/authentication_spec.rb +29 -0
  33. data/spec/lib/api/billing_spec.rb +71 -0
  34. data/spec/lib/api/connect_spec.rb +40 -0
  35. data/spec/lib/api/connection_spec.rb +59 -0
  36. data/spec/lib/api/disconnect_spec.rb +40 -0
  37. data/spec/lib/openvpn_password_authenticator_spec.rb +52 -0
  38. data/spec/lib/option/base_spec.rb +26 -0
  39. data/spec/lib/option/i2p_spec.rb +19 -0
  40. data/spec/lib/option/proxy_spec.rb +32 -0
  41. data/spec/lib/option/repository_spec.rb +25 -0
  42. data/spec/lib/signer_spec.rb +21 -0
  43. data/spec/lib/system/openvpn_status_log_parser_spec.rb +25 -0
  44. data/spec/lib/system/openvpn_status_log_reader_spec.rb +33 -0
  45. data/spec/lib/system/openvpn_status_spec.rb +22 -0
  46. data/spec/spec_helper.rb +9 -0
  47. metadata +133 -0
data/lib/option/i2p.rb ADDED
@@ -0,0 +1,28 @@
1
+ module Option
2
+ class I2p < Base
3
+ def activate!
4
+ add_firewall_routes
5
+ end
6
+
7
+ def deactivate!
8
+ remove_firewall_routes
9
+ end
10
+
11
+ private
12
+
13
+ def add_firewall_routes
14
+ system "/sbin/iptables -t filter -D INPUT -p tcp -m tcp --dport 8118 -j DROP"
15
+ system "/sbin/iptables -t filter -A INPUT -s #{virtual_ip} -p tcp -m tcp --dport 8118 -j ACCEPT"
16
+ system "/sbin/iptables -t filter -A INPUT -p tcp -m tcp --dport 8118 -j DROP"
17
+ system "/sbin/iptables -t nat -A PREROUTING -p udp -m udp -s #{virtual_ip} --dport 53 -j DNAT --to-destination #{server_virtual_ip}:53"
18
+ system "/sbin/iptables -t nat -A PREROUTING -p tcp -m tcp -s #{virtual_ip} --dport 53 -j DNAT --to-destination #{server_virtual_ip}:53"
19
+ true
20
+ end
21
+
22
+ def remove_firewall_routes
23
+ system "/sbin/iptables -D INPUT -s #{virtual_ip} -p tcp -m tcp --dport 8118 -j ACCEPT"
24
+ system "/sbin/iptables -t nat -D PREROUTING -p udp -m udp -s #{virtual_ip} --dport 53 -j DNAT --to-destination #{server_virtual_ip}:53"
25
+ system "/sbin/iptables -t nat -D PREROUTING -p tcp -m tcp -s #{virtual_ip} --dport 53 -j DNAT --to-destination #{server_virtual_ip}:53"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ module Option
2
+ class Proxy < Base
3
+ def activate!
4
+ relay_port = start_proxy_daemon
5
+ add_firewall_rules(relay_port)
6
+ end
7
+
8
+ def deactivate!
9
+ relay_port = relay_manager.free(virtual_ip)
10
+ remove_firewall_rules(relay_port)
11
+ kill_proxy_daemon(relay_port)
12
+ end
13
+
14
+ private
15
+
16
+ def start_proxy_daemon
17
+ relay_port = relay_manager.next_available_port
18
+ relay_manager.lock(virtual_ip, relay_port)
19
+ system "/etc/openvpn/any_proxy -l :#{relay_port} -p '#{attributes['host']}:#{attributes['port']}' &"
20
+ relay_port
21
+ end
22
+
23
+ def add_firewall_rules(relay_port)
24
+ system "iptables -A PREROUTING -t nat -s #{virtual_ip}/32 -p tcp --dport 80 -j REDIRECT --to-port #{relay_port}"
25
+ true
26
+ end
27
+
28
+ def kill_proxy_daemon(relay_port)
29
+ pids = `ps -ef | grep #{relay_port} | awk '{ print $2 }'`
30
+ relay_pid = pids.split( /\r?\n/ ).first
31
+ `kill #{relay_pid} &`
32
+ true
33
+ end
34
+
35
+ def remove_firewall_rules(relay_port)
36
+ system "iptables -D PREROUTING -t nat -s #{virtual_ip}/32 -p tcp --dport 80 -j REDIRECT --to-port #{relay_port}"
37
+ end
38
+
39
+ def relay_manager
40
+ RelayManager.new
41
+ end
42
+
43
+ class RelayManager
44
+ RELAY_PORTS = [8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099, 8010]
45
+ STATUS_FILE = '/etc/openvpn/relays.txt'
46
+
47
+ def lock(host, port)
48
+ File.open(STATUS_FILE, 'a') { |f| f.puts("#{host} #{port} ") }
49
+ end
50
+
51
+ def free(host)
52
+ list = File.readlines(STATUS_FILE).map { |l| l.split(' ') }
53
+ port = list.find { |e| e[0] == host }[1]
54
+ updated_list = list.reject { |e| e[0] == host }.join()
55
+ File.open(STATUS_FILE, 'w') { |file| file.write(updated_list) }
56
+ port
57
+ end
58
+
59
+ def next_available_port
60
+ used_ports = File.readlines(STATUS_FILE).map { |l| l.split(' ')[1] }
61
+ available_ports = RELAY_PORTS - used_ports
62
+ available_ports.sample
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ module Option
2
+ class Repository
3
+ class << self
4
+ def find_by_code(code)
5
+ index[code.to_s] || raise(OptionNotFound)
6
+ end
7
+
8
+ private
9
+
10
+ def index
11
+ @index ||= {
12
+ 'i2p' => Option::I2p,
13
+ 'proxy' => Option::Proxy
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/signer.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'digest/md5'
2
+
3
+ class Signer
4
+ def self.sign_hash(hash, key)
5
+ Digest::MD5.hexdigest("#{hash.values.sort.join}#{key}")
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module System
2
+ class OpenvpnStatus
3
+ attr_accessor :clients_list
4
+
5
+ class << self
6
+ def current_virtual_address
7
+ ENV['ifconfig_pool_remote_ip']
8
+ end
9
+
10
+ def server_virtual_address
11
+ ENV['ifconfig_local']
12
+ end
13
+ end
14
+
15
+ def initialize
16
+ @clients_list = {}
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module System
2
+ class OpenvpnStatusLogParser
3
+ attr_accessor :status, :clients_block, :text
4
+
5
+ def initialize(text)
6
+ @status = System::OpenvpnStatus.new
7
+ @clients_block = false
8
+ @text = text
9
+ parse
10
+ end
11
+
12
+ def parse
13
+ @text.lines.each do |line|
14
+ line_tokens = line.strip.split(',')
15
+ parse_client_virtual_ips(line_tokens)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def parse_client_virtual_ips(tokens)
22
+ @clients_block = false if tokens[0] == 'GLOBAL STATS'
23
+ if clients_block
24
+ common_name, ip_address = tokens[1], tokens[0]
25
+ @status.clients_list[common_name] = ip_address
26
+ end
27
+ @clients_block = true if tokens[0] == 'Virtual Address'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ module System
2
+ class OpenvpnStatusLogReader
3
+ LOGFILE_PATH = '/etc/openvpn/openvpn-status.log'
4
+
5
+ attr_accessor :log_content
6
+
7
+ class << self
8
+ def vpn_ip(common_name)
9
+ reader = new
10
+ reader.vpn_ip_for common_name
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ read_logfile
16
+ end
17
+
18
+ def vpn_ip_for(common_name)
19
+ status = System::OpenvpnStatusLogParser.new(log_content).status
20
+ status.clients_list[common_name]
21
+ end
22
+
23
+ private
24
+
25
+ def read_logfile
26
+ @log_content = File.read(LOGFILE_PATH)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'securevpn-xyz-http-hooks'
3
+ s.version = '1.3.3'
4
+ s.date = '2014-04-27'
5
+ s.summary = "HTTP hooks for OpenVPN server on securevpn.xyz"
6
+ s.description = "Trigger on openvpn events and notify HTTP API for securevpn.xyz"
7
+ s.authors = ["Victor Ivanov at securevpn.xyz"]
8
+ s.email = 'admin@securevpn.xyz'
9
+ s.files = `git ls-files`.split($/)
10
+ s.homepage = 'https://securevpn.xyz'
11
+ s.executables = ['openvpn-activate', 'openvpn-authenticate', 'openvpn-connect', 'openvpn-disconnect']
12
+
13
+ s.add_development_dependency 'rspec', ['>= 0']
14
+ s.add_development_dependency 'mocha'
15
+ s.add_development_dependency 'webmock'
16
+ end
@@ -0,0 +1,10 @@
1
+ OpenVPN CLIENT LIST
2
+ Updated,Fri Jun 6 16:42:48 2014
3
+ Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
4
+ e0c187715fa9e1bb9fd96882dfa7af22,5.57.222.68:52248,294554,526171,Fri Jun 6 16:42:02 2014
5
+ ROUTING TABLE
6
+ Virtual Address,Common Name,Real Address,Last Ref
7
+ 10.77.2.6,e0c187715fa9e1bb9fd96882dfa7af22,5.57.222.68:52248,Fri Jun 6 16:42:47 2014
8
+ GLOBAL STATS
9
+ Max bcast/mcast queue length,0
10
+ END
@@ -0,0 +1,11 @@
1
+ OpenVPN CLIENT LIST
2
+ Updated,Fri Jun 6 16:42:48 2014
3
+ Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
4
+ e0c187715fa9e1bb9fd96882dfa7af22,5.57.222.68:52248,294554,526171,Fri Jun 6 16:42:02 2014
5
+ ROUTING TABLE
6
+ Virtual Address,Common Name,Real Address,Last Ref
7
+ 10.77.2.6,login1,5.57.222.68:52248,Fri Jun 6 16:42:47 2014
8
+ 10.77.2.5,login2,5.57.222.68:52248,Fri Jun 6 16:42:47 2014
9
+ GLOBAL STATS
10
+ Max bcast/mcast queue length,0
11
+ END
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Activation do
4
+ subject { described_class.new }
5
+
6
+ describe '.activate' do
7
+ context 'successful activation' do
8
+ before do
9
+ stub_request(:post, 'api.securevpn.xyz/api/activate')
10
+ end
11
+
12
+ it 'saves key' do
13
+ subject.expects(:save_auth_key)
14
+ subject.activate
15
+ end
16
+ end
17
+
18
+ context 'not successful activation' do
19
+ before do
20
+ stub_request(:post, 'api.securevpn.xyz/api/activate').to_return(status: '404')
21
+ end
22
+
23
+ it 'raises error' do
24
+ expect {
25
+ subject.activate
26
+ }.to raise_error
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Authentication do
4
+ subject { described_class.new('login', 'password') }
5
+
6
+ describe '.valid_credentials?' do
7
+ before do
8
+ subject.stubs(:auth_key).returns('key')
9
+ stub_request(:post, 'api.securevpn.xyz/api/auth').
10
+ to_return(status: status)
11
+ end
12
+
13
+ context 'status 200' do
14
+ let(:status) { 200 }
15
+
16
+ it 'returns true' do
17
+ expect(subject.valid_credentials?).to be_true
18
+ end
19
+ end
20
+
21
+ context 'status 404' do
22
+ let(:status) { 404 }
23
+
24
+ it 'returns false' do
25
+ expect(subject.valid_credentials?).to be_false
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Billing do
4
+ subject { described_class.new }
5
+
6
+ it "returns api url" do
7
+ expect(subject.host_with_port).to include "http://"
8
+ end
9
+
10
+ it "reads file with auth key" do
11
+ File.stubs(:read).with(described_class::KEY_PATH).returns("key")
12
+ expect(subject.auth_key).to eq "key"
13
+ end
14
+
15
+ it "returns machine hostname" do
16
+ Socket.stubs(:gethostname).returns("hostname.dev")
17
+ expect(subject.hostname).to eq "hostname.dev"
18
+ end
19
+
20
+ describe ".success_api_call?" do
21
+ let(:api_call_result) { mock() }
22
+
23
+ before do
24
+ api_call_result.stubs(:code).returns(code)
25
+ subject.stubs(:api_call_result).returns(api_call_result)
26
+ end
27
+
28
+ context "result is 404" do
29
+ let(:code) { "404" }
30
+
31
+ it "api call is not successful" do
32
+ expect(subject.success_api_call?).to be_false
33
+ end
34
+ end
35
+
36
+ context "result is 200" do
37
+ let(:code) { "200" }
38
+
39
+ it "api call is successful" do
40
+ expect(subject.success_api_call?).to be_true
41
+ end
42
+ end
43
+ end
44
+
45
+ describe ".api_call_result" do
46
+ before do
47
+ stub_request(:post, 'api.securevpn.xyz/api/auth')
48
+ end
49
+
50
+ it "fetches HTTP response" do
51
+ subject.stubs(:action).returns("auth")
52
+ subject.stubs(:data).returns({})
53
+ subject.stubs(:auth_key).returns("key")
54
+
55
+ subject.api_call_result
56
+ expect(subject.success_api_call?).to be_true
57
+ end
58
+ end
59
+
60
+ it "returns URI for api call" do
61
+ action = "auth"
62
+ subject.stubs(:action).returns(action)
63
+ expect(subject.uri).to eq URI("http://#{described_class::API_HOST}/api/#{action}")
64
+ end
65
+
66
+ it "adds signature to params hash" do
67
+ subject.stubs(:auth_key).returns("key")
68
+ subject.stubs(:data).returns({})
69
+ expect(subject.signed_data).not_to be_empty
70
+ end
71
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Connect do
4
+ subject { described_class.new }
5
+
6
+ describe '#invoke' do
7
+ before do
8
+ subject.stubs(:response).returns(
9
+ JSON.parse(
10
+ '{"id":101,"common_name":"login","options":["i2p", "proxy"],"option_attributes":{"i2p":{"attr1": "value1"},"proxy":{"attr2": "value2"}}}'
11
+ )
12
+ )
13
+ subject.stubs(:trigger_script_return)
14
+ subject.stubs(:success_api_call?).returns(api_call_validation_result)
15
+ end
16
+
17
+ context 'successfull script call' do
18
+ let(:api_call_validation_result) { true }
19
+
20
+ it 'activates options' do
21
+ Option::I2p.any_instance.expects(:activate!)
22
+ Option::Proxy.any_instance.expects(:activate!)
23
+ subject.invoke
24
+ end
25
+ end
26
+
27
+ context 'unsuccessfull call' do
28
+ let(:api_call_validation_result) { false }
29
+
30
+ it 'does not activate options' do
31
+ Option::I2p.any_instance.expects(:activate!).never
32
+ subject.invoke
33
+ end
34
+ end
35
+ end
36
+
37
+ it 'represents connect action' do
38
+ expect(subject.action).to eq 'connect'
39
+ end
40
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Connection do
4
+ subject { described_class.new }
5
+
6
+ describe '.invoke_if_valid_api_call' do
7
+ before do
8
+ subject.stubs(:success_api_call?).returns(success_status)
9
+ subject.expects(:exit).with(exit_status)
10
+ end
11
+
12
+ context 'success' do
13
+ let(:success_status) { true }
14
+ let(:exit_status) { 0 }
15
+
16
+ it 'exits with zero status' do
17
+ subject.invoke_if_valid_api_call { "empty block" }
18
+ end
19
+ end
20
+
21
+ context 'failure' do
22
+ let(:success_status) { false }
23
+ let(:exit_status) { 1 }
24
+
25
+ it 'exits with non zero status' do
26
+ subject.invoke_if_valid_api_call
27
+ end
28
+ end
29
+ end
30
+
31
+ describe 'attributes' do
32
+ let(:option_name) { "i2p" }
33
+ let(:common_name) { "login" }
34
+
35
+ before do
36
+ subject.stubs(:response).returns(
37
+ JSON.parse(
38
+ "{\"id\":101,\"common_name\":\"#{common_name}\",\"options\":[\"#{option_name}\", \"proxy\"],\"option_attributes\":{\"i2p\":{\"attr1\": \"value1\"},\"proxy\":{\"attr2\": \"value2\"}}}"
39
+ )
40
+ )
41
+ end
42
+
43
+ describe '.options' do
44
+ it 'returns options array' do
45
+ expect(subject.options.class).to eq Hash
46
+ end
47
+
48
+ it 'includes option name' do
49
+ expect(subject.options['i2p'][:option_class]).to eq Option::I2p
50
+ end
51
+ end
52
+
53
+ describe '.common_name' do
54
+ it 'returns common name' do
55
+ expect(subject.common_name).to eq common_name
56
+ end
57
+ end
58
+ end
59
+ end