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.
data/LICENSE ADDED
File without changes
data/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # Stubby
2
+
3
+ A local DNS and HTTP server combo that provides a package manager
4
+ solution to configuring network systems on a development machine. This
5
+ is currently only designed to run on OS X.
6
+
7
+ Use it to:
8
+
9
+ * manage your dev domains (like pow, with lethal power)
10
+
11
+ * distribute a spec for your API so developers can run basic tests without
12
+ hitting your dev server.
13
+
14
+ * point a client to the right version of an app without editing a hosts file.
15
+
16
+ * lock down access to a dev system only to users running a stubby.json config
17
+ from your project.
18
+
19
+ ## Installation
20
+
21
+ Install the stubby gem:
22
+
23
+ > $ sudo gem install stubby
24
+
25
+ ## Available Options
26
+
27
+ > $ sudo stubby -h
28
+ > Commands:
29
+ > stubby env NAME # Switch stubby environment
30
+ > stubby help [COMMAND] # Describe available commands or one specific command
31
+ > stubby search # View all available stubs
32
+ > stubby start ENVIRONMENT # Starts stubby HTTP and DNS servers, default env ENVIRONMENT
33
+ > stubby status # View current rules
34
+
35
+ ## Getting Started
36
+
37
+ Stubby uses `Stubfile.json` for configuration. This file includes a mapping of
38
+ environments to a number of rules that define server configurations and stub
39
+ usage for the project.
40
+
41
+ > cd ~/MyProject
42
+ > cat Stubfile.json
43
+ > {
44
+ > "test": {
45
+ > "dependencies": {
46
+ > "example": "staging"
47
+ > },
48
+ >
49
+ > "(https?:\/\/)?example.com": "http://localhost:3000"
50
+ > },
51
+ >
52
+ > "staging": {
53
+ > "dependencies": {
54
+ > "example": "staging"
55
+ > },
56
+ >
57
+ > "example.com": "dns-cname://aws..."
58
+ > }
59
+ > }
60
+
61
+ > $ sudo stubby start
62
+
63
+ The 'test' and 'staging' modes for this project both include rules for the
64
+ 'example' stub, and then define a single rule of their own. Stubby starts
65
+ by default in the 'development' environment, so with this `Stubfile.json`,
66
+ the stubby server is not yet modifying any requests. In a new terminal:
67
+
68
+ > $ sudo stubby env test
69
+
70
+ Switches stubby to test mode. Now the 'example' stub is activated, and
71
+ additionally any requests to http or https versions of example.com are
72
+ routed to http://localhost:3000. Let's take a look at the rules applied:
73
+
74
+ > $ sudo bin/stubby status
75
+ > {
76
+ > "rules":{
77
+ > "example":{
78
+ > "_comment":"All SMTP traffic (NOT YET FUNCTIONAL)",
79
+ > "admin.example.com":"10.0.1.1",
80
+ > "admin2.example.com":"dns-cname://admin.example.com",
81
+ > "(http?://)?merchant.example.com":"http://10.0.1.1",
82
+ > "(https?://)?.*.example.io":"http://10.0.1.1",
83
+ > "(https?://)?.*mail.*yahoo.*":"http://en.wikipedia.org/wiki/RTFM",
84
+ > "(https?://)?yahoo.com":"https-redirect://duckduckgo.com",
85
+ > "stubby\\..*":"file:///var/www/tmp",
86
+ > "api.example.com":"file://~/.stubby/example/files",
87
+ > "smtp://.*":"log:///var/log/out.txt"
88
+ > },
89
+ > "_":{
90
+ > "dependencies":{
91
+ > "example":"staging"
92
+ > },
93
+ > "(https?://)?example.com":"http://localhost:3000"
94
+ > }
95
+ > },
96
+ > "environment":"test"
97
+ > }
98
+
99
+ This shows us all activated rules. the "_" indicates that the rules are loaded
100
+ from the current `Stubfile.json`. We also see that requests to yahoo.com are
101
+ redirected to https://duckduckgo.com:
102
+
103
+ To revert the system back to normal, just CTRL-C from the main stubby process.
104
+ This will revert any changes made to configure DNS servers for all network
105
+ interfaces and will shut down the stubby server.
106
+
107
+ ## Stubbing
108
+
109
+ A stub is a folder named with the name of the stub that contains a stubby.json file. The stubby.json file contains a hash with the available
110
+ modes. Each mode contains a set of rules that define how to route DNS and how to handle potential extension requests (redirects, file server, etc).
111
+
112
+ Installed stubs are in the ~/.stubby folder:
113
+
114
+ > $ ls ~/.stubby
115
+ > example system.json
116
+
117
+ The example folder is the `example` stub, and the system.json file contains the agent configurations. You don't need to manually edit it.
118
+
119
+ > $ find ~/.stubby/example
120
+ > ... example
121
+ > ... example/files
122
+ > ... example/hello.html
123
+ > ... example/stubby.json
124
+
125
+ The example/stubby.json file has two modes, staging, and production:
126
+
127
+ > cat ~/.stubby/example/stubby.json
128
+ { "staging": {...}, "production": {...} }
129
+
130
+ Each environment contains a number of rules:
131
+
132
+ { "staging": {
133
+ "MATCH_REGEXP": "INSTRUCTION"
134
+ } ... }
135
+
136
+ When a request is made, either DNS or HTTP (important), the request is
137
+ compared against the MATCH_REGEXP. If matched, the INSTRUCTION is executed. Since the same rules are consulted for DNS and HTTP, if you are
138
+ trying to overwrite a domain, you should make sure the match won't exclude
139
+ simply the host. For example, to proxy web traffic from test.example.com
140
+ to a server at 10.0.1.5:
141
+
142
+ "test.example.com": "http://10.0.1.5"
143
+
144
+ This results in
145
+
146
+ > $ dig test.example.com
147
+ ...
148
+ ;; ANSWER SECTION:
149
+ test.example.com. 0 IN A 172.16.123.1
150
+
151
+ 172.16.123.1 is the stubby host url (TODO: configurable). All requests
152
+ to http://test.example.com are routed to the stubby web server at that
153
+ address.
154
+
155
+ > $ curl test.example.com
156
+
157
+ Issues a request handled by the stubby web server, which proxies the request to 172.16.123.1.
158
+
159
+ ## Contributing a Stub
160
+
161
+ Fork this repository, update the index.json file, and submit a pull request. For
162
+ this major version, the remote registry will just be the index.json file from
163
+ this project's github.
164
+
165
+ ### DNS Only
166
+
167
+ To simply override DNS for test.example.com, you can create an A record on lookup:
168
+
169
+ "test.example.com": "10.0.1.5"
170
+
171
+ Or a CNAME, if no IP is given:
172
+
173
+ "test.example.com": "test.example2.com"
174
+
175
+ But you can be explicit in the INSTRUCTION:
176
+
177
+ "test.example.com": "dns-a://10.0.1.5"
178
+ "test.example.com": "dns-cname://test.example2.com"
179
+
180
+ Using the dns-#{name} convention, you can create simple references to
181
+ any dns record type. TODO: need to allow mx record priority somehow.
182
+
183
+ ### File Server
184
+
185
+ Because stubby can intercept HTTP requests, it includes a base set of functionality that allows you two serve files directly from the stub. Given a rule:
186
+
187
+ "api.example.com": "file://~/.stubby/example/files"
188
+
189
+ DNS will resolve to the stubby server:
190
+
191
+ > $ dig api.example.com
192
+ ...
193
+ api.example.com. 0 IN A 172.16.123.1
194
+
195
+ And a web request to api.example.com will serve files from the ~/.stubby/example/files directory:
196
+
197
+ > $ curl http://api.example.com/hello.html
198
+ > <html><head></head><body>Hello</body></html>
199
+
200
+ This is designed to allow you to create API stubs (success responses, for instance).
201
+
202
+
203
+ ### HTTP Redirects
204
+
205
+ Given a rule:
206
+
207
+ "(https?:\/\/)?yahoo.com": "http-redirect://duckduckgo.com"
208
+
209
+ DNS will resolve to the stubby server, and the web request to http://yahoo.com will redirect to http://duckduckgo.com.
210
+
211
+ ### Vision
212
+
213
+ * protocol in instruction becomes a plugin system. dns-cname:, for instance,
214
+ could be handled by the dns plugin. If it didn't exist when Stubfile.json was
215
+ being installed, it would be installed.
216
+ * proxy traffic on ports and send to log systems:
217
+ ":25": "log-smtp://"
218
+ ":3306": "log-mysql://"
219
+ * web app front-end: show emails sent, mysql queries made, etc.
data/bin/stubby ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'stubby'
6
+ require 'stubby/cli'
7
+
8
+ Stubby::CLI::Application.start(ARGV)
@@ -0,0 +1,160 @@
1
+ require 'thor'
2
+
3
+ require 'stubby/registry'
4
+ require 'stubby/extensions/dns'
5
+ require 'stubby/extensions/http'
6
+
7
+ module Stubby
8
+ module CLI
9
+ class Application < Thor
10
+ default_task :start
11
+
12
+ # TODO: filesystem watch all config directories for change
13
+ desc "start ENVIRONMENT", "Starts stubby HTTP and DNS servers"
14
+ long_desc <<-LONGDESC
15
+ > $ sudo stubby start [ENVIRONMENT='development']
16
+
17
+ Starts the stubby HTTP and DNS servers and loads the configuration
18
+ from `Stubfile.json` for the named environment. If no environment
19
+ is given, we default to 'development'
20
+
21
+ An environment need not actually match a name in `Stubfile.json`.
22
+ This allows you to use environments named in dependencies but not
23
+ in the application. If no rules match the environment, Stubby
24
+ just won't override any behaviors.
25
+ LONGDESC
26
+
27
+ def start(environment="development")
28
+ unless File.exists?("Stubfile.json")
29
+ puts "[ERROR]: Stubfile.json not found!"
30
+ return
31
+ end
32
+
33
+ unless permissions?
34
+ puts "[ERROR]: ATM I need to be run with sudo..."
35
+ return
36
+ end
37
+
38
+ if master_running?
39
+ puts "[ERROR]: Stubby's already running!"
40
+ return
41
+ end
42
+
43
+ environments = MultiJson.load(File.read("Stubfile.json"))
44
+
45
+ File.write(pidfile, Process.pid)
46
+
47
+ master = Stubby::Master.new(environments)
48
+ master.environment = environment
49
+ master.run!
50
+ end
51
+
52
+ desc "env NAME", "Switch stubby environment"
53
+ long_desc <<-LONGDESC
54
+ > $ sudo stubby env test
55
+ > {"status":"ok"}
56
+ LONGDESC
57
+ def env(name=nil)
58
+ unless master_running?
59
+ puts "[ERROR]: Stubby must be running to run 'environment'"
60
+ return
61
+ end
62
+
63
+ puts HTTPI.post("http://#{STUBBY_MASTER}:9000/environment.json", environment: name).body
64
+ end
65
+
66
+ desc "search", "View all available stubs"
67
+ long_desc <<-LONGDESC
68
+ View all available registered stubs. These are stubs that you can use
69
+ as dependencies in Stubfile.json.
70
+
71
+ > $ sudo stubby search
72
+ > {
73
+ > "example":[
74
+ > {
75
+ > "name":"example",
76
+ > "version":"v0.0.1",
77
+ > ...
78
+ > }
79
+ > ],
80
+ > "spreedly":[
81
+ > {
82
+ > "name":"spreedly",
83
+ > "version":"v0.0.1",
84
+ > ...
85
+ > }
86
+ > ]
87
+ > }
88
+
89
+ Wildcard supported for search:
90
+
91
+ > $ sudo stubby search ex*
92
+ > {
93
+ > "example":[
94
+ > {
95
+ > "name":"example",
96
+ > "version":"v0.0.1",
97
+ > ...
98
+ > }
99
+ > ]
100
+ > }
101
+
102
+ LONGDESC
103
+ def search(name=nil)
104
+ if master_running?
105
+ available = MultiJson.load(HTTPI.get("http://#{STUBBY_MASTER}:9000/stubs/available.json").body)
106
+ else
107
+ available = Stubby::Api.registry.index
108
+ end
109
+
110
+ puts MultiJson.dump(available.select { |key, ri|
111
+ File.fnmatch(name || "*", key)
112
+ }, pretty: true)
113
+ end
114
+
115
+ desc "status", "View current rules"
116
+ long_desc <<-LONGDESC
117
+ > $ sudo bin/stubby status
118
+ > {
119
+ > "rules":{
120
+ > "example":{
121
+ > "admin.example.com":"10.0.1.1",
122
+ > ...
123
+ > },
124
+ > "_":{
125
+ > "dependencies":{
126
+ > "example":"staging"
127
+ > },
128
+ > "(https?://)?example.com":"http://localhost:3000"
129
+ > }
130
+ > },
131
+ > "environment":"test"
132
+ > }
133
+ LONGDESC
134
+ def status
135
+ if master_running?
136
+ environment = MultiJson.load(HTTPI.get("http://#{STUBBY_MASTER}:9000/environment.json").body)["environment"]
137
+ activated = MultiJson.load(HTTPI.get("http://#{STUBBY_MASTER}:9000/stubs/activated.json").body)
138
+ puts MultiJson.dump({ "rules" => activated, "environment" => environment }, pretty: true)
139
+ else
140
+ puts MultiJson.dump(status: "error", message: "Stubby currently not running")
141
+ end
142
+ end
143
+
144
+ private
145
+ def pidfile
146
+ @pidfile ||= File.expand_path("~/.stubby/pid")
147
+ end
148
+
149
+ def master_running?
150
+ Process.kill(0, File.read(pidfile).to_i)
151
+ rescue
152
+ false
153
+ end
154
+
155
+ def permissions?
156
+ `whoami`.strip == "root"
157
+ end
158
+ end
159
+ end
160
+ end
data/lib/stubby/cli.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'thor'
2
+
3
+ require 'stubby/cli/application'
@@ -0,0 +1,58 @@
1
+ # This module extracts the OS level DNS configuration dependency for OSX.
2
+ module Extensions
3
+ module DNS
4
+ module OSX
5
+ private
6
+ def servers_for(interface)
7
+ servers = `networksetup -getdnsservers '#{interface}'`
8
+
9
+ if servers =~ /There aren't any DNS Servers/
10
+ return ["empty"]
11
+ else
12
+ return servers.split("\n")
13
+ end
14
+ end
15
+
16
+ def setup_reference(interface)
17
+ return if interface.include? "*"
18
+ @network_interfaces[interface] = servers_for(interface)
19
+ puts "[INFO] #{interface} configured with Stubby DNS. Will restore to #{@network_interfaces[interface]}"
20
+ `networksetup -setdnsservers '#{interface}' #{STUBBY_MASTER}`
21
+ end
22
+
23
+ def teardown_reference(interface, servers)
24
+ `networksetup -setdnsservers '#{interface}' #{servers.join(" ")}`
25
+ puts "[INFO] #{interface} original DNS settings restored #{servers.join(" ")}"
26
+ rescue => e
27
+ puts e.inspect
28
+ end
29
+
30
+ def setup_references
31
+ # TODO: if we connect to a new network, we'd like that to use us, too
32
+ return if @network_interfaces
33
+
34
+ @network_interfaces = {}
35
+ `networksetup listallnetworkservices`.split("\n").each do |interface|
36
+ next if interface.include? '*'
37
+ setup_reference(interface)
38
+ end
39
+
40
+ flush
41
+ end
42
+
43
+ def teardown_references
44
+ interfaces, @network_interfaces = @network_interfaces, nil
45
+
46
+ (interfaces || []).each do |interface, servers|
47
+ teardown_reference(interface, servers)
48
+ end
49
+
50
+ flush
51
+ end
52
+
53
+ def flush
54
+ `dscacheutil -flushcache`
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,115 @@
1
+ require 'rubydns'
2
+ require 'ipaddress'
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
+
25
+ def process(name, resource_class, transaction)
26
+ body = HTTPI.post("http://#{STUBBY_MASTER}:9000/rules/search.json", trigger: name).body
27
+
28
+ instruction = MultiJson.load(body)
29
+
30
+ if instruction.nil? or instruction == "@"
31
+ transaction.passthrough!(UPSTREAM)
32
+ return
33
+ end
34
+
35
+ url = URI.parse(instruction)
36
+
37
+ if url.scheme.to_s.empty?
38
+ url = URI.parse("dns-a://" + instruction)
39
+ elsif (url.scheme.to_s =~ /^dns-.*/).nil?
40
+ url.host = STUBBY_MASTER
41
+ end
42
+
43
+ response_resource_class = resource url.scheme.gsub('dns-', '')
44
+
45
+ if url.host.to_s.empty?
46
+ url.host = STUBBY_MASTER
47
+ end
48
+
49
+ if !IPAddress.valid?(url.host) and response_resource_class == IN::A
50
+ response_resource_class = IN::CNAME
51
+ end
52
+
53
+ response = url.host
54
+
55
+ if response_resource_class == IN::CNAME
56
+ response = Resolv::DNS::Name.create(url.host)
57
+ end
58
+
59
+ puts "DNS: #{name} => #{response}-#{resource_class.name})"
60
+
61
+ transaction.respond!(response,
62
+ :resource_class => response_resource_class,
63
+ :ttl => 0)
64
+ end
65
+
66
+ def run!(session, options)
67
+ return if options[:dns] == false
68
+ trap("INT"){ stop! }
69
+
70
+ @session = session
71
+ setup_references and run_dns_server
72
+ end
73
+
74
+ def stop!
75
+ teardown_references and stop_dns_server
76
+ end
77
+
78
+
79
+ private
80
+
81
+ def resource(pattern)
82
+ return IN::A unless pattern.respond_to? :to_sym
83
+
84
+ {
85
+ a: IN::A,
86
+ aaaa: IN::AAAA,
87
+ srv: IN::SRV,
88
+ wks: IN::WKS,
89
+ minfo: IN::MINFO,
90
+ mx: IN::MX,
91
+ ns: IN::NS,
92
+ ptr: IN::PTR,
93
+ soa: IN::SOA,
94
+ txt: IN::TXT,
95
+ cname: IN::CNAME
96
+ }[pattern.to_sym] || IN::A
97
+ end
98
+
99
+ def run_dns_server
100
+ logger.level = Logger::INFO
101
+
102
+ EventMachine.run do
103
+ run(:listen => [[:tcp, STUBBY_MASTER, 53],
104
+ [:udp, STUBBY_MASTER, 53]])
105
+ end
106
+ end
107
+
108
+ def stop_dns_server
109
+ fire :stop
110
+ EventMachine::stop_event_loop
111
+ rescue
112
+ end
113
+ end
114
+ end
115
+ end