1# frozen_string_literal: false
2#
3# httpserver.rb -- HTTPServer Class
4#
5# Author: IPR -- Internet Programming with Ruby -- writers
6# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
7# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
8# reserved.
9#
10# $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $
11
12require 'io/wait'
13require_relative 'server'
14require_relative 'httputils'
15require_relative 'httpstatus'
16require_relative 'httprequest'
17require_relative 'httpresponse'
18require_relative 'httpservlet'
19require_relative 'accesslog'
20
21module WEBrick
22  class HTTPServerError < ServerError; end
23
24  ##
25  # An HTTP Server
26
27  class HTTPServer < ::WEBrick::GenericServer
28    ##
29    # Creates a new HTTP server according to +config+
30    #
31    # An HTTP server uses the following attributes:
32    #
33    # :AccessLog:: An array of access logs.  See WEBrick::AccessLog
34    # :BindAddress:: Local address for the server to bind to
35    # :DocumentRoot:: Root path to serve files from
36    # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler
37    # :HTTPVersion:: The HTTP version of this server
38    # :Port:: Port to listen on
39    # :RequestCallback:: Called with a request and response before each
40    #                    request is serviced.
41    # :RequestTimeout:: Maximum time to wait between requests
42    # :ServerAlias:: Array of alternate names for this server for virtual
43    #                hosting
44    # :ServerName:: Name for this server for virtual hosting
45
46    def initialize(config={}, default=Config::HTTP)
47      super(config, default)
48      @http_version = HTTPVersion::convert(@config[:HTTPVersion])
49
50      @mount_tab = MountTable.new
51      if @config[:DocumentRoot]
52        mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
53              @config[:DocumentRootOptions])
54      end
55
56      unless @config[:AccessLog]
57        @config[:AccessLog] = [
58          [ $stderr, AccessLog::COMMON_LOG_FORMAT ],
59          [ $stderr, AccessLog::REFERER_LOG_FORMAT ]
60        ]
61      end
62
63      @virtual_hosts = Array.new
64    end
65
66    ##
67    # Processes requests on +sock+
68
69    def run(sock)
70      while true
71        req = create_request(@config)
72        res = create_response(@config)
73        server = self
74        begin
75          timeout = @config[:RequestTimeout]
76          while timeout > 0
77            break if sock.to_io.wait_readable(0.5)
78            break if @status != :Running
79            timeout -= 0.5
80          end
81          raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running
82          raise HTTPStatus::EOFError if sock.eof?
83          req.parse(sock)
84          res.request_method = req.request_method
85          res.request_uri = req.request_uri
86          res.request_http_version = req.http_version
87          res.keep_alive = req.keep_alive?
88          server = lookup_server(req) || self
89          if callback = server[:RequestCallback]
90            callback.call(req, res)
91          elsif callback = server[:RequestHandler]
92            msg = ":RequestHandler is deprecated, please use :RequestCallback"
93            @logger.warn(msg)
94            callback.call(req, res)
95          end
96          server.service(req, res)
97        rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
98          res.set_error(ex)
99        rescue HTTPStatus::Error => ex
100          @logger.error(ex.message)
101          res.set_error(ex)
102        rescue HTTPStatus::Status => ex
103          res.status = ex.code
104        rescue StandardError => ex
105          @logger.error(ex)
106          res.set_error(ex, true)
107        ensure
108          if req.request_line
109            if req.keep_alive? && res.keep_alive?
110              req.fixup()
111            end
112            res.send_response(sock)
113            server.access_log(@config, req, res)
114          end
115        end
116        break if @http_version < "1.1"
117        break unless req.keep_alive?
118        break unless res.keep_alive?
119      end
120    end
121
122    ##
123    # Services +req+ and fills in +res+
124
125    def service(req, res)
126      if req.unparsed_uri == "*"
127        if req.request_method == "OPTIONS"
128          do_OPTIONS(req, res)
129          raise HTTPStatus::OK
130        end
131        raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found."
132      end
133
134      servlet, options, script_name, path_info = search_servlet(req.path)
135      raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet
136      req.script_name = script_name
137      req.path_info = path_info
138      si = servlet.get_instance(self, *options)
139      @logger.debug(format("%s is invoked.", si.class.name))
140      si.service(req, res)
141    end
142
143    ##
144    # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS
145    # requests are allowed.
146
147    def do_OPTIONS(req, res)
148      res["allow"] = "GET,HEAD,POST,OPTIONS"
149    end
150
151    ##
152    # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation
153    # time
154
155    def mount(dir, servlet, *options)
156      @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
157      @mount_tab[dir] = [ servlet, options ]
158    end
159
160    ##
161    # Mounts +proc+ or +block+ on +dir+ and calls it with a
162    # WEBrick::HTTPRequest and WEBrick::HTTPResponse
163
164    def mount_proc(dir, proc=nil, &block)
165      proc ||= block
166      raise HTTPServerError, "must pass a proc or block" unless proc
167      mount(dir, HTTPServlet::ProcHandler.new(proc))
168    end
169
170    ##
171    # Unmounts +dir+
172
173    def unmount(dir)
174      @logger.debug(sprintf("unmount %s.", dir))
175      @mount_tab.delete(dir)
176    end
177    alias umount unmount
178
179    ##
180    # Finds a servlet for +path+
181
182    def search_servlet(path)
183      script_name, path_info = @mount_tab.scan(path)
184      servlet, options = @mount_tab[script_name]
185      if servlet
186        [ servlet, options, script_name, path_info ]
187      end
188    end
189
190    ##
191    # Adds +server+ as a virtual host.
192
193    def virtual_host(server)
194      @virtual_hosts << server
195      @virtual_hosts = @virtual_hosts.sort_by{|s|
196        num = 0
197        num -= 4 if s[:BindAddress]
198        num -= 2 if s[:Port]
199        num -= 1 if s[:ServerName]
200        num
201      }
202    end
203
204    ##
205    # Finds the appropriate virtual host to handle +req+
206
207    def lookup_server(req)
208      @virtual_hosts.find{|s|
209        (s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) &&
210        (s[:Port].nil?        || req.port == s[:Port])           &&
211        ((s[:ServerName].nil?  || req.host == s[:ServerName]) ||
212         (!s[:ServerAlias].nil? && s[:ServerAlias].find{|h| h === req.host}))
213      }
214    end
215
216    ##
217    # Logs +req+ and +res+ in the access logs.  +config+ is used for the
218    # server name.
219
220    def access_log(config, req, res)
221      param = AccessLog::setup_params(config, req, res)
222      @config[:AccessLog].each{|logger, fmt|
223        logger << AccessLog::format(fmt+"\n", param)
224      }
225    end
226
227    ##
228    # Creates the HTTPRequest used when handling the HTTP
229    # request. Can be overridden by subclasses.
230    def create_request(with_webrick_config)
231      HTTPRequest.new(with_webrick_config)
232    end
233
234    ##
235    # Creates the HTTPResponse used when handling the HTTP
236    # request. Can be overridden by subclasses.
237    def create_response(with_webrick_config)
238      HTTPResponse.new(with_webrick_config)
239    end
240
241    ##
242    # Mount table for the path a servlet is mounted on in the directory space
243    # of the server.  Users of WEBrick can only access this indirectly via
244    # WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and
245    # WEBrick::HTTPServer#search_servlet
246
247    class MountTable # :nodoc:
248      def initialize
249        @tab = Hash.new
250        compile
251      end
252
253      def [](dir)
254        dir = normalize(dir)
255        @tab[dir]
256      end
257
258      def []=(dir, val)
259        dir = normalize(dir)
260        @tab[dir] = val
261        compile
262        val
263      end
264
265      def delete(dir)
266        dir = normalize(dir)
267        res = @tab.delete(dir)
268        compile
269        res
270      end
271
272      def scan(path)
273        @scanner =~ path
274        [ $&, $' ]
275      end
276
277      private
278
279      def compile
280        k = @tab.keys
281        k.sort!
282        k.reverse!
283        k.collect!{|path| Regexp.escape(path) }
284        @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)")
285      end
286
287      def normalize(dir)
288        ret = dir ? dir.dup : ""
289        ret.sub!(%r|/+\z|, "")
290        ret
291      end
292    end
293  end
294end
295