ec2-api-proxy 0.1.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.
data/README ADDED
@@ -0,0 +1,58 @@
1
+ = ec2-api-proxy
2
+
3
+ == Description
4
+
5
+ ec2-api-proxy is a proxy server for EC2 API.
6
+
7
+ == Source Code
8
+
9
+ https://bitbucket.org/winebarrel/ec2-api-proxy
10
+
11
+ == Dependency
12
+
13
+ * EventMachine
14
+ * RExec
15
+ * Dalli
16
+
17
+ == Install
18
+
19
+ gem install ec2-api-proxy
20
+
21
+ == Example
22
+
23
+ shell> ec2-api-proxy start
24
+ Starting daemon...
25
+ Waiting for daemon to start...
26
+ Daemon status: running pid=56921
27
+
28
+ shell> eapctl status
29
+ ---
30
+ memcached: localhost:11211
31
+ expires: 60
32
+ compress: false
33
+ debug: false
34
+ shell> eapctl set-debug true
35
+
36
+ shell> export HTTP_PROXY=localhost:8080
37
+ shell> aws ec2 describe-instances --endpoint-url=http://ec2.ap-northeast-1.amazonaws.com # It does not support HTTPS still
38
+ {
39
+ "Reservations": [
40
+ {
41
+ ...
42
+ shell> aws ec2 describe-instances --endpoint-url=http://ec2.ap-northeast-1.amazonaws.com
43
+ {
44
+ "Reservations": [
45
+ {
46
+ ...
47
+
48
+ shell> tail -f /var/log/ec2-api-proxy.log
49
+ ...
50
+ D, [2013-07-15T20:41:28.472712 #76304] DEBUG -- : proxy: connect from 127.0.0.1:58456
51
+ D, [2013-07-15T20:41:28.529715 #76304] DEBUG -- : proxy: cache miss
52
+ D, [2013-07-15T20:41:29.051745 #76304] DEBUG -- : proxy: unbind connection from 127.0.0.1:58456
53
+ D, [2013-07-15T20:41:29.051745 #76304] DEBUG -- : backend: unbind connection
54
+ D, [2013-07-15T20:41:29.052745 #76304] DEBUG -- : backend: set cache (length=22330)
55
+ D, [2013-07-15T20:41:31.202868 #76304] DEBUG -- : proxy: connect from 127.0.0.1:58459
56
+ D, [2013-07-15T20:41:31.207869 #76304] DEBUG -- : proxy: cache hit (length=22330)
57
+ D, [2013-07-15T20:41:31.209869 #76304] DEBUG -- : proxy: unbind connection from 127.0.0.1:58459
58
+
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'drb/drb'
5
+ require 'optparse'
6
+ require 'ec2-api-proxy/constants'
7
+ require 'yaml'
8
+
9
+ COMMAND_NAME = File.basename(__FILE__)
10
+
11
+ COMMANDS = {
12
+ 'status' => lambda {|server_options|
13
+ h = {}
14
+ server_options.to_hash.each {|k, v| h[k.to_s] = v }
15
+ puts YAML.dump(h)
16
+ },
17
+ 'set-expires' => lambda {|server_options, v|
18
+ server_options[:expires] = v.to_i
19
+ },
20
+ 'set-debug' => lambda {|server_options, v|
21
+ server_options[:debug] = !!(/#{Regexp.escape(v)}/i =~ 'true')
22
+ },
23
+ }
24
+
25
+ options = nil
26
+
27
+ ARGV.options do |parser|
28
+ cmds = COMMANDS.keys
29
+
30
+ options = {
31
+ :socket => "/var/tmp/#{APP_NAME}.sock",
32
+ }
33
+
34
+ parser.on('-S', '--socket=SOCK_FILE') {|v| options[:socket] = v }
35
+
36
+ help_and_exit = lambda do |v|
37
+ $stderr.puts parser.help.sub(COMMAND_NAME, "#{COMMAND_NAME} {#{cmds.join('|')}}")
38
+ exit 1
39
+ end
40
+
41
+ parser.on('-h', '--help', &help_and_exit)
42
+
43
+ parser.parse!
44
+
45
+ unless ARGV.length >= 1 and cmds.include?(ARGV[0])
46
+ help_and_exit.call(true)
47
+ end
48
+ end # parse options
49
+
50
+ server_options = DRbObject.new_with_uri("drbunix:#{options[:socket]}")
51
+
52
+ cmd = COMMANDS.fetch(ARGV[0])
53
+
54
+ if cmd.arity > 1
55
+ cmd.call(server_options, ARGV[1])
56
+ else
57
+ cmd.call(server_options)
58
+ end
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'optparse'
5
+ require 'ec2-api-proxy/constants'
6
+ require 'ec2-api-proxy/server'
7
+
8
+ options = nil
9
+
10
+ ARGV.options do |parser|
11
+ daemon_commands = %w(start stop restart status)
12
+
13
+ options = {
14
+ :addr => '0.0.0.0',
15
+ :port => 8080,
16
+ :memcached => 'localhost:11211',
17
+ :expires => 60,
18
+ :compress => false,
19
+ :working_dir => '/var',
20
+ :socket => "/var/tmp/#{APP_NAME}.sock",
21
+ :debug => false,
22
+ }
23
+
24
+ parser.on('-a', '--addr=ADDR' ) {|v| options[:addr] = v }
25
+ parser.on('-p', '--port=PORT' ) {|v| options[:port] = v.to_i }
26
+ parser.on('-m', '--memcached=HOST:PORT') {|v| options[:memcached] = v }
27
+ parser.on('-e', '--expires=SECOND' ) {|v| options[:expires] = v.to_i }
28
+ parser.on('-c', '--compress' ) { options[:compress] = true }
29
+ parser.on('-n', '--threads=SIZE' ) {|v| options[:threads] = v.to_i }
30
+ parser.on('-W', '--working-dir=DIR' ) {|v| options[:working_dir] = v }
31
+ parser.on('-S', '--socket=SOCK_FILE' ) {|v| options[:socket] = v }
32
+ parser.on('', '--debug' ) { options[:debug] = true }
33
+
34
+ help_and_exit = lambda do |v|
35
+ $stderr.puts parser.help.sub(APP_NAME, "#{APP_NAME} {#{daemon_commands.join('|')}}")
36
+ exit 1
37
+ end
38
+
39
+ parser.on('-h', '--help', &help_and_exit)
40
+
41
+ parser.parse!
42
+
43
+ unless ARGV.length == 1 and daemon_commands.include?(ARGV[0])
44
+ help_and_exit.call(true)
45
+ end
46
+ end # parse options
47
+
48
+ EC2APIProxy::Server.options = options
49
+ EC2APIProxy::Server.daemonize
@@ -0,0 +1,46 @@
1
+ require 'eventmachine'
2
+
3
+ module EC2APIProxy
4
+ class Backend < EM::Connection
5
+ def initialize(proxy, memcached_key)
6
+ @proxy = proxy
7
+ @logger = proxy.logger
8
+ @memcached_key = memcached_key
9
+ @data_list = []
10
+ end
11
+
12
+ def unbind
13
+ if @proxy and @proxy.options[:debug]
14
+ @logger.debug("backend: unbind connection")
15
+ end
16
+
17
+ unless @data_list.empty?
18
+ data = @data_list.join
19
+ @proxy.memcached.set(@memcached_key, data, @proxy.options[:expires])
20
+
21
+ if @proxy and @proxy.options[:debug]
22
+ @logger.debug("backend: set cache (length=#{data.length})")
23
+ end
24
+ end
25
+ end
26
+
27
+ def receive_data(data)
28
+ if not @proxy or @proxy.error?
29
+ @logger.info("backend: proxy connection error: #{data.inspect}")
30
+ close_proxy_connection
31
+ close_connection_after_writing
32
+ else
33
+ @data_list << data
34
+ @proxy.send_data(data)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def close_proxy_connection
41
+ @proxy.close_connection if @proxy
42
+ rescue Exception => e
43
+ @logger.warn("#{e.class.name}: #{e.message}")
44
+ end
45
+ end # Backend
46
+ end # EC2APIProxy
@@ -0,0 +1,2 @@
1
+ APP_NAME = 'ec2-api-proxy'
2
+ Version = '0.1.0'
@@ -0,0 +1,129 @@
1
+ require 'eventmachine'
2
+ require 'ec2-api-proxy/backend'
3
+ require 'logger'
4
+ require 'uri'
5
+ require 'cgi'
6
+
7
+ module EC2APIProxy
8
+ class Proxy < EM::Connection
9
+ attr_reader :options
10
+ attr_reader :logger
11
+ attr_reader :memcached
12
+
13
+ def initialize(options)
14
+ @options = options
15
+ @memcached = Dalli::Client.new(options[:memcached], :compress => options[:compress])
16
+ @logger = Logger.new($stdout)
17
+ end
18
+
19
+ def post_init
20
+ if (peername = get_peername)
21
+ @connect_from = Socket.unpack_sockaddr_in(peername)
22
+ end
23
+
24
+ if @options[:debug]
25
+ port, ip = @connect_from
26
+ @logger.debug("proxy: connect from #{ip}:#{port}")
27
+ end
28
+ end
29
+
30
+ def unbind
31
+ close_backend_connection
32
+
33
+ if @options[:debug]
34
+ port, ip = @connect_from
35
+ @logger.debug("proxy: unbind connection from #{ip}:#{port}")
36
+ end
37
+ end
38
+
39
+ def receive_data(data)
40
+ EM.defer {
41
+ begin
42
+ endpoint, params = parse_proxy_request(data)
43
+ key = params_to_key(params)
44
+
45
+ if (cache = @memcached.get(key))
46
+ @logger.debug("proxy: cache hit (length=#{cache.length})") if @options[:debug]
47
+ send_data(cache)
48
+ close_connection_after_writing
49
+ else
50
+ @logger.debug("proxy: cache miss") if @options[:debug]
51
+ @backend = EM.connect(endpoint.host, endpoint.port, EC2APIProxy::Backend, self, key)
52
+
53
+ if @backend.error?
54
+ @logger.error("proxy: backend connection error: #{data.inspect}")
55
+ send_error('ec2-api-proxy-backend-connection-error', 'backend connection error')
56
+ close_backend_connection
57
+ close_connection_after_writing
58
+ else
59
+ @backend.send_data(data)
60
+ end
61
+ end
62
+ rescue => e
63
+ @logger.warn("#{e.class.name}: #{e.message}")
64
+ send_error(e.class.name, e.message)
65
+ close_backend_connection
66
+ close_connection_after_writing
67
+ end
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def parse_proxy_request(data)
74
+ request_method = nil
75
+ endpoint = nil
76
+
77
+ data.sub!(%r{\A(GET|POST) (https?://[^/]+)}) do
78
+ request_method = $1
79
+ endpoint = $2
80
+ "#{request_method} "
81
+ end
82
+
83
+ endpoint = URI.parse(endpoint)
84
+ params = nil
85
+
86
+ if request_method =~ /GET/i
87
+ params = CGI.parse(endpoint.query)
88
+ else
89
+ params = CGI.parse(data.split(/\r\n\r\n/, 2).last)
90
+ end
91
+
92
+ [endpoint, params]
93
+ end
94
+
95
+ def params_to_key(params)
96
+ params = params.dup
97
+
98
+ %w(
99
+ SignatureVersion
100
+ AWSAccessKeyId
101
+ Timestamp
102
+ SignatureMethod
103
+ Version
104
+ Signature
105
+ ).each {|i| params.delete(i) }
106
+
107
+ params.sort.map {|k, v| "#{k}=#{v}" }.join('&')
108
+ end
109
+
110
+ def send_error(code, message)
111
+ send_data(<<-EOS)
112
+ <Response>
113
+ <Errors>
114
+ <Error>
115
+ <Code>#{code}</Code>
116
+ <Message>#{message}</Message>
117
+ </Error>
118
+ </Errors>
119
+ </Response>
120
+ EOS
121
+ end
122
+
123
+ def close_backend_connection
124
+ @backend.close_connection if @backend
125
+ rescue Exception => e
126
+ @logger.warn("#{e.class.name}: #{e.message}")
127
+ end
128
+ end # Proxy
129
+ end # EC2APIProxy
@@ -0,0 +1,48 @@
1
+ require 'drb/drb'
2
+ require 'fileutils'
3
+ require 'eventmachine'
4
+ require 'rexec'
5
+ require 'rexec/daemon'
6
+ require 'dalli'
7
+
8
+ require 'ec2-api-proxy/proxy'
9
+
10
+ module RExec; module Daemon; class Base
11
+ def self.daemon_name; APP_NAME; end
12
+ end; end; end
13
+
14
+ module EC2APIProxy
15
+ class Server < RExec::Daemon::Base
16
+
17
+ class << self
18
+ def options=(options)
19
+ @@options = options
20
+ @@base_directory = options[:working_dir]
21
+ end
22
+
23
+ def run
24
+ @@control_options = {
25
+ :memcached => @@options[:memcached],
26
+ :expires => @@options[:expires],
27
+ :compress => @@options[:compress],
28
+ :debug => @@options[:debug],
29
+ }
30
+
31
+ # start DRb
32
+ FileUtils.rm_f(@@options[:socket])
33
+ DRb.start_service("drbunix:#{@@options[:socket]}", @@control_options)
34
+ File.chmod(0700, @@options[:socket])
35
+ at_exit { FileUtils.rm_f(@@options[:socket]) }
36
+
37
+ EM.epoll
38
+ EM.threadpool_size = @@options[:threads] if @@options[:threads]
39
+
40
+ EM.run {
41
+ EM.start_server(@@options[:addr], @@options[:port], EC2APIProxy::Proxy,
42
+ @@control_options)
43
+ }
44
+ end
45
+ end # self
46
+
47
+ end # Server
48
+ end # EC2APIProxy
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2-api-proxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - winebarrel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: rexec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.5.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.5.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: dalli
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.6.4
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.4
62
+ description:
63
+ email: sgwr_dts@yahoo.co.jp
64
+ executables:
65
+ - ec2-api-proxy
66
+ - eapctl
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - README
71
+ - bin/eapctl
72
+ - bin/ec2-api-proxy
73
+ - lib/ec2-api-proxy/backend.rb
74
+ - lib/ec2-api-proxy/constants.rb
75
+ - lib/ec2-api-proxy/proxy.rb
76
+ - lib/ec2-api-proxy/server.rb
77
+ homepage: https://bitbucket.org/winebarrel/ec2-api-proxy
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.23
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: ec2-api-proxy is a proxy server for EC2 API.
101
+ test_files: []