Commit 85a44b0b authored by Terence Lee's avatar Terence Lee

Merge pull request #16 from hone/ordered_routing

Ordered Routing
parents 3c0917e4 a4ae2e4f
...@@ -120,6 +120,15 @@ Using the headers key, you can set custom response headers. It uses the same ope ...@@ -120,6 +120,15 @@ Using the headers key, you can set custom response headers. It uses the same ope
} }
``` ```
### Route Ordering
* Root Files
* Clean URLs
* Proxies
* Redirects
* Custom Routes
* 404
## Testing ## Testing
For testing we use Docker to replicate Heroku locally. You'll need to have [it setup locally](https://docs.docker.com/installation/). We're also using rspec for testing with Ruby. You'll need to have those setup and install those deps: For testing we use Docker to replicate Heroku locally. You'll need to have [it setup locally](https://docs.docker.com/installation/). We're also using rspec for testing with Ruby. You'll need to have those setup and install those deps:
......
require 'json' require 'json'
require 'uri'
require_relative 'nginx_config_util' require_relative 'nginx_config_util'
class NginxConfig class NginxConfig
...@@ -13,11 +14,16 @@ class NginxConfig ...@@ -13,11 +14,16 @@ class NginxConfig
if hash["origin"][-1] != "/" if hash["origin"][-1] != "/"
json["proxies"][loc].merge!("origin" => hash["origin"] + "/") json["proxies"][loc].merge!("origin" => hash["origin"] + "/")
end end
uri = URI(hash["origin"])
json["proxies"][loc]["path"] = uri.path
uri.path = ""
json["proxies"][loc]["host"] = uri.to_s
end end
json["clean_urls"] ||= false json["clean_urls"] ||= false
json["https_only"] ||= false json["https_only"] ||= false
json["routes"] ||= {} json["routes"] ||= {}
json["routes"] = Hash[json["routes"].map {|route, target| [NginxConfigUtil.to_regex(route), target] }] json["routes"] = NginxConfigUtil.parse_routes(json["routes"])
json["redirects"] ||= {} json["redirects"] ||= {}
json["error_page"] ||= nil json["error_page"] ||= nil
json["debug"] ||= ENV['STATIC_DEBUG'] json["debug"] ||= ENV['STATIC_DEBUG']
......
...@@ -16,4 +16,32 @@ module NginxConfigUtil ...@@ -16,4 +16,32 @@ module NginxConfigUtil
end end
segments.join segments.join
end end
def self.parse_routes(json)
routes = json.map do |route, target|
[to_regex(route), target]
end
Hash[routes]
end
def self.match_proxies(proxies, uri)
return false unless proxies
proxies.each do |proxy|
return proxy if Regexp.compile("^#{proxy}") =~ uri
end
false
end
def self.match_redirects(redirects, uri)
return false unless redirects
redirects.each do |redirect|
return redirect if redirect == uri
end
false
end
end end
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))
USER_CONFIG = "/app/static.json"
config = {}
config = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req = Nginx::Request.new
uri = req.var.uri
proxies = config["proxies"] || {}
redirects = config["redirects"] || {}
if proxy = NginxConfigUtil.match_proxies(proxies.keys, uri)
"@#{proxy}"
elsif redirect = NginxConfigUtil.match_redirects(redirects.keys, uri)
"@#{redirect}"
else
"@404"
end
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))
USER_CONFIG = "/app/static.json"
config = {}
config = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req = Nginx::Request.new
uri = req.var.uri
nginx_route = req.var.route
routes = NginxConfigUtil.parse_routes(config["routes"])
proxies = config["proxies"] || {}
redirects = config["redirects"] || {}
if NginxConfigUtil.match_proxies(proxies.keys, uri) || NginxConfigUtil.match_redirects(redirects.keys, uri)
# this will always fail, so try_files uses the callback
uri
else
"/#{routes[nginx_route]}"
end
...@@ -41,19 +41,20 @@ http { ...@@ -41,19 +41,20 @@ http {
error_page 404 500 /<%= error_page %>; error_page 404 500 /<%= error_page %>;
<% end %> <% end %>
<% if clean_urls %>
location / { location / {
mruby_post_read_handler /app/bin/config/lib/ngx_mruby/headers.rb cache; mruby_post_read_handler /app/bin/config/lib/ngx_mruby/headers.rb cache;
try_files $uri $uri/ $uri.html; mruby_set $fallback /app/bin/config/lib/ngx_mruby/routes_fallback.rb cache;
<% if clean_urls %>
try_files $uri $uri/ $uri.html $fallback;
<% else %>
try_files $uri $uri/ $fallback;
<% end %>
} }
<% if clean_urls %>
location ~ \.html$ { location ~ \.html$ {
try_files $uri =404; try_files $uri =404;
} }
<% else %>
location / {
mruby_post_read_handler /app/bin/config/lib/ngx_mruby/headers.rb cache;
}
<% end %> <% end %>
<% if https_only %> <% if https_only %>
...@@ -62,15 +63,16 @@ http { ...@@ -62,15 +63,16 @@ http {
} }
<% end %> <% end %>
<% redirects.each do |path, hash| %>
location <%= path %> {
return <%= hash['status'] || 301 %> <%= hash['url'] %>;
}
<% end %>
<% routes.each do |route, path| %> <% routes.each do |route, path| %>
location ~ ^<%= route %>$ { location ~ ^<%= route %>$ {
try_files /<%= path %> =404; set $route <%= route %>;
mruby_set $path /app/bin/config/lib/ngx_mruby/routes_path.rb cache;
mruby_set $fallback /app/bin/config/lib/ngx_mruby/routes_fallback.rb cache;
<% if clean_urls %>
try_files $uri $uri/ /$uri.html $path $fallback;
<% else %>
try_files $uri $path $fallback;
<% end %>
} }
<% end %> <% end %>
...@@ -79,5 +81,26 @@ http { ...@@ -79,5 +81,26 @@ http {
proxy_pass <%= hash['origin'] %>; proxy_pass <%= hash['origin'] %>;
} }
<% end %> <% end %>
# need this b/c setting $fallback to =404 will try #{root}=404 instead of returning a 404
location @404 {
return 404;
}
# fallback proxy named match
<% proxies.each do |location, hash| %>
location @<%= location %> {
rewrite ^<%= location %>(.*)$ <%= hash['path'] %>/$1 break;
proxy_pass <%= hash['host'] %>;
}
<% end %>
# fallback redirects named match
<% redirects.each do |path, hash| %>
location @<%= path %> {
return <%= hash['status'] || 301 %> <%= hash['url'] %>;
}
<% end %>
} }
} }
{
"routes": {
"/**": "index.html"
},
"redirects": {
"/old/gone": {
"url": "/gone.html",
"status": 302
}
}
}
{
"routes": {
"/**": "index.html"
},
"redirects": {
"/old/gone": {
"url": "/gone.html",
"status": 302
}
},
"clean_urls": true
}
{
"routes": {
"/**": "index.html"
},
"redirects": {
"/old/gone": {
"url": "/gone.html",
"status": 302
}
},
"https_only": true
}
{
"redirects": {
"/old/gone": {
"url": "/gone.html",
"status": 302
},
"/no_redirect": {
"url": "/gone.html",
"status": 302
}
},
"clean_urls": true
}
{
"redirects": {
"/old/gone": {
"url": "/gone.html",
"status": 302
},
"/no_redirect.html": {
"url": "/gone.html",
"status": 302
}
}
}
{
"routes": {
"/**": {
"path": "index.html",
"excepts": [
"/assets/*",
"/api/**"
]
}
}
}
...@@ -162,6 +162,40 @@ STATIC_JSON ...@@ -162,6 +162,40 @@ STATIC_JSON
expect(response.body.chomp).to eq("api") expect(response.body.chomp).to eq("api")
end end
end end
context "with custom routes" do
before do
File.open(static_json_path, "w") do |file|
file.puts <<STATIC_JSON
{
"proxies": {
"/api/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}/foo"
},
"/proxy/": {
"origin": "http://#{AppRunner::HOST_IP}:#{AppRunner::HOST_PORT}/foo"
}
},
"routes": {
"/api/**": "index.html"
}
}
STATIC_JSON
end
end
it "should take precedence over a custom route" do
response = app.get("/api/bar/")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("api")
end
it "should proxy if there is no matching custom route" do
response = app.get("/proxy/bar/")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("api")
end
end
end end
describe "custom headers" do describe "custom headers" do
...@@ -323,4 +357,144 @@ STATIC_JSON ...@@ -323,4 +357,144 @@ STATIC_JSON
end end
end end
end end
describe "ordering" do
let(:name) { "ordering" }
it "should serve files in the correct order" do
app.run do
response = app.get("/assets/app.js")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("{}")
response = app.get("/old/gone")
expect(response.code).to eq("302")
expect(app.get(response["location"]).body.chomp).to eq("goodbye")
response = app.get("/foo")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("hello world")
end
end
context "https" do
let(:name) { "ordering_https" }
it "should serve files in the correct order" do
app.run do
response = app.get("/assets/app.js")
expect(response.code).to eq("301")
uri = URI(response['location'])
expect(uri.path).to eq("/assets/app.js")
expect(uri.scheme).to eq("https")
end
end
end
context "clean_urls" do
let(:name) { "ordering_clean_urls" }
it "should honor clean urls" do
app.run do
response = app.get("/gone")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("goodbye")
response = app.get("/gone.html")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("goodbye")
response = app.get("/bar")
expect(response.code).to eq("301")
response = app.get(response["Location"])
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("bar")
response = app.get("/assets/app.js")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("{}")
response = app.get("/old/gone")
expect(response.code).to eq("302")
expect(app.get(response["location"]).body.chomp).to eq("goodbye")
response = app.get("/foo")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("hello world")
end
end
end
context "without custom routes" do
let(:name) { "ordering_without_custom_routes" }
it "should still respect ordering" do
app.run do
response = app.get("/gone.html")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("goodbye")
response = app.get("/no_redirect.html")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("no_redirect")
response = app.get("/bar")
expect(response.code).to eq("301")
response = app.get(response["Location"])
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("bar")
response = app.get("/assets/app.js")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("{}")
response = app.get("/old/gone")
expect(response.code).to eq("302")
expect(app.get(response["location"]).body.chomp).to eq("goodbye")
response = app.get("/foo")
expect(response.code).to eq("404")
end
end
end
context "with clean urls without custom routes" do
let(:name) { "ordering_with_clean_urls_without_custom_routes" }
it "should still respect ordering" do
app.run do
response = app.get("/gone")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("goodbye")
response = app.get("/gone.html")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("goodbye")
response = app.get("/no_redirect")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("no_redirect")
response = app.get("/bar")
expect(response.code).to eq("301")
response = app.get(response["Location"])
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("bar")
response = app.get("/assets/app.js")
expect(response.code).to eq("200")
expect(response.body.chomp).to eq("{}")
response = app.get("/old/gone")
expect(response.code).to eq("302")
expect(app.get(response["location"]).body.chomp).to eq("goodbye")
response = app.get("/foo")
expect(response.code).to eq("404")
end
end
end
end
end end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment