stubby 0.0.10 → 0.0.11
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.
- 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
|