stubby 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,8 +26,10 @@ module Extensions
26
26
  super(:server_settings => server_settings)
27
27
  end
28
28
 
29
- def adapter(name, &block)
30
- adapters[name] = block
29
+ def adapter(*names, &block)
30
+ names.each do |name|
31
+ adapters[name] = block
32
+ end
31
33
  end
32
34
 
33
35
  def adapters
@@ -39,15 +41,7 @@ module Extensions
39
41
  set :static, false
40
42
 
41
43
  adapter "http-redirect" do
42
- url.scheme = "http"
43
- url.path = request.path if url.path.to_s.empty?
44
- redirect to(url.to_s)
45
- end
46
-
47
- adapter "https-redirect" do
48
- url.scheme = "https"
49
- url.path = request.path if url.path.to_s.empty?
50
- redirect to(url.to_s)
44
+ redirect to(instruction_params["to"])
51
45
  end
52
46
 
53
47
  adapter "file" do
@@ -73,37 +67,54 @@ module Extensions
73
67
  not_found(paths.join(",\n"))
74
68
  end
75
69
 
76
- adapter "default" do
70
+ adapter "http-proxy" do
71
+ url.scheme = "http"
72
+
77
73
  if url.path.empty?
78
74
  # Proxy all requests, preserve incoming path
79
75
  out = url.dup
80
76
  out.path = request.path
81
77
  request = HTTPI::Request.new
82
78
  request.url = out.to_s
79
+ else
80
+ # Proxy to the given path
81
+ request = HTTPI::Request.new
82
+ request.url = url.to_s
83
+ end
83
84
 
84
- response = HTTPI.get(request)
85
+ response = HTTPI.get(request)
86
+ response.headers.delete "transfer-encoding"
87
+ response.headers.delete "connection"
85
88
 
86
- response.headers.delete "transfer-encoding"
87
- response.headers.delete "connection"
89
+ status(response.code)
90
+ headers(response.headers)
91
+ body(response.body)
88
92
 
89
- status(response.code)
90
- headers(response.headers)
91
- body(response.body)
93
+ end
94
+
95
+ adapter "https-proxy" do
96
+ url.scheme = "https"
92
97
 
98
+ if url.path.empty?
99
+ # Proxy all requests, preserve incoming path
100
+ out = url.dup
101
+ out.path = request.path
102
+ request = HTTPI::Request.new
103
+ request.url = out.to_s
93
104
  else
94
105
  # Proxy to the given path
95
106
  request = HTTPI::Request.new
96
107
  request.url = url.to_s
108
+ end
97
109
 
98
- response = HTTPI.get(request)
110
+ response = HTTPI.get(request)
111
+ response.headers.delete "transfer-encoding"
112
+ response.headers.delete "connection"
99
113
 
100
- response.headers.delete "transfer-encoding"
101
- response.headers.delete "connection"
114
+ status(response.code)
115
+ headers(response.headers)
116
+ body(response.body)
102
117
 
103
- status(response.code)
104
- headers(response.headers)
105
- body(response.body)
106
- end
107
118
  end
108
119
 
109
120
  get(//) do
@@ -126,10 +137,14 @@ module Extensions
126
137
  end
127
138
 
128
139
  def instruction
129
- MultiJson.load(HTTPI.post("http://#{STUBBY_MASTER}:9000/rules/search.json",
140
+ @instruction ||= MultiJson.load(HTTPI.post("http://#{STUBBY_MASTER}:9000/rules/search.json",
130
141
  trigger: "#{request.scheme}://#{request.host}").body)
131
142
  end
132
143
 
144
+ def instruction_params
145
+ Rack::Utils.parse_nested_query url.query
146
+ end
147
+
133
148
  def url
134
149
  @url ||= URI.parse(instruction)
135
150
  end
@@ -181,6 +196,24 @@ module Extensions
181
196
  HTTPApp.run!(session)
182
197
  end
183
198
 
199
+ # http://blah.com => localhost:3000
200
+ # =>
201
+ # http://blah.com => http-proxy://localhost:3000
202
+ def expand_rule(trigger, instruction, proto='http')
203
+ u = URI.parse(instruction)
204
+
205
+ (if u.scheme.nil?
206
+ { trigger => "http-proxy://#{instruction}" }
207
+ elsif u.scheme == "http"
208
+ u.scheme = "http-proxy"
209
+ { trigger => u.to_s }
210
+ else
211
+ { trigger => instruction }
212
+ end).merge({
213
+ "#{trigger.gsub(proto + "://", "dns://")}/a" => "dns-a://#{STUBBY_MASTER}"
214
+ })
215
+ end
216
+
184
217
  def stop!
185
218
  HTTPApp.quit!
186
219
  end
@@ -197,6 +230,10 @@ module Extensions
197
230
  def stop!
198
231
  HTTPSApp.quit!
199
232
  end
233
+
234
+ def expand_rule(trigger, instruction)
235
+ super(trigger, instruction, "https")
236
+ end
200
237
  end
201
238
  end
202
239
  end
@@ -0,0 +1,47 @@
1
+ require 'mail_catcher'
2
+
3
+ module Extensions
4
+ module SMTP
5
+ class Server
6
+ def run!(session, options)
7
+ @process = Process.fork {
8
+ $0 = "stubby: [extension worker sub] Extensions::SMTP::Server"
9
+
10
+ sleep 2
11
+
12
+ HTTPI.post("http://#{STUBBY_MASTER}:9000/stubs/transient/activate.json",
13
+ options: MultiJson.dump(smtp_stub), key: "_smtp")
14
+
15
+ MailCatcher.run! smtp_ip: STUBBY_MASTER,
16
+ smtp_port: 25,
17
+ http_ip: STUBBY_MASTER,
18
+ http_port: 9001,
19
+ daemon: false
20
+ }
21
+
22
+ trap("INT", important: true){
23
+ stop!
24
+ }
25
+
26
+ sleep
27
+ end
28
+
29
+ def smtp_stub
30
+ {
31
+ "dns://outbox.stubby.dev/a" => "dns-a://#{STUBBY_MASTER}",
32
+ "http://outbox.stubby.dev" => "http://#{STUBBY_MASTER}:9001"
33
+ }
34
+ end
35
+
36
+ def expand_rule(trigger, instruction)
37
+ {
38
+ "#{trigger.gsub("smtp://", "dns://")}/mx" => "dns-mx://#{STUBBY_MASTER}/?priority=10"
39
+ }
40
+ end
41
+
42
+ def stop!
43
+ Process.shutdown(@process)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ $tracked = {}
2
+
3
+ module Kernel
4
+ alias _trap trap
5
+
6
+ def trap(*args, &block)
7
+ #puts "kernel::trap: #{args.inspect}\n---------------\n #{caller.join("\n")}\n\n"
8
+
9
+ if args.last.is_a? Hash
10
+ options = args.pop
11
+ else
12
+ options = {}
13
+ end
14
+
15
+ if options[:important]
16
+ track(args.first, _trap(*args, &block))
17
+ else
18
+ if tracked?(args.first)
19
+ puts "kernel::trap: #{args.inspect} ignoring"
20
+ else
21
+ _trap(*args, &block)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+ def track(signal, child_pid)
28
+ (($tracked ||= {})[Process.pid] ||= {})[signal] = child_pid
29
+ end
30
+
31
+ def tracked?(signal)
32
+ !!($tracked[Process.pid][signal])
33
+ rescue
34
+ false
35
+ end
36
+ end
data/lib/stubby/master.rb CHANGED
@@ -5,8 +5,9 @@ module Stubby
5
5
  class << self
6
6
  attr_accessor :enabled_stubs
7
7
  attr_accessor :registry
8
+ attr_accessor :master
8
9
  attr_accessor :environments, :environment
9
-
10
+
10
11
  def enabled_stubs
11
12
  @enabled_stubs ||= {}
12
13
  end
@@ -19,6 +20,8 @@ module Stubby
19
20
  @enabled_stubs = nil
20
21
  @environment = nil
21
22
  @registry = nil
23
+
24
+ yield if block_given?
22
25
  end
23
26
 
24
27
  def env_settings
@@ -26,26 +29,50 @@ module Stubby
26
29
  end
27
30
 
28
31
  def environment=(name)
29
- reset
30
- @environment = name
32
+ reset do
33
+ @environment = name
31
34
 
32
- (env_settings["dependencies"] || []).each do |depname, mode|
33
- activate(depname, mode)
34
- end
35
+ (env_settings["dependencies"] || []).each do |depname, mode|
36
+ activate(depname, mode)
37
+ end
35
38
 
36
- env_settings.delete("dependencies")
39
+ env_settings.delete("dependencies")
40
+ activate_transient(env_settings)
41
+ end
42
+ end
37
43
 
38
- activate_transient(env_settings)
44
+ def activate(source, mode)
45
+ registry_item = RegistryItem.new(source)
46
+ self.enabled_stubs[source] = registry_item.stub(mode)
39
47
  end
40
48
 
41
- def activate(name, mode)
42
- registry_item = registry.latest(name)
43
- registry_item.install
44
- self.enabled_stubs[name] = registry_item.stub(mode)
49
+ def activate_transient(options, key="_")
50
+ puts "Transient activation #{options.inspect}, #{key}"
51
+ self.enabled_stubs[key] = TransientStub.new(options)
45
52
  end
46
53
 
47
- def activate_transient(options)
48
- self.enabled_stubs["_"] = TransientStub.new(options)
54
+ def expand_rules(options)
55
+ options.inject({}) do |new_opts, (trigger, instruction)|
56
+ if instruction.is_a? Hash # dependency modes
57
+ new_opts[trigger] = instruction
58
+ else
59
+ instruction = instruction.gsub("@", STUBBY_MASTER)
60
+
61
+ protocol, url = trigger.split("://")
62
+ url, protocol = protocol, :default if url.nil?
63
+
64
+ extension = master.extensions[protocol.to_sym]
65
+
66
+ if extension
67
+ new_opts.delete(trigger)
68
+ new_opts.merge!(extension.expand_rule(trigger, instruction))
69
+ else
70
+ raise "No `#{extension}` extension found for trigger: #{trigger}"
71
+ end
72
+ end
73
+
74
+ new_opts
75
+ end
49
76
  end
50
77
  end
51
78
 
@@ -87,7 +114,7 @@ module Stubby
87
114
  end
88
115
 
89
116
  post "/stubs/transient/activate.json" do
90
- Api.activate_transient(params[:options])
117
+ Api.activate_transient(MultiJson.load(params[:options]), params[:key])
91
118
  json status: "ok"
92
119
  end
93
120
 
@@ -107,14 +134,17 @@ module Stubby
107
134
  attr_accessor :extensions, :config
108
135
 
109
136
  def initialize(environments)
110
- @extensions = [
111
- Extensions::DNS::Server.new,
112
- Extensions::HTTP::Server.new,
113
- Extensions::HTTP::SSLServer.new
114
- ]
137
+ @extensions = {
138
+ default: Extensions::Default.new,
139
+ dns: Extensions::DNS::Server.new,
140
+ http: Extensions::HTTP::Server.new,
141
+ https: Extensions::HTTP::SSLServer.new,
142
+ smtp: Extensions::SMTP::Server.new
143
+ }
115
144
 
116
145
  @config = Api
117
146
  @config.environments = environments
147
+ @config.master = self
118
148
  end
119
149
 
120
150
  def environment=(environment)
@@ -122,50 +152,61 @@ module Stubby
122
152
  end
123
153
 
124
154
  def run!(options={})
125
- begin
126
- assume_network_interface
127
-
128
- running.each do |process|
129
- puts "wait for #{process}"
130
- Process.waitpid(process)
131
- end
132
- ensure
133
- unassume_network_interface
155
+ run_network do
156
+ run_master do
157
+ run_extensions
158
+ end
134
159
  end
135
160
  end
136
161
 
137
162
  private
138
- def stop_extensions
163
+ def run_network
164
+ assume_network_interface
165
+ yield
166
+ ensure
167
+ unassume_network_interface
168
+ end
169
+
170
+ def run_master
171
+ $0 = "stubby: master"
172
+
173
+ Api.run! do |server|
174
+ yield
175
+ end
176
+ end
177
+
178
+ def run_extensions
179
+ running.each do |process|
180
+ Process.waitpid(process)
181
+ end
182
+ end
183
+
184
+ def stop!
139
185
  puts "Shutting down..."
140
186
 
187
+ Api.stop!
188
+
141
189
  running.each do |process|
142
- Process.kill("INT", process)
190
+ Process.shutdown(process)
143
191
  end
144
192
 
145
193
  puts "Bye."
146
194
  end
147
195
 
148
196
  def running
149
- @running ||= [run_master_api, run_extensions].flatten
150
- end
151
-
152
- def run_master_api
153
- Process.fork {
154
- $0 = "stubby: [config api]"
155
- Api.run!
156
- }
197
+ @running ||= run_extensions
157
198
  end
158
199
 
159
200
  def run_extensions
160
- @running_extensions ||= @extensions.collect { |plugin|
201
+ @running_extensions ||= @extensions.collect { |name, plugin|
161
202
  Process.fork {
162
203
  $0 = "stubby: [extension worker] #{plugin.class.name}"
163
204
  plugin.run!(self, {})
164
205
  }
165
206
  }
166
207
 
167
- trap("INT") {
168
- stop_extensions
208
+ trap("INT", important: true) {
209
+ stop!
169
210
  }
170
211
 
171
212
  return @running_extensions
@@ -0,0 +1,22 @@
1
+ require 'timeout'
2
+
3
+ module Process extend self
4
+ def running?(pid)
5
+ !!(kill(0, pid))
6
+ rescue
7
+ false
8
+ end
9
+
10
+ def shutdown(pid, timeout=10, sig1="TERM", sig2="KILL")
11
+ puts "Shutting down: #{pid}"
12
+
13
+ kill(sig1, pid)
14
+
15
+ Timeout::timeout(timeout) do
16
+ sleep 1 and puts "." while running?(pid)
17
+ end
18
+ rescue Timeout::Error
19
+ kill(sig2, pid)
20
+ end
21
+
22
+ end
@@ -3,46 +3,32 @@ require 'httpi'
3
3
 
4
4
  module Stubby
5
5
  class RegistryItem
6
- attr_accessor :name, :version, :source, :location
6
+ attr_accessor :name, :source
7
7
 
8
- def initialize(name, version, source)
9
- @name = name
10
- @version = version
11
- @source = source
12
- @location = "~/.stubby/#{name}"
8
+ def initialize(source)
9
+ @source = URI.parse(source)
10
+ @name = @source.path
13
11
  end
14
12
 
15
- def version
16
- @version.slice(1, @version.length)
13
+ def install
14
+ `mkdir -p #{path}`
15
+ `cd #{path} && git clone #{@source} .`
17
16
  end
18
17
 
19
- def install
20
- if File.exists? source
21
- uninstall
22
- `ln -s #{source} ~/.stubby/#{name}`
23
- else
24
- `mkdir -p ~/.stubby`
25
- download source, "~/.stubby/#{name}.zip"
26
- `curl #{source} > ~/.stubby/#{name}.zip`
27
- `unzip -d ~/.stubby/ #{source}`
28
- `rm ~/.stubby/#{name}.zip`
29
- end
18
+ def path
19
+ "~/.stubby/#{@source.path}"
30
20
  end
31
21
 
32
22
  def uninstall
33
- `rm -rf ~/.stubby/#{name}`
23
+ `rm -rf ~/.stubby/#{@source.path}`
34
24
  end
35
25
 
36
26
  def installed?
37
- File.exists? @location
38
- end
39
-
40
- def download(source, destination)
41
- `curl #{source} #{destination}`
27
+ File.exists? path
42
28
  end
43
29
 
44
30
  def config
45
- File.join("~", ".stubby", name, "stubby.json")
31
+ "~/.stubby/#{@source.path}/stubby.json"
46
32
  end
47
33
 
48
34
  def stub(target=nil)
@@ -52,97 +38,12 @@ module Stubby
52
38
  end
53
39
 
54
40
  class Registry
55
- def index
56
- Hash[(remote_index || local_index).collect { |name, versions|
57
- [name, versions.collect { |version, source|
58
- RegistryItem.new name, version, source
59
- }]
60
- }]
61
- end
62
-
63
- def versions(name)
64
- if index[name]
65
- index[name].sort { |x, y|
66
- Gem::Version.new(y.version) <=> Gem::Version.new(x.version)
67
- }
68
- else
69
- []
70
- end
71
- end
72
-
73
- def version(name, version)
74
- version = version.gsub("v", "")
75
-
76
- index[name].detect { |stub|
77
- stub.version == version
78
- }
79
- end
80
-
81
- def latest(name)
82
- versions(name).first
83
- end
84
-
85
- def install(name, opts={})
86
- source = opts[:source]
87
- v = opts[:version]
88
-
89
- if name =~ /https?:\/\//
90
- source = name
91
- name = File.basename(name).split(".").first
92
- RegistryItem.new(name, "v1.0.0", source).install
93
- else
94
- stub = v.nil? ? latest(name) : version(name, v)
95
-
96
- if stub
97
- stub.install
98
- elsif source
99
- add_new_source(name, source, v)
100
- else
101
- puts "[ERROR] Cannot find #{name} at #{v}"
102
- end
103
- end
41
+ def install(source)
42
+ RegistryItem.new(source).install
104
43
  end
105
44
 
106
- def uninstall(name)
107
- # TODO: we're not doing a search of the installed stubs'
108
- # version, but we have a convention of using a ~/.stubby/NAME
109
- # location, so this shouldn't be a problem for the POC
110
- if name =~ /https?:\/\//
111
- name = File.basename(name).split(".").first
112
- end
113
-
114
- latest(name).uninstall
115
- end
116
-
117
-
118
- private
119
-
120
- def remote_index
121
- response = HTTPI.get("http://github.com/jkassemi/stubby/index.json")
122
- MultiJson.load(response.body) if response.code == 200
123
- end
124
-
125
- def local_index
126
- MultiJson.load(File.read(File.expand_path(File.join('~', '.stubby', "index.json"))))
127
- rescue
128
- {}
129
- end
130
-
131
- def write_local_index(&block)
132
- File.open File.expand_path(File.join('~', '.stubby', "index.json")), "w", &block
133
- end
134
-
135
- def add_new_source(name, source, v=nil)
136
- version = v.nil? ? 'v0.0.1' : v
137
-
138
- item = RegistryItem.new name, version, source
139
- item.install
140
-
141
- current_index = local_index
142
-
143
- write_local_index do |index|
144
- index.puts MultiJson.dump(local_index.merge({item.name => {item.version => item.location}}))
145
- end
45
+ def uninstall(source)
46
+ RegistryItem.new(source).uninstall
146
47
  end
147
48
  end
148
49
  end
data/lib/stubby/stub.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'pry'
2
-
3
1
  module Stubby
4
2
  class Stub
5
3
  attr_accessor :target, :path, :modes
@@ -12,8 +10,11 @@ module Stubby
12
10
 
13
11
  def modes
14
12
  @modes ||= MultiJson.load(File.read(path))
15
- rescue
16
- {}
13
+ rescue => e
14
+ if File.exists?(path)
15
+ puts "[INFO] Problem parsing #{path}"
16
+ raise e
17
+ end
17
18
  end
18
19
 
19
20
  def path=(v)
@@ -25,8 +26,14 @@ module Stubby
25
26
  @path = File.expand_path(v)
26
27
  end
27
28
 
29
+ def target=(environment)
30
+ @environment = environment
31
+ @options = nil
32
+ options
33
+ end
34
+
28
35
  def options
29
- modes[target] || {}
36
+ @options ||= expand(modes[@environment] || {})
30
37
  end
31
38
 
32
39
  def search(trigger)
@@ -38,11 +45,16 @@ module Stubby
38
45
 
39
46
  return nil
40
47
  end
48
+
49
+ private
50
+ def expand(options)
51
+ Stubby::Api.expand_rules(options)
52
+ end
41
53
  end
42
54
 
43
55
  class TransientStub < Stub
44
56
  def initialize(options)
45
- @options = options
57
+ @options = expand(options)
46
58
  end
47
59
 
48
60
  def modes
data/lib/stubby.rb CHANGED
@@ -1,10 +1,17 @@
1
1
  STUBBY_MASTER="172.16.123.1"
2
2
 
3
+ require 'oj'
3
4
  require 'multi_json'
5
+ require 'rubygems'
6
+ require 'bundler/setup'
4
7
  require 'stubby/extensions/dns/osx'
5
8
  require 'stubby/extensions/dns'
6
9
  require 'stubby/extensions/http'
10
+ require 'stubby/extensions/smtp'
7
11
  require 'stubby/extensions/reload'
12
+ require 'stubby/extensions/default'
8
13
  require 'stubby/registry'
9
14
  require 'stubby/stub'
10
15
  require 'stubby/master'
16
+ require 'stubby/process'
17
+ require 'stubby/kernel'