Fix #3791/#3478
* Add support for SSL through command line switches. * Add suppport for file/index.html > file.html > directory. * Add support for custom-headers through configuration. * Modernize and split up the serve. * Add a few basic tests.
This commit is contained in:
parent
d10dc01290
commit
8efbdc01ff
|
@ -1,161 +1,196 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve < Command
|
||||
|
||||
class << self
|
||||
COMMAND_OPTIONS = {
|
||||
"ssl_cert" => ["--ssl-cert [CERT]", "X.509 (SSL) certificate."],
|
||||
"host" => ["host", "-H", "--host [HOST]", "Host to bind to"],
|
||||
"open_url" => ["-o", "--open-url", "Launch your browser with your site."],
|
||||
"detach" => ["-B", "--detach", "Run the server in the background (detach)"],
|
||||
"ssl_key" => ["--ssl-key [KEY]", "X.509 (SSL) Private Key."],
|
||||
"port" => ["-P", "--port [PORT]", "Port to listen on"],
|
||||
"baseurl" => ["-b", "--baseurl [URL]", "Base URL"],
|
||||
"skip_initial_build" => ["skip_initial_build", "--skip-initial-build",
|
||||
"Skips the initial site build which occurs before the server is started."]
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
def init_with_program(prog)
|
||||
prog.command(:serve) do |c|
|
||||
c.syntax 'serve [options]'
|
||||
c.description 'Serve your site locally'
|
||||
c.alias :server
|
||||
c.alias :s
|
||||
prog.command(:serve) do |cmd|
|
||||
cmd.description "Serve your site locally"
|
||||
cmd.syntax "serve [options]"
|
||||
cmd.alias :server
|
||||
cmd.alias :s
|
||||
|
||||
add_build_options(c)
|
||||
add_build_options(cmd)
|
||||
COMMAND_OPTIONS.each do |key, val|
|
||||
cmd.option key, *val
|
||||
end
|
||||
|
||||
c.option 'detach', '-B', '--detach', 'Run the server in the background (detach)'
|
||||
c.option 'port', '-P', '--port [PORT]', 'Port to listen on'
|
||||
c.option 'host', '-H', '--host [HOST]', 'Host to bind to'
|
||||
c.option 'baseurl', '-b', '--baseurl [URL]', 'Base URL'
|
||||
c.option 'skip_initial_build', '--skip-initial-build', 'Skips the initial site build which occurs before the server is started.'
|
||||
c.option 'open_url', '-o', '--open-url', 'Opens the local URL in your default browser'
|
||||
|
||||
c.action do |args, options|
|
||||
options["serving"] = true
|
||||
options["watch"] = true unless options.key?("watch")
|
||||
Jekyll::Commands::Build.process(options)
|
||||
Jekyll::Commands::Serve.process(options)
|
||||
cmd.action do |_, opts|
|
||||
opts["serving"] = true
|
||||
opts["watch" ] = true unless opts.key?("watch")
|
||||
Build.process(opts)
|
||||
Serve.process(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Boot up a WEBrick server which points to the compiled site's root.
|
||||
def process(options)
|
||||
options = configuration_from_options(options)
|
||||
destination = options['destination']
|
||||
#
|
||||
|
||||
def process(opts)
|
||||
opts = configuration_from_options(opts)
|
||||
destination = opts["destination"]
|
||||
setup(destination)
|
||||
|
||||
s = WEBrick::HTTPServer.new(webrick_options(options))
|
||||
s.unmount("")
|
||||
server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }
|
||||
server.mount(opts["baseurl"], Servlet, destination, file_handler_opts)
|
||||
Jekyll.logger.info "Server address:", server_address(server, opts)
|
||||
launch_browser server, opts if opts["open_url"]
|
||||
boot_or_detach server, opts
|
||||
end
|
||||
|
||||
s.mount(
|
||||
options['baseurl'],
|
||||
custom_file_handler,
|
||||
destination,
|
||||
file_handler_options
|
||||
)
|
||||
# Do a base pre-setup of WEBRick so that everything is in place
|
||||
# when we get ready to party, checking for an setting up an error page
|
||||
# and making sure our destination exists.
|
||||
|
||||
private
|
||||
def setup(destination)
|
||||
require_relative "serve/servlet"
|
||||
|
||||
server_address_str = server_address(s, options)
|
||||
Jekyll.logger.info "Server address:", server_address_str
|
||||
|
||||
if options["open_url"]
|
||||
command = Utils::Platforms.windows?? "start" : Utils::Platforms.osx?? \
|
||||
"open" : "xdg-open"
|
||||
|
||||
system command, server_address_str
|
||||
FileUtils.mkdir_p(destination)
|
||||
if File.exist?(File.join(destination, "404.html"))
|
||||
WEBrick::HTTPResponse.class_eval do
|
||||
def create_error_page
|
||||
@header["Content-Type"] = "text/html; charset=UTF-8"
|
||||
@body = IO.read(File.join(@config[:DocumentRoot], "404.html"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
private
|
||||
def webrick_opts(opts)
|
||||
opts = {
|
||||
:JekyllOptions => opts,
|
||||
:DoNotReverseLookup => true,
|
||||
:MimeTypes => mime_types,
|
||||
:DocumentRoot => opts["destination"],
|
||||
:StartCallback => start_callback(opts["detach"]),
|
||||
:BindAddress => opts["host"],
|
||||
:Port => opts["port"],
|
||||
:DirectoryIndex => %W(
|
||||
index.htm
|
||||
index.html
|
||||
index.rhtml
|
||||
index.cgi
|
||||
index.xml
|
||||
)
|
||||
}
|
||||
|
||||
enable_ssl(opts)
|
||||
enable_logging(opts)
|
||||
opts
|
||||
end
|
||||
|
||||
# Recreate NondisclosureName under utf-8 circumstance
|
||||
|
||||
private
|
||||
def file_handler_opts
|
||||
WEBrick::Config::FileHandler.merge({
|
||||
:FancyIndexing => true,
|
||||
:NondisclosureName => [
|
||||
'.ht*','~*'
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
private
|
||||
def server_address(server, opts)
|
||||
address = server.config[:BindAddress]
|
||||
baseurl = "#{opts["baseurl"]}/" if opts["baseurl"]
|
||||
port = server.config[:Port]
|
||||
|
||||
"http://#{address}:#{port}#{baseurl}"
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
private
|
||||
def launch_browser(server, opts)
|
||||
command = Utils::Platforms.windows?? "start" : Utils::Platforms.osx?? "open" : "xdg-open"
|
||||
system command, server_address(server, opts)
|
||||
end
|
||||
|
||||
# Keep in our area with a thread or detach the server as requested
|
||||
# by the user. This method determines what we do based on what you
|
||||
# ask us to do.
|
||||
|
||||
private
|
||||
def boot_or_detach(server, opts)
|
||||
if opts["detach"]
|
||||
pid = Process.fork do
|
||||
server.start
|
||||
end
|
||||
|
||||
if options['detach'] # detach the server
|
||||
pid = Process.fork { s.start }
|
||||
Process.detach(pid)
|
||||
Jekyll.logger.info "Server detached with pid '#{pid}'.", "Run `pkill -f jekyll' or `kill -9 #{pid}' to stop the server."
|
||||
else # create a new server thread, then join it with current terminal
|
||||
t = Thread.new { s.start }
|
||||
trap("INT") { s.shutdown }
|
||||
Jekyll.logger.info "Server detached with pid '#{pid}'.", \
|
||||
"Run `pkill -f jekyll' or `kill -9 #{pid}' to stop the server."
|
||||
else
|
||||
t = Thread.new { server.start }
|
||||
trap("INT") { server.shutdown }
|
||||
t.join
|
||||
end
|
||||
end
|
||||
|
||||
def setup(destination)
|
||||
require 'webrick'
|
||||
# Make the stack verbose if the user requests it.
|
||||
|
||||
FileUtils.mkdir_p(destination)
|
||||
|
||||
# monkey patch WEBrick using custom 404 page (/404.html)
|
||||
if File.exist?(File.join(destination, '404.html'))
|
||||
WEBrick::HTTPResponse.class_eval do
|
||||
def create_error_page
|
||||
@header['content-type'] = "text/html; charset=UTF-8"
|
||||
@body = IO.read(File.join(@config[:DocumentRoot], '404.html'))
|
||||
end
|
||||
end
|
||||
end
|
||||
private
|
||||
def enable_logging(opts)
|
||||
opts[:AccessLog] = []
|
||||
level = WEBrick::Log.const_get(opts[:JekyllOptions]["verbose"] ? :DEBUG : :WARN)
|
||||
opts[:Logger] = WEBrick::Log.new($stdout, level)
|
||||
end
|
||||
|
||||
def webrick_options(config)
|
||||
opts = {
|
||||
:BindAddress => config['host'],
|
||||
:DirectoryIndex => %w(index.html index.htm index.cgi index.rhtml index.xml),
|
||||
:DocumentRoot => config['destination'],
|
||||
:DoNotReverseLookup => true,
|
||||
:MimeTypes => mime_types,
|
||||
:Port => config['port'],
|
||||
:StartCallback => start_callback(config['detach'])
|
||||
}
|
||||
# Add SSL to the stack if the user triggers --enable-ssl and they
|
||||
# provide both types of certificates commonly needed. Raise if they
|
||||
# forget to add one of the certificates.
|
||||
|
||||
if config['verbose']
|
||||
opts.merge!({
|
||||
:Logger => WEBrick::Log.new($stdout, WEBrick::Log::DEBUG)
|
||||
})
|
||||
else
|
||||
opts.merge!({
|
||||
:AccessLog => [],
|
||||
:Logger => WEBrick::Log.new([], WEBrick::Log::WARN)
|
||||
})
|
||||
private
|
||||
def enable_ssl(opts)
|
||||
return if !opts[:JekyllOptions]["ssl_cert"] && !opts[:JekyllOptions]["ssl_key"]
|
||||
if !opts[:JekyllOptions]["ssl_cert"] || !opts[:JekyllOptions]["ssl_key"]
|
||||
raise RuntimeError, "--ssl-cert or --ssl-key missing."
|
||||
end
|
||||
|
||||
opts
|
||||
end
|
||||
|
||||
# Custom WEBrick FileHandler servlet for serving "/file.html" at "/file"
|
||||
# when no exact match is found. This mirrors the behavior of GitHub
|
||||
# Pages and many static web server configs.
|
||||
def custom_file_handler
|
||||
Class.new WEBrick::HTTPServlet::FileHandler do
|
||||
def search_file(req, res, basename)
|
||||
if file = super
|
||||
file
|
||||
else
|
||||
super(req, res, "#{basename}.html")
|
||||
end
|
||||
end
|
||||
end
|
||||
require "openssl"; require "webrick/https"
|
||||
source_key = Jekyll.sanitized_path(opts[:JekyllOptions]["source"], opts[:JekyllOptions]["ssl_key" ])
|
||||
source_certificate = Jekyll.sanitized_path(opts[:JekyllOptions]["source"], opts[:JekyllOptions]["ssl_cert"])
|
||||
opts[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read(source_certificate))
|
||||
opts[:SSLPrivateKey ] = OpenSSL::PKey::RSA.new(File.read(source_key))
|
||||
opts[:EnableSSL] = true
|
||||
end
|
||||
|
||||
private
|
||||
def start_callback(detached)
|
||||
unless detached
|
||||
Proc.new { Jekyll.logger.info "Server running...", "press ctrl-c to stop." }
|
||||
proc do
|
||||
Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def mime_types
|
||||
mime_types_file = File.expand_path('../mime.types', File.dirname(__FILE__))
|
||||
WEBrick::HTTPUtils::load_mime_types(mime_types_file)
|
||||
file = File.expand_path('../mime.types', File.dirname(__FILE__))
|
||||
WEBrick::HTTPUtils.load_mime_types(file)
|
||||
end
|
||||
|
||||
def server_address(server, options)
|
||||
baseurl = "#{options['baseurl']}/" if options['baseurl']
|
||||
[
|
||||
"http://",
|
||||
server.config[:BindAddress],
|
||||
":",
|
||||
server.config[:Port],
|
||||
baseurl || ""
|
||||
].map(&:to_s).join("")
|
||||
end
|
||||
|
||||
# recreate NondisclosureName under utf-8 circumstance
|
||||
def file_handler_options
|
||||
WEBrick::Config::FileHandler.merge({
|
||||
:FancyIndexing => true,
|
||||
:NondisclosureName => ['.ht*','~*']
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
require "webrick"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
class Servlet < WEBrick::HTTPServlet::FileHandler
|
||||
HEADER_DEFAULTS = {}
|
||||
|
||||
def initialize(server, root, callbacks)
|
||||
extract_headers(server.config[:JekyllOptions])
|
||||
super
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
def do_GET(req, res)
|
||||
res.header.merge!(@headers) if @headers.any?
|
||||
return super
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# file > file/index.html > file.html > directory -> Having a directory
|
||||
# with the same name as a file will result in the file being served the
|
||||
# way that Nginx behaves (probably not exactly...) For browsing.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def search_file(req, res, basename)
|
||||
return file if (file = super) || (file = super req, res, "#{basename}.html")
|
||||
|
||||
file = "#{req.path.gsub(/\/\Z/, "")}.html"
|
||||
if file && File.file?(File.join(@config[:DocumentRoot], file))
|
||||
return ".html"
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def extract_headers(opts)
|
||||
@headers = add_defaults(opts.fetch("webrick", {}).fetch("headers", {}))
|
||||
end
|
||||
|
||||
def add_defaults(opts)
|
||||
control_development_cache(opts)
|
||||
HEADER_DEFAULTS.each_with_object(opts) do |(k, v), h|
|
||||
h[k] = v if !h[k]
|
||||
end
|
||||
end
|
||||
|
||||
def control_development_cache(opts)
|
||||
if !opts.has_key?("Cache-Control") && Jekyll.env == "development"
|
||||
opts["Cache-Control"] = "private, max-age=0, proxy-revalidate, no-store, no-cache, must-revalidate"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -352,6 +352,24 @@ before your site is served.
|
|||
<p><code class="flag">--skip-initial-build</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="setting">
|
||||
<td>
|
||||
<p class="name"><strong>X.509 (SSL) Private Key</strong></p>
|
||||
<p class="description">SSL Private Key.</p>
|
||||
</td>
|
||||
<td class="align-center">
|
||||
<p><code class="flag">--ssl-key</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="setting">
|
||||
<td>
|
||||
<p class="name"><strong>X.509 (SSL) Certificate</strong></p>
|
||||
<p class="description">SSL Public certificate.</p>
|
||||
</td>
|
||||
<td class="align-center">
|
||||
<p><code class="flag">--ssl-cert</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -364,6 +382,24 @@ before your site is served.
|
|||
</p>
|
||||
</div>
|
||||
|
||||
## Custom WEBRick Headers
|
||||
|
||||
You can provide custom headers for your site by adding them to `_config.yml`
|
||||
|
||||
{% highlight yaml %}
|
||||
# File: _config.yml
|
||||
webrick:
|
||||
headers:
|
||||
My-Header: My-Value
|
||||
My-Other-Header: My-Other-Value
|
||||
{% endhighlight %}
|
||||
|
||||
### Defaults
|
||||
|
||||
We only provide on default and that's a Content-Type header that disables
|
||||
caching in development so that you don't have to fight with Chrome's aggressive
|
||||
caching when you are in development mode.
|
||||
|
||||
## Specifying a Jekyll environment at build time
|
||||
|
||||
In the build (or serve) arguments, you can specify a Jekyll environment and value. The build will then apply this value in any conditional statements in your content.
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
require "webrick"
|
||||
require "mercenary"
|
||||
require "helper"
|
||||
|
||||
class TestCommandsServe < JekyllUnitTest
|
||||
def custom_opts(what)
|
||||
@cmd.send(
|
||||
:webrick_opts, what
|
||||
)
|
||||
end
|
||||
|
||||
context "with a program" do
|
||||
setup do
|
||||
@merc, @cmd = nil, Jekyll::Commands::Serve
|
||||
Mercenary.program(:jekyll) do |p|
|
||||
@merc = @cmd.init_with_program(
|
||||
p
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
should "label itself" do
|
||||
assert_equal(
|
||||
@merc.name, :serve
|
||||
)
|
||||
end
|
||||
|
||||
should "have aliases" do
|
||||
assert_includes @merc.aliases, :s
|
||||
assert_includes @merc.aliases, :server
|
||||
end
|
||||
|
||||
should "have a description" do
|
||||
refute_nil(
|
||||
@merc.description
|
||||
)
|
||||
end
|
||||
|
||||
should "have an action" do
|
||||
refute_empty(
|
||||
@merc.actions
|
||||
)
|
||||
end
|
||||
|
||||
should "not have an empty options set" do
|
||||
refute_empty(
|
||||
@merc.options
|
||||
)
|
||||
end
|
||||
|
||||
context "with custom options" do
|
||||
should "create a default set of mimetypes" do
|
||||
refute_nil custom_opts({})[
|
||||
:MimeTypes
|
||||
]
|
||||
end
|
||||
|
||||
should "use user destinations" do
|
||||
assert_equal "foo", custom_opts({ "destination" => "foo" })[
|
||||
:DocumentRoot
|
||||
]
|
||||
end
|
||||
|
||||
should "use user port" do
|
||||
# WHAT?!?!1 Over 9000? That's impossible.
|
||||
assert_equal 9001, custom_opts( { "port" => 9001 })[
|
||||
:Port
|
||||
]
|
||||
end
|
||||
|
||||
context "verbose" do
|
||||
should "debug when verbose" do
|
||||
assert_equal custom_opts({ "verbose" => true })[:Logger].level, 5
|
||||
end
|
||||
|
||||
should "warn when not verbose" do
|
||||
assert_equal custom_opts({})[:Logger].level, 3
|
||||
end
|
||||
end
|
||||
|
||||
context "enabling ssl" do
|
||||
should "raise if enabling without key or cert" do
|
||||
assert_raises RuntimeError do
|
||||
custom_opts({
|
||||
"ssl_key" => "foo"
|
||||
})
|
||||
end
|
||||
|
||||
assert_raises RuntimeError do
|
||||
custom_opts({
|
||||
"ssl_key" => "foo"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
should "allow SSL with a key and cert" do
|
||||
expect(OpenSSL::PKey::RSA).to receive(:new).and_return("c2")
|
||||
expect(OpenSSL::X509::Certificate).to receive(:new).and_return("c1")
|
||||
allow(File).to receive(:read).and_return("foo")
|
||||
|
||||
result = custom_opts({
|
||||
"ssl_cert" => "foo",
|
||||
"source" => "bar",
|
||||
"enable_ssl" => true,
|
||||
"ssl_key" => "bar"
|
||||
})
|
||||
|
||||
assert result[:EnableSSL]
|
||||
assert_equal result[:SSLPrivateKey ], "c2"
|
||||
assert_equal result[:SSLCertificate], "c1"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue