static-rails 0.0.3 → 0.0.4

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.
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