deploy-agent 1.0.0

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 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: []