Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

All hacks necessary to use JCD and Mongrel pieces instead of GCD and …

…CTParser.
  • Loading branch information...
commit 066f47a0a067e633cf5f3dc4ca5452a072f40ba3 1 parent 9bd7769
Charles Oliver Nutter authored January 22, 2011

Showing 1 changed file with 129 additions and 122 deletions. Show diff stats Hide diff stats

  1. 251  lib/control_tower/rack_socket.rb
251  lib/control_tower/rack_socket.rb
... ...
@@ -1,11 +1,9 @@
1 1
 # This file is covered by the Ruby license. See COPYING for more details.
2 2
 # Copyright (C) 2009-2010, Apple Inc. All rights reserved.
3 3
 
4  
-framework 'Foundation'
5  
-require 'CTParser'
  4
+require 'jcd'
6 5
 require 'stringio'
7  
-
8  
-CTParser # Making sure the Objective-C class is pre-loaded
  6
+require 'mongrel'
9 7
 
10 8
 module ControlTower
11 9
   class RackSocket
@@ -30,89 +28,76 @@ def initialize(host, port, server, concurrent)
30 28
 
31 29
     def open
32 30
       @status = :open
  31
+      @classifier = Mongrel::URIClassifier.new
33 32
       while (@status == :open)
34  
-        connection = @socket.accept
35  
-
36  
-        @request_queue.async(@request_group) do
37  
-          env = { 'rack.errors' => $stderr,
38  
-                  'rack.multiprocess' => false,
39  
-                  'rack.multithread' => @multithread,
40  
-                  'rack.run_once' => false,
41  
-                  'rack.version' => VERSION }
42  
-          resp = nil
43  
-          x_sendfile_header = 'X-Sendfile'
44  
-          x_sendfile = nil
45  
-          begin
46  
-            request_data = parse!(connection, env)
47  
-            if request_data
48  
-              request_data['REMOTE_ADDR'] = connection.addr[3]
49  
-              status, headers, body = @app.call(request_data)
50  
-
51  
-              # If there's an X-Sendfile header, we'll use sendfile(2)
52  
-              if headers.has_key?(x_sendfile_header)
53  
-                x_sendfile = headers[x_sendfile_header]
54  
-                x_sendfile = ::File.open(x_sendfile, 'r') unless x_sendfile.kind_of? IO
55  
-                x_sendfile_size = x_sendfile.stat.size
56  
-                headers.delete(x_sendfile_header)
57  
-                headers['Content-Length'] = x_sendfile_size
58  
-              end
59  
-
60  
-              # Unless somebody's already set it for us (or we don't need it), set the Content-Length
61  
-              unless (status == -1 ||
62  
-                      (status >= 100 and status <= 199) ||
63  
-                      status == 204 ||
64  
-                      status == 304 ||
65  
-                      headers.has_key?('Content-Length'))
66  
-                headers['Content-Length'] = if body.respond_to?(:each)
67  
-                                              size = 0
68  
-                                              body.each { |x| size += x.bytesize }
69  
-                                              size
70  
-                                            else
71  
-                                              body.bytesize
72  
-                                            end
73  
-              end
74  
-
75  
-              # TODO -- We don't handle keep-alive connections yet
76  
-              headers['Connection'] = 'close'
77  
-
78  
-              resp = "HTTP/1.1 #{status}\r\n"
79  
-              headers.each do |header, value|
80  
-                resp << "#{header}: #{value}\r\n"
81  
-              end
82  
-              resp << "\r\n"
83  
-
84  
-              # Start writing the response
85  
-              connection.write resp
86  
-
87  
-              # Write the body
88  
-              if x_sendfile
89  
-                connection.sendfile(x_sendfile, 0, x_sendfile_size)
90  
-              elsif body.respond_to?(:each)
91  
-                body.each do |chunk|
92  
-                  connection.write chunk
  33
+        conn = @socket.accept
  34
+        
  35
+        conn.tap do |connection|
  36
+          @request_queue.async(@request_group) do
  37
+            begin
  38
+              request, response = parse!(connection)
  39
+              if request
  40
+                env = {}.replace(request.params)
  41
+                env.delete "HTTP_CONTENT_TYPE"
  42
+                env.delete "HTTP_CONTENT_LENGTH"
  43
+                
  44
+                env["SCRIPT_NAME"] = ""  if env["SCRIPT_NAME"] == "/"
  45
+                
  46
+                rack_input = request.body || StringIO.new('')
  47
+                rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  48
+                
  49
+                env.update({"rack.version" => Rack::VERSION,
  50
+                  "rack.input" => rack_input,
  51
+                  "rack.errors" => $stderr,
  52
+                  
  53
+                  "rack.multithread" => true,
  54
+                  "rack.multiprocess" => false, # ???
  55
+                  "rack.run_once" => false,
  56
+                  
  57
+                  "rack.url_scheme" => "http",
  58
+                  })
  59
+                  env["QUERY_STRING"] ||= ""
  60
+                  
  61
+                  status, headers, body = @app.call(env)
  62
+                  
  63
+                begin
  64
+                  response.status = status.to_i
  65
+                  response.send_status(nil)
  66
+                  
  67
+                  headers.each { |k, vs|
  68
+                    vs.split("\n").each { |v|
  69
+                      response.header[k] = v
  70
+                    }
  71
+                  }
  72
+                  response.send_header
  73
+                  
  74
+                  body.each { |part|
  75
+                    response.write part
  76
+                    response.socket.flush
  77
+                  }
  78
+                ensure
  79
+                  body.close  if body.respond_to? :close
93 80
                 end
  81
+                
94 82
               else
95  
-                connection.write body
  83
+                $stderr.puts "Error: No request data received!"
96 84
               end
97  
-
98  
-            else
99  
-              $stderr.puts "Error: No request data received!"
100  
-            end
101  
-          rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
102  
-            $stderr.puts "Error: Connection terminated!"
103  
-          rescue Object => e
104  
-            if resp.nil? && !connection.closed?
105  
-              connection.write "HTTP/1.1 400\r\n\r\n"
106  
-            else
107  
-              # We have a response, but there was trouble sending it:
108  
-              $stderr.puts "Error: Problem transmitting data -- #{e.inspect}"
109  
-              $stderr.puts e.backtrace.join("\n")
  85
+            rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
  86
+              $stderr.puts "Error: Connection terminated!"
  87
+            rescue Object => e
  88
+              if response.nil? && !connection.closed?
  89
+                connection.write "HTTP/1.1 400\r\n\r\n"
  90
+              else
  91
+                # We have a response, but there was trouble sending it:
  92
+                $stderr.puts "Error: Problem transmitting data -- #{e.inspect}"
  93
+                $stderr.puts e.backtrace.join("\n")
  94
+              end
  95
+            ensure
  96
+              # We should clean up after our tempfile, if we used one.
  97
+              # input = env['rack.input']
  98
+              # input.unlink if input.class == Tempfile
  99
+              connection.close rescue nil
110 100
             end
111  
-          ensure
112  
-            # We should clean up after our tempfile, if we used one.
113  
-            input = env['rack.input']
114  
-            input.unlink if input.class == Tempfile
115  
-            connection.close rescue nil
116 101
           end
117 102
         end
118 103
       end
@@ -133,49 +118,71 @@ def close
133 118
 
134 119
     private
135 120
 
136  
-    def parse!(connection, env)
137  
-      parser = Thread.current[:http_parser] ||= CTParser.new
138  
-      parser.reset
139  
-      data = NSMutableData.alloc.init
140  
-      data.increaseLengthBy(1) # add sentinel
141  
-      parsing_headers = true # Parse headers first
142  
-      nread = 0
143  
-      content_length = 0
144  
-      content_uploaded = 0
145  
-      connection_handle = NSFileHandle.alloc.initWithFileDescriptor(connection.fileno)
146  
-
147  
-      while (parsing_headers || content_uploaded < content_length) do
148  
-        # Read the availableData on the socket and give up if there's nothing
149  
-        incoming_bytes = connection_handle.availableData
150  
-        return nil if incoming_bytes.length == 0
151  
-
152  
-        # Until the headers are done being parsed, we'll parse them
153  
-        if parsing_headers
154  
-          data.setLength(data.length - 1) # Remove sentinel
155  
-          data.appendData(incoming_bytes)
156  
-          data.increaseLengthBy(1) # Add sentinel
157  
-          nread = parser.parseData(data, forEnvironment: env, startingAt: nread)
158  
-          if parser.finished == 1
159  
-            parsing_headers = false # We're done, now on to receiving the body
160  
-            content_length = env['CONTENT_LENGTH'].to_i
161  
-            content_uploaded = env['rack.input'].length
  121
+    def parse!(client)
  122
+      begin
  123
+        parser = Mongrel::HttpParser.new
  124
+        params = Mongrel::HttpParams.new
  125
+        request = nil
  126
+        data = client.readpartial(Mongrel::Const::CHUNK_SIZE)
  127
+        nparsed = 0
  128
+
  129
+        # Assumption: nparsed will always be less since data will get filled with more
  130
+        # after each parsing.  If it doesn't get more then there was a problem
  131
+        # with the read operation on the client socket.  Effect is to stop processing when the
  132
+        # socket can't fill the buffer for further parsing.
  133
+        while nparsed < data.length
  134
+          nparsed = parser.execute(params, data, nparsed)
  135
+
  136
+          if parser.finished?
  137
+            if not params[Mongrel::Const::REQUEST_PATH]
  138
+              # it might be a dumbass full host request header
  139
+              uri = URI.parse(params[Mongrel::Const::REQUEST_URI])
  140
+              params[Mongrel::Const::REQUEST_PATH] = uri.path
  141
+            end
  142
+
  143
+            raise "No REQUEST PATH" if not params[Mongrel::Const::REQUEST_PATH]
  144
+
  145
+            script_name, path_info, handlers = @classifier.resolve(params[Mongrel::Const::REQUEST_PATH])
  146
+            
  147
+            params[Mongrel::Const::PATH_INFO] = path_info
  148
+            params[Mongrel::Const::SCRIPT_NAME] = script_name
  149
+
  150
+            # From http://www.ietf.org/rfc/rfc3875 :
  151
+            # "Script authors should be aware that the REMOTE_ADDR and REMOTE_HOST
  152
+            #  meta-variables (see sections 4.1.8 and 4.1.9) may not identify the
  153
+            #  ultimate source of the request.  They identify the client for the
  154
+            #  immediate request to the server; that client may be a proxy, gateway,
  155
+            #  or other intermediary acting on behalf of the actual source client."
  156
+            params[Mongrel::Const::REMOTE_ADDR] = client.peeraddr.last
  157
+            
  158
+            notifiers = []
  159
+            request = Mongrel::HttpRequest.new(params, client, notifiers)
  160
+
  161
+            # in the case of large file uploads the user could close the socket, so skip those requests
  162
+            raise "closed request" if request.body == nil  # nil signals from HttpRequest::initialize that the request was aborted
  163
+
  164
+            # request is good so far, continue processing the response
  165
+            response = Mongrel::HttpResponse.new(client)
  166
+            
  167
+            return request, response
  168
+          else
  169
+            # Parser is not done, queue up more data to read and continue parsing
  170
+            chunk = client.readpartial(Mongrel::Const::CHUNK_SIZE)
  171
+            break if !chunk or chunk.length == 0  # read failed, stop processing
  172
+
  173
+            data << chunk
  174
+            if data.length >= Mongrel::Const::MAX_HEADER
  175
+              raise Mongrel::HttpParserError.new("HEADER is longer than allowed, aborting client early.")
  176
+            end
162 177
           end
163  
-        else # Done parsing headers, now just collect request body:
164  
-          content_uploaded += incoming_bytes.length
165  
-          env['rack.input'].appendData(incoming_bytes)
166 178
         end
  179
+      rescue Mongrel::HttpParserError => e
  180
+        STDERR.puts "#{Time.now}: HTTP parse error, malformed request (#{params[Mongrel::Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #{e.inspect}"
  181
+        STDERR.puts "#{Time.now}: REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n"
  182
+      rescue Exception => e
  183
+        STDERR.puts "#{Time.now}: Read error: #{e.inspect}"
  184
+        STDERR.puts e.backtrace.join("\n")
167 185
       end
168  
-
169  
-      if content_length > 1024 * 1024
170  
-        body_file = Tempfile.new('control-tower-request-body-')
171  
-        NSFileHandle.alloc.initWithFileDescriptor(body_file.fileno).writeData(env['rack.input'])
172  
-        body_file.rewind
173  
-        env['rack.input'] = body_file
174  
-      else
175  
-        env['rack.input'] = StringIO.new(env['rack.input'])
176  
-      end
177  
-      # Returning what we've got...
178  
-      return env
179 186
     end
180 187
   end
181 188
 end

0 notes on commit 066f47a

Please sign in to comment.
Something went wrong with that request. Please try again.