stubby 0.0.1 → 0.0.2

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.
@@ -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'