stubby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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'