securevpn-xyz-http-hooks 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/LICENSE +19 -0
- data/README.md +22 -0
- data/bin/openvpn-activate +6 -0
- data/bin/openvpn-authenticate +8 -0
- data/bin/openvpn-connect +6 -0
- data/bin/openvpn-disconnect +7 -0
- data/lib/api/activation.rb +37 -0
- data/lib/api/authentication.rb +23 -0
- data/lib/api/billing.rb +55 -0
- data/lib/api/connect.rb +21 -0
- data/lib/api/connection.rb +70 -0
- data/lib/api/disconnect.rb +21 -0
- data/lib/exceptions/option_not_found.rb +2 -0
- data/lib/openvpn-http-hooks.rb +19 -0
- data/lib/openvpn_password_authenticator.rb +18 -0
- data/lib/option/base.rb +30 -0
- data/lib/option/i2p.rb +28 -0
- data/lib/option/proxy.rb +66 -0
- data/lib/option/repository.rb +18 -0
- data/lib/signer.rb +7 -0
- data/lib/system/openvpn_status.rb +19 -0
- data/lib/system/openvpn_status_log_parser.rb +30 -0
- data/lib/system/openvpn_status_log_reader.rb +29 -0
- data/openvpn-http-hooks.gemspec +16 -0
- data/spec/fixtures/active_connections.txt +10 -0
- data/spec/fixtures/multiple_connections.txt +11 -0
- data/spec/lib/api/activation_spec.rb +30 -0
- data/spec/lib/api/authentication_spec.rb +29 -0
- data/spec/lib/api/billing_spec.rb +71 -0
- data/spec/lib/api/connect_spec.rb +40 -0
- data/spec/lib/api/connection_spec.rb +59 -0
- data/spec/lib/api/disconnect_spec.rb +40 -0
- data/spec/lib/openvpn_password_authenticator_spec.rb +52 -0
- data/spec/lib/option/base_spec.rb +26 -0
- data/spec/lib/option/i2p_spec.rb +19 -0
- data/spec/lib/option/proxy_spec.rb +32 -0
- data/spec/lib/option/repository_spec.rb +25 -0
- data/spec/lib/signer_spec.rb +21 -0
- data/spec/lib/system/openvpn_status_log_parser_spec.rb +25 -0
- data/spec/lib/system/openvpn_status_log_reader_spec.rb +33 -0
- data/spec/lib/system/openvpn_status_spec.rb +22 -0
- data/spec/spec_helper.rb +9 -0
- 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
|
data/lib/option/proxy.rb
ADDED
@@ -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,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
|