1
1
# frozen_string_literal: true
2
2
3
+ require "device_detector"
4
+
3
5
class TurboBoost ::Commands ::EntryMiddleware
4
6
PATH = "/turbo-boost-command-invocation"
7
+ PARAM = "turbo_boost_command"
5
8
6
9
def initialize ( app )
7
10
@app = app
8
11
end
9
12
10
13
def call ( env )
11
14
request = Rack ::Request . new ( env )
12
- modify! request if modify? ( request )
15
+
16
+ # a command was not requested, pass through and exit early
17
+ return @app . call ( env ) unless command_request? ( request )
18
+
19
+ # a command was requested
20
+ return [ 403 , { "Content-Type" => "text/plain" } , [ "Forbidden" ] ] if untrusted_client? ( request )
21
+ modify_request! ( request ) if modify_request? ( request )
13
22
@app . call env
14
23
end
15
24
16
25
private
17
26
18
- # Returns the MIME type for TurboBoost Command invocations.
19
27
def mime_type
20
- Mime ::Type . lookup_by_extension ( :turbo_boost )
28
+ @mime_type ||= Mime ::Type . lookup_by_extension ( :turbo_boost )
29
+ end
30
+
31
+ # Indicates if the client's user agent is trusted (i.e. known and not a bot)
32
+ #
33
+ # @param request [Rack::Request] the request to check
34
+ # @return [Boolean]
35
+ def trusted_client? ( request )
36
+ client = DeviceDetector . new ( request . env [ "HTTP_USER_AGENT" ] )
37
+ return false unless client . known?
38
+ return false if client . bot?
39
+ true
40
+ rescue => error
41
+ puts "#{ self . class . name } failed to determine if the client is valid! #{ error . message } "
42
+ false
43
+ end
44
+
45
+ # Indicates if the client's user agent is untrusted (i.e. unknown or a bot)
46
+ #
47
+ # @param request [Rack::Request] the request to check
48
+ # @return [Boolean]
49
+ def untrusted_client? ( request )
50
+ !trusted_client? ( request )
51
+ end
52
+
53
+ # Indicates if the request is invoking a TurboBoost Command.
54
+ #
55
+ # @param request [Rack::Request] the request to check
56
+ # @return [Boolean]
57
+ def command_request? ( request )
58
+ return false unless request . post?
59
+ return false unless request . path . start_with? ( PATH ) || request . params . key? ( PARAM )
60
+ true
61
+ end
62
+
63
+ # The TurboBoost Command params.
64
+ #
65
+ # @param request [Rack::Request] the request to extract the params from
66
+ # @return [Hash]
67
+ def command_params ( request )
68
+ return { } unless command_request? ( request )
69
+ return request . params [ PARAM ] if request . params . key? ( PARAM )
70
+ JSON . parse ( request . body . string )
21
71
end
22
72
23
73
# Indicates whether or not the request is a TurboBoost Command invocation that requires modifications
24
74
# before we hand things over to Rails.
25
75
#
76
+ # @note The form and method drivers DO NOT modify the request;
77
+ # instead, they let Rails mechanics handle the request as normal.
78
+ #
26
79
# @param request [Rack::Request] the request to check
27
80
# @return [Boolean] true if the request is a TurboBoost Command invocation, false otherwise
28
- def modify ?( request )
81
+ def modify_request ?( request )
29
82
return false unless request . post?
30
83
return false unless request . path . start_with? ( PATH )
31
84
return false unless mime_type && request . env [ "HTTP_ACCEPT" ] &.include? ( mime_type )
@@ -35,11 +88,6 @@ def modify?(request)
35
88
false
36
89
end
37
90
38
- def convert_to_get_request? ( driver )
39
- return true if driver == "frame" || driver == "window"
40
- false
41
- end
42
-
43
91
# Modifies the given POST request so Rails sees it as GET.
44
92
#
45
93
# The posted JSON body content holds the TurboBoost Command meta data.
@@ -65,28 +113,42 @@ def convert_to_get_request?(driver)
65
113
# }
66
114
#
67
115
# @param request [Rack::Request] the request to modify
68
- def modify !( request )
69
- params = JSON . parse ( request . body . string )
116
+ def modify_request !( request )
117
+ params = command_params ( request )
70
118
uri = URI . parse ( params [ "src" ] )
71
119
72
120
request . env . tap do |env |
73
121
# Store the command params in the environment
74
122
env [ "turbo_boost_command_params" ] = params
75
123
76
- # Update the URI, PATH_INFO, and QUERY_STRING
124
+ # Change URI and path
77
125
env [ "REQUEST_URI" ] = uri . to_s if env . key? ( "REQUEST_URI" )
78
- env [ "PATH_INFO" ] = uri . path
126
+ env [ "REQUEST_PATH" ] = uri . path
127
+ env [ "PATH_INFO" ] = begin
128
+ script_name = Rails . application . config . relative_url_root
129
+ path_info = uri . path . sub ( /^#{ Regexp . escape ( script_name . to_s ) } / , "" )
130
+ path_info . empty? ? "/" : path_info
131
+ end
132
+
133
+ # Change query string
79
134
env [ "QUERY_STRING" ] = uri . query . to_s
135
+ env . delete ( "rack.request.query_hash" )
80
136
81
- # Change the method from POST to GET
82
- if convert_to_get_request? ( params [ "driver" ] )
83
- env [ "REQUEST_METHOD" ] = "GET"
137
+ # Clear form data
138
+ env . delete ( "rack.request.form_input" )
139
+ env . delete ( "rack.request.form_hash" )
140
+ env . delete ( "rack.request.form_vars" )
141
+ env . delete ( "rack.request.form_pairs" )
84
142
85
- # Clear the body and related headers so the appears and behaves like a GET
86
- env [ "rack.input" ] = StringIO . new
87
- env [ "CONTENT_LENGTH" ] = "0"
88
- env . delete ( "CONTENT_TYPE" )
89
- end
143
+ # Clear the body so we can change the the method to GET
144
+ env [ "rack.input" ] = StringIO . new
145
+ env [ "CONTENT_LENGTH" ] = "0"
146
+ env [ "content-length" ] = "0"
147
+ env . delete ( "CONTENT_TYPE" )
148
+ env . delete ( "content-type" )
149
+
150
+ # Change the method to GET
151
+ env [ "REQUEST_METHOD" ] = "GET"
90
152
end
91
153
rescue => error
92
154
puts "#{ self . class . name } failed to modify the request! #{ error . message } "
0 commit comments