securevpn-xyz-http-hooks 1.3.3

Sign up to get free protection for your applications and to get access to all the features.
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