stubby 0.0.10 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +48 -10
- data/lib/stubby.rb +2 -0
- data/lib/stubby/cli/application.rb +36 -1
- data/lib/stubby/extensions/default.rb +66 -60
- data/lib/stubby/extensions/dns.rb +106 -101
- data/lib/stubby/extensions/dns/osx.rb +61 -41
- data/lib/stubby/extensions/http.rb +186 -181
- data/lib/stubby/extensions/reload.rb +38 -32
- data/lib/stubby/extensions/smtp.rb +47 -41
- data/lib/stubby/master.rb +34 -14
- data/lib/stubby/registry.rb +0 -1
- metadata +54 -16
- checksums.yaml +0 -7
data/README.md
CHANGED
@@ -1,17 +1,55 @@
|
|
1
1
|
# Stubby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
Stubby makes your development environment act more like your
|
4
|
+
production environment.
|
5
|
+
|
6
|
+
A local DNS and HTTP server combo that provides a declarative
|
7
|
+
environment based configuration for talking to servers that
|
8
|
+
act exactly like production but aren't.
|
9
|
+
|
10
|
+
## Philosophy
|
11
|
+
|
12
|
+
Centralized configuration systems instruct your application how to
|
13
|
+
function on the host environment. Stubby prefers that your application ask
|
14
|
+
the environment to mold itself to the needs of the application.
|
15
|
+
|
16
|
+
Consider your app is a manager sent to Germany to lead an automotive
|
17
|
+
operation. The manager has a language dictionary and translates each order
|
18
|
+
to German before giving it to the team. Consider the following interaction:
|
19
|
+
|
20
|
+
"Where's the screwdriver?"
|
21
|
+
"Wo ist der Schraubenzieher?"
|
22
|
+
|
23
|
+
=>
|
24
|
+
|
25
|
+
"Im roten Feld über die Straße"
|
26
|
+
"In the red box down the street"
|
27
|
+
|
28
|
+
# Get screwdriver from red box,
|
29
|
+
# screw bolt into metal
|
30
|
+
|
31
|
+
This is what we tell our applications to do when we use a centralized
|
32
|
+
configuration:
|
33
|
+
|
34
|
+
"Where's the database?"
|
35
|
+
ENV["DATABASE_URI"]
|
36
|
+
"mysql://blah/"
|
37
|
+
|
38
|
+
# Connect to database
|
39
|
+
# Execute query
|
40
|
+
|
41
|
+
Stubby is a translator in this instance. Since Stubby knows that you need
|
42
|
+
a screwdriver, and it knows where you look for it, Stubby will make sure
|
43
|
+
that the screwdriver your manager needs is where your manager expects it to be.
|
44
|
+
|
45
|
+
## Uses
|
46
|
+
|
47
|
+
* manage your .dev domains (or any random old TLD)
|
48
|
+
|
49
|
+
* stub APIs so you can run tests locally
|
12
50
|
|
13
51
|
* get your team on the right system with the proper hosts settings.
|
14
|
-
|
52
|
+
|
15
53
|
## Development Status
|
16
54
|
|
17
55
|
The Stubfile.json format and the extension / adapter
|
data/lib/stubby.rb
CHANGED
@@ -50,6 +50,37 @@ module Stubby
|
|
50
50
|
master.run!
|
51
51
|
end
|
52
52
|
|
53
|
+
desc "halt", "Shut down if running, restore if not"
|
54
|
+
def halt
|
55
|
+
if master_running?
|
56
|
+
stop
|
57
|
+
else
|
58
|
+
restore
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "stop", "Stops a running stubby process"
|
63
|
+
def stop
|
64
|
+
if master_running?
|
65
|
+
Process.kill("INT", File.read(pidfile).to_i)
|
66
|
+
|
67
|
+
while master_running?
|
68
|
+
puts "." and sleep 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "restore", "Restore defaults"
|
74
|
+
def restore
|
75
|
+
if master_running?
|
76
|
+
puts "[ERROR] Stubby needs to be shut down first"
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
|
80
|
+
master = Stubby::Master.new({})
|
81
|
+
master.restore!
|
82
|
+
end
|
83
|
+
|
53
84
|
desc "env NAME", "Switch stubby environment"
|
54
85
|
long_desc <<-LONGDESC
|
55
86
|
> $ sudo stubby env test
|
@@ -95,7 +126,11 @@ module Stubby
|
|
95
126
|
|
96
127
|
private
|
97
128
|
def pidfile
|
98
|
-
@pidfile ||=
|
129
|
+
@pidfile ||= (
|
130
|
+
home = File.expand_path("~/.stubby")
|
131
|
+
FileUtils.mkdir_p(home) unless File.exists?(home)
|
132
|
+
File.join(home, "pid")
|
133
|
+
)
|
99
134
|
end
|
100
135
|
|
101
136
|
def master_running?
|
@@ -1,72 +1,78 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
1
|
+
module Stubby
|
2
|
+
module Extensions
|
3
|
+
class Default
|
4
|
+
def initialize
|
4
5
|
|
5
|
-
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
+
def run!(*args)
|
8
9
|
|
9
|
-
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
+
def stop!(*args)
|
12
13
|
|
13
|
-
|
14
|
+
end
|
15
|
+
|
16
|
+
def restore!(*args)
|
14
17
|
|
15
|
-
def expand_rule(trigger, instruction)
|
16
|
-
# Default expansion:
|
17
|
-
# "example.com": "localhost:3000"
|
18
|
-
#
|
19
|
-
# =>
|
20
|
-
# "dns://example.com": "@"
|
21
|
-
# "http://example.com": "http-redirect://blank?to=https://example.com&code=302"
|
22
|
-
# "https://example.com": "http-proxy://localhost:3000"
|
23
|
-
#
|
24
|
-
# "example.com:4000": "localhost:3000"
|
25
|
-
#
|
26
|
-
# =>
|
27
|
-
# ERROR: port in trigger unsupported
|
28
|
-
#
|
29
|
-
# "dns://example.com": "@"
|
30
|
-
# "http://example.com:4000"
|
31
|
-
#
|
32
|
-
# "example.com": "http-redirect://localhost:3000"
|
33
|
-
#
|
34
|
-
# =>
|
35
|
-
# "dns://example.com": "@"
|
36
|
-
# "http://example.com": "http-redirect://?blank?to=http://localhost:3000&code=302"
|
37
|
-
# =====================================
|
38
|
-
#
|
39
|
-
# ".*\\.stubby.dev": "file:///var/www/tmp
|
40
|
-
#
|
41
|
-
# =>
|
42
|
-
#
|
43
|
-
# "dns://.*\\.stubby.dev": "@",
|
44
|
-
# "http://.*\\.stubby.dev": "file:///var/www/tmp",
|
45
|
-
# "https://.*\\.stubby.dev": "file:///var/www/tmp",
|
46
|
-
scheme, remains = instruction.split("://")
|
47
|
-
scheme, remains = remains, scheme if remains.nil?
|
48
|
-
|
49
|
-
if scheme.nil?
|
50
|
-
expand_bare(trigger, instruction)
|
51
|
-
else
|
52
|
-
expand_with_protocol(trigger, instruction)
|
53
18
|
end
|
54
|
-
end
|
55
19
|
|
56
|
-
|
57
|
-
|
58
|
-
"
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
20
|
+
def expand_rule(trigger, instruction)
|
21
|
+
# Default expansion:
|
22
|
+
# "example.com": "localhost:3000"
|
23
|
+
#
|
24
|
+
# =>
|
25
|
+
# "dns://example.com": "@"
|
26
|
+
# "http://example.com": "http-redirect://blank?to=https://example.com&code=302"
|
27
|
+
# "https://example.com": "http-proxy://localhost:3000"
|
28
|
+
#
|
29
|
+
# "example.com:4000": "localhost:3000"
|
30
|
+
#
|
31
|
+
# =>
|
32
|
+
# ERROR: port in trigger unsupported
|
33
|
+
#
|
34
|
+
# "dns://example.com": "@"
|
35
|
+
# "http://example.com:4000"
|
36
|
+
#
|
37
|
+
# "example.com": "http-redirect://localhost:3000"
|
38
|
+
#
|
39
|
+
# =>
|
40
|
+
# "dns://example.com": "@"
|
41
|
+
# "http://example.com": "http-redirect://?blank?to=http://localhost:3000&code=302"
|
42
|
+
# =====================================
|
43
|
+
#
|
44
|
+
# ".*\\.stubby.dev": "file:///var/www/tmp
|
45
|
+
#
|
46
|
+
# =>
|
47
|
+
#
|
48
|
+
# "dns://.*\\.stubby.dev": "@",
|
49
|
+
# "http://.*\\.stubby.dev": "file:///var/www/tmp",
|
50
|
+
# "https://.*\\.stubby.dev": "file:///var/www/tmp",
|
51
|
+
scheme, remains = instruction.split("://")
|
52
|
+
scheme, remains = remains, scheme if remains.nil?
|
53
|
+
|
54
|
+
if scheme.nil?
|
55
|
+
expand_bare(trigger, instruction)
|
56
|
+
else
|
57
|
+
expand_with_protocol(trigger, instruction)
|
58
|
+
end
|
59
|
+
end
|
63
60
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
61
|
+
def expand_bare(trigger, instruction)
|
62
|
+
{
|
63
|
+
"dns://#{trigger}/a" => "dns-a://#{STUBBY_MASTER}",
|
64
|
+
"http://#{trigger}" => "http-redirect://blank?to=https://#{trigger}&code=302",
|
65
|
+
"https://#{trigger}" => "http-proxy://#{instruction}"
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def expand_with_protocol(trigger, instruction)
|
70
|
+
{
|
71
|
+
"dns://#{trigger}/a" => "dns-a://#{STUBBY_MASTER}",
|
72
|
+
"http://#{trigger}" => instruction,
|
73
|
+
"https://#{trigger}" => instruction
|
74
|
+
}
|
75
|
+
end
|
70
76
|
end
|
71
77
|
end
|
72
78
|
end
|
@@ -1,131 +1,136 @@
|
|
1
1
|
require 'rubydns'
|
2
2
|
require 'ipaddress'
|
3
3
|
require 'uri'
|
4
|
-
require 'pry'
|
5
|
-
|
6
|
-
module Extensions
|
7
|
-
module DNS
|
8
|
-
class UnsupportedOS < Exception; end
|
9
|
-
|
10
|
-
class Server < RubyDNS::Server
|
11
|
-
UPSTREAM = RubyDNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]])
|
12
|
-
IN = Resolv::DNS::Resource::IN
|
13
|
-
|
14
|
-
case RbConfig::CONFIG['host_os']
|
15
|
-
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
16
|
-
raise UnsupportedOS, "Sorry, Windows is not currently supported"
|
17
|
-
when /darwin|mac os/
|
18
|
-
include Extensions::DNS::OSX
|
19
|
-
when /linux/
|
20
|
-
raise UnsupportedOS, "Sorry, Linux is not currently supported"
|
21
|
-
else
|
22
|
-
raise UnsupportedOS, "Sorry, #{RbConfig::CONFIG['host_os']} wasn't recognized"
|
23
|
-
end
|
24
4
|
|
25
|
-
|
26
|
-
|
27
|
-
|
5
|
+
module Stubby
|
6
|
+
module Extensions
|
7
|
+
module DNS
|
8
|
+
class UnsupportedOS < Exception; end
|
9
|
+
|
10
|
+
class Server < RubyDNS::Server
|
11
|
+
UPSTREAM = RubyDNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]])
|
12
|
+
IN = Resolv::DNS::Resource::IN
|
13
|
+
|
14
|
+
case RbConfig::CONFIG['host_os']
|
15
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
16
|
+
raise UnsupportedOS, "Sorry, Windows is not currently supported"
|
17
|
+
when /darwin|mac os/
|
18
|
+
include Stubby::Extensions::DNS::OSX
|
19
|
+
when /linux/
|
20
|
+
raise UnsupportedOS, "Sorry, Linux is not currently supported"
|
21
|
+
else
|
22
|
+
raise UnsupportedOS, "Sorry, #{RbConfig::CONFIG['host_os']} wasn't recognized"
|
23
|
+
end
|
28
24
|
|
29
|
-
|
25
|
+
def process(name, resource_class, transaction)
|
26
|
+
body = HTTPI.post("http://#{STUBBY_MASTER}:9000/rules/search.json",
|
27
|
+
trigger: "dns://#{name}/#{symbol_from_resource_class(resource_class)}").body
|
30
28
|
|
31
|
-
|
32
|
-
transaction.passthrough!(UPSTREAM)
|
33
|
-
return
|
34
|
-
end
|
29
|
+
instruction = MultiJson.load(body)
|
35
30
|
|
36
|
-
|
31
|
+
if instruction.nil? or instruction == "@"
|
32
|
+
transaction.passthrough!(UPSTREAM)
|
33
|
+
return
|
34
|
+
end
|
37
35
|
|
38
|
-
|
36
|
+
url = URI.parse(instruction)
|
39
37
|
|
40
|
-
|
41
|
-
response_resource_class = IN::CNAME
|
42
|
-
end
|
38
|
+
response_resource_class = resource url.scheme.gsub('dns-', '')
|
43
39
|
|
44
|
-
|
40
|
+
if !IPAddress.valid?(url.host) and response_resource_class == IN::A
|
41
|
+
response_resource_class = IN::CNAME
|
42
|
+
end
|
45
43
|
|
46
|
-
|
47
|
-
response = Resolv::DNS::Name.create(url.host)
|
48
|
-
end
|
44
|
+
response = url.host
|
49
45
|
|
50
|
-
|
46
|
+
if [IN::CNAME, IN::MX].include? response_resource_class
|
47
|
+
response = Resolv::DNS::Name.create(url.host)
|
48
|
+
end
|
51
49
|
|
52
|
-
|
53
|
-
transaction.respond!(10, response,
|
54
|
-
:resource_class => response_resource_class,
|
55
|
-
:ttl => 0)
|
56
|
-
else
|
57
|
-
transaction.respond!(response,
|
58
|
-
:resource_class => response_resource_class,
|
59
|
-
:ttl => 0)
|
60
|
-
end
|
61
|
-
end
|
50
|
+
puts "DNS: #{name} => #{response}-#{resource_class.name})"
|
62
51
|
|
63
|
-
|
64
|
-
|
65
|
-
|
52
|
+
if response_resource_class == IN::MX
|
53
|
+
transaction.respond!(10, response,
|
54
|
+
:resource_class => response_resource_class,
|
55
|
+
:ttl => 0)
|
56
|
+
else
|
57
|
+
transaction.respond!(response,
|
58
|
+
:resource_class => response_resource_class,
|
59
|
+
:ttl => 0)
|
60
|
+
end
|
61
|
+
end
|
66
62
|
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
def run!(session, options)
|
64
|
+
return if options[:dns] == false
|
65
|
+
trap("INT"){ stop! }
|
70
66
|
|
71
|
-
|
72
|
-
|
73
|
-
|
67
|
+
@session = session
|
68
|
+
setup_references and run_dns_server
|
69
|
+
end
|
74
70
|
|
75
|
-
|
76
|
-
|
77
|
-
|
71
|
+
def stop!
|
72
|
+
teardown_references and stop_dns_server
|
73
|
+
end
|
78
74
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
if i.scheme.nil?
|
83
|
-
{ t.to_s => "dns-a://#{instruction}" }
|
84
|
-
else
|
85
|
-
{ t.to_s => instruction }
|
75
|
+
def restore!
|
76
|
+
restore_references
|
86
77
|
end
|
87
|
-
|
78
|
+
|
79
|
+
def expand_rule(trigger, instruction)
|
80
|
+
i = URI.parse(instruction)
|
81
|
+
t = URI.parse(trigger)
|
82
|
+
|
83
|
+
# If not specifying a record type, match a
|
84
|
+
t.path = "/a" if t.path.empty?
|
88
85
|
|
89
|
-
|
86
|
+
if i.scheme.nil?
|
87
|
+
{ t.to_s => "dns-a://#{instruction}" }
|
88
|
+
else
|
89
|
+
{ t.to_s => instruction }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
90
94
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
+
def resource(pattern)
|
96
|
+
return IN::A unless pattern.respond_to? :to_sym
|
97
|
+
symbol_to_resource_class[pattern.to_sym] || IN::A
|
98
|
+
end
|
95
99
|
|
96
|
-
|
97
|
-
|
98
|
-
|
100
|
+
def symbol_from_resource_class(klass)
|
101
|
+
symbol_to_resource_class.invert[klass] || :a
|
102
|
+
end
|
99
103
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
104
|
+
def symbol_to_resource_class
|
105
|
+
{
|
106
|
+
a: IN::A,
|
107
|
+
aaaa: IN::AAAA,
|
108
|
+
srv: IN::SRV,
|
109
|
+
wks: IN::WKS,
|
110
|
+
minfo: IN::MINFO,
|
111
|
+
mx: IN::MX,
|
112
|
+
ns: IN::NS,
|
113
|
+
ptr: IN::PTR,
|
114
|
+
soa: IN::SOA,
|
115
|
+
txt: IN::TXT,
|
116
|
+
cname: IN::CNAME
|
117
|
+
}
|
118
|
+
end
|
115
119
|
|
116
|
-
|
117
|
-
|
120
|
+
def run_dns_server
|
121
|
+
logger.level = Logger::INFO
|
118
122
|
|
119
|
-
|
120
|
-
|
121
|
-
|
123
|
+
EventMachine.run do
|
124
|
+
run(:listen => [[:tcp, STUBBY_MASTER, 53],
|
125
|
+
[:udp, STUBBY_MASTER, 53]])
|
126
|
+
end
|
122
127
|
end
|
123
|
-
end
|
124
128
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
+
def stop_dns_server
|
130
|
+
fire :stop
|
131
|
+
EventMachine::stop_event_loop
|
132
|
+
rescue
|
133
|
+
end
|
129
134
|
end
|
130
135
|
end
|
131
136
|
end
|