stubby 0.0.1

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