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.
- 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
|