stubby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +0 -0
- data/README.md +219 -0
- data/bin/stubby +8 -0
- data/lib/stubby/cli/application.rb +160 -0
- data/lib/stubby/cli.rb +3 -0
- data/lib/stubby/extensions/dns/osx.rb +58 -0
- data/lib/stubby/extensions/dns.rb +115 -0
- data/lib/stubby/extensions/http.rb +203 -0
- data/lib/stubby/extensions/reload.rb +46 -0
- data/lib/stubby/master.rb +182 -0
- data/lib/stubby/registry.rb +148 -0
- data/lib/stubby/stub.rb +56 -0
- data/lib/stubby.rb +10 -0
- metadata +251 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'liquid'
|
4
|
+
require 'httpi'
|
5
|
+
require 'rack/ssl'
|
6
|
+
|
7
|
+
module Extensions
|
8
|
+
module HTTP
|
9
|
+
class NotFoundException < Exception
|
10
|
+
end
|
11
|
+
|
12
|
+
class HTTPApp < Sinatra::Base
|
13
|
+
class << self
|
14
|
+
def port
|
15
|
+
80
|
16
|
+
end
|
17
|
+
|
18
|
+
def run!(session, server_settings={})
|
19
|
+
puts self.inspect + ": " + port.to_s
|
20
|
+
|
21
|
+
set :bind, STUBBY_MASTER
|
22
|
+
set :port, port
|
23
|
+
set :stubby_session, session
|
24
|
+
set :server, 'thin'
|
25
|
+
|
26
|
+
super(:server_settings => server_settings)
|
27
|
+
end
|
28
|
+
|
29
|
+
def adapter(name, &block)
|
30
|
+
adapters[name] = block
|
31
|
+
end
|
32
|
+
|
33
|
+
def adapters
|
34
|
+
@@adapters ||= {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
set :run, false
|
39
|
+
set :static, false
|
40
|
+
|
41
|
+
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)
|
51
|
+
end
|
52
|
+
|
53
|
+
adapter "file" do
|
54
|
+
paths = []
|
55
|
+
|
56
|
+
if url.host == "-"
|
57
|
+
paths << File.expand_path(File.join("~/.stubby/#{request.host}", request.path))
|
58
|
+
paths << File.expand_path(File.join("/usr/local/stubby/#{request.host}", request.path))
|
59
|
+
else
|
60
|
+
paths << File.expand_path(File.join(url.path, request.path))
|
61
|
+
end
|
62
|
+
|
63
|
+
paths.each do |path|
|
64
|
+
next if path.index(url.path) != 0
|
65
|
+
|
66
|
+
p = [path, File.join(path, "index.html")].select { |path|
|
67
|
+
File.exists?(path) and !File.directory?(path)
|
68
|
+
}.first
|
69
|
+
|
70
|
+
send_file(p) and break unless p.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
not_found(paths.join(",\n"))
|
74
|
+
end
|
75
|
+
|
76
|
+
adapter "default" do
|
77
|
+
if url.path.empty?
|
78
|
+
# Proxy all requests, preserve incoming path
|
79
|
+
out = url.dup
|
80
|
+
out.path = request.path
|
81
|
+
request = HTTPI::Request.new
|
82
|
+
request.url = out.to_s
|
83
|
+
|
84
|
+
response = HTTPI.get(request)
|
85
|
+
|
86
|
+
response.headers.delete "transfer-encoding"
|
87
|
+
response.headers.delete "connection"
|
88
|
+
|
89
|
+
status(response.code)
|
90
|
+
headers(response.headers)
|
91
|
+
body(response.body)
|
92
|
+
|
93
|
+
else
|
94
|
+
# Proxy to the given path
|
95
|
+
request = HTTPI::Request.new
|
96
|
+
request.url = url.to_s
|
97
|
+
|
98
|
+
response = HTTPI.get(request)
|
99
|
+
|
100
|
+
response.headers.delete "transfer-encoding"
|
101
|
+
response.headers.delete "connection"
|
102
|
+
|
103
|
+
status(response.code)
|
104
|
+
headers(response.headers)
|
105
|
+
body(response.body)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
get(//) do
|
110
|
+
if instruction.nil?
|
111
|
+
not_found
|
112
|
+
elsif adapter=self.class.adapters[url.scheme]
|
113
|
+
instance_eval &adapter
|
114
|
+
else
|
115
|
+
instance_eval &self.class.adapters["default"]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
def forbidden
|
121
|
+
[403, "Forbidden"]
|
122
|
+
end
|
123
|
+
|
124
|
+
def not_found(resource="*unknown*")
|
125
|
+
[404, "Not Found:\n #{resource}"]
|
126
|
+
end
|
127
|
+
|
128
|
+
def instruction
|
129
|
+
MultiJson.load(HTTPI.post("http://#{STUBBY_MASTER}:9000/rules/search.json",
|
130
|
+
trigger: "#{request.scheme}://#{request.host}").body)
|
131
|
+
end
|
132
|
+
|
133
|
+
def url
|
134
|
+
@url ||= URI.parse(instruction)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# TODO: get SSL support running. Self signed cert
|
139
|
+
class HTTPSApp < HTTPApp
|
140
|
+
use Rack::SSL
|
141
|
+
|
142
|
+
class << self
|
143
|
+
def port
|
144
|
+
443
|
145
|
+
end
|
146
|
+
|
147
|
+
def run!(session)
|
148
|
+
set :bind, STUBBY_MASTER
|
149
|
+
set :port, port
|
150
|
+
set :stubby_session, session
|
151
|
+
|
152
|
+
#super(session, {
|
153
|
+
# :SSLEnable => true,
|
154
|
+
# :SSLCertName => %w[CN localhost]
|
155
|
+
#})
|
156
|
+
|
157
|
+
Rack::Handler::Thin.run(self, {
|
158
|
+
:Host => STUBBY_MASTER,
|
159
|
+
:Port => 443
|
160
|
+
}) do |server|
|
161
|
+
server.ssl = true
|
162
|
+
server.ssl_options = {
|
163
|
+
:verify_peer => false
|
164
|
+
}
|
165
|
+
end
|
166
|
+
rescue => e
|
167
|
+
puts "#{e.inspect}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class Server
|
173
|
+
def initialize
|
174
|
+
@log = Logger.new(STDOUT)
|
175
|
+
end
|
176
|
+
|
177
|
+
def run!(session, options)
|
178
|
+
return if options[:http] == false
|
179
|
+
|
180
|
+
@session = session
|
181
|
+
HTTPApp.run!(session)
|
182
|
+
end
|
183
|
+
|
184
|
+
def stop!
|
185
|
+
HTTPApp.quit!
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class SSLServer < Server
|
190
|
+
def run!(session, options)
|
191
|
+
return if options[:https] == false
|
192
|
+
|
193
|
+
@session = session
|
194
|
+
HTTPSApp.run!(session)
|
195
|
+
end
|
196
|
+
|
197
|
+
def stop!
|
198
|
+
HTTPSApp.quit!
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'listen'
|
2
|
+
|
3
|
+
module Extensions
|
4
|
+
class Reload
|
5
|
+
def run!(session, options)
|
6
|
+
@options = options
|
7
|
+
@session = session
|
8
|
+
return if @options[:reload] == false
|
9
|
+
|
10
|
+
listener.start
|
11
|
+
sleep 1 while @listener
|
12
|
+
puts 'run! done'
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop!
|
16
|
+
return if @options[:reload] == false
|
17
|
+
@listener.stop and @listener = nil if @listener
|
18
|
+
puts 'stop! done'
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def root_path
|
23
|
+
@session.system.root_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def session_config_path
|
27
|
+
@session.system.session_config_path
|
28
|
+
end
|
29
|
+
|
30
|
+
def listener
|
31
|
+
return @listener if @listener
|
32
|
+
|
33
|
+
puts "NEW LISTENER"
|
34
|
+
|
35
|
+
@listener ||= Listen.to(root_path, debug: true) do |modified, added, removed|
|
36
|
+
(modified + added).each do |mpath|
|
37
|
+
puts "[INFO] Detected change, test identical #{mpath}"
|
38
|
+
if File.identical?(session_config_path, mpath)
|
39
|
+
puts "[INFO] Detected config change, reloading #{mpath}..."
|
40
|
+
@session.system.reload
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'sinatra/json'
|
2
|
+
|
3
|
+
module Stubby
|
4
|
+
class Api < Sinatra::Base
|
5
|
+
class << self
|
6
|
+
attr_accessor :enabled_stubs
|
7
|
+
attr_accessor :registry
|
8
|
+
attr_accessor :environments, :environment
|
9
|
+
|
10
|
+
def enabled_stubs
|
11
|
+
@enabled_stubs ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def registry
|
15
|
+
@registry ||= Stubby::Registry.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset
|
19
|
+
@enabled_stubs = nil
|
20
|
+
@environment = nil
|
21
|
+
@registry = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def env_settings
|
25
|
+
(@environments[environment] || {}).dup
|
26
|
+
end
|
27
|
+
|
28
|
+
def environment=(name)
|
29
|
+
reset
|
30
|
+
@environment = name
|
31
|
+
|
32
|
+
(env_settings["dependencies"] || []).each do |depname, mode|
|
33
|
+
activate(depname, mode)
|
34
|
+
end
|
35
|
+
|
36
|
+
env_settings.delete("dependencies")
|
37
|
+
|
38
|
+
activate_transient(env_settings)
|
39
|
+
end
|
40
|
+
|
41
|
+
def activate(name, mode)
|
42
|
+
registry_item = registry.latest(name)
|
43
|
+
registry_item.install
|
44
|
+
self.enabled_stubs[name] = registry_item.stub(mode)
|
45
|
+
end
|
46
|
+
|
47
|
+
def activate_transient(options)
|
48
|
+
self.enabled_stubs["_"] = TransientStub.new(options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
set :bind, STUBBY_MASTER
|
53
|
+
set :port, 9000
|
54
|
+
set :run, false
|
55
|
+
set :static, false
|
56
|
+
|
57
|
+
get "/status" do
|
58
|
+
json status: "ok"
|
59
|
+
end
|
60
|
+
|
61
|
+
get "/stubs/available.json" do
|
62
|
+
json Api.registry.index
|
63
|
+
end
|
64
|
+
|
65
|
+
get "/stubs/activated.json" do
|
66
|
+
json Hash[Api.enabled_stubs.collect { |name, stub|
|
67
|
+
[name, stub.options]
|
68
|
+
}]
|
69
|
+
end
|
70
|
+
|
71
|
+
post "/reset.json" do
|
72
|
+
Api.reset
|
73
|
+
json status: "ok"
|
74
|
+
end
|
75
|
+
|
76
|
+
get "/environment.json" do
|
77
|
+
json environment: (Api.environment || "undefined")
|
78
|
+
end
|
79
|
+
|
80
|
+
post "/environment.json" do
|
81
|
+
Api.environment = params[:environment]
|
82
|
+
json status: "ok"
|
83
|
+
end
|
84
|
+
|
85
|
+
get "/environments.json" do
|
86
|
+
json environments: Api.environments
|
87
|
+
end
|
88
|
+
|
89
|
+
post "/stubs/transient/activate.json" do
|
90
|
+
Api.activate_transient(params[:options])
|
91
|
+
json status: "ok"
|
92
|
+
end
|
93
|
+
|
94
|
+
post "/stubs/activate.json" do
|
95
|
+
Api.activate(params[:name], params[:mode])
|
96
|
+
json status: "ok"
|
97
|
+
end
|
98
|
+
|
99
|
+
post "/rules/search.json" do
|
100
|
+
json Api.enabled_stubs.collect { |_, stub|
|
101
|
+
stub.search(params[:trigger])
|
102
|
+
}.compact.first
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Master
|
107
|
+
attr_accessor :extensions, :config
|
108
|
+
|
109
|
+
def initialize(environments)
|
110
|
+
@extensions = [
|
111
|
+
Extensions::DNS::Server.new,
|
112
|
+
Extensions::HTTP::Server.new,
|
113
|
+
Extensions::HTTP::SSLServer.new
|
114
|
+
]
|
115
|
+
|
116
|
+
@config = Api
|
117
|
+
@config.environments = environments
|
118
|
+
end
|
119
|
+
|
120
|
+
def environment=(environment)
|
121
|
+
@config.environment = environment
|
122
|
+
end
|
123
|
+
|
124
|
+
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
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
def stop_extensions
|
139
|
+
puts "Shutting down..."
|
140
|
+
|
141
|
+
running.each do |process|
|
142
|
+
Process.kill("INT", process)
|
143
|
+
end
|
144
|
+
|
145
|
+
puts "Bye."
|
146
|
+
end
|
147
|
+
|
148
|
+
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
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
def run_extensions
|
160
|
+
@running_extensions ||= @extensions.collect { |plugin|
|
161
|
+
Process.fork {
|
162
|
+
$0 = "stubby: [extension worker] #{plugin.class.name}"
|
163
|
+
plugin.run!(self, {})
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
trap("INT") {
|
168
|
+
stop_extensions
|
169
|
+
}
|
170
|
+
|
171
|
+
return @running_extensions
|
172
|
+
end
|
173
|
+
|
174
|
+
def assume_network_interface
|
175
|
+
`ifconfig lo0 alias #{STUBBY_MASTER}`
|
176
|
+
end
|
177
|
+
|
178
|
+
def unassume_network_interface
|
179
|
+
`ifconfig lo0 -alias #{STUBBY_MASTER}`
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'httpi'
|
3
|
+
|
4
|
+
module Stubby
|
5
|
+
class RegistryItem
|
6
|
+
attr_accessor :name, :version, :source, :location
|
7
|
+
|
8
|
+
def initialize(name, version, source)
|
9
|
+
@name = name
|
10
|
+
@version = version
|
11
|
+
@source = source
|
12
|
+
@location = "~/.stubby/#{name}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
@version.slice(1, @version.length)
|
17
|
+
end
|
18
|
+
|
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
|
30
|
+
end
|
31
|
+
|
32
|
+
def uninstall
|
33
|
+
`rm -rf ~/.stubby/#{name}`
|
34
|
+
end
|
35
|
+
|
36
|
+
def installed?
|
37
|
+
File.exists? @location
|
38
|
+
end
|
39
|
+
|
40
|
+
def download(source, destination)
|
41
|
+
`curl #{source} #{destination}`
|
42
|
+
end
|
43
|
+
|
44
|
+
def config
|
45
|
+
File.join("~", ".stubby", name, "stubby.json")
|
46
|
+
end
|
47
|
+
|
48
|
+
def stub(target=nil)
|
49
|
+
install unless installed?
|
50
|
+
Stub.new(config, target)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
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
|
104
|
+
end
|
105
|
+
|
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
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/stubby/stub.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'pry'
|
2
|
+
|
3
|
+
module Stubby
|
4
|
+
class Stub
|
5
|
+
attr_accessor :target, :path, :modes
|
6
|
+
|
7
|
+
# TODO: target is mode, rename
|
8
|
+
def initialize(path, target=nil)
|
9
|
+
self.path = path
|
10
|
+
self.target = target
|
11
|
+
end
|
12
|
+
|
13
|
+
def modes
|
14
|
+
@modes ||= MultiJson.load(File.read(path))
|
15
|
+
rescue
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
def path=(v)
|
20
|
+
unless v and File.exists?(File.expand_path(v))
|
21
|
+
puts "'#{v}' not found. Use --config to specify a different config file"
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
@path = File.expand_path(v)
|
26
|
+
end
|
27
|
+
|
28
|
+
def options
|
29
|
+
modes[target] || {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def search(trigger)
|
33
|
+
options.each do |rule, instruction|
|
34
|
+
if Regexp.new(rule, Regexp::EXTENDED | Regexp::IGNORECASE).match(trigger)
|
35
|
+
return instruction
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class TransientStub < Stub
|
44
|
+
def initialize(options)
|
45
|
+
@options = options
|
46
|
+
end
|
47
|
+
|
48
|
+
def modes
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
|
52
|
+
def options
|
53
|
+
@options
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/stubby.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
STUBBY_MASTER="172.16.123.1"
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
require 'stubby/extensions/dns/osx'
|
5
|
+
require 'stubby/extensions/dns'
|
6
|
+
require 'stubby/extensions/http'
|
7
|
+
require 'stubby/extensions/reload'
|
8
|
+
require 'stubby/registry'
|
9
|
+
require 'stubby/stub'
|
10
|
+
require 'stubby/master'
|