stubby 0.0.1

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