static-rails 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04cd8c43f2a9d6ff2da0768d8daa7024ca470acb94a2444540d26411dad45675
4
- data.tar.gz: 55246dbc95a32e8517db8bc8b21136f9a3cac60ba18b3cd9bee27ef2501ae774
3
+ metadata.gz: 14d31f3821438735e307f98dd5f4a48215d0b1d678915369a7abd459b3b40145
4
+ data.tar.gz: bc7b15c689693e533a75ac378db060396cde1839edb0acffc04c30e18872bf77
5
5
  SHA512:
6
- metadata.gz: e59dd52fb103bc1add1ad27a05ac83eff4b04b09ecc7bb08b73d2fd071279a87e8ae05e73c2f43a64eda4e2c1c77281d0fd8650a5c4aea2d04c2088980606127
7
- data.tar.gz: 548aa50ee427be8b0a3842b4000a07dbac7b0513cf9a712ab46dcd8903a829a58f395345549b921b154d5acdfa235fdc482e7e357ffcbdffec6a07aaf0d34319
6
+ metadata.gz: f645e5e515d712acd3c1a81935d560d3ef0f2be3a42e5fd42af4320cf6a4ae25e14843b0c076dfda4ec5b980a12cc436cfbef527768704f97c6e89ac2e7e17f6
7
+ data.tar.gz: 61600757a2e549dc5bff49b8fe01da9c0f45eac4a1fd6ba815ea4aafb5655270bfa457ac76c98a0927b98e61bcf741b40a0089f0cfb55f612ad0de386b0fa9d4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.0.4
2
+
3
+ * Add a cookie named `_csrf_token` by default to all static site requests, so
4
+ that your static sites can make CSRF-protected requests of your server
5
+ ([#4](https://github.com/testdouble/static-rails/pull/4))
6
+
1
7
  ## 0.0.3
2
8
 
3
9
  * Add `url_skip_paths_starting_with` array of strings option to site
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- static-rails (0.0.3)
4
+ static-rails (0.0.4)
5
5
  rack-proxy (~> 0.6)
6
6
  railties (>= 5.0.0)
7
7
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CircleCI](https://circleci.com/gh/testdouble/static-rails.svg?style=svg)](https://circleci.com/gh/testdouble/static-rails)
4
4
 
5
- ## What is this thing?
5
+ ## Build and serve your static sites from your Rails app
6
6
 
7
7
  **tl;dr in development, static-rails runs your static site generators &
8
8
  proxies requests; in production, it compiles and serves the final assets**
@@ -25,18 +25,18 @@ without forcing you to abandon your monolithic Rails architecture.
25
25
 
26
26
  Here's what it does:
27
27
 
28
- * In `development` and `test` environments, the gem will run launch each site's
29
- `serve_command` and proxy to them any requests that match their configured
30
- `url_subdomain` and `url_root_path` properties
28
+ * In development, static-rails launches your sites' local servers and then
29
+ proxies any requests to wherever you've mounted them in your Rails app so you
30
+ can start a single server and transition work between your static sites and
31
+ Rails app seamlessly
31
32
 
32
- * When running `rake assets:precompile` (typically performed during a deploy),
33
- the `compile_command` you've configured will be executed for each of your
34
- sites
33
+ * When deploying, static-rails will compile all your static assets when `rake
34
+ assets:precompile` is run, meaning your assets will be built automatically
35
+ when pushed to a platform like Heroku
35
36
 
36
- * In `production`, the gem will host each of your sites' assets out of your
37
- configured `compile_dir` using the same middleware code that Rails uses to
38
- host assets out of `public/`. (Putting a performant CDN in front of everything
39
- remains an exercise for the reader.)
37
+ * In production, static-rails will serve your sites' compiled assets from disk
38
+ with a similar features and performance to what you're familiar with if you've
39
+ ever hosted files out of your `public/` directory
40
40
 
41
41
  ## Install
42
42
 
@@ -46,7 +46,7 @@ Add this to your Gemfile:
46
46
  gem "static-rails"
47
47
  ```
48
48
 
49
- Then run this generator to create a configuration file
49
+ Then run this generator to create a configuration file
50
50
  `config/initializers/static.rb`:
51
51
 
52
52
  ```
@@ -54,11 +54,105 @@ $ rails g static_rails:initializer
54
54
  ```
55
55
 
56
56
  You can check out the configuration options in the [generated file's
57
- comments](/lib/generators/templates/static.rb).
57
+ comments]().
58
58
 
59
59
  Want an example of setting things up? You're in luck, there's an [example
60
60
  app](/example) right in this repo!
61
61
 
62
+ ## Configuring the gem
63
+
64
+ **(Want to dive right in? The generated initializer [enumerates every
65
+ option](/lib/generators/templates/static.rb) and the [example app's
66
+ config](https://github.com/testdouble/static-rails/blob/master/example/config/initializers/static.rb)
67
+ sets up 4 sites.)**
68
+
69
+ ### Top-level configuration
70
+
71
+ So, what should you stick in your initializer's `StaticRails.config do |config|`
72
+ block? These options are set right off the `config` object and control the
73
+ overall behavior of the gem itself, across all your static sites:
74
+
75
+ * **config.proxy_requests** (Default: `!Rails.env.production?`) when true,
76
+ the gem's middleware requests that match where you've mounted your static site
77
+ and proxy them to the development server
78
+
79
+ * **config.serve_compiled_assets** (Default: `Rails.env.production?`) when true,
80
+ the gem's middleware will find your static assets on disk and serve them using
81
+ the same code that Rails uses to serve files out of `public/`
82
+
83
+ * **config.ping_server_timeout** (Default: `5`) the number of seconds that (when
84
+ `proxy_requests` is true, that the gem will wait for a response from a static
85
+ site's server on any given request before timing out and raising an error
86
+
87
+ * **config.set_csrf_token_cookie** (Default: `true`) when true, the gem's
88
+ middleware will set a cookie named `_csrf_token` with each request of your
89
+ static site. You can use this to set the `'x-csrf-token'` header on any
90
+ requests from your site back to routes hosted by the Rails app that are
91
+ [protected from CSRF
92
+ forgery](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
93
+ (if you're not using Rails' cookie store for sessions, turn this off)
94
+
95
+ ### Configuring your static sites themselves
96
+
97
+ To tell the gem about your static sites, assign an array of hashes as `sites`
98
+ (e.g. `config.sites = [{…}]`). Each of those hashes have the following options:
99
+
100
+ * **name** (Required) A unique name for the site (primarily used for
101
+ logging)
102
+
103
+ * **source_dir** (Required) The file path (relative to the Rails app's root) to
104
+ the static site's project directory
105
+
106
+ * **url_subdomain** (Default: `nil`) Constrains the static site's assets to only
107
+ be served for requests to a given subdomain (e.g. for a Rails app hosting
108
+ `example.com`, a Hugo site at `blog.example.com` would set this to `"blog"`)
109
+
110
+ * **url_root_path** (Default: `/`) The base URL path at which to mount the
111
+ static site (e.g. if you want your Jekyll site hosted at `example.com/docs`,
112
+ you'd set this to `/docs`). For most static site generators, you'll want to
113
+ configure it to serve assets from the same path so links and other references
114
+ are correct (see below for examples)
115
+
116
+ * **url_skip_paths_starting_with** (Default: `[]`) If you want to mount your
117
+ static site to `/` but allow the Rails app to serve APIs from `/api`, you can
118
+ set the path prefix `["/api"]` here to tell the gem's middleware not to try to
119
+ proxy or serve the request from your static site, but rather let Rails handle
120
+ it
121
+
122
+ * **start_server** (Default `!Rails.env.production?) When true, the gem will
123
+ start the site's server (and if it ever exits, restart it) as your Rails app
124
+ is booting up. All output from the server will be forwarded to STDOUT/STDERR
125
+
126
+ * **server_command** (Required if `start_server` is true) the command to run to
127
+ start the site's server, from the working directory of `source_dir` (e.g.
128
+ `hugo server`)
129
+
130
+ * **ping_server** (Default: true) if this and `start_server` are both true, then
131
+ wait to proxy any requests until the server is accepting TCP connections
132
+
133
+ * **env** (Default: `{}`) any environment variables you need to pass to either
134
+ the server or compile commands (e.g. `env: {"BUNDLE_PATH" =>
135
+ "vendor/bundle"}`). Note that this configuration file is Ruby, so if you need
136
+ to provide different env vars based on Rails environment, you have the power
137
+ to do that!
138
+
139
+ * **server_host** (Default: `localhost`) the host your static site's server will
140
+ run on
141
+
142
+ * **server_port** (Required if `proxy_requests` is true) the port your static
143
+ site's server will accept requests on
144
+
145
+ * **server_path** (Default: `"/"`) the root URL path to which requests should be
146
+ proxied
147
+
148
+ * **compile_comand** (Required) the command to be run by both the
149
+ `static:compile` and `assets:precompile` Rake commands (e.g. `npm run build`),
150
+ with working directory set to the site's `source_dir`
151
+
152
+ * **compile_dir** (Required when `serve_compiled_assets` is true) the root file
153
+ path to which production assets are compiled, relative to the site's
154
+ `source_dir`
155
+
62
156
  ## Configuring your static site generators
63
157
 
64
158
  Assuming you won't be mounting your static site to your app's root `/` path,
@@ -104,9 +198,10 @@ To mitigate this, there are a few things you can do:
104
198
  base path with `{{ "/" | relURL }}` (given the above `baseURL`, this will
105
199
  render `"/marketing/"`)
106
200
 
107
- Also, because Hugo will serve `/livereload.js` from the root, live-reloading probably
201
+ Also, because Hugo will serve `/livereload.js` from the root, live-reloading probably
108
202
  won't work in development when running through the static-rails proxy.
109
- You might consider disabling it with `--disableLiveReload`.
203
+ You might consider disabling it with `--disableLiveReload` unless you're serving
204
+ Hugo from a root path ("`/`").
110
205
 
111
206
  A static-rails config for a Hugo configuration in `sites` might look like:
112
207
 
@@ -9,6 +9,11 @@ StaticRails.config do |config|
9
9
  # (Applies when a site has both start_server and ping_server set to true)
10
10
  # config.ping_server_timeout = 5
11
11
 
12
+ # When true, both the proxy & static asset middleware will set a cookie
13
+ # named "_csrf_token" to the Rails CSRF token, allowing any client-side
14
+ # API requests to take advantage of Rails' request forgery protection
15
+ # config.set_csrf_token_cookie = true
16
+
12
17
  # The list of static sites you are hosting with static-rails.
13
18
  # Note that order matters! Request will be forwarded to the first site that
14
19
  # matches the subdomain and root path (this probably means you want any sites
@@ -22,11 +22,15 @@ module StaticRails
22
22
  # Number of seconds to wait on sites to confirm servers are ready
23
23
  attr_accessor :ping_server_timeout
24
24
 
25
+ # When true, a cookie named "_csrf_token" will be set by static-rails middleware
26
+ attr_accessor :set_csrf_token_cookie
27
+
25
28
  def initialize
26
29
  @sites = []
27
30
  @proxy_requests = !Rails.env.production?
28
31
  @serve_compiled_assets = Rails.env.production?
29
32
  @ping_server_timeout = 5
33
+ @set_csrf_token_cookie = true
30
34
  end
31
35
 
32
36
  attr_reader :sites
@@ -0,0 +1,20 @@
1
+ require_relative "gets_csrf_token"
2
+
3
+ module StaticRails
4
+ class CsrfMiddleware
5
+ def initialize(app)
6
+ @app = app
7
+ @gets_csrf_token = GetsCsrfToken.new
8
+ end
9
+
10
+ def call(env)
11
+ if env["__static_rails_evil_request_for_csrf_token"]
12
+ req = Rack::Request.new(env)
13
+ [200, {}, [@gets_csrf_token.call(req)]].tap do
14
+ end
15
+ else
16
+ @app.call(env)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module StaticRails
2
+ class DeterminesWhetherToHandleRequest
3
+ def initialize
4
+ @matches_request_to_static_site = MatchesRequestToStaticSite.new
5
+ end
6
+
7
+ def call(env)
8
+ req = Rack::Request.new(env)
9
+
10
+ (req.get? || req.head?) &&
11
+ (StaticRails.config.proxy_requests || StaticRails.config.serve_compiled_assets) &&
12
+ @matches_request_to_static_site.call(req)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module StaticRails
2
+ class GetsCsrfToken
3
+ def call(req)
4
+ masked_authenticity_token(req.session)
5
+ end
6
+
7
+ private
8
+
9
+ def masked_authenticity_token(session, form_options: {})
10
+ ActionController::RequestForgeryProtection.instance_method(:masked_authenticity_token).bind(self).call(session, form_options)
11
+ end
12
+
13
+ def real_csrf_token(session)
14
+ ActionController::RequestForgeryProtection.instance_method(:real_csrf_token).bind(self).call(session)
15
+ end
16
+
17
+ def xor_byte_strings(s1, s2)
18
+ ActionController::RequestForgeryProtection.instance_method(:xor_byte_strings).bind(self).call(s1, s2)
19
+ end
20
+
21
+ def per_form_csrf_tokens
22
+ false
23
+ end
24
+ end
25
+ end
@@ -14,22 +14,21 @@ module StaticRails
14
14
 
15
15
  def perform_request(env)
16
16
  return @app.call(env) unless StaticRails.config.proxy_requests
17
- ServerStore.instance.ensure_all_servers_are_started
18
17
 
19
- req = Rack::Request.new(env)
20
18
  server_store = ServerStore.instance
19
+ server_store.ensure_all_servers_are_started
21
20
  server_store.ensure_servers_are_up
22
21
 
22
+ req = Rack::Request.new(env)
23
23
  if (req.get? || req.head?) && (site = @matches_request_to_static_site.call(req))
24
24
  if site.ping_server && (server = server_store.server_for(site))
25
25
  server.wait_until_ready
26
26
  end
27
27
 
28
28
  @backend = URI("http://#{site.server_host}:#{site.server_port}")
29
-
30
29
  env["HTTP_HOST"] = @backend.host
31
30
  env["PATH_INFO"] = forwarding_path(site, req)
32
- env["HTTP_COOKIE"] = ""
31
+
33
32
  super(env)
34
33
  else
35
34
  @app.call(env)
@@ -1,7 +1,7 @@
1
1
  require_relative "rack_server_check"
2
2
  require_relative "server_store"
3
- require_relative "proxy_middleware"
4
- require_relative "static_middleware"
3
+ require_relative "site_middleware"
4
+ require_relative "site_plus_csrf_middleware"
5
5
 
6
6
  module StaticRails
7
7
  class Railtie < ::Rails::Railtie
@@ -9,14 +9,9 @@ module StaticRails
9
9
  load "tasks/static-rails.rake"
10
10
  end
11
11
 
12
- # Note that user initializer won't have run yet, but we seem to need to
13
- # register the middleware by now if it's going to properly get added to the
14
- # stack. So if the user overrides these flags' defaults, the middleware will
15
- # still be added but will be responsible itself for skipping each request
16
- if StaticRails.config.proxy_requests
17
- config.app_middleware.insert_before 0, ProxyMiddleware
18
- elsif StaticRails.config.serve_compiled_assets
19
- config.app_middleware.insert_before 0, StaticMiddleware
12
+ initializer "static_rails.middleware" do
13
+ config.app_middleware.insert_before 0, SiteMiddleware
14
+ config.app_middleware.use SitePlusCsrfMiddleware
20
15
  end
21
16
 
22
17
  config.after_initialize do |app|
@@ -0,0 +1,63 @@
1
+ require_relative "proxy_middleware"
2
+ require_relative "static_middleware"
3
+ require_relative "determines_whether_to_handle_request"
4
+ require_relative "gets_csrf_token"
5
+
6
+ module StaticRails
7
+ class SiteMiddleware
8
+ PATH_INFO_OBFUSCATION = "JujJVj31M3SpzTjIGBJ2-3iE0lKXOIOlbLuk9Lxwe-Ll2uLuwH5KD8dmt1MqyZ"
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ @proxy_middleware = ProxyMiddleware.new(app)
13
+ @static_middleware = StaticMiddleware.new(app)
14
+ @determines_whether_to_handle_request = DeterminesWhetherToHandleRequest.new
15
+ end
16
+
17
+ def call(env)
18
+ return @app.call(env) unless @determines_whether_to_handle_request.call(env)
19
+
20
+ if require_csrf_before_processing_request? && !csrf_token_is_set?(env)
21
+ # You might be asking yourself what the hell is going on here. In short,
22
+ # This middleware sits at the top of the stack, which is too early to
23
+ # set a CSRF token in a cookie. Therefore, we've placed a subclass of
24
+ # this middleware named SitePlusCsrfMiddleware near the bottom of the
25
+ # middleware stack, which is slower but comes after Session::CookieStore
26
+ # and therefore can write _csrf_token to the cookie. As a result, the
27
+ # observable behavior to the user is identical, but the first request
28
+ # to set the cookie will be marginally slower because it needs to go
29
+ # deeper down the Rails middleware stack
30
+ #
31
+ # But! Between these two is ActionDispatch::Static. In the odd case that
32
+ # a path that this middleware would serve happens to match the name of
33
+ # a path in public/, kicking down the middleware stack would result in
34
+ # that file being served instead of our deeper middleware being called.
35
+ # So to work around this we're just making the PATH_INFO property so
36
+ # ugly that there's no chance it'll match anything. When our subclass
37
+ # gets its shot at this request, it'll know to remove the path
38
+ # obfuscation from PATH_INFO and go about its business.
39
+ #
40
+ # See, easy!
41
+ #
42
+ # (By the way, this was all Matthew Draper's bright idea. You can
43
+ # compliment him here: https://github.com/matthewd )
44
+ @app.call(env.merge("PATH_INFO" => env["PATH_INFO"] + PATH_INFO_OBFUSCATION))
45
+ elsif StaticRails.config.proxy_requests
46
+ @proxy_middleware.call(env)
47
+ elsif StaticRails.config.serve_compiled_assets
48
+ @static_middleware.call(env)
49
+ end
50
+ end
51
+
52
+ protected
53
+
54
+ # Override this in subclass since it'll call super(env) and deal itself
55
+ def require_csrf_before_processing_request?
56
+ StaticRails.config.set_csrf_token_cookie
57
+ end
58
+
59
+ def csrf_token_is_set?(env)
60
+ Rack::Request.new(env).cookies.has_key?("_csrf_token")
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ require_relative "site_middleware"
2
+ require_relative "determines_whether_to_handle_request"
3
+ require_relative "gets_csrf_token"
4
+
5
+ module StaticRails
6
+ class SitePlusCsrfMiddleware < SiteMiddleware
7
+ def initialize(app)
8
+ @determines_whether_to_handle_request = DeterminesWhetherToHandleRequest.new
9
+ @gets_csrf_token = GetsCsrfToken.new
10
+ super
11
+ end
12
+
13
+ def call(env)
14
+ return @app.call(env) unless @determines_whether_to_handle_request.call(env)
15
+
16
+ env = env.merge(
17
+ "PATH_INFO" => env["PATH_INFO"].gsub(/#{PATH_INFO_OBFUSCATION}/, "")
18
+ )
19
+ status, headers, body = super(env)
20
+
21
+ if StaticRails.config.set_csrf_token_cookie
22
+ req = Rack::Request.new(env)
23
+ res = Rack::Response.new(body, status, headers)
24
+ res.set_cookie("_csrf_token", {
25
+ value: @gets_csrf_token.call(req),
26
+ path: "/"
27
+ })
28
+ res.finish
29
+ else
30
+ [status, headers, body]
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def require_csrf_before_processing_request?
37
+ false
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module StaticRails
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: static-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-11 00:00:00.000000000 Z
11
+ date: 2020-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -62,7 +62,10 @@ files:
62
62
  - lib/static-rails.rb
63
63
  - lib/static-rails/compile.rb
64
64
  - lib/static-rails/configuration.rb
65
+ - lib/static-rails/csrf_middleware.rb
66
+ - lib/static-rails/determines_whether_to_handle_request.rb
65
67
  - lib/static-rails/error.rb
68
+ - lib/static-rails/gets_csrf_token.rb
66
69
  - lib/static-rails/matches_request_to_static_site.rb
67
70
  - lib/static-rails/proxy_middleware.rb
68
71
  - lib/static-rails/rack_server_check.rb
@@ -70,6 +73,8 @@ files:
70
73
  - lib/static-rails/server.rb
71
74
  - lib/static-rails/server_store.rb
72
75
  - lib/static-rails/site.rb
76
+ - lib/static-rails/site_middleware.rb
77
+ - lib/static-rails/site_plus_csrf_middleware.rb
73
78
  - lib/static-rails/static_middleware.rb
74
79
  - lib/static-rails/version.rb
75
80
  - lib/static-rails/waits_for_connection.rb