deploy-agent 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b5fedd1d54542b1fa80f5f083698d4fac13200fd
4
+ data.tar.gz: f2783980bf24d0f54aeef1f2b7894c5f37b33cbb
5
+ SHA512:
6
+ metadata.gz: 986c809ff4bc0655bc586599a14e84cc7708c4f9f2a7f77fec3b57054336af2ec4e0038847483687fb1a85a8bd06f69d9056d0394c67ce0552f9d99c46cca613
7
+ data.tar.gz: 9b3547acefee6e6bd3d4dca8c85baf15aaa791382e75796a413b4bec689cc147711083965bf1d70f3177d65e039fce67d76e9365b514cd7a440e4d76ddbccd0d
data/bin/deploy-agent ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'deploy_agent'
4
+
5
+ DeployAgent::CLI.new.dispatch(ARGV)
@@ -0,0 +1,14 @@
1
+ require 'deploy_agent/certificate_manager'
2
+ require 'deploy_agent/server_connection'
3
+ require 'deploy_agent/destination_connection'
4
+ require 'deploy_agent/cli'
5
+ require 'deploy_agent/agent'
6
+
7
+ module DeployAgent
8
+ CONFIG_PATH = File.expand_path('~/.deploy')
9
+ CERTIFICATE_PATH = File.expand_path('~/.deploy/agent.crt')
10
+ KEY_PATH = File.expand_path('~/.deploy/agent.key')
11
+ CA_PATH = File.expand_path('~/.deploy/ca.crt')
12
+ PID_PATH = File.expand_path('~/.deploy/agent.pid')
13
+ LOG_PATH = File.expand_path('~/.deploy/agent.log')
14
+ end
@@ -0,0 +1,36 @@
1
+ gem 'nio4r', '1.2.1'
2
+ require 'nio'
3
+ require 'logger'
4
+
5
+ module DeployAgent
6
+ class Agent
7
+
8
+ def run
9
+ nio_selector = NIO::Selector.new
10
+ target = ENV['DEPLOY_AGENT_PROXY_IP'] || 'agent.deployhq.com'
11
+ ServerConnection.new(self, target, nio_selector, !ENV['DEPLOY_AGENT_NOVERIFY'])
12
+
13
+ loop do
14
+ nio_selector.select do |monitor|
15
+ monitor.value.rx_data if monitor.readable?
16
+ monitor.value.tx_data if monitor.writeable?
17
+ end
18
+ end
19
+ rescue ServerConnection::ServerDisconnected
20
+ retry
21
+ end
22
+
23
+ def logger
24
+ @logger ||= begin
25
+ if $background
26
+ logger = Logger.new(LOG_PATH, 5, 10240)
27
+ logger.level = Logger::INFO
28
+ logger
29
+ else
30
+ Logger.new(STDOUT)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,85 @@
1
+ gem 'rb-readline', '0.5.4'
2
+ require 'readline'
3
+ require 'net/https'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module DeployAgent
8
+ class CertificateManager
9
+
10
+ def certificate_uri
11
+ URI(ENV['DEPLOY_AGENT_CERTIFICATE_URL'] || 'https://api.deployhq.com/api/v1/agents/create')
12
+ end
13
+
14
+ def generate_certificate
15
+ puts 'This tool will assist you in generating a certificate for your Deploy agent.'
16
+ puts
17
+ if File.file?(CERTIFICATE_PATH)
18
+ puts "***************************** WARNING *****************************"
19
+ puts "The Deploy agent has already been configured. Are you sure you wish"
20
+ puts "to remove the existing certificate and generate a new one?"
21
+ puts
22
+ Readline.completion_proc = Proc.new {}
23
+ begin
24
+ response = Readline.readline("Remove existing certificate? [no]: ", true)
25
+ rescue Interrupt => e
26
+ puts
27
+ Process.exit(1)
28
+ end
29
+ unless response == 'yes'
30
+ Process.exit(1)
31
+ end
32
+ puts
33
+ end
34
+ puts 'Please enter a name for this agent.'
35
+ Readline.completion_proc = Proc.new {}
36
+ begin
37
+ name = Readline.readline("Agent Name: ", true)
38
+ rescue Interrupt => e
39
+ puts
40
+ Process.exit(1)
41
+ end
42
+ if name.length < 2
43
+ puts "Name must be at least 2 characters."
44
+ Process.exit(1)
45
+ else
46
+ uri = certificate_uri
47
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
48
+ request = Net::HTTP::Post.new(uri)
49
+ request.body = {:name => name}.to_json
50
+ request['Content-Type'] = 'application/json'
51
+ response = http.request request
52
+ response_hash = JSON.parse(response.body)
53
+ if response_hash['status'] == 'success'
54
+ FileUtils.mkdir_p(CONFIG_PATH)
55
+ File.write(CA_PATH, response_hash['data']['ca'])
56
+ File.write(CERTIFICATE_PATH, response_hash['data']['certificate'])
57
+ File.write(KEY_PATH, response_hash['data']['private_key'])
58
+ puts
59
+ puts "Certificate has been successfully generated and installed."
60
+ puts
61
+ puts "You can now associate this Agent with your Deploy account."
62
+ puts "Browse to Settings -> Agents in your account and enter the code below:"
63
+ puts
64
+ puts " >> #{response_hash['data']['claim_code']} <<"
65
+ puts
66
+ puts "You can start the agent using the following command:"
67
+ puts
68
+ puts " # deploy-agent start"
69
+ puts
70
+ else
71
+ puts
72
+ puts "An error occurred obtaining a certificate."
73
+ puts "Please contact support, quoting the debug information below:"
74
+ puts
75
+ puts response.inspect
76
+ puts response.body
77
+ puts
78
+ Process.exit(1)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,100 @@
1
+ module DeployAgent
2
+ class CLI
3
+
4
+ def dispatch(arguments)
5
+ methods = self.public_methods(false).delete_if { |n| n == :dispatch }.sort
6
+ if arguments[0] && methods.include?(arguments[0].to_sym)
7
+ public_send(arguments[0])
8
+ else
9
+ puts "Usage: deploy-agent [#{methods.join('|')}]"
10
+ end
11
+ end
12
+
13
+ def setup
14
+ CertificateManager.new.generate_certificate
15
+ end
16
+
17
+ def ensure_configured
18
+ unless File.file?(CERTIFICATE_PATH)
19
+ puts 'Deploy agent is not configured. Please run "deploy-agent setup" first.'
20
+ Process.exit(1)
21
+ end
22
+ end
23
+
24
+ def restart
25
+ stop
26
+ while(is_running?)
27
+ sleep 0.5
28
+ end
29
+ start
30
+ end
31
+
32
+ def start
33
+ if is_running?
34
+ puts "Deploy agent already running. Process ID #{pid_from_file}"
35
+ Process.exit(1)
36
+ else
37
+ ensure_configured
38
+ pid = fork do
39
+ $background = true
40
+ write_pid
41
+ run
42
+ end
43
+ puts "Deploy agent started. Process ID #{pid}"
44
+ Process.detach(pid)
45
+ end
46
+ end
47
+
48
+ def stop
49
+ if is_running?
50
+ pid = pid_from_file
51
+ Process.kill('TERM', pid)
52
+ puts "Deploy agent stopped. Process ID #{pid}"
53
+ else
54
+ puts "Deploy agent is not running"
55
+ Process.exit(1)
56
+ end
57
+ end
58
+
59
+ def status
60
+ if is_running?
61
+ puts "Deploy agent is running. PID #{pid_from_file}"
62
+ else
63
+ puts "Deploy agent is not running."
64
+ Process.exit(1)
65
+ end
66
+ end
67
+
68
+ def run
69
+ ensure_configured
70
+ Agent.new.run
71
+ end
72
+
73
+ private
74
+
75
+ def is_running?
76
+ if pid = pid_from_file
77
+ Process.kill(0, pid)
78
+ true
79
+ else
80
+ false
81
+ end
82
+ rescue Errno::ESRCH
83
+ false
84
+ rescue Errno::EPERM
85
+ true
86
+ end
87
+
88
+ def pid_from_file
89
+ File.read(PID_PATH).to_i
90
+ rescue Errno::ENOENT
91
+ nil
92
+ end
93
+
94
+ def write_pid
95
+ File.open(PID_PATH, 'w') { |f| f.write Process.pid.to_s }
96
+ at_exit { File.delete(PID_PATH) if File.exists?(PID_PATH) }
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,106 @@
1
+ require 'socket'
2
+
3
+ module DeployAgent
4
+ # The DestinationConnection class managea a connection to a backend server
5
+ class DestinationConnection
6
+ attr_reader :socket
7
+
8
+ # Create a connection to a backend server
9
+ def initialize(host, port, id, nio_selector, server_connection)
10
+ @agent = server_connection.agent
11
+ @id = id
12
+ @nio_selector = nio_selector
13
+ @server_connection = server_connection
14
+
15
+ # Check the IP address and create a socket
16
+ ipaddr = IPAddr.new(host)
17
+ if ipaddr.ipv4?
18
+ @tcp_socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
19
+ else
20
+ @tcp_socket = Socket.new(Socket::Constants::AF_INET6, Socket::Constants::SOCK_STREAM, 0)
21
+ end
22
+ # Begin the connection attempt in the background
23
+ @sockaddr = Socket.sockaddr_in(port.to_i, host.to_s)
24
+ begin
25
+ @tcp_socket.connect_nonblock(@sockaddr)
26
+ # We don't expect to get here, but it's OK if we do
27
+ @status = :connected
28
+ @nio_monitor = @nio_selector.register(@tcp_socket, :r)
29
+ rescue IO::WaitWritable
30
+ # This is expected, we will get a callback when the connection completes
31
+ @status = :connecting
32
+ @nio_monitor = @nio_selector.register(@tcp_socket, :w)
33
+ end
34
+ @nio_monitor.value = self
35
+
36
+ # Set up a send buffer
37
+ @tx_buffer = String.new.force_encoding('BINARY')
38
+ end
39
+
40
+ # Queue data to be send to the backend
41
+ def send_data(data)
42
+ @tx_buffer << data
43
+ @nio_monitor.interests = :rw
44
+ end
45
+
46
+ def tx_data
47
+ # This might get called when there's data to send, but also
48
+ # when a connections completes or fails.
49
+ if @status == :connecting
50
+ begin
51
+ @tcp_socket.connect_nonblock(@sockaddr)
52
+ rescue IO::WaitWritable
53
+ # This shouldn't happen. If it does, ignore it and
54
+ # wait a bit longer until the connection completes
55
+ return
56
+ rescue => e
57
+ @agent.logger.info "[#{@id}] Connection failed: #{e.message.to_s}"
58
+ # Something went wrong connecting, inform the Deploy Server
59
+ close
60
+ @server_connection.send_connection_error(@id, e.message.to_s)
61
+ return
62
+ end
63
+ @agent.logger.info "[#{@id}] Connected to destination"
64
+ @server_connection.send_connection_success(@id)
65
+ @status = :connected
66
+ end
67
+ if @status == :connected && @tx_buffer.bytesize > 0
68
+ bytes_sent = @tcp_socket.write_nonblock(@tx_buffer)
69
+ if bytes_sent >= @tx_buffer.bytesize
70
+ @tx_buffer = String.new.force_encoding('BINARY')
71
+ else
72
+ @tx_buffer = @tx_buffer[bytes_sent..-1]
73
+ end
74
+ end
75
+ if @status == :connected && @tx_buffer.bytesize == 0
76
+ # Nothing more to send, wait for inbound data only
77
+ @nio_monitor.interests = :r
78
+ end
79
+ rescue Errno::ECONNRESET
80
+ # The backend has closed the connection. Inform the Deploy server.
81
+ @server_connection.send_connection_close(@id)
82
+ # Ensure everything is tidied up
83
+ close
84
+ end
85
+
86
+ def rx_data
87
+ # Received data from backend. Pass this along to the Deploy server
88
+ data = @tcp_socket.readpartial(10240)
89
+ @agent.logger.debug "[#{@id}] #{data.bytesize} bytes received from destination"
90
+ @server_connection.send_data(@id, data)
91
+ rescue EOFError, Errno::ECONNRESET
92
+ @agent.logger.info "[#{@id}] Destination closed connection"
93
+ # The backend has closed the connection. Inform the Deploy server.
94
+ @server_connection.send_connection_close(@id)
95
+ # Ensure everything is tidied up
96
+ close
97
+ end
98
+
99
+ def close
100
+ @nio_selector.deregister(@tcp_socket)
101
+ @server_connection.destination_connections.delete(@id)
102
+ @tcp_socket.close
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,165 @@
1
+ require 'socket'
2
+ require 'ipaddr'
3
+ require 'openssl'
4
+
5
+ module DeployAgent
6
+ # The ServerConnection class deals with all communication with the Deploy server
7
+ class ServerConnection
8
+ class ServerDisconnected < StandardError;end
9
+ attr_reader :destination_connections, :agent
10
+ attr_writer :nio_monitor
11
+
12
+ # Create a secure TLS connection to the Deploy server
13
+ def initialize(agent, server_host, nio_selector, check_certificate=true)
14
+ @agent = agent
15
+ @agent.logger.info "Attempting to connect to #{server_host}"
16
+ @destination_connections = {}
17
+ @nio_selector = nio_selector
18
+
19
+ # Create a TCP socket to the Deploy server
20
+ @tcp_socket = TCPSocket.new(server_host, 7777)
21
+
22
+ # Configure an OpenSSL context with server vertification
23
+ ctx = OpenSSL::SSL::SSLContext.new
24
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
25
+ # Load the agent certificate and key used to authenticate this agent
26
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(CERTIFICATE_PATH))
27
+ ctx.key = OpenSSL::PKey::RSA.new(File.read(KEY_PATH))
28
+ # Load the Deploy CA used to verify the server
29
+ ctx.ca_file = CA_PATH
30
+
31
+ # Create the secure connection
32
+ @socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, ctx)
33
+ @socket.connect
34
+ # Check the remote certificate
35
+ @socket.post_connection_check(server_host) if check_certificate
36
+ # Create send and receive buffers
37
+ @tx_buffer = String.new.force_encoding('BINARY')
38
+ @rx_buffer = String.new.force_encoding('BINARY')
39
+
40
+ @nio_monitor = @nio_selector.register(@tcp_socket, :r)
41
+ @nio_monitor.value = self
42
+
43
+ @agent.logger.info "Successfully connected to server"
44
+ rescue => e
45
+ @agent.logger.info "Something went wrong connecting to server."
46
+ # Sleep between 10 and 20 seconds
47
+ random_sleep = rand(10) + 10
48
+ @agent.logger.info "#{e.to_s} #{e.message}"
49
+ @agent.logger.info "Retrying in #{random_sleep} seconds."
50
+ sleep random_sleep
51
+ retry
52
+ end
53
+
54
+ # Receive and process packets from the control server
55
+ def rx_data
56
+ # Ensure all received data is read
57
+ @rx_buffer << @socket.readpartial(10240)
58
+ while(@socket.pending > 0)
59
+ @rx_buffer << @socket.readpartial(10240)
60
+ end
61
+ # Wait until we have a complete packet of data
62
+ while @rx_buffer.bytesize >=2 && @rx_buffer.bytesize >= @rx_buffer[0,2].unpack('n')[0]
63
+ length = @rx_buffer.slice!(0,2).unpack('n')[0]
64
+ # Extract the packet from the buffered stream
65
+ packet = @rx_buffer.slice!(0,length-2)
66
+ # Check what type of packet we have received
67
+ case packet.bytes[0]
68
+ when 1
69
+ # Process new connection request
70
+ id = packet[1,2].unpack('n')[0]
71
+ host, port = packet[3..-1].split('/', 2)
72
+ @agent.logger.info "[#{id}] Connection request from server: #{host}:#{port}"
73
+ begin
74
+ # Create conenction to the final destination and save info by id
75
+ @destination_connections[id] = DestinationConnection.new(host, port, id, @nio_selector, self)
76
+ rescue => e
77
+ # Something went wrong, inform the Deploy server
78
+ @agent.logger.error "An error occurred: #{e.message}"
79
+ @agent.logger.error e.backtrace
80
+ send_connection_error(id, e.message)
81
+ end
82
+ when 3
83
+ # Process a connection close request
84
+ id = packet[1,2].unpack('n')[0]
85
+ if @destination_connections[id]
86
+ @agent.logger.info "[#{id}] Close requested by server, closing"
87
+ @destination_connections[id].close
88
+ else
89
+ @agent.logger.info "[#{id}] Close requested by server, not open"
90
+ end
91
+ when 4
92
+ # Data incoming, send it to the backend
93
+ id = packet[1,2].unpack('n')[0]
94
+ @agent.logger.debug "[#{id}] #{packet.bytesize} bytes received from server"
95
+ @destination_connections[id].send_data(packet[3..-1])
96
+ when 5
97
+ # This is a shutdown request. Disconnect and don't re-attempt connection.
98
+ @agent.logger.warn "Server rejected connection. Shutting down."
99
+ @agent.logger.warn packet[1..-1]
100
+ Process.exit(0)
101
+ when 6
102
+ # This is a shutdown request. Disconnect and don't re-attempt connection.
103
+ @agent.logger.warn "Server requested reconnect. Closing connection."
104
+ close
105
+ end
106
+ end
107
+ rescue EOFError, Errno::ECONNRESET
108
+ close
109
+ end
110
+
111
+ # Notify server of successful connection
112
+ def send_connection_success(id)
113
+ send_packet([2, id, 0].pack('CnC'))
114
+ end
115
+
116
+ # Notify server of failed connection
117
+ def send_connection_error(id, reason)
118
+ send_packet([2, id, 1, reason].pack('CnCa*'))
119
+ end
120
+
121
+ # Notify server of closed connection
122
+ def send_connection_close(id)
123
+ send_packet([3, id].pack('Cn'))
124
+ end
125
+
126
+ # Proxy data (coming from the backend) to the Deploy server
127
+ def send_data(id, data)
128
+ send_packet([4, id, data].pack('Cna*'))
129
+ end
130
+
131
+ # Called by event loop to send all waiting packets to the Deploy server
132
+ def tx_data
133
+ bytes_sent = @socket.write_nonblock(@tx_buffer[0,1024])
134
+ # Send as much data as possible
135
+ if bytes_sent >= @tx_buffer.bytesize
136
+ @tx_buffer = String.new.force_encoding('BINARY')
137
+ @nio_monitor.interests = :r
138
+ else
139
+ # If we didn't manage to send all the data, leave
140
+ # the remaining data in the send buffer
141
+ @tx_buffer.slice!(0, bytes_sent)
142
+ end
143
+ rescue EOFError, Errno::ECONNRESET
144
+ close
145
+ end
146
+
147
+ private
148
+
149
+ def close
150
+ @agent.logger.info "Server disconnected, terminating all connections"
151
+ @destination_connections.values.each{ |s| s.close }
152
+ @nio_selector.deregister(@tcp_socket)
153
+ @socket.close
154
+ @tcp_socket.close
155
+ raise ServerDisconnected
156
+ end
157
+
158
+ # Queue a packet of data to be sent to the Deploy server
159
+ def send_packet(data)
160
+ @tx_buffer << [data.bytesize+2, data].pack('na*')
161
+ @nio_monitor.interests = :rw
162
+ end
163
+
164
+ end
165
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deploy-agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - aTech Media
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nio4r
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rb-readline
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.4
41
+ description: This gem allows you to configure a secure proxy through which Deploy
42
+ can forward connections
43
+ email:
44
+ - support@deployhq.com
45
+ executables:
46
+ - deploy-agent
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - bin/deploy-agent
51
+ - lib/deploy_agent.rb
52
+ - lib/deploy_agent/agent.rb
53
+ - lib/deploy_agent/certificate_manager.rb
54
+ - lib/deploy_agent/cli.rb
55
+ - lib/deploy_agent/destination_connection.rb
56
+ - lib/deploy_agent/server_connection.rb
57
+ homepage: https://www.deployhq.com/
58
+ licenses: []
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.6.8
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: The Deploy agent
80
+ test_files: []