em-proxy 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -34,7 +34,7 @@ EventMachine Proxy DSL for writing high-performance transparent / intercepting p
34
34
  end
35
35
 
36
36
  # termination logic
37
- conn.on_finish do |backend|
37
+ conn.on_finish do |backend, name|
38
38
  p [:on_finish, name]
39
39
 
40
40
  # terminate connection (in duplex mode, you can terminate when prod is done)
@@ -47,4 +47,33 @@ For more examples see the /examples directory.
47
47
  - Duplicating traffic
48
48
  - Selective forwarding
49
49
  - Beanstalkd interceptor
50
- - etc.
50
+ - etc.
51
+
52
+ A schema-free MySQL proof of concept, via an EM-Proxy server:
53
+ - http://www.igvita.com/2010/03/01/schema-free-mysql-vs-nosql/
54
+ - Code in examples/schemaless-mysql
55
+
56
+ == License
57
+
58
+ (The MIT License)
59
+
60
+ Copyright (c) 2010 Ilya Grigorik
61
+
62
+ Permission is hereby granted, free of charge, to any person obtaining
63
+ a copy of this software and associated documentation files (the
64
+ 'Software'), to deal in the Software without restriction, including
65
+ without limitation the rights to use, copy, modify, merge, publish,
66
+ distribute, sublicense, and/or sell copies of the Software, and to
67
+ permit persons to whom the Software is furnished to do so, subject to
68
+ the following conditions:
69
+
70
+ The above copyright notice and this permission notice shall be
71
+ included in all copies or substantial portions of the Software.
72
+
73
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
74
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
75
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
76
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
77
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
78
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
79
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "em-proxy"
7
+ gemspec.summary = "EventMachine Proxy DSL"
8
+ gemspec.description = gemspec.summary
9
+ gemspec.email = "ilya@igvita.com"
10
+ gemspec.homepage = "http://github.com/igrigorik/em-proxy"
11
+ gemspec.authors = ["Ilya Grigorik"]
12
+ gemspec.add_dependency('eventmachine', '>= 0.12.9')
13
+ gemspec.rubyforge_project = "em-proxy"
14
+ end
15
+
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -0,0 +1,26 @@
1
+ require 'lib/em-proxy'
2
+
3
+ Proxy.start(:host => "0.0.0.0", :port => 80, :debug => false) do |conn|
4
+ # Specifying :relay_server or :relay_client is useful if only requests or responses
5
+ # need to be processed. The proxy throughput will roughly double.
6
+ conn.server :srv, :host => "127.0.0.1", :port => 81, :relay_client => true, :relay_server => true
7
+
8
+ conn.on_connect do
9
+ p [:on_connect, "#{conn.peer.join(':')} connected"]
10
+ end
11
+
12
+ # modify / process request stream
13
+ # on_data will not be called when :relay_server => true is passed as server option
14
+ conn.on_data do |data|
15
+ p [:on_data, data]
16
+ data
17
+ end
18
+
19
+ # modify / process response stream
20
+ # on_response will not be called when :relay_client => true is passed as server option
21
+ conn.on_response do |backend, resp|
22
+ p [:on_response, backend, resp]
23
+ # resp = "HTTP/1.1 200 OK\r\nConnection: close\r\nDate: Thu, 30 Apr 2009 03:53:28 GMT\r\nContent-Type: text/plain\r\n\r\nHar!"
24
+ resp
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ require "rubygems"
2
+ require "mysql" # gem install ruby-mysql -v 2.9.2
3
+
4
+ my = Mysql.connect('127.0.0.1', 'root', '', 'noschema', 3307)
5
+
6
+ p my.list_tables
7
+ __END__
8
+
9
+ # look ma, no schema! ooh yeah.
10
+ my.query("create table posts")
11
+
12
+ # insert a few records into our schemaless MySQL store... :-)
13
+ my.query("insert into posts values('first post'('author', 'Ilya'),('nick', 'igrigorik'))")
14
+ my.query("insert into posts values('author:Ilya','location:Waterloo')")
15
+
16
+ my.query("select author from posts") { |r| puts r }
17
+ my.query("select nick from posts") { |r| puts r }
@@ -0,0 +1,181 @@
1
+ require "lib/em-proxy"
2
+ require "em-mysql"
3
+ require "stringio"
4
+ require "fiber"
5
+
6
+ Proxy.start(:host => "0.0.0.0", :port => 3307) do |conn|
7
+ conn.server :mysql, :host => "127.0.0.1", :port => 3306, :relay_server => true
8
+
9
+ QUERY_CMD = 3
10
+ MAX_PACKET_LENGTH = 2**24-1
11
+
12
+ # open a direct connection to MySQL for the schema-free coordination logic
13
+ @mysql = EventMachine::MySQL.new(:host => 'localhost', :database => 'noschema')
14
+
15
+ conn.on_data do |data|
16
+ fiber = Fiber.new {
17
+ p [:original_request, data]
18
+
19
+ overhead, chunks, seq = data[0,4].unpack("CvC")
20
+ type, sql = data[4, data.size].unpack("Ca*")
21
+
22
+ p [:request, [overhead, chunks, seq], [type, sql]]
23
+
24
+ if type == QUERY_CMD
25
+ query = sql.downcase.split
26
+ p [:query, query]
27
+
28
+ # TODO: can probably switch to http://github.com/omghax/sql
29
+ # for AST query parsing & mods.
30
+
31
+ case query.first
32
+ when "create" then
33
+ # Allow schemaless table creation, ex: 'create table posts'
34
+ # By creating a table with a single id for key storage, aka
35
+ # rewrite to: 'create table posts (id varchar(255))'. All
36
+ # future attribute tables will be created on demand at
37
+ # insert time of a new record
38
+ overload = "(id varchar(255), UNIQUE(id));"
39
+ query += [overload]
40
+ overhead += overload.size + 1
41
+
42
+ p [:create_new_schema_free_table, query, data]
43
+
44
+ when "insert" then
45
+ # Overload the INSERT syntax to allow for nested parameters
46
+ # inside the statement. ex:
47
+ # INSERT INTO posts (id, author, nickname, ...) VALUES (
48
+ # 'ilya', 'Ilya Grigorik', 'igrigorik'
49
+ # )
50
+ #
51
+ # The following query will be mapped into 3 distinct tables:
52
+ # => 'posts' table will store the key
53
+ # => 'posts_author' will store key, value
54
+ # => 'posts_nickname' will store key, value
55
+ #
56
+ # or, in SQL..
57
+ #
58
+ # => insert into posts values("ilya");
59
+ # => create table posts_author (id varchar(40), value varchar(255), UNIQUE(id));
60
+ # => insert into posts_author values("ilya", "Ilya Grigorik");
61
+ # => ... repeat for every attribute
62
+ #
63
+ # If the table post_value has not been seen before, it will
64
+ # be created on the fly. Hence allowing us to add and remove
65
+ # keys and values at will. :-)
66
+ #
67
+ # P.S. There is probably cleaner syntax for this, but hey...
68
+
69
+
70
+ if insert = sql.match(/\((.*?)\).*?\((.*?)\)/)
71
+ data = {}
72
+ table = query[2]
73
+ keys = insert[1].split(',').map!{|s| s.strip}
74
+ values = insert[2].scan(/([^\'|\"]+)/).flatten.reject {|s| s.strip == ','}
75
+ keys.each_with_index {|k,i| data[k] = values[i]}
76
+
77
+ data.each do |key, value|
78
+ next if key == 'id'
79
+ attr_sql = "insert into #{table}_#{key} values('#{data['id']}', '#{value}')"
80
+
81
+ q = @mysql.query(attr_sql)
82
+ q.errback { |res|
83
+ # if the attribute table for this model does not yet exist then create it!
84
+ # - yes, there is a race condition here, add fiber logic later
85
+ if res.is_a?(Mysql::Error) and res.message =~ /Table.*doesn\'t exist/
86
+
87
+ table_sql = "create table #{table}_#{key} (id varchar(255), value varchar(255), UNIQUE(id))"
88
+ tc = @mysql.query(table_sql)
89
+ tc.callback { @mysql.query(attr_sql) }
90
+ end
91
+ }
92
+
93
+ p [:inserted_attr, table, key, value]
94
+ end
95
+
96
+ # override the query to insert the key into posts table
97
+ query = query[0,3] + ["VALUES('#{data['id']}')"]
98
+ overhead = query.join(" ").size + 1
99
+
100
+ p [:insert, query]
101
+ end
102
+
103
+ when "select" then
104
+ # Overload the select call to perform a multi-join in the background
105
+ # and rewrite the attribute names to fool the client into thinking it
106
+ # all came from the same table.
107
+ #
108
+ # To figure out which tables we need to join on, do the simple / dumb
109
+ # approach and issue a 'show tables like key_%' to do 'runtime
110
+ # introspection'. Could easily cache this, but that's for later.
111
+ #
112
+ # Ex, a 'select * from posts' query with one value (author) would be
113
+ # rewritten into the following query:
114
+ #
115
+ # SELECT posts.id as id, posts_author.value as author FROM posts
116
+ # LEFT OUTER JOIN posts_author ON posts_author.id = posts.id
117
+ # WHERE posts.id = "ilya";
118
+
119
+ select = sql.match(/select(.*?)from\s([^\s]+)/)
120
+ where = sql.match(/where\s([^=]+)\s?=\s?'?"?([^\s'"]+)'?"?/)
121
+ attrs, table = select[1].strip.split(','), select[2] if select
122
+ key = where[2] if where
123
+
124
+ if select
125
+ p [:select, select, attrs, where]
126
+
127
+ tables = @mysql.query("show tables like '#{table}_%'")
128
+ tables.callback { |res|
129
+ fiber.resume(res.all_hashes.collect(&:values).flatten.collect{ |c|
130
+ c.split('_').last
131
+ })
132
+ }
133
+ tables = Fiber.yield
134
+
135
+ p [:select_tables, tables]
136
+
137
+ # build the select statements, hide the tables behind each attribute
138
+ join = "select #{table}.id as id "
139
+ tables.each do |column|
140
+ join += " , #{table}_#{column}.value as #{column} "
141
+ end
142
+
143
+ # add the joins to stich it all together
144
+ join += " FROM #{table} "
145
+ tables.each do |column|
146
+ join += " LEFT OUTER JOIN #{table}_#{column} ON #{table}_#{column}.id = #{table}.id "
147
+ end
148
+
149
+ join += " WHERE #{table}.id = '#{key}' " if key
150
+
151
+ query = [join]
152
+ overhead = join.size + 1
153
+
154
+ p [:join_query, join]
155
+ end
156
+ end
157
+
158
+ # repack the query data and forward to server
159
+ # - have to split message on packet boundaries
160
+
161
+ seq, data = 0, []
162
+ query = StringIO.new([type, query.join(" ")].pack("Ca*"))
163
+ while q = query.read(MAX_PACKET_LENGTH)
164
+ data.push [q.length % 256, q.length / 256, seq].pack("CvC") + q
165
+ seq = (seq + 1) % 256
166
+ end
167
+
168
+ p [:final_query, data, chunks, overhead]
169
+ puts "-" * 100
170
+ end
171
+
172
+ [data].flatten.each do |chunk|
173
+ conn.relay_to_servers(chunk)
174
+ end
175
+
176
+ :async # we will render results later
177
+ }
178
+
179
+ fiber.resume
180
+ end
181
+ end
data/lib/em-proxy.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  $:.unshift(File.dirname(__FILE__) + '/../lib')
2
2
 
3
+
3
4
  require "rubygems"
4
5
  require "eventmachine"
6
+ require "socket"
5
7
 
6
8
  %w[ backend proxy connection ].each do |file|
7
9
  require "em-proxy/#{file}"
@@ -7,6 +7,7 @@ module EventMachine
7
7
  def on_data(&blk); @on_data = blk; end
8
8
  def on_response(&blk); @on_response = blk; end
9
9
  def on_finish(&blk); @on_finish = blk; end
10
+ def on_connect(&blk); blk.call; end
10
11
 
11
12
  ##### EventMachine
12
13
  def initialize(options)
@@ -18,6 +19,11 @@ module EventMachine
18
19
  debug [:connection, data]
19
20
  processed = @on_data.call(data)
20
21
 
22
+ return if processed == :async or processed.nil?
23
+ relay_to_servers(processed)
24
+ end
25
+
26
+ def relay_to_servers(processed)
21
27
  if processed.is_a? Array
22
28
  data, servers = *processed
23
29
 
@@ -32,7 +38,7 @@ module EventMachine
32
38
  s.send_data data unless data.nil?
33
39
  end
34
40
  end
35
-
41
+
36
42
  #
37
43
  # initialize connections to backend servers
38
44
  #
@@ -40,11 +46,20 @@ module EventMachine
40
46
  srv = EventMachine::connect(opts[:host], opts[:port], EventMachine::ProxyServer::Backend, @debug) do |c|
41
47
  c.name = name
42
48
  c.plexer = self
49
+ c.proxy_incoming_to(self, 10240) if opts[:relay_server]
43
50
  end
51
+ self.proxy_incoming_to(srv, 10240) if opts[:relay_client]
44
52
 
45
53
  @servers[name] = srv
46
54
  end
47
55
 
56
+ #
57
+ # [ip, port] of the connected client
58
+ #
59
+ def peer
60
+ @peer ||= Socket.unpack_sockaddr_in(get_peername).reverse
61
+ end
62
+
48
63
  #
49
64
  # relay data from backend server to client
50
65
  #
@@ -69,21 +84,24 @@ module EventMachine
69
84
  @servers[name] = nil
70
85
 
71
86
  # if all connections are terminated downstream, then notify client
72
- close_connection if @servers.values.compact.size.zero?
87
+ close_connection_after_writing if @servers.values.compact.size.zero?
73
88
 
74
89
  if @on_finish
75
90
  @on_finish.call(name)
76
- @on_finish.call(:done) if @servers.values.compact.size.zero?
91
+
92
+ # not sure if this is required
93
+ # @on_finish.call(:done) if @servers.values.compact.size.zero?
77
94
  end
78
95
  end
79
96
 
80
97
  private
81
98
 
82
99
  def debug(*data)
83
- return unless @debug
84
- require 'pp'
85
- pp data
86
- puts
100
+ if @debug
101
+ require 'pp'
102
+ pp data
103
+ puts
104
+ end
87
105
  end
88
106
  end
89
107
  end
@@ -9,7 +9,7 @@ class Proxy
9
9
 
10
10
  EventMachine::start_server(options[:host], options[:port],
11
11
  EventMachine::ProxyServer::Connection, options) do |c|
12
- c.instance_eval(&blk)
12
+ blk.call(c)
13
13
  end
14
14
  end
15
15
  end
data/spec/proxy_spec.rb CHANGED
@@ -9,7 +9,7 @@ describe Proxy do
9
9
 
10
10
  it "should recieve data on port 8080" do
11
11
  EM.run do
12
- EventMachine.add_timer(2) do
12
+ EventMachine.add_timer(0.1) do
13
13
  EventMachine::HttpRequest.new('http://127.0.0.1:8080/test').get({:timeout => 1})
14
14
  end
15
15
 
@@ -22,9 +22,27 @@ describe Proxy do
22
22
  end
23
23
  end
24
24
 
25
+ it "should call the on_connect callback" do
26
+ connected = false
27
+ EM.run do
28
+ EventMachine.add_timer(0.1) do
29
+ EventMachine::HttpRequest.new('http://127.0.0.1:8080/test').get({:timeout => 1})
30
+ end
31
+
32
+ Proxy.start(:host => "0.0.0.0", :port => 8080) do |conn|
33
+ conn.on_connect do
34
+ connected = true
35
+ EventMachine.stop
36
+ end
37
+ end
38
+ end
39
+ connected.should == true
40
+ end
41
+
42
+
25
43
  it "should transparently redirect TCP traffic to google" do
26
44
  EM.run do
27
- EventMachine.add_timer(2) do
45
+ EventMachine.add_timer(0.1) do
28
46
  EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
29
47
  end
30
48
 
@@ -43,7 +61,7 @@ describe Proxy do
43
61
 
44
62
  it "should duplex TCP traffic to two backends google & yahoo" do
45
63
  EM.run do
46
- EventMachine.add_timer(2) do
64
+ EventMachine.add_timer(0.1) do
47
65
  EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
48
66
  end
49
67
 
@@ -72,7 +90,7 @@ describe Proxy do
72
90
 
73
91
  it "should intercept & alter response from Google" do
74
92
  EM.run do
75
- EventMachine.add_timer(2) do
93
+ EventMachine.add_timer(0.1) do
76
94
  http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
77
95
  http.errback { failed }
78
96
  http.callback {
@@ -93,7 +111,7 @@ describe Proxy do
93
111
 
94
112
  it "should invoke on_finish callback when connection is terminated" do
95
113
  EM.run do
96
- EventMachine.add_timer(2) do
114
+ EventMachine.add_timer(0.1) do
97
115
  EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
98
116
  end
99
117
 
@@ -108,4 +126,40 @@ describe Proxy do
108
126
  end
109
127
  end
110
128
  end
129
+
130
+ it "should not invoke on_data when :relay_client is passed as server option" do
131
+ lambda {
132
+ EM.run do
133
+ EventMachine.add_timer(0.1) do
134
+ http =EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
135
+ http.callback { EventMachine.stop }
136
+ end
137
+
138
+ Proxy.start(:host => "0.0.0.0", :port => 8080) do |conn|
139
+ conn.server :goog, :host => "google.com", :port => 80, :relay_client => true
140
+ conn.on_data { |data| raise "Should not be here"; data }
141
+ conn.on_response { |backend, resp| resp }
142
+
143
+ end
144
+ end
145
+ }.should_not raise_error
146
+ end
147
+
148
+ it "should not invoke on_response when :relay_server is passed as server option" do
149
+ lambda {
150
+ EM.run do
151
+ EventMachine.add_timer(0.1) do
152
+ http =EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get({:timeout => 1})
153
+ http.callback { EventMachine.stop }
154
+ end
155
+
156
+ Proxy.start(:host => "0.0.0.0", :port => 8080) do |conn|
157
+ conn.server :goog, :host => "google.com", :port => 80, :relay_server => true
158
+ conn.on_data { |data| data }
159
+ conn.on_response { |backend, resp| raise "Should not be here"; }
160
+
161
+ end
162
+ end
163
+ }.should_not raise_error
164
+ end
111
165
  end
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-proxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 2
9
+ version: 0.1.2
5
10
  platform: ruby
6
11
  authors:
7
12
  - Ilya Grigorik
@@ -9,19 +14,23 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2009-10-25 00:00:00 -04:00
17
+ date: 2010-03-26 00:00:00 -04:00
13
18
  default_executable:
14
19
  dependencies:
15
20
  - !ruby/object:Gem::Dependency
16
21
  name: eventmachine
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
20
24
  requirements:
21
25
  - - ">="
22
26
  - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 12
30
+ - 9
23
31
  version: 0.12.9
24
- version:
32
+ type: :runtime
33
+ version_requirements: *id001
25
34
  description: EventMachine Proxy DSL
26
35
  email: ilya@igvita.com
27
36
  executables: []
@@ -32,12 +41,15 @@ extra_rdoc_files:
32
41
  - README.rdoc
33
42
  files:
34
43
  - README.rdoc
44
+ - Rakefile
35
45
  - VERSION
36
46
  - examples/appserver.rb
37
47
  - examples/beanstalkd_interceptor.rb
38
48
  - examples/duplex.rb
39
49
  - examples/line_interceptor.rb
40
50
  - examples/port_forward.rb
51
+ - examples/relay_port_forward.rb
52
+ - examples/schemaless-mysql/mysql_interceptor.rb
41
53
  - examples/selective_forward.rb
42
54
  - examples/smtp_spam_filter.rb
43
55
  - examples/smtp_whitelist.rb
@@ -60,18 +72,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
72
  requirements:
61
73
  - - ">="
62
74
  - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
63
77
  version: "0"
64
- version:
65
78
  required_rubygems_version: !ruby/object:Gem::Requirement
66
79
  requirements:
67
80
  - - ">="
68
81
  - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
69
84
  version: "0"
70
- version:
71
85
  requirements: []
72
86
 
73
87
  rubyforge_project: em-proxy
74
- rubygems_version: 1.3.5
88
+ rubygems_version: 1.3.6
75
89
  signing_key:
76
90
  specification_version: 3
77
91
  summary: EventMachine Proxy DSL
@@ -83,6 +97,9 @@ test_files:
83
97
  - examples/duplex.rb
84
98
  - examples/line_interceptor.rb
85
99
  - examples/port_forward.rb
100
+ - examples/relay_port_forward.rb
101
+ - examples/schemaless-mysql/client.rb
102
+ - examples/schemaless-mysql/mysql_interceptor.rb
86
103
  - examples/selective_forward.rb
87
104
  - examples/smtp_spam_filter.rb
88
105
  - examples/smtp_whitelist.rb