ruby/lib/rubygems/server.rb

878 строки
23 KiB
Ruby

# frozen_string_literal: true
require 'webrick'
require 'zlib'
require 'erb'
require 'uri'
require 'rubygems'
require 'rubygems/rdoc'
##
# Gem::Server and allows users to serve gems for consumption by
# `gem --remote-install`.
#
# gem_server starts an HTTP server on the given port and serves the following:
# * "/" - Browsing of gem spec files for installed gems
# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index
# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs
# name/version/platform index
# * "/quick/" - Individual gemspecs
# * "/gems" - Direct access to download the installable gems
# * "/rdoc?q=" - Search for installed rdoc documentation
#
# == Usage
#
# gem_server = Gem::Server.new Gem.dir, 8089, false
# gem_server.run
#
#--
# TODO Refactor into a real WEBrick servlet to remove code duplication.
class Gem::Server
attr_reader :spec_dirs
include ERB::Util
include Gem::UserInteraction
SEARCH = <<-ERB.freeze
<form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc">
<div id="search" style="float:right">
<label for="q">Filter/Search</label>
<input id="q" type="text" style="width:10em" name="q">
<button type="submit" style="display:none"></button>
</div>
</form>
ERB
DOC_TEMPLATE = <<-'ERB'.freeze
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>RubyGems Documentation Index</title>
<link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
</head>
<body>
<div id="fileHeader">
<%= SEARCH %>
<h1>RubyGems Documentation Index</h1>
</div>
<!-- banner header -->
<div id="bodyContent">
<div id="contextContent">
<div id="description">
<h1>Summary</h1>
<p>There are <%=values["gem_count"]%> gems installed:</p>
<p>
<%= values["specs"].map { |v| "<a href=\"##{u v["name"]}\">#{h v["name"]}</a>" }.join ', ' %>.
<h1>Gems</h1>
<dl>
<% values["specs"].each do |spec| %>
<dt>
<% if spec["first_name_entry"] then %>
<a name="<%=h spec["name"]%>"></a>
<% end %>
<b><%=h spec["name"]%> <%=h spec["version"]%></b>
<% if spec["ri_installed"] || spec["rdoc_installed"] then %>
<a href="<%=spec["doc_path"]%>">[rdoc]</a>
<% else %>
<span title="rdoc not installed">[rdoc]</span>
<% end %>
<% if spec["homepage"] then %>
<a href="<%=uri_encode spec["homepage"]%>" title="<%=h spec["homepage"]%>">[www]</a>
<% else %>
<span title="no homepage available">[www]</span>
<% end %>
<% if spec["has_deps"] then %>
- depends on
<%= spec["dependencies"].map { |v| "<a href=\"##{u v["name"]}\">#{h v["name"]}</a>" }.join ', ' %>.
<% end %>
</dt>
<dd>
<%=spec["summary"]%>
<% if spec["executables"] then %>
<br/>
<% if spec["only_one_executable"] then %>
Executable is
<% else %>
Executables are
<%end%>
<%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{h v["executable"]}</span>"}.join ', ' %>.
<%end%>
<br/>
<br/>
</dd>
<% end %>
</dl>
</div>
</div>
</div>
<div id="validator-badges">
<p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
</div>
</body>
</html>
ERB
# CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
RDOC_CSS = <<-CSS.freeze
body {
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 90%;
margin: 0;
margin-left: 40px;
padding: 0;
background: white;
}
h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
h1 { font-size: 150%; }
h2,h3,h4 { margin-top: 1em; }
a { background: #eef; color: #039; text-decoration: none; }
a:hover { background: #039; color: #eef; }
/* Override the base stylesheets Anchor inside a table cell */
td > a {
background: transparent;
color: #039;
text-decoration: none;
}
/* and inside a section title */
.section-title > a {
background: transparent;
color: #eee;
text-decoration: none;
}
/* === Structural elements =================================== */
div#index {
margin: 0;
margin-left: -40px;
padding: 0;
font-size: 90%;
}
div#index a {
margin-left: 0.7em;
}
div#index .section-bar {
margin-left: 0px;
padding-left: 0.7em;
background: #ccc;
font-size: small;
}
div#classHeader, div#fileHeader {
width: auto;
color: white;
padding: 0.5em 1.5em 0.5em 1.5em;
margin: 0;
margin-left: -40px;
border-bottom: 3px solid #006;
}
div#classHeader a, div#fileHeader a {
background: inherit;
color: white;
}
div#classHeader td, div#fileHeader td {
background: inherit;
color: white;
}
div#fileHeader {
background: #057;
}
div#classHeader {
background: #048;
}
.class-name-in-header {
font-size: 180%;
font-weight: bold;
}
div#bodyContent {
padding: 0 1.5em 0 1.5em;
}
div#description {
padding: 0.5em 1.5em;
background: #efefef;
border: 1px dotted #999;
}
div#description h1,h2,h3,h4,h5,h6 {
color: #125;;
background: transparent;
}
div#validator-badges {
text-align: center;
}
div#validator-badges img { border: 0; }
div#copyright {
color: #333;
background: #efefef;
font: 0.75em sans-serif;
margin-top: 5em;
margin-bottom: 0;
padding: 0.5em 2em;
}
/* === Classes =================================== */
table.header-table {
color: white;
font-size: small;
}
.type-note {
font-size: small;
color: #DEDEDE;
}
.xxsection-bar {
background: #eee;
color: #333;
padding: 3px;
}
.section-bar {
color: #333;
border-bottom: 1px solid #999;
margin-left: -20px;
}
.section-title {
background: #79a;
color: #eee;
padding: 3px;
margin-top: 2em;
margin-left: -30px;
border: 1px solid #999;
}
.top-aligned-row { vertical-align: top }
.bottom-aligned-row { vertical-align: bottom }
/* --- Context section classes ----------------------- */
.context-row { }
.context-item-name { font-family: monospace; font-weight: bold; color: black; }
.context-item-value { font-size: small; color: #448; }
.context-item-desc { color: #333; padding-left: 2em; }
/* --- Method classes -------------------------- */
.method-detail {
background: #efefef;
padding: 0;
margin-top: 0.5em;
margin-bottom: 1em;
border: 1px dotted #ccc;
}
.method-heading {
color: black;
background: #ccc;
border-bottom: 1px solid #666;
padding: 0.2em 0.5em 0 0.5em;
}
.method-signature { color: black; background: inherit; }
.method-name { font-weight: bold; }
.method-args { font-style: italic; }
.method-description { padding: 0 0.5em 0 0.5em; }
/* --- Source code sections -------------------- */
a.source-toggle { font-size: 90%; }
div.method-source-code {
background: #262626;
color: #ffdead;
margin: 1em;
padding: 0.5em;
border: 1px dashed #999;
overflow: hidden;
}
div.method-source-code pre { color: #ffdead; overflow: hidden; }
/* --- Ruby keyword styles --------------------- */
.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
.ruby-constant { color: #7fffd4; background: transparent; }
.ruby-keyword { color: #00ffff; background: transparent; }
.ruby-ivar { color: #eedd82; background: transparent; }
.ruby-operator { color: #00ffee; background: transparent; }
.ruby-identifier { color: #ffdead; background: transparent; }
.ruby-node { color: #ffa07a; background: transparent; }
.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
.ruby-regexp { color: #ffa07a; background: transparent; }
.ruby-value { color: #7fffd4; background: transparent; }
CSS
RDOC_NO_DOCUMENTATION = <<-'ERB'.freeze
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Found documentation</title>
<link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
</head>
<body>
<div id="fileHeader">
<%= SEARCH %>
<h1>No documentation found</h1>
</div>
<div id="bodyContent">
<div id="contextContent">
<div id="description">
<p>No gems matched <%= h query.inspect %></p>
<p>
Back to <a href="/">complete gem index</a>
</p>
</div>
</div>
</div>
<div id="validator-badges">
<p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
</div>
</body>
</html>
ERB
RDOC_SEARCH_TEMPLATE = <<-'ERB'.freeze
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Found documentation</title>
<link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
</head>
<body>
<div id="fileHeader">
<%= SEARCH %>
<h1>Found documentation</h1>
</div>
<!-- banner header -->
<div id="bodyContent">
<div id="contextContent">
<div id="description">
<h1>Summary</h1>
<p><%=doc_items.length%> documentation topics found.</p>
<h1>Topics</h1>
<dl>
<% doc_items.each do |doc_item| %>
<dt>
<b><%=doc_item[:name]%></b>
<a href="<%=u doc_item[:url]%>">[rdoc]</a>
</dt>
<dd>
<%=h doc_item[:summary]%>
<br/>
<br/>
</dd>
<% end %>
</dl>
<p>
Back to <a href="/">complete gem index</a>
</p>
</div>
</div>
</div>
<div id="validator-badges">
<p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
</div>
</body>
</html>
ERB
def self.run(options)
new(options[:gemdir], options[:port], options[:daemon],
options[:launch], options[:addresses]).run
end
def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
Gem::RDoc.load_rdoc
Socket.do_not_reverse_lookup = true
@gem_dirs = Array gem_dirs
@port = port
@daemon = daemon
@launch = launch
@addresses = addresses
logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
@server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
@spec_dirs = @gem_dirs.map {|gem_dir| File.join gem_dir, 'specifications' }
@spec_dirs.reject! {|spec_dir| !File.directory? spec_dir }
reset_gems
@have_rdoc_4_plus = nil
end
def add_date(res)
res['date'] = @spec_dirs.map do |spec_dir|
File.stat(spec_dir).mtime
end.max
end
def uri_encode(str)
str.gsub(URI::UNSAFE) do |match|
match.each_byte.map {|c| sprintf('%%%02X', c.ord) }.join
end
end
def doc_root(gem_name)
if have_rdoc_4_plus?
"/doc_root/#{u gem_name}/"
else
"/doc_root/#{u gem_name}/rdoc/index.html"
end
end
def have_rdoc_4_plus?
@have_rdoc_4_plus ||=
Gem::Requirement.new('>= 4.0.0.preview2').satisfied_by? Gem::RDoc.rdoc_version
end
def latest_specs(req, res)
reset_gems
res['content-type'] = 'application/x-gzip'
add_date res
latest_specs = Gem::Specification.latest_specs
specs = latest_specs.sort.map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
##
# Creates server sockets based on the addresses option. If no addresses
# were given a server socket for all interfaces is created.
def listen(addresses = @addresses)
addresses = [nil] unless addresses
listeners = 0
addresses.each do |address|
begin
@server.listen address, @port
@server.listeners[listeners..-1].each do |listener|
host, port = listener.addr.values_at 2, 1
host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
say "Server started at http://#{host}:#{port}"
end
listeners = @server.listeners.length
rescue SystemCallError
next
end
end
if @server.listeners.empty?
say "Unable to start a server."
say "Check for running servers or your --bind and --port arguments"
terminate_interaction 1
end
end
def prerelease_specs(req, res)
reset_gems
res['content-type'] = 'application/x-gzip'
add_date res
specs = Gem::Specification.select do |spec|
spec.version.prerelease?
end.sort.map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
def quick(req, res)
reset_gems
res['content-type'] = 'text/plain'
add_date res
case req.request_uri.path
when %r{^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)\.gemspec\.rz$} then
marshal_format, full_name = $1, $2
specs = Gem::Specification.find_all_by_full_name(full_name)
selector = full_name.inspect
if specs.empty?
res.status = 404
res.body = "No gems found matching #{selector}"
elsif specs.length > 1
res.status = 500
res.body = "Multiple gems found matching #{selector}"
elsif marshal_format
res['content-type'] = 'application/x-deflate'
res.body << Gem.deflate(Marshal.dump(specs.first))
end
else
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
end
end
def root(req, res)
reset_gems
add_date res
raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
req.path == '/'
specs = []
total_file_count = 0
Gem::Specification.each do |spec|
total_file_count += spec.files.size
deps = spec.dependencies.map do |dep|
{
"name" => dep.name,
"type" => dep.type,
"version" => dep.requirement.to_s,
}
end
deps = deps.sort_by {|dep| [dep["name"].downcase, dep["version"]] }
deps.last["is_last"] = true unless deps.empty?
# executables
executables = spec.executables.sort.collect {|exec| {"executable" => exec} }
executables = nil if executables.empty?
executables.last["is_last"] = true if executables
# Pre-process spec homepage for safety reasons
begin
homepage_uri = URI.parse(spec.homepage)
if [URI::HTTP, URI::HTTPS].member? homepage_uri.class
homepage_uri = spec.homepage
else
homepage_uri = "."
end
rescue URI::InvalidURIError
homepage_uri = "."
end
specs << {
"authors" => spec.authors.sort.join(", "),
"date" => spec.date.to_s,
"dependencies" => deps,
"doc_path" => doc_root(spec.full_name),
"executables" => executables,
"only_one_executable" => (executables && executables.size == 1),
"full_name" => spec.full_name,
"has_deps" => !deps.empty?,
"homepage" => homepage_uri,
"name" => spec.name,
"rdoc_installed" => Gem::RDoc.new(spec).rdoc_installed?,
"ri_installed" => Gem::RDoc.new(spec).ri_installed?,
"summary" => spec.summary,
"version" => spec.version.to_s,
}
end
specs << {
"authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
"dependencies" => [],
"doc_path" => doc_root("rubygems-#{Gem::VERSION}"),
"executables" => [{"executable" => 'gem', "is_last" => true}],
"only_one_executable" => true,
"full_name" => "rubygems-#{Gem::VERSION}",
"has_deps" => false,
"homepage" => "https://guides.rubygems.org/",
"name" => 'rubygems',
"ri_installed" => true,
"summary" => "RubyGems itself",
"version" => Gem::VERSION,
}
specs = specs.sort_by {|spec| [spec["name"].downcase, spec["version"]] }
specs.last["is_last"] = true
# tag all specs with first_name_entry
last_spec = nil
specs.each do |spec|
is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
spec["first_name_entry"] = is_first
last_spec = spec
end
# create page from template
template = ERB.new(DOC_TEMPLATE)
res['content-type'] = 'text/html'
values = { "gem_count" => specs.size.to_s, "specs" => specs,
"total_file_count" => total_file_count.to_s }
# suppress 1.9.3dev warning about unused variable
values = values
result = template.result binding
res.body = result
end
##
# Can be used for quick navigation to the rdoc documentation. You can then
# define a search shortcut for your browser. E.g. in Firefox connect
# 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can
# directly open the ActionPack documentation by typing 'rdoc actionp'. If
# there are multiple hits for the search term, they are presented as a list
# with links.
#
# Search algorithm aims for an intuitive search:
# 1. first try to find the gems and documentation folders which name
# starts with the search term
# 2. search for entries, that *contain* the search term
# 3. show all the gems
#
# If there is only one search hit, user is immediately redirected to the
# documentation for the particular gem, otherwise a list with results is
# shown.
#
# === Additional trick - install documentation for Ruby core
#
# Note: please adjust paths accordingly use for example 'locate yaml.rb' and
# 'gem environment' to identify directories, that are specific for your
# local installation
#
# 1. install Ruby sources
# cd /usr/src
# sudo apt-get source ruby
#
# 2. generate documentation
# rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \
# /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
#
# By typing 'rdoc core' you can now access the core documentation
def rdoc(req, res)
query = req.query['q']
show_rdoc_for_pattern("#{query}*", res) && return
show_rdoc_for_pattern("*#{query}*", res) && return
template = ERB.new RDOC_NO_DOCUMENTATION
res['content-type'] = 'text/html'
res.body = template.result binding
end
##
# Updates the server to use the latest installed gems.
def reset_gems # :nodoc:
Gem::Specification.dirs = @gem_dirs
end
##
# Returns true and prepares http response, if rdoc for the requested gem
# name pattern was found.
#
# The search is based on the file system content, not on the gems metadata.
# This allows additional documentation folders like 'core' for the Ruby core
# documentation - just put it underneath the main doc folder.
def show_rdoc_for_pattern(pattern, res)
found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select do |path|
File.exist? File.join(path, 'rdoc/index.html')
end
case found_gems.length
when 0
return false
when 1
new_path = File.basename(found_gems[0])
res.status = 302
res['Location'] = doc_root new_path
return true
else
doc_items = []
found_gems.each do |file_name|
base_name = File.basename(file_name)
doc_items << {
:name => base_name,
:url => doc_root(new_path),
:summary => ''
}
end
template = ERB.new(RDOC_SEARCH_TEMPLATE)
res['content-type'] = 'text/html'
result = template.result binding
res.body = result
return true
end
end
def run
listen
WEBrick::Daemon.start if @daemon
@server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
@server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
@server.mount_proc "/latest_specs.#{Gem.marshal_version}",
method(:latest_specs)
@server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
method(:latest_specs)
@server.mount_proc "/prerelease_specs.#{Gem.marshal_version}",
method(:prerelease_specs)
@server.mount_proc "/prerelease_specs.#{Gem.marshal_version}.gz",
method(:prerelease_specs)
@server.mount_proc "/quick/", method(:quick)
@server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
res['content-type'] = 'text/css'
add_date res
res.body << RDOC_CSS
end
@server.mount_proc "/", method(:root)
@server.mount_proc "/rdoc", method(:rdoc)
file_handlers = {
'/gems' => '/cache/',
}
if have_rdoc_4_plus?
@server.mount '/doc_root', RDoc::Servlet, '/doc_root'
else
file_handlers['/doc_root'] = '/doc/'
end
@gem_dirs.each do |gem_dir|
file_handlers.each do |mount_point, mount_dir|
@server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
File.join(gem_dir, mount_dir), true)
end
end
trap("INT") { @server.shutdown; exit! }
trap("TERM") { @server.shutdown; exit! }
launch if @launch
@server.start
end
def specs(req, res)
reset_gems
add_date res
specs = Gem::Specification.sort_by(&:sort_obj).map do |spec|
platform = spec.original_platform || Gem::Platform::RUBY
[spec.name, spec.version, platform]
end
specs = Marshal.dump specs
if req.path =~ /\.gz$/
specs = Gem::Util.gzip specs
res['content-type'] = 'application/x-gzip'
else
res['content-type'] = 'application/octet-stream'
end
if req.request_method == 'HEAD'
res['content-length'] = specs.length
else
res.body << specs
end
end
def launch
listeners = @server.listeners.map{|l| l.addr[2] }
# TODO: 0.0.0.0 == any, not localhost.
host = listeners.any?{|l| l == '0.0.0.0' } ? 'localhost' : listeners.first
say "Launching browser to http://#{host}:#{@port}"
system("#{@launch} http://#{host}:#{@port}")
end
end