1require 'webrick'
2require 'zlib'
3require 'erb'
4
5require 'rubygems'
6require 'rubygems/rdoc'
7
8##
9# Gem::Server and allows users to serve gems for consumption by
10# `gem --remote-install`.
11#
12# gem_server starts an HTTP server on the given port and serves the following:
13# * "/" - Browsing of gem spec files for installed gems
14# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index
15# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs
16#   name/version/platform index
17# * "/quick/" - Individual gemspecs
18# * "/gems" - Direct access to download the installable gems
19# * "/rdoc?q=" - Search for installed rdoc documentation
20#
21# == Usage
22#
23#   gem_server = Gem::Server.new Gem.dir, 8089, false
24#   gem_server.run
25#
26#--
27# TODO Refactor into a real WEBrick servlet to remove code duplication.
28
29class Gem::Server
30
31  attr_reader :spec_dirs
32
33  include ERB::Util
34  include Gem::UserInteraction
35
36  SEARCH = <<-SEARCH
37      <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc">
38        <div id="search" style="float:right">
39          <label for="q">Filter/Search</label>
40          <input id="q" type="text" style="width:10em" name="q">
41          <button type="submit" style="display:none"></button>
42        </div>
43      </form>
44  SEARCH
45
46  DOC_TEMPLATE = <<-'DOC_TEMPLATE'
47  <?xml version="1.0" encoding="iso-8859-1"?>
48  <!DOCTYPE html
49       PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
50       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
51
52  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
53  <head>
54    <title>RubyGems Documentation Index</title>
55    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
56  </head>
57  <body>
58    <div id="fileHeader">
59<%= SEARCH %>
60      <h1>RubyGems Documentation Index</h1>
61    </div>
62    <!-- banner header -->
63
64  <div id="bodyContent">
65    <div id="contextContent">
66      <div id="description">
67        <h1>Summary</h1>
68  <p>There are <%=values["gem_count"]%> gems installed:</p>
69  <p>
70  <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
71  <h1>Gems</h1>
72
73  <dl>
74  <% values["specs"].each do |spec| %>
75    <dt>
76    <% if spec["first_name_entry"] then %>
77      <a name="<%=spec["name"]%>"></a>
78    <% end %>
79
80    <b><%=spec["name"]%> <%=spec["version"]%></b>
81
82    <% if spec["ri_installed"] then %>
83      <a href="<%=spec["doc_path"]%>">[rdoc]</a>
84    <% elsif spec["rdoc_installed"] then %>
85      <a href="<%=spec["doc_path"]%>">[rdoc]</a>
86    <% else %>
87      <span title="rdoc not installed">[rdoc]</span>
88    <% end %>
89
90    <% if spec["homepage"] then %>
91      <a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a>
92    <% else %>
93      <span title="no homepage available">[www]</span>
94    <% end %>
95
96    <% if spec["has_deps"] then %>
97     - depends on
98      <%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
99    <% end %>
100    </dt>
101    <dd>
102    <%=spec["summary"]%>
103    <% if spec["executables"] then %>
104      <br/>
105
106      <% if spec["only_one_executable"] then %>
107          Executable is
108      <% else %>
109          Executables are
110      <%end%>
111
112      <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>.
113
114    <%end%>
115    <br/>
116    <br/>
117    </dd>
118  <% end %>
119  </dl>
120
121      </div>
122     </div>
123    </div>
124  <div id="validator-badges">
125    <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
126  </div>
127  </body>
128  </html>
129  DOC_TEMPLATE
130
131  # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
132  RDOC_CSS = <<-RDOC_CSS
133body {
134    font-family: Verdana,Arial,Helvetica,sans-serif;
135    font-size:   90%;
136    margin: 0;
137    margin-left: 40px;
138    padding: 0;
139    background: white;
140}
141
142h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
143h1 { font-size: 150%; }
144h2,h3,h4 { margin-top: 1em; }
145
146a { background: #eef; color: #039; text-decoration: none; }
147a:hover { background: #039; color: #eef; }
148
149/* Override the base stylesheets Anchor inside a table cell */
150td > a {
151  background: transparent;
152  color: #039;
153  text-decoration: none;
154}
155
156/* and inside a section title */
157.section-title > a {
158  background: transparent;
159  color: #eee;
160  text-decoration: none;
161}
162
163/* === Structural elements =================================== */
164
165div#index {
166    margin: 0;
167    margin-left: -40px;
168    padding: 0;
169    font-size: 90%;
170}
171
172
173div#index a {
174    margin-left: 0.7em;
175}
176
177div#index .section-bar {
178   margin-left: 0px;
179   padding-left: 0.7em;
180   background: #ccc;
181   font-size: small;
182}
183
184
185div#classHeader, div#fileHeader {
186    width: auto;
187    color: white;
188    padding: 0.5em 1.5em 0.5em 1.5em;
189    margin: 0;
190    margin-left: -40px;
191    border-bottom: 3px solid #006;
192}
193
194div#classHeader a, div#fileHeader a {
195    background: inherit;
196    color: white;
197}
198
199div#classHeader td, div#fileHeader td {
200    background: inherit;
201    color: white;
202}
203
204
205div#fileHeader {
206    background: #057;
207}
208
209div#classHeader {
210    background: #048;
211}
212
213
214.class-name-in-header {
215  font-size:  180%;
216  font-weight: bold;
217}
218
219
220div#bodyContent {
221    padding: 0 1.5em 0 1.5em;
222}
223
224div#description {
225    padding: 0.5em 1.5em;
226    background: #efefef;
227    border: 1px dotted #999;
228}
229
230div#description h1,h2,h3,h4,h5,h6 {
231    color: #125;;
232    background: transparent;
233}
234
235div#validator-badges {
236    text-align: center;
237}
238div#validator-badges img { border: 0; }
239
240div#copyright {
241    color: #333;
242    background: #efefef;
243    font: 0.75em sans-serif;
244    margin-top: 5em;
245    margin-bottom: 0;
246    padding: 0.5em 2em;
247}
248
249
250/* === Classes =================================== */
251
252table.header-table {
253    color: white;
254    font-size: small;
255}
256
257.type-note {
258    font-size: small;
259    color: #DEDEDE;
260}
261
262.xxsection-bar {
263    background: #eee;
264    color: #333;
265    padding: 3px;
266}
267
268.section-bar {
269   color: #333;
270   border-bottom: 1px solid #999;
271    margin-left: -20px;
272}
273
274
275.section-title {
276    background: #79a;
277    color: #eee;
278    padding: 3px;
279    margin-top: 2em;
280    margin-left: -30px;
281    border: 1px solid #999;
282}
283
284.top-aligned-row {  vertical-align: top }
285.bottom-aligned-row { vertical-align: bottom }
286
287/* --- Context section classes ----------------------- */
288
289.context-row { }
290.context-item-name { font-family: monospace; font-weight: bold; color: black; }
291.context-item-value { font-size: small; color: #448; }
292.context-item-desc { color: #333; padding-left: 2em; }
293
294/* --- Method classes -------------------------- */
295.method-detail {
296    background: #efefef;
297    padding: 0;
298    margin-top: 0.5em;
299    margin-bottom: 1em;
300    border: 1px dotted #ccc;
301}
302.method-heading {
303  color: black;
304  background: #ccc;
305  border-bottom: 1px solid #666;
306  padding: 0.2em 0.5em 0 0.5em;
307}
308.method-signature { color: black; background: inherit; }
309.method-name { font-weight: bold; }
310.method-args { font-style: italic; }
311.method-description { padding: 0 0.5em 0 0.5em; }
312
313/* --- Source code sections -------------------- */
314
315a.source-toggle { font-size: 90%; }
316div.method-source-code {
317    background: #262626;
318    color: #ffdead;
319    margin: 1em;
320    padding: 0.5em;
321    border: 1px dashed #999;
322    overflow: hidden;
323}
324
325div.method-source-code pre { color: #ffdead; overflow: hidden; }
326
327/* --- Ruby keyword styles --------------------- */
328
329.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
330
331.ruby-constant  { color: #7fffd4; background: transparent; }
332.ruby-keyword { color: #00ffff; background: transparent; }
333.ruby-ivar    { color: #eedd82; background: transparent; }
334.ruby-operator  { color: #00ffee; background: transparent; }
335.ruby-identifier { color: #ffdead; background: transparent; }
336.ruby-node    { color: #ffa07a; background: transparent; }
337.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
338.ruby-regexp  { color: #ffa07a; background: transparent; }
339.ruby-value   { color: #7fffd4; background: transparent; }
340  RDOC_CSS
341
342  RDOC_NO_DOCUMENTATION = <<-'NO_DOC'
343<?xml version="1.0" encoding="iso-8859-1"?>
344<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
345          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
346<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
347  <head>
348    <title>Found documentation</title>
349    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
350  </head>
351  <body>
352    <div id="fileHeader">
353<%= SEARCH %>
354      <h1>No documentation found</h1>
355    </div>
356
357    <div id="bodyContent">
358      <div id="contextContent">
359        <div id="description">
360          <p>No gems matched <%= h query.inspect %></p>
361
362          <p>
363            Back to <a href="/">complete gem index</a>
364          </p>
365
366        </div>
367      </div>
368    </div>
369    <div id="validator-badges">
370      <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
371    </div>
372  </body>
373</html>
374  NO_DOC
375
376  RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH'
377<?xml version="1.0" encoding="iso-8859-1"?>
378<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
379          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
380<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
381  <head>
382    <title>Found documentation</title>
383    <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
384  </head>
385  <body>
386    <div id="fileHeader">
387<%= SEARCH %>
388      <h1>Found documentation</h1>
389    </div>
390    <!-- banner header -->
391
392    <div id="bodyContent">
393      <div id="contextContent">
394        <div id="description">
395          <h1>Summary</h1>
396          <p><%=doc_items.length%> documentation topics found.</p>
397          <h1>Topics</h1>
398
399          <dl>
400          <% doc_items.each do |doc_item| %>
401            <dt>
402              <b><%=doc_item[:name]%></b>
403              <a href="<%=doc_item[:url]%>">[rdoc]</a>
404            </dt>
405            <dd>
406              <%=doc_item[:summary]%>
407              <br/>
408              <br/>
409            </dd>
410          <% end %>
411          </dl>
412
413          <p>
414            Back to <a href="/">complete gem index</a>
415          </p>
416
417        </div>
418      </div>
419    </div>
420    <div id="validator-badges">
421      <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
422    </div>
423  </body>
424</html>
425  RDOC_SEARCH
426
427  def self.run(options)
428    new(options[:gemdir], options[:port], options[:daemon],
429        options[:launch], options[:addresses]).run
430  end
431
432  def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
433    Gem::RDoc.load_rdoc
434    Socket.do_not_reverse_lookup = true
435
436    @gem_dirs  = Array gem_dirs
437    @port      = port
438    @daemon    = daemon
439    @launch    = launch
440    @addresses = addresses
441
442    logger  = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
443    @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
444
445    @spec_dirs = @gem_dirs.map { |gem_dir| File.join gem_dir, 'specifications' }
446    @spec_dirs.reject! { |spec_dir| !File.directory? spec_dir }
447
448    reset_gems
449
450    @have_rdoc_4_plus = nil
451  end
452
453  def add_date res
454    res['date'] = @spec_dirs.map do |spec_dir|
455      File.stat(spec_dir).mtime
456    end.max
457  end
458
459  def doc_root gem_name
460    if have_rdoc_4_plus? then
461      "/doc_root/#{gem_name}/"
462    else
463      "/doc_root/#{gem_name}/rdoc/index.html"
464    end
465  end
466
467  def have_rdoc_4_plus?
468    @have_rdoc_4_plus ||=
469      Gem::Requirement.new('>= 4.0.0.preview2').satisfied_by? Gem::RDoc.rdoc_version
470  end
471
472  def latest_specs(req, res)
473    reset_gems
474
475    res['content-type'] = 'application/x-gzip'
476
477    add_date res
478
479    latest_specs = Gem::Specification.latest_specs
480
481    specs = latest_specs.sort.map do |spec|
482      platform = spec.original_platform || Gem::Platform::RUBY
483      [spec.name, spec.version, platform]
484    end
485
486    specs = Marshal.dump specs
487
488    if req.path =~ /\.gz$/ then
489      specs = Gem.gzip specs
490      res['content-type'] = 'application/x-gzip'
491    else
492      res['content-type'] = 'application/octet-stream'
493    end
494
495    if req.request_method == 'HEAD' then
496      res['content-length'] = specs.length
497    else
498      res.body << specs
499    end
500  end
501
502  ##
503  # Creates server sockets based on the addresses option.  If no addresses
504  # were given a server socket for all interfaces is created.
505
506  def listen addresses = @addresses
507    addresses = [nil] unless addresses
508
509    listeners = 0
510
511    addresses.each do |address|
512      begin
513        @server.listen address, @port
514        @server.listeners[listeners..-1].each do |listener|
515          host, port = listener.addr.values_at 2, 1
516          host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
517          say "Server started at http://#{host}:#{port}"
518        end
519
520        listeners = @server.listeners.length
521      rescue SystemCallError
522        next
523      end
524    end
525
526    if @server.listeners.empty? then
527      say "Unable to start a server."
528      say "Check for running servers or your --bind and --port arguments"
529      terminate_interaction 1
530    end
531  end
532
533  def quick(req, res)
534    reset_gems
535
536    res['content-type'] = 'text/plain'
537    add_date res
538
539    case req.request_uri.path
540    when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
541      marshal_format, name, version, platform = $1, $2, $3, $4
542      specs = Gem::Specification.find_all_by_name name, version
543
544      selector = [name, version, platform].map(&:inspect).join ' '
545
546      platform = if platform then
547                   Gem::Platform.new platform.sub(/^-/, '')
548                 else
549                   Gem::Platform::RUBY
550                 end
551
552      specs = specs.select { |s| s.platform == platform }
553
554      if specs.empty? then
555        res.status = 404
556        res.body = "No gems found matching #{selector}"
557      elsif specs.length > 1 then
558        res.status = 500
559        res.body = "Multiple gems found matching #{selector}"
560      elsif marshal_format then
561        res['content-type'] = 'application/x-deflate'
562        res.body << Gem.deflate(Marshal.dump(specs.first))
563      end
564    else
565      raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
566    end
567  end
568
569  def root(req, res)
570    reset_gems
571
572    add_date res
573
574    raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
575      req.path == '/'
576
577    specs = []
578    total_file_count = 0
579
580    Gem::Specification.each do |spec|
581      total_file_count += spec.files.size
582      deps = spec.dependencies.map { |dep|
583        {
584          "name"    => dep.name,
585          "type"    => dep.type,
586          "version" => dep.requirement.to_s,
587        }
588      }
589
590      deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
591      deps.last["is_last"] = true unless deps.empty?
592
593      # executables
594      executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
595      executables = nil if executables.empty?
596      executables.last["is_last"] = true if executables
597
598      specs << {
599        "authors"             => spec.authors.sort.join(", "),
600        "date"                => spec.date.to_s,
601        "dependencies"        => deps,
602        "doc_path"            => doc_root(spec.full_name),
603        "executables"         => executables,
604        "only_one_executable" => (executables && executables.size == 1),
605        "full_name"           => spec.full_name,
606        "has_deps"            => !deps.empty?,
607        "homepage"            => spec.homepage,
608        "name"                => spec.name,
609        "rdoc_installed"      => Gem::RDoc.new(spec).rdoc_installed?,
610        "ri_installed"        => Gem::RDoc.new(spec).ri_installed?,
611        "summary"             => spec.summary,
612        "version"             => spec.version.to_s,
613      }
614    end
615
616    specs << {
617      "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
618      "dependencies" => [],
619      "doc_path" => doc_root("rubygems-#{Gem::VERSION}"),
620      "executables" => [{"executable" => 'gem', "is_last" => true}],
621      "only_one_executable" => true,
622      "full_name" => "rubygems-#{Gem::VERSION}",
623      "has_deps" => false,
624      "homepage" => "http://docs.rubygems.org/",
625      "name" => 'rubygems',
626      "ri_installed" => true,
627      "summary" => "RubyGems itself",
628      "version" => Gem::VERSION,
629    }
630
631    specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
632    specs.last["is_last"] = true
633
634    # tag all specs with first_name_entry
635    last_spec = nil
636    specs.each do |spec|
637      is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
638      spec["first_name_entry"] = is_first
639      last_spec = spec
640    end
641
642    # create page from template
643    template = ERB.new(DOC_TEMPLATE)
644    res['content-type'] = 'text/html'
645
646    values = { "gem_count" => specs.size.to_s, "specs" => specs,
647               "total_file_count" => total_file_count.to_s }
648
649    # suppress 1.9.3dev warning about unused variable
650    values = values
651
652    result = template.result binding
653    res.body = result
654  end
655
656  ##
657  # Can be used for quick navigation to the rdoc documentation.  You can then
658  # define a search shortcut for your browser.  E.g. in Firefox connect
659  # 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can
660  # directly open the ActionPack documentation by typing 'rdoc actionp'. If
661  # there are multiple hits for the search term, they are presented as a list
662  # with links.
663  #
664  # Search algorithm aims for an intuitive search:
665  # 1. first try to find the gems and documentation folders which name
666  #    starts with the search term
667  # 2. search for entries, that *contain* the search term
668  # 3. show all the gems
669  #
670  # If there is only one search hit, user is immediately redirected to the
671  # documentation for the particular gem, otherwise a list with results is
672  # shown.
673  #
674  # === Additional trick - install documentation for ruby core
675  #
676  # Note: please adjust paths accordingly use for example 'locate yaml.rb' and
677  # 'gem environment' to identify directories, that are specific for your
678  # local installation
679  #
680  # 1. install ruby sources
681  #      cd /usr/src
682  #      sudo apt-get source ruby
683  #
684  # 2. generate documentation
685  #      rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \
686  #        /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
687  #
688  # By typing 'rdoc core' you can now access the core documentation
689
690  def rdoc(req, res)
691    query = req.query['q']
692    show_rdoc_for_pattern("#{query}*", res) && return
693    show_rdoc_for_pattern("*#{query}*", res) && return
694
695    template = ERB.new RDOC_NO_DOCUMENTATION
696
697    res['content-type'] = 'text/html'
698    res.body = template.result binding
699  end
700
701  ##
702  # Updates the server to use the latest installed gems.
703
704  def reset_gems # :nodoc:
705    Gem::Specification.dirs = @gem_dirs
706  end
707
708  ##
709  # Returns true and prepares http response, if rdoc for the requested gem
710  # name pattern was found.
711  #
712  # The search is based on the file system content, not on the gems metadata.
713  # This allows additional documentation folders like 'core' for the ruby core
714  # documentation - just put it underneath the main doc folder.
715
716  def show_rdoc_for_pattern(pattern, res)
717    found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path|
718      File.exist? File.join(path, 'rdoc/index.html')
719    }
720    case found_gems.length
721    when 0
722      return false
723    when 1
724      new_path = File.basename(found_gems[0])
725      res.status = 302
726      res['Location'] = doc_root new_path
727      return true
728    else
729      doc_items = []
730      found_gems.each do |file_name|
731        base_name = File.basename(file_name)
732        doc_items << {
733          :name    => base_name,
734          :url     => doc_root(new_path),
735          :summary => ''
736        }
737      end
738
739      template = ERB.new(RDOC_SEARCH_TEMPLATE)
740      res['content-type'] = 'text/html'
741      result = template.result binding
742      res.body = result
743      return true
744    end
745  end
746
747  def run
748    listen
749
750    WEBrick::Daemon.start if @daemon
751
752    @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
753    @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
754
755    @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
756                       method(:latest_specs)
757    @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
758                       method(:latest_specs)
759
760    @server.mount_proc "/quick/", method(:quick)
761
762    @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
763      res['content-type'] = 'text/css'
764      add_date res
765      res.body << RDOC_CSS
766    end
767
768    @server.mount_proc "/", method(:root)
769
770    @server.mount_proc "/rdoc", method(:rdoc)
771
772    file_handlers = {
773      '/gems' => '/cache/',
774    }
775
776    if have_rdoc_4_plus? then
777      @server.mount '/doc_root', RDoc::Servlet, '/doc_root'
778    else
779      file_handlers['/doc_root'] = '/doc/'
780    end
781
782    @gem_dirs.each do |gem_dir|
783      file_handlers.each do |mount_point, mount_dir|
784        @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
785                      File.join(gem_dir, mount_dir), true)
786      end
787    end
788
789    trap("INT") { @server.shutdown; exit! }
790    trap("TERM") { @server.shutdown; exit! }
791
792    launch if @launch
793
794    @server.start
795  end
796
797  def specs(req, res)
798    reset_gems
799
800    add_date res
801
802    specs = Gem::Specification.sort_by(&:sort_obj).map do |spec|
803      platform = spec.original_platform || Gem::Platform::RUBY
804      [spec.name, spec.version, platform]
805    end
806
807    specs = Marshal.dump specs
808
809    if req.path =~ /\.gz$/ then
810      specs = Gem.gzip specs
811      res['content-type'] = 'application/x-gzip'
812    else
813      res['content-type'] = 'application/octet-stream'
814    end
815
816    if req.request_method == 'HEAD' then
817      res['content-length'] = specs.length
818    else
819      res.body << specs
820    end
821  end
822
823  def launch
824    listeners = @server.listeners.map{|l| l.addr[2] }
825
826    # TODO: 0.0.0.0 == any, not localhost.
827    host = listeners.any?{|l| l == '0.0.0.0'} ? 'localhost' : listeners.first
828
829    say "Launching browser to http://#{host}:#{@port}"
830
831    system("#{@launch} http://#{host}:#{@port}")
832  end
833end
834