Merge pull request #1528 from maxmeyer/feature/listener

Support Bind to address for middleman
This commit is contained in:
Thomas Reynolds 2015-07-16 15:12:09 -07:00
commit d2f8dc9932
42 changed files with 2029 additions and 52 deletions

View file

@ -14,6 +14,7 @@ AllCops:
- 'middleman-core/fixtures/**/*' - 'middleman-core/fixtures/**/*'
- 'middleman-core/features/**/*' - 'middleman-core/features/**/*'
- 'middleman-core/spec/**/*' - 'middleman-core/spec/**/*'
DisplayCopNames: true
LineLength: LineLength:
Enabled: false Enabled: false
MethodLength: MethodLength:

View file

@ -9,7 +9,7 @@ gem 'pry', '~> 0.10', group: :development
gem 'aruba', '~> 0.7.4' gem 'aruba', '~> 0.7.4'
gem 'rspec', '~> 3.0' gem 'rspec', '~> 3.0'
gem 'fivemat', '~> 1.3' gem 'fivemat', '~> 1.3'
gem 'cucumber', '~> 1.3' gem 'cucumber', '~> 2.0'
# Optional middleman dependencies, included for tests # Optional middleman dependencies, included for tests
gem 'less', '2.3', require: false gem 'less', '2.3', require: false
@ -20,6 +20,9 @@ gem 'sinatra', '>= 1.4', require: false
gem 'redcarpet', '>= 3.1', require: false unless RUBY_ENGINE == 'jruby' gem 'redcarpet', '>= 3.1', require: false unless RUBY_ENGINE == 'jruby'
gem 'asciidoctor', '~> 0.1', require: false gem 'asciidoctor', '~> 0.1', require: false
# Dns server to test preview server
gem 'rubydns', '~> 1.0.1', require: false
# To test javascript # To test javascript
gem 'poltergeist', '~> 1.6.0', require: false gem 'poltergeist', '~> 1.6.0', require: false

View file

@ -21,7 +21,7 @@ Cucumber::Rake::Task.new do |t|
exempt_tags << '--tags ~@encoding' unless Object.const_defined?(:Encoding) exempt_tags << '--tags ~@encoding' unless Object.const_defined?(:Encoding)
exempt_tags << '--tags ~@nowindows' if Gem.win_platform? exempt_tags << '--tags ~@nowindows' if Gem.win_platform?
exempt_tags << '--tags ~@travishatesme' if ENV['TRAVIS'] == 'true' exempt_tags << '--tags ~@travishatesme' if ENV['TRAVIS'] == 'true'
t.cucumber_opts = "--color #{exempt_tags.join(' ')} --strict --format #{ENV['CUCUMBER_FORMAT'] || 'Fivemat'}" t.cucumber_opts = "--require features --color #{exempt_tags.join(' ')} --strict --format #{ENV['CUCUMBER_FORMAT'] || 'Fivemat'}"
end end
Cucumber::Rake::Task.new(:cucumber_wip) do |t| Cucumber::Rake::Task.new(:cucumber_wip) do |t|

1
middleman-core/.rspec Normal file
View file

@ -0,0 +1 @@
--color

View file

@ -0,0 +1,2 @@
default: --require features --tags ~@wip
wip: --require features --tags @wip

View file

@ -0,0 +1,532 @@
Feature: Run the preview server
As a software developer
I want to start the preview server
In order to view my changes immediately in the browser
Background:
Given a fixture app "preview-server-app"
And the default aruba timeout is 30 seconds
Scenario: Start the server with defaults
When I run `middleman server` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
And the output should contain:
"""
View your site at "http://
"""
And the output should contain:
"""
Inspect your site configuration at "http://
"""
Scenario: Start the server with defaults in verbose mode
When I run `middleman server --verbose` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::4567", "0.0.0.0:4567"
"""
And the output should contain:
"""
View your site at "http://
"""
And the output should contain:
"""
Inspect your site configuration at "http://
"""
@ruby-2.1
Scenario: Start the server with defaults in verbose mode, when a local mdns server resolves the local hostname
Given I start a mdns server for the local hostname
When I run `middleman server --verbose` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::4567", "0.0.0.0:4567"
"""
And the output should contain:
"""
View your site at "http://
"""
And the output should contain:
"""
Inspect your site configuration at "http://
"""
Scenario: Start the server with bind address 127.0.0.1
Given I have a local hosts file with:
"""
# <ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost.localdomain localhost
"""
When I run `middleman server --verbose --bind-address 127.0.0.1` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with bind address 127.0.0.1 configured via config.rb
Given I have a local hosts file with:
"""
# <ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost.localdomain localhost
"""
And a file named "config.rb" with:
"""
set :bind_address, '127.0.0.1'
"""
When I run `middleman server --verbose` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with bind address 127.0.0.5
This will have no hostname attached because the hosts file, the DNS server
and the MDNS-server do not know anything about 127.0.0.5
When I run `middleman server --verbose --bind-address 127.0.0.5` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.5:4567"
"""
And the output should contain:
"""
View your site at "http://127.0.0.5:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://127.0.0.5:4567/__middleman"
"""
Scenario: Start the server with bind address ::1
Given a file named ".hosts" with:
"""
# <ip-address> <hostname.domain.org> <hostname>
::1 localhost.localdomain localhost
"""
When I run `middleman server --verbose --bind-address ::1` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "::1:4567"
"""
And the output should contain:
"""
View your site at "http://[::1]:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://[::1]:4567/__middleman"
"""
Scenario: Start the server with bind address 0.0.0.0
When I run `middleman server --verbose --bind-address 0.0.0.0` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "0.0.0.0:4567"
"""
And the output should contain:
"""
View your site at "http://
"""
And the output should contain:
"""
Inspect your site configuration at "http://
"""
Scenario: Start the server with bind address ::
When I run `middleman server --verbose --bind-address ::` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::4567"
"""
And the output should contain:
"""
View your site at "http://
"""
And the output should contain:
"""
Inspect your site configuration at "http://
"""
Scenario: Start the server with server name "localhost"
Given I have a local hosts file with:
"""
# <ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost.localdomain localhost
"""
When I run `middleman server --verbose --server-name localhost` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://localhost:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://localhost:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with server name "localhost" configured via config.rb
Given I have a local hosts file with:
"""
# <ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost.localdomain localhost
"""
And a file named "config.rb" with:
"""
set :server_name, 'localhost'
"""
When I run `middleman server --verbose` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://localhost:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://localhost:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with server name "localhost" and bind address "127.0.0.1"
Given I have a local hosts file with:
"""
# <ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost.localdomain localhost
"""
When I run `middleman server --verbose --server-name localhost --bind-address 127.0.0.1` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://localhost:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://localhost:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with server name "127.0.0.1"
When I run `middleman server --verbose --server-name 127.0.0.1` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://127.0.0.1:4567/__middleman"
"""
Scenario: Start the server with server name "::1"
When I run `middleman server --verbose --server-name ::1` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "::1:4567"
"""
And the output should contain:
"""
View your site at "http://[::1]:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://[::1]:4567/__middleman"
"""
Scenario: Start the server with https
When I run `middleman server --verbose --https` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::4567", "0.0.0.0:4567"
"""
And the output should contain:
"""
View your site at "https://
"""
And the output should contain:
"""
Inspect your site configuration at "https://
"""
Scenario: Start the server with port 65432
When I run `middleman server --verbose --port 65432` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::65432", "0.0.0.0:65432"
"""
Scenario: Start the server with port 65432 configured via config.rb
Given a file named "config.rb" with:
"""
set :port, 65432
"""
When I run `middleman server --verbose` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to ":::65432", "0.0.0.0:65432"
"""
Scenario: Start the server when port is blocked by other middleman instance
Given `middleman server` is running in background
When I run `middleman server --verbose` interactively
And I stop all commands if the output of the last command contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman uses a different port
"""
Scenario: Start the server with bind address 1.1.1.1
This should fail, because "1.1.1.1" is not an interface available on this computer.
Given a file named ".hosts" with:
"""
1.1.1.1 www.example.com www
"""
When I run `middleman server --verbose --bind-address 1.1.1.1` interactively
And I stop middleman if the output contains:
"""
Running Middleman failed:
"""
Then the output should contain:
"""
Bind address "1.1.1.1" is not available on your system
"""
Scenario: Start the server with server name www.example.com and bind address 0.0.0.0
This should fail, because the user can just use `--server-name`. It does
not make sense for `middleman` to only listen on `0.0.0.0` (IPv4 all
interfaces), but not on `::` (IPv6 all interfaces). There are other tools
like `iptables` (Linux-only) or better some `kernel`-configurations to make
this possible.
When I run `middleman server --verbose --server-name www.example.com --bind-address 0.0.0.0` interactively
And I stop middleman if the output contains:
"""
Running Middleman failed:
"""
Then the output should contain:
"""
Undefined combination of options "--server-name" and "--bind-address".
"""
Scenario: Start the server with server name "www.example.com" and bind address "127.0.0.1"
This should fail because the server name does not resolve to the ip address.
Given a file named ".hosts" with:
"""
1.1.1.1 www.example.com www
"""
When I run `middleman server --verbose --server-name www.example.com --bind-address 127.0.0.1` interactively
And I stop middleman if the output contains:
"""
Running Middleman failed:
"""
Then the output should contain:
"""
Server name "www.example.com" does not resolve to bind address "127.0.0.1". Please fix that and try again.
"""
Scenario: Start the server with server name "garbage.example.com"
When I run `middleman server --verbose --server-name garbage.example.com` interactively
And I stop middleman if the output contains:
"""
Running Middleman failed:
"""
Then the output should contain:
"""
Server name "garbage.example.com" does not resolve to an ip address. Please fix that and try again.
"""
Scenario: Start the server with server name "www.example.com" and the network name server is used to resolve the server name
Given I have a local hosts file with:
"""
# empty
"""
And I start a mdns server with:
"""
# empty
"""
And I start a dns server with:
"""
www.example.com: 127.0.0.1
"""
When I run `middleman server --verbose --server-name www.example.com` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://www.example.com:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://www.example.com:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""
@ruby-2.1
Scenario: Start the server with server name "host.local" and the link local name server is used to resolve the server name
To make the mdns resolver resolve a name, it needs to end with ".local".
Otherwise the resolver returns [].
Given I have a local hosts file with:
"""
# empty
"""
And I start a mdns server with:
"""
host.local: 127.0.0.1
"""
When I run `middleman server --verbose --server-name host.local` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://host.local:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://host.local:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""
@ruby-2.1
Scenario: Start the server with server name "host" and the link local name server is used to resolve the server name
To make the mdns resolver resolve a name, it needs to end with ".local". If
a plain hostname is given `middleman` appends ".local" automatically.
Given I have a local hosts file with:
"""
# empty
"""
And I start a mdns server with:
"""
host.local: 127.0.0.1
"""
When I run `middleman server --verbose --server-name host` interactively
And I stop middleman if the output contains:
"""
Inspect your site configuration
"""
Then the output should contain:
"""
The Middleman preview server is bind to "127.0.0.1:4567"
"""
And the output should contain:
"""
View your site at "http://host.local:4567", "http://127.0.0.1:4567"
"""
And the output should contain:
"""
Inspect your site configuration at "http://host.local:4567/__middleman", "http://127.0.0.1:4567/__middleman"
"""

View file

@ -0,0 +1,33 @@
#!/usr/bin/env ruby
require 'rubydns'
require 'psych'
db_file = ARGV[0]
port = ARGV[1] || 5300
db = if File.file? db_file
$stderr.puts 'Found dns db'
Psych.load_file(db_file)
else
$stderr.puts 'Found no dns db. Use default db.'
{
/www\.example\.org/ => '1.1.1.1'
}
end
interfaces = [
[:udp, "127.0.0.1", port],
[:tcp, "127.0.0.1", port]
]
# Start the RubyDNS server
RubyDNS::run_server(:listen => interfaces) do
db.each do |matcher, result|
match(matcher, Resolv::DNS::Resource::IN::A) do |transaction|
transaction.respond!(result)
end
end
end

View file

@ -0,0 +1,11 @@
page "/fake.html", :proxy => "/real.html", :layout => false
ignore "/should_be_ignored.html"
page "/should_be_ignored2.html", :ignore => true
page "/target_ignore.html", :proxy => "/should_be_ignored3.html", :ignore => true
%w(one two).each do |num|
page "/fake/#{num}.html", :proxy => "/real/index.html" do
@num = num
end
end

View file

@ -0,0 +1,11 @@
page "/fake.html", :proxy => "/real.html", :layout => false
ignore "/should_be_ignored.html"
page "/should_be_ignored2.html", :ignore => true
page "/target_ignore.html", :proxy => "/should_be_ignored3.html", :ignore => true
%w(one two).each do |num|
page "/fake/#{num}.html", :proxy => "/real/index.html" do
@num = num
end
end

View file

@ -0,0 +1 @@
<h1>Welcome</h1>

View file

@ -0,0 +1,9 @@
<html>
<head>
<title>My Sample Site</title>
<!-- Comment in layout -->
</head>
<body>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Custom Layout</title>
</head>
<body>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1 @@
I am real

View file

@ -0,0 +1,5 @@
---
layout: false
---
I am real: <%= @num %>

View file

@ -0,0 +1 @@
<h1>Ignore me!</h1>

View file

@ -0,0 +1 @@
<h1>Ignore me! 2</h1>

View file

@ -0,0 +1 @@
<h1>Ignore me! 3</h1>

View file

@ -0,0 +1 @@
Static, no code!

View file

@ -71,7 +71,11 @@ module Middleman
# Which server name should be used # Which server name should be used
# @return [NilClass, String] # @return [NilClass, String]
config.define_setting :host, nil, 'The preview host name' config.define_setting :server_name, nil, 'The server name of preview server'
# Which bind address the preview server should use
# @return [NilClass, String]
config.define_setting :bind_address, nil, 'The bind address of the preview server'
# Whether to serve the preview server over HTTPS. # Whether to serve the preview server over HTTPS.
# @return [Boolean] # @return [Boolean]

View file

@ -14,9 +14,12 @@ module Middleman::Cli
method_option :port, method_option :port,
aliases: '-p', aliases: '-p',
desc: 'The port Middleman will listen on' desc: 'The port Middleman will listen on'
method_option :host, method_option :server_name,
aliases: '-h', aliases: '-s',
desc: 'The host name Middleman will use' desc: 'The server name name Middleman will use'
method_option :bind_address,
aliases: '-b',
desc: 'The bind address Middleman will listen on'
method_option :https, method_option :https,
type: :boolean, type: :boolean,
desc: 'Serve the preview server over SSL/TLS' desc: 'Serve the preview server over SSL/TLS'
@ -68,8 +71,9 @@ module Middleman::Cli
params = { params = {
port: options['port'], port: options['port'],
bind_address: options['bind_address'],
https: options['https'], https: options['https'],
host: options['host'], server_name: options['server_name'],
ssl_certificate: options['ssl_certificate'], ssl_certificate: options['ssl_certificate'],
ssl_private_key: options['ssl_private_key'], ssl_private_key: options['ssl_private_key'],
environment: options['environment'], environment: options['environment'],

View file

@ -0,0 +1,73 @@
require 'resolv'
require 'middleman-core/dns_resolver/network_resolver'
require 'middleman-core/dns_resolver/hosts_resolver'
module Middleman
# This resolves IP address to names and vice versa
class DnsResolver
private
attr_reader :resolvers
public
# Create resolver
#
# First the local resolver is used. If environment variable HOSTSRC is
# given this file is used for local name lookup.
#
# @param [#getnames, #getaddresses] network_resolver
# The resolver which uses a network name server to resolve ip addresses
# and names.
#
# @param [#getnames, #getaddresses] local_resolver
# The resolver uses /etc/hosts on POSIX-systems and
# C:\Windows\System32\drivers\etc\hosts on Windows-operating systems to
# resolve ip addresses and names.
#
# First the local resolver is queried. If this raises an error or returns
# nil or [] the network resolver is queried.
def initialize(opts={})
@resolvers = []
@resolvers << opts.fetch(:hosts_resolver, HostsResolver.new)
if RUBY_VERSION >= '2.1'
require 'middleman-core/dns_resolver/local_link_resolver'
@resolvers << opts.fetch(:local_link_resolver, LocalLinkResolver.new)
end
@resolvers << opts.fetch(:network_resolver, NetworkResolver.new)
end
# Get names for given ip
#
# @param [String] ip
# The ip which should be resolved.
def names_for(ip)
resolvers.each do |r|
names = r.getnames(ip)
return names unless names.nil? || names.empty?
end
[]
end
# Get ips for given name
#
# First the local resolver is used. On POSIX-systems /etc/hosts is used. On
# Windows C:\Windows\System32\drivers\etc\hosts is used.
#
# @param [String] name
# The name which should be resolved.
def ips_for(name)
resolvers.each do |r|
ips = r.getaddresses(name)
return ips unless ips.nil? || ips.empty?
end
[]
end
end
end

View file

@ -0,0 +1,52 @@
module Middleman
class DnsResolver
# Use network name server to resolve ips and names
class BasicNetworkResolver
private
attr_reader :resolver, :timeouts
public
def initialize(opts={})
@timeouts = opts.fetch(:timeouts, 2)
end
# Get names for ip
#
# @param [#to_s] ip
# The ip to resolve into names
#
# @return [Array]
# Array of Names
def getnames(ip)
resolver.getnames(ip.to_s).map(&:to_s)
rescue Resolv::ResolvError, Errno::EADDRNOTAVAIL
[]
end
# Get ips for name
#
# @param [#to_s] name
# The name to resolve into ips
#
# @return [Array]
# Array of ipaddresses
def getaddresses(name)
resolver.getaddresses(name.to_s).map(&:to_s)
rescue Resolv::ResolvError, Errno::EADDRNOTAVAIL
[]
end
# Set timeout for lookup
#
# @param [Integer] value
# The timeout value
def timeouts=(timeouts)
return if RUBY_VERSION < '2'
resolver.timeouts = timeouts
end
end
end
end

View file

@ -0,0 +1,63 @@
module Middleman
class DnsResolver
# Use network name server to resolve ips and names
class HostsResolver
private
attr_reader :resolver
public
def initialize(opts={})
# using the splat operator works around a non-existing HOSTSRC variable
# using nil as input does not work, but `*[]` does and then Resolv::Hosts
# uses its defaults
@resolver = opts.fetch(:resolver, Resolv::Hosts.new(*hosts_file))
end
# Get names for ip
#
# @param [#to_s] ip
# The ip to resolve into names
#
# @return [Array]
# Array of Names
def getnames(ip)
resolver.getnames(ip.to_s).map(&:to_s)
rescue Resolv::ResolvError
[]
end
# Get ips for name
#
# @param [#to_s] name
# The name to resolve into ips
#
# @return [Array]
# Array of ipaddresses
def getaddresses(name)
resolver.getaddresses(name.to_s).map(&:to_s)
rescue Resolv::ResolvError
[]
end
private
# Path to hosts file
#
# This looks for MM_HOSTSRC in your environment
#
# @return [Array]
# This needs to be an array, to make the splat operator work
#
# @example
# # <ip> <hostname>
# 127.0.0.1 localhost.localhost localhost
def hosts_file
return [ENV['MM_HOSTSRC']] if ENV.key?('MM_HOSTSRC') && File.file?(ENV['MM_HOSTSRC'])
[]
end
end
end
end

View file

@ -0,0 +1,44 @@
require 'middleman-core/dns_resolver/basic_network_resolver'
module Middleman
class DnsResolver
# Use network name server to resolve ips and names
class LocalLinkResolver < BasicNetworkResolver
def initialize(opts={})
super
@timeouts = opts.fetch(:timeouts, 1)
@resolver = opts.fetch(:resolver, Resolv::MDNS.new(nameserver_config))
self.timeouts = timeouts
end
private
# Hosts + Ports for MDNS resolver
#
# This looks for MM_MDNSRC in your environment. If you are going to use
# IPv6-addresses: Make sure you do not forget to add the port at the end.
#
# MM_MDNSRC=ip:port ip:port
#
# @return [Hash]
# Returns the configuration for the nameserver
#
# @example
# export MM_MDNSRC="224.0.0.251:5353 ff02::fb:5353"
#
def nameserver_config
return unless ENV.key?('MM_MDNSRC') && ENV['MM_MDNSRC']
address, port = ENV['MM_MDNSRC'].split(/:/)
{
nameserver_port: [[address, port.to_i]]
}
rescue StandardError
{}
end
end
end
end

View file

@ -0,0 +1,42 @@
require 'middleman-core/dns_resolver/basic_network_resolver'
module Middleman
class DnsResolver
# Use network name server to resolve ips and names
class NetworkResolver < BasicNetworkResolver
def initialize(opts={})
super
@resolver = opts.fetch(:resolver, Resolv::DNS.new(nameserver_config))
self.timeouts = timeouts
end
private
# Hosts + Ports for MDNS resolver
#
# This looks for MM_MDNSRC in your environment. If you are going to use
# IPv6-addresses: Make sure you do not forget to add the port at the end.
#
# MM_MDNSRC=ip:port ip:port
#
# @return [Hash]
# Returns the configuration for the nameserver
#
# @example
# export MM_MDNSRC="224.0.0.251:5353 ff02::fb:5353"
#
def nameserver_config
return unless ENV.key?('MM_DNSRC') && ENV['MM_DNSRC']
address, port = ENV['MM_DNSRC'].split(/:/)
{
nameserver_port: [[address, port.to_i]]
}
rescue StandardError
{}
end
end
end
end

View file

@ -1,15 +1,16 @@
require 'webrick' require 'webrick'
require 'webrick/https' require 'webrick/https'
require 'openssl' require 'openssl'
require 'socket'
require 'middleman-core/meta_pages' require 'middleman-core/meta_pages'
require 'middleman-core/logger' require 'middleman-core/logger'
require 'middleman-core/preview_server/server_information'
require 'middleman-core/preview_server/server_url'
# rubocop:disable GlobalVars # rubocop:disable GlobalVars
module Middleman module Middleman
module PreviewServer class PreviewServer
class << self class << self
attr_reader :app, :host, :port, :ssl_certificate, :ssl_private_key, :environment attr_reader :app, :ssl_certificate, :ssl_private_key, :environment, :server_information
delegate :logger, to: :app delegate :logger, to: :app
def https? def https?
@ -19,13 +20,31 @@ module Middleman
# Start an instance of Middleman::Application # Start an instance of Middleman::Application
# @return [void] # @return [void]
def start(opts={}) def start(opts={})
# Do not buffer output, otherwise testing of output does not work
$stdout.sync = true
$stderr.sync = true
@options = opts @options = opts
@server_information = ServerInformation.new
mount_instance(new_app) # New app evaluates the middleman configuration. Since this can be
# invalid as well, we need to evaluate the configuration BEFORE
# checking for validity
the_app = new_app
# And now comes the check
unless server_information.valid?
logger.fatal %(== Running Middleman failed: #{server_information.reason}. Please fix that and try again.)
exit 1
end
mount_instance(the_app)
logger.debug %(== Server information is provided by #{server_information.handler})
logger.debug %(== The Middleman is running in "#{environment}" environment) logger.debug %(== The Middleman is running in "#{environment}" environment)
logger.info "== The Middleman is standing watch at #{uri} (#{uri(public_ip)})" logger.debug format('== The Middleman preview server is bind to %s', ServerUrl.new(hosts: server_information.listeners, port: server_information.port, https: https?).to_bind_addresses.join(', '))
logger.info "== Inspect your site configuration at #{uri + '__middleman'}" logger.info format('== View your site at %s', ServerUrl.new(hosts: server_information.site_addresses, port: server_information.port, https: https?).to_urls.join(', '))
logger.info format('== Inspect your site configuration at %s', ServerUrl.new(hosts: server_information.site_addresses, port: server_information.port, https: https?).to_config_urls.join(', '))
@initialized ||= false @initialized ||= false
return if @initialized return if @initialized
@ -118,14 +137,22 @@ module Middleman
config[:environment] = opts[:environment].to_sym if opts[:environment] config[:environment] = opts[:environment].to_sym if opts[:environment]
config[:port] = opts[:port] if opts[:port] config[:port] = opts[:port] if opts[:port]
config[:host] = opts[:host].presence || Socket.gethostname.tr(' ', '+') config[:bind_address] = opts[:bind_address]
config[:server_name] = opts[:server_name]
config[:https] = opts[:https] unless opts[:https].nil? config[:https] = opts[:https] unless opts[:https].nil?
config[:ssl_certificate] = opts[:ssl_certificate] if opts[:ssl_certificate] config[:ssl_certificate] = opts[:ssl_certificate] if opts[:ssl_certificate]
config[:ssl_private_key] = opts[:ssl_private_key] if opts[:ssl_private_key] config[:ssl_private_key] = opts[:ssl_private_key] if opts[:ssl_private_key]
end end
@host = @app.config[:host] # store configured port to make a check later on possible
@port = @app.config[:port] configured_port = @app.config[:port]
# Use configuration values to set `bind_address` etc. in
# `server_information`
server_information.use @app.config
logger.warn format('== The Middleman uses a different port "%s" then the configured one "%s" because some other server is listening on that port.', server_information.port, configured_port) unless @app.config[:port] == configured_port
@https = @app.config[:https] @https = @app.config[:https]
@environment = @app.config[:environment] @environment = @app.config[:environment]
@ -190,9 +217,10 @@ module Middleman
# @return [void] # @return [void]
def setup_webrick(is_logging) def setup_webrick(is_logging)
http_opts = { http_opts = {
Port: port, Port: server_information.port,
AccessLog: [], AccessLog: [],
ServerName: host, ServerName: server_information.server_name,
BindAddress: server_information.bind_address.to_s,
DoNotReverseLookup: true DoNotReverseLookup: true
} }
@ -205,7 +233,7 @@ module Middleman
http_opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new File.read ssl_private_key http_opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new File.read ssl_private_key
else else
# use a generated self-signed cert # use a generated self-signed cert
cert, key = create_self_signed_cert(1024, [['CN', host]], 'Middleman Preview Server') cert, key = create_self_signed_cert(1024, [['CN', server_information.server_name]], server_information.site_addresses, 'Middleman Preview Server')
http_opts[:SSLCertificate] = cert http_opts[:SSLCertificate] = cert
http_opts[:SSLPrivateKey] = key http_opts[:SSLPrivateKey] = key
end end
@ -217,20 +245,17 @@ module Middleman
http_opts[:Logger] = ::WEBrick::Log.new(nil, 0) http_opts[:Logger] = ::WEBrick::Log.new(nil, 0)
end end
attempts_left = 4
tried_ports = []
begin begin
::WEBrick::HTTPServer.new(http_opts) ::WEBrick::HTTPServer.new(http_opts)
rescue Errno::EADDRINUSE rescue Errno::EADDRINUSE
logger.error "== Port #{port} is unavailable. Either close the instance of Middleman already running on #{port} or start this Middleman on a new port with: --port=#{unused_tcp_port}" logger.error %(== Port "#{http_opts[:Port]}" is in use. This should not have happened. Please start "middleman server" again.)
exit(1)
end end
end end
# Copy of https://github.com/nahi/ruby/blob/webrick_trunk/lib/webrick/ssl.rb#L39 # Copy of https://github.com/nahi/ruby/blob/webrick_trunk/lib/webrick/ssl.rb#L39
# that uses a different serial number each time the cert is generated in order to # that uses a different serial number each time the cert is generated in order to
# avoid errors in Firefox. Also doesn't print out stuff to $stderr unnecessarily. # avoid errors in Firefox. Also doesn't print out stuff to $stderr unnecessarily.
def create_self_signed_cert(bits, cn, comment) def create_self_signed_cert(bits, cn, aliases, comment)
rsa = OpenSSL::PKey::RSA.new(bits) rsa = OpenSSL::PKey::RSA.new(bits)
cert = OpenSSL::X509::Certificate.new cert = OpenSSL::X509::Certificate.new
cert.version = 2 cert.version = 2
@ -254,6 +279,8 @@ module Middleman
aki = ef.create_extension('authorityKeyIdentifier', aki = ef.create_extension('authorityKeyIdentifier',
'keyid:always,issuer:always') 'keyid:always,issuer:always')
cert.add_extension(aki) cert.add_extension(aki)
cert.add_extension ef.create_extension('subjectAltName', aliases.map { |d| "DNS: #{d}" }.join(','))
cert.sign(rsa, OpenSSL::Digest::SHA1.new) cert.sign(rsa, OpenSSL::Digest::SHA1.new)
[cert, rsa] [cert, rsa]
@ -306,29 +333,6 @@ module Middleman
end end
end end
end end
# Returns the URI the preview server will run on
# @return [URI]
def uri(host=@host)
scheme = https? ? 'https' : 'http'
URI("#{scheme}://#{host}:#{@port}/")
end
# An IPv4 address on this machine which should be externally addressable.
# @return [String]
def public_ip
ip = Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }
ip ? ip.ip_address : '127.0.0.1'
end
# Returns unused TCP port
# @return [Fixnum]
def unused_tcp_port
server = TCPServer.open(0)
port = server.addr[1]
server.close
port
end
end end
class FilteredWebrickLog < ::WEBrick::Log class FilteredWebrickLog < ::WEBrick::Log

View file

@ -0,0 +1,81 @@
require 'ipaddr'
module Middleman
class PreviewServer
# Checks for input of preview server
module Checks
# This one will get all default setup
class BasicCheck; end
# This checks if the server name resolves to the bind_address
#
# If the users enters:
#
# 1. server_name: www.example.com (10.0.0.1)
# 2. bind_address: 127.0.0.01
#
# This validation will fail
class ServerNameResolvesToBindAddress < BasicCheck
private
attr_reader :resolver
public
def initialize
@resolver = DnsResolver.new
end
# Validate
#
# @param [Information] information
# The information to be validated
def validate(information)
return if resolver.ips_for(information.server_name).include? information.bind_address
information.valid = false
information.reason = format('Server name "%s" does not resolve to bind address "%s"', information.server_name, information.bind_address)
end
end
# This validation fails if the user chooses to use an ip address which is
# not available on his/her system
class InterfaceIsAvailableOnSystem < BasicCheck
# Validate
#
# @param [Information] information
# The information to be validated
def validate(information)
return if information.bind_address.blank? || information.local_network_interfaces.include?(information.bind_address.to_s) || %w(0.0.0.0 ::).any? { |b| information.bind_address == b } || IPAddr.new('127.0.0.0/8').include?(information.bind_address.to_s)
information.valid = false
information.reason = format('Bind address "%s" is not available on your system. Please use one of %s', information.bind_address, information.local_network_interfaces.map { |i| %("#{i}") }.join(', '))
end
end
# This one requires a bind address if the user entered a server name
#
# If the `bind_address` is blank this check will fail
class RequiresBindAddressIfServerNameIsGiven < BasicCheck
def validate(information)
return unless information.bind_address.blank?
information.valid = false
information.reason = format('Server name "%s" does not resolve to an ip address', information.server_name)
end
end
# This validation always fails
class DenyAnyAny < BasicCheck
# Validate
#
# @param [Information] information
# The information to be validated
def validate(information)
information.valid = false
information.reason = 'Undefined combination of options "--server-name" and "--bind-address". If you think this is wrong, please file a bug at "https://github.com/middleman/middleman"'
end
end
end
end
end

View file

@ -0,0 +1,273 @@
require 'ipaddr'
require 'active_support/core_ext/object/blank'
require 'middleman-core/preview_server/checks'
require 'middleman-core/preview_server/server_hostname'
require 'middleman-core/preview_server/server_ip_address'
module Middleman
class PreviewServer
# Basic information class to wrap common behaviour
class BasicInformation
private
attr_reader :checks, :network_interfaces_inventory
public
attr_accessor :bind_address, :server_name, :port, :reason, :valid
attr_reader :listeners, :site_addresses
# Create instance
#
# @param [String] bind_address
# The bind address of the server
#
# @param [String] server_name
# The name of the server
#
# @param [Integer] port
# The port to listen on
def initialize(opts={})
@bind_address = ServerIpAddress.new(opts[:bind_address])
@server_name = ServerHostname.new(opts[:server_name])
@port = opts[:port]
@valid = true
@site_addresses = []
@listeners = []
@checks = []
# This needs to be check for each use case. Otherwise `Webrick` will
# complain about that.
@checks << Checks::InterfaceIsAvailableOnSystem.new
end
# Is the given information valid?
def valid?
valid == true
end
# Pass "self" to validator
#
# @param [#validate] validator
# The validator
def validate_me(validator)
validator.validate self, checks
end
def resolve_me(*)
fail NoMethodError
end
# Get network information
#
# @param [#network_interfaces] inventory
# Get list of available network interfaces
def show_me_network_interfaces(inventory)
@network_interfaces_inventory = inventory
end
# Default is to get all network interfaces
def local_network_interfaces
network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:all)
end
end
# This only is used if no other parser is available
#
# The "default" behaviour is to fail because of "Checks::DenyAnyAny"
class DefaultInformation < BasicInformation
def initialize(*args)
super
# Make this fail
@checks << Checks::DenyAnyAny.new
end
def resolve_me(*); end
# Always true
def self.matches?(*)
true
end
end
# This one is used if no bind address and no server name is given
class AllInterfaces < BasicInformation
def initialize(*args)
super
after_init
end
def self.matches?(opts={})
opts[:bind_address].blank? && opts[:server_name].blank?
end
# Resolve ips
def resolve_me(resolver)
hostname = ServerHostname.new(Socket.gethostname)
hostname_ips = resolver.ips_for(hostname)
network_interface = ServerIpAddress.new(Array(local_network_interfaces).first)
resolved_name = ServerHostname.new(resolver.names_for(network_interface).first)
if includes_array? local_network_interfaces, hostname_ips
@server_name = hostname
@site_addresses << hostname
network_interface = ServerIpAddress.new((local_network_interfaces & hostname_ips).first)
elsif RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
@server_name = hostname
@site_addresses << hostname
elsif !resolved_name.blank?
@server_name = resolved_name
@site_addresses << resolved_name
else
@server_name = network_interface
end
@site_addresses << network_interface
self
end
private
def includes_array?(a, b)
!(a & b).empty?
end
def after_init
@listeners << ServerIpAddress.new('::')
@listeners << ServerIpAddress.new('0.0.0.0')
end
end
# This is used if bind address is 0.0.0.0, the server name needs to be
# blank
class AllIpv4Interfaces < AllInterfaces
def self.matches?(opts={})
opts[:bind_address] == '0.0.0.0' && opts[:server_name].blank?
end
# Use only ipv4 interfaces
def local_network_interfaces
network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:ipv4)
end
private
def after_init
@listeners << ServerIpAddress.new('0.0.0.0')
end
end
# This is used if bind address is ::, the server name needs to be blank
class AllIpv6Interfaces < AllInterfaces
def self.matches?(opts={})
opts[:bind_address] == '::' && opts[:server_name].blank?
end
# Use only ipv6 interfaces
def local_network_interfaces
network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:ipv6)
end
private
def after_init
@listeners << ServerIpAddress.new('::')
end
end
# Used if a bind address is given and the server name is blank
class BindAddressInformation < BasicInformation
def initialize(*args)
super
@listeners << bind_address
@site_addresses << bind_address
end
def self.matches?(opts={})
!opts[:bind_address].blank? && opts[:server_name].blank?
end
# Resolv
def resolve_me(resolver)
@server_name = ServerHostname.new(resolver.names_for(bind_address).first)
@site_addresses << @server_name unless @server_name.blank?
self
end
end
# Use if server name is given and bind address is blank
class ServerNameInformation < BasicInformation
def initialize(*args)
super
@checks << Checks::RequiresBindAddressIfServerNameIsGiven.new
@site_addresses << server_name
end
def resolve_me(resolver)
@bind_address = ServerIpAddress.new(resolver.ips_for(server_name).first)
unless bind_address.blank?
@listeners << bind_address
@site_addresses << bind_address
end
self
end
def self.matches?(opts={})
opts[:bind_address].blank? && !opts[:server_name].blank?
end
end
# Only used if bind address and server name are given and bind address is
# not :: or 0.0.0.0
class BindAddressAndServerNameInformation < BasicInformation
def initialize(*args)
super
@listeners << bind_address
@site_addresses << server_name
@site_addresses << bind_address
@checks << Checks::ServerNameResolvesToBindAddress.new
end
def self.matches?(opts={})
!opts[:bind_address].blank? && !opts[:server_name].blank? && !%w(:: 0.0.0.0).include?(opts[:bind_address])
end
def resolve_me(*); end
end
# If the server name is either an ipv4 or ipv6 address, e.g. 127.0.0.1 or
# ::1, use this one
class ServerNameIsIpInformation < BasicInformation
def initialize(opts={})
super
ip = ServerIpAddress.new(server_name.to_s)
@listeners << ip
@site_addresses << ip
end
def resolve_me(*); end
def self.matches?(opts={})
ip = IPAddr.new(opts[:server_name])
ip.ipv4? || ip.ipv6?
rescue
false
end
end
end
end

View file

@ -0,0 +1,65 @@
require 'middleman-core/preview_server/server_ip_address'
module Middleman
class PreviewServer
# This holds information about local network interfaces on the user systemd
class NetworkInterfaceInventory
# Return all ip interfaces
class All
def network_interfaces
ipv4_addresses = Socket.ip_address_list.select(&:ipv4?).map { |ai| ServerIpv4Address.new(ai.ip_address) }
ipv6_addresses = Socket.ip_address_list.select(&:ipv6?).map { |ai| ServerIpv6Address.new(ai.ip_address) }
ipv4_addresses + ipv6_addresses
end
def self.match?(*)
true
end
end
# Return all ipv4 interfaces
class Ipv4
def network_interfaces
Socket.ip_address_list.select { |ai| ai.ipv4? && !ai.ipv4_loopback? }.map { |ai| ServerIpv4Address.new(ai.ip_address) }
end
def self.match?(type)
:ipv4 == type
end
end
# Return all ipv6 interfaces
class Ipv6
def network_interfaces
Socket.ip_address_list.select { |ai| ai.ipv6? && !ai.ipv6_loopback? }.map { |ai| ServerIpv6Address.new(ai.ip_address) }
end
def self.match?(type)
:ipv6 == type
end
end
private
attr_reader :types
public
def initialize
@types = []
@types << Ipv4
@types << Ipv6
@types << All
end
# Return ip interfaces
#
# @param [Symbol] type
# The type of interface which should be returned
def network_interfaces(type=:all)
types.find { |t| t.match? type.to_sym }.new.network_interfaces
end
end
end
end

View file

@ -0,0 +1,39 @@
module Middleman
class PreviewServer
class ServerHostname
class ServerFullHostname < SimpleDelegator
def to_s
__getobj__
end
def self.match?(*)
true
end
alias_method :to_browser, :to_s
end
class ServerPlainHostname < SimpleDelegator
def to_s
__getobj__ + '.local'
end
def self.match?(name)
# rubocop:disable Style/CaseEquality
name != 'localhost' && /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?\.?$/ === name
# rubocop:enable Style/CaseEquality
end
alias_method :to_browser, :to_s
end
def self.new(string)
@names = []
@names << ServerPlainHostname
@names << ServerFullHostname
@names.find { |n| n.match? string }.new(string)
end
end
end
end

View file

@ -0,0 +1,144 @@
require 'middleman-core/dns_resolver'
require 'middleman-core/preview_server/information'
require 'middleman-core/preview_server/network_interface_inventory'
require 'middleman-core/preview_server/tcp_port_prober'
require 'middleman-core/preview_server/server_information_validator'
module Middleman
class PreviewServer
# This class holds all information which the preview server needs to setup a listener
#
# * server name
# * bind address
# * port
#
# Furthermore it probes for a free tcp port, if the default one 4567 is not available.
class ServerInformation
private
attr_reader :resolver, :validator, :network_interface_inventory, :informations, :tcp_port_prober
public
def initialize(opts={})
@resolver = opts.fetch(:resolver, DnsResolver.new)
@validator = opts.fetch(:validator, ServerInformationValidator.new)
@network_interface_inventory = opts.fetch(:network_interface_inventory, NetworkInterfaceInventory.new)
@tcp_port_prober = opts.fetch(:tcp_port_prober, TcpPortProber.new)
@informations = []
@informations << AllInterfaces
@informations << AllIpv4Interfaces
@informations << AllIpv6Interfaces
@informations << ServerNameIsIpInformation
@informations << ServerNameInformation
@informations << BindAddressInformation
@informations << BindAddressAndServerNameInformation
@informations << DefaultInformation
end
# The information
#
# Is cached
def information
return @information if @information
# The `DefaultInformation`-class always returns `true`, so there's
# always a klass available and find will never return nil
listener_klass = informations.find { |l| l.matches? bind_address: @bind_address, server_name: @server_name }
@information = listener_klass.new(bind_address: @bind_address, server_name: @server_name)
@information.show_me_network_interfaces(network_interface_inventory)
@information.resolve_me(resolver)
@information.port = tcp_port_prober.port(@port)
@information.validate_me(validator)
@information
end
# Use a middleman configuration to get information
#
# @param [#[]] config
# The middleman config
def use(config)
@bind_address = config[:bind_address]
@port = config[:port]
@server_name = config[:server_name]
config[:bind_address] = bind_address
config[:port] = port
config[:server_name] = server_name
end
# Make information of internal server class avaible to make debugging
# easier. This can be used to log the class which was used to determine
# the preview server settings
#
# @return [String]
# The name of the class
def handler
information.class.to_s
end
# Is the server information valid?
#
# This is used to output a helpful error message, which can be stored in
# `#reason`.
#
# @return [TrueClass, FalseClass]
# The result
def valid?
information.valid?
end
# The reason why the information is NOT valid
#
# @return [String]
# The reason why the information is not valid
def reason
information.reason
end
# The server name
#
# @return [String]
# The name of the server
def server_name
information.server_name
end
# The bind address of server
#
# @return [String]
# The bind address of the server
def bind_address
information.bind_address
end
# The port on which the server should listen
#
# @return [Integer]
# The port number
def port
information.port
end
# A list of site addresses
#
# @return [Array]
# A list of addresses which can be used to access the middleman preview
# server
def site_addresses
information.site_addresses
end
# A list of listeners
#
# @return [Array]
# A list of bind address where the
def listeners
information.listeners
end
end
end
end

View file

@ -0,0 +1,18 @@
module Middleman
class PreviewServer
# Validate user input
class ServerInformationValidator
# Validate the input
#
# @param [ServerInformation] information
# The information instance which holds information about the preview
# server settings
#
# @param [Array] checks
# A list of checks which should be evaluated
def validate(information, checks)
checks.each { |c| c.validate information }
end
end
end
end

View file

@ -0,0 +1,55 @@
require 'ipaddr'
require 'forwardable'
module Middleman
class PreviewServer
class ServerIpAddress
def self.new(ip_address)
@parser = []
@parser << ServerIpv6Address
@parser << ServerIpv4Address
@parser.find { |p| p.match? ip_address }.new(ip_address)
end
end
class BasicServerIpAddress < SimpleDelegator
end
class ServerIpv4Address < BasicServerIpAddress
def to_browser
__getobj__.to_s
end
def self.match?(*)
true
end
end
class ServerIpv6Address < BasicServerIpAddress
def to_s
__getobj__.sub(/%.*$/, '')
end
def to_browser
format('[%s]', to_s)
end
if RUBY_VERSION < '2'
def self.match?(str)
str = str.to_s.sub(/%.*$/, '')
IPAddr.new(str).ipv6?
rescue StandardError
false
end
else
def self.match?(str)
str = str.to_s.sub(/%.*$/, '')
IPAddr.new(str).ipv6?
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
false
end
end
end
end
end

View file

@ -0,0 +1,50 @@
require 'ipaddr'
module Middleman
class PreviewServer
# This builds the server urls for the preview server
class ServerUrl
private
attr_reader :hosts, :port, :https
public
def initialize(opts={})
@hosts = opts.fetch(:hosts)
@port = opts.fetch(:port)
@https = opts.fetch(:https, false)
end
# Return bind addresses
#
# @return [Array]
# List of bind addresses of format host:port
def to_bind_addresses
hosts.map { |l| format('"%s:%s"', l.to_s, port) }
end
# Return server urls
#
# @return [Array]
# List of urls of format http://host:port
def to_urls
hosts.map { |l| format('"%s://%s:%s"', https? ? 'https' : 'http', l.to_browser, port) }
end
# Return server config urls
#
# @return [Array]
# List of urls of format http://host:port/__middleman
def to_config_urls
hosts.map { |l| format('"%s://%s:%s/__middleman"', https? ? 'https' : 'http', l.to_browser, port) }
end
private
def https?
https == true
end
end
end
end

View file

@ -0,0 +1,29 @@
module Middleman
class PreviewServer
# Probe for tcp ports
#
# This one first tries `try_port` if this is not available use the free
# port returned by TCPServer.
class TcpPortProber
# Check for port
#
# @param [Integer] try_port
# The port to be checked
#
# @return [Integer]
# The port
def port(try_port)
server = TCPServer.open(try_port)
server.close
try_port
rescue
server = TCPServer.open(0)
port = server.addr[1]
server.close
port
end
end
end
end

View file

@ -3,6 +3,7 @@ require 'aruba/jruby'
require 'middleman-core/step_definitions/middleman_steps' require 'middleman-core/step_definitions/middleman_steps'
require 'middleman-core/step_definitions/builder_steps' require 'middleman-core/step_definitions/builder_steps'
require 'middleman-core/step_definitions/server_steps' require 'middleman-core/step_definitions/server_steps'
require 'middleman-core/step_definitions/commandline_steps'
# Monkeypatch for windows support # Monkeypatch for windows support
module ArubaMonkeypatch module ArubaMonkeypatch

View file

@ -0,0 +1,88 @@
When /^I stop (?:middleman|all commands) if the output( of the last command)? contains:$/ do |last_command, expected|
begin
Timeout.timeout(exit_timeout) do
loop do
fail "You need to start middleman interactively first." unless @interactive
if unescape(@interactive.output) =~ Regexp.new(unescape(expected))
only_processes.each { |p| p.terminate }
break
end
sleep 0.1
end
end
rescue ChildProcess::TimeoutError, TimeoutError
@interactive.terminate
ensure
announcer.stdout @interactive.stdout
announcer.stderr @interactive.stderr
end
end
# Make it just a long running process
Given /`(.*?)` is running in background/ do |cmd|
run(cmd, 120)
end
Given /I have a local hosts file with:/ do |string|
step 'I set the environment variables to:', table(
%(
| variable | value |
| MM_HOSTSRC | .hosts |
)
)
step 'a file named ".hosts" with:', string
end
Given /I start a dns server with:/ do |string|
@dns_server.terminate if defined? @dns_server
port = 5300
db_file = 'dns.db'
step 'I set the environment variables to:', table(
%(
| variable | value |
| MM_DNSRC | 127.0.0.1:#{port}|
)
)
set_env 'PATH', File.expand_path(File.join(current_dir, 'bin')) + ':' + ENV['PATH']
write_file db_file, string
@dns_server = run("dns_server.rb #{db_file} #{port}", 120)
end
Given /I start a mdns server with:/ do |string|
@mdns_server.terminate if defined? @mdns_server
port = 5301
db_file = 'mdns.db'
step 'I set the environment variables to:', table(
%(
| variable | value |
| MM_MDNSRC | 127.0.0.1:#{port}|
)
)
set_env 'PATH', File.expand_path(File.join(current_dir, 'bin')) + ':' + ENV['PATH']
write_file db_file, string
@mdns_server = run("dns_server.rb #{db_file} #{port}", 120)
end
Given /I start a mdns server for the local hostname/ do
step %(I start a mdns server with:), "#{Socket.gethostname}: 127.0.0.1"
end
# Make sure each and every process is really dead
After do
only_processes.each { |p| p.terminate }
end
Before '@ruby-2.1' do
skip_this_scenario if RUBY_VERSION < '2.1'
end

View file

@ -0,0 +1,118 @@
require 'spec_helper'
require 'middleman-core/dns_resolver'
RSpec.describe Middleman::DnsResolver do
subject(:resolver) do
described_class.new(
hosts_resolver: hosts_resolver,
local_link_resolver: local_link_resolver,
network_resolver: network_resolver
)
end
let(:hosts_resolver) { instance_double('Middleman::DnsResolver::HostsResolver') }
let(:local_link_resolver) { instance_double('Middleman::DnsResolver::LocalLinkResolver') }
let(:network_resolver) { instance_double('Middleman::DnsResolver::NetworkResolver') }
before :each do
allow(network_resolver).to receive(:timeouts=)
end
describe '#names_for' do
context 'when hosts resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getnames).with(unresolved_ip).and_return(resolved_names)
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).not_to receive(:getnames)
end
expect(network_resolver).not_to receive(:getnames)
end
let(:unresolved_ip) { '127.0.0.1' }
let(:resolved_names) { %w(localhost) }
it { expect(resolver.names_for(unresolved_ip)).to eq resolved_names }
end
context 'when local link resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getnames).with(unresolved_ip).and_return([])
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).to receive(:getnames).with(unresolved_ip).and_return(resolved_names)
expect(network_resolver).not_to receive(:getnames)
else
expect(network_resolver).to receive(:getnames).with(unresolved_ip).and_return(resolved_names)
end
end
let(:unresolved_ip) { '127.0.0.1' }
let(:resolved_names) { %w(localhost) }
it { expect(resolver.names_for(unresolved_ip)).to eq resolved_names }
end
context 'when network resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getnames).with(unresolved_ip).and_return([])
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).to receive(:getnames).with(unresolved_ip).and_return([])
end
expect(network_resolver).to receive(:getnames).with(unresolved_ip).and_return(resolved_names)
end
let(:unresolved_ip) { '127.0.0.1' }
let(:resolved_names) { %w(localhost) }
it { expect(resolver.names_for(unresolved_ip)).to eq resolved_names }
end
end
describe '#ips_for' do
context 'when hosts resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getaddresses).with(unresolved_ips).and_return(resolved_name)
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).not_to receive(:getaddresses)
end
expect(network_resolver).not_to receive(:getaddresses)
end
let(:unresolved_ips) { '127.0.0.1' }
let(:resolved_name) { %w(localhost) }
it { expect(resolver.ips_for(unresolved_ips)).to eq resolved_name }
end
context 'when local link resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getaddresses).with(unresolved_ips).and_return([])
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).to receive(:getaddresses).with(unresolved_ips).and_return(resolved_name)
expect(network_resolver).not_to receive(:getaddresses)
else
expect(network_resolver).to receive(:getaddresses).with(unresolved_ips).and_return(resolved_name)
end
end
let(:unresolved_ips) { '127.0.0.1' }
let(:resolved_name) { %w(localhost) }
it { expect(resolver.ips_for(unresolved_ips)).to eq resolved_name }
end
context 'when network resolver can resolve name' do
before :each do
expect(hosts_resolver).to receive(:getaddresses).with(unresolved_ips).and_return([])
if RUBY_VERSION >= '2.1'
expect(local_link_resolver).to receive(:getaddresses).with(unresolved_ips).and_return([])
end
expect(network_resolver).to receive(:getaddresses).with(unresolved_ips).and_return(resolved_name)
end
let(:unresolved_ips) { '127.0.0.1' }
let(:resolved_name) { %w(localhost) }
it { expect(resolver.ips_for(unresolved_ips)).to eq resolved_name }
end
end
end

View file

@ -0,0 +1,39 @@
require 'spec_helper'
require 'middleman-core/preview_server/server_hostname'
RSpec.describe Middleman::PreviewServer::ServerHostname do
subject(:hostname) { described_class.new(string) }
let(:string) { 'www.example.com' }
describe '#to_s' do
context 'when hostname' do
it { expect(hostname.to_s).to eq string }
end
context 'when ipv4' do
let(:string) { '127.0.0.1' }
it { expect(hostname.to_s).to eq string }
end
context 'when ipv6' do
let(:string) { '2607:f700:8000:12e:b3d9:1cba:b52:aa1b' }
it { expect(hostname.to_s).to eq string }
end
end
describe '#to_browser' do
context 'when hostname' do
it { expect(hostname.to_browser).to eq string }
end
context 'when ipv4' do
let(:string) { '127.0.0.1' }
it { expect(hostname.to_browser).to eq string }
end
context 'when ipv6' do
let(:string) { '::1' }
it { expect(hostname.to_browser).to eq string }
end
end
end

View file

@ -0,0 +1,43 @@
require 'spec_helper'
require 'middleman-core/preview_server/server_ip_address'
RSpec.describe Middleman::PreviewServer::ServerIpAddress do
subject(:ip_address) { described_class.new(string) }
let(:string) { '127.0.0.1' }
describe '#to_s' do
context 'when ipv4' do
let(:string) { '127.0.0.1' }
it { expect(ip_address.to_s).to eq string }
end
context 'when ipv6' do
context 'without suffix' do
let(:string) { '2607:f700:8000:12e:b3d9:1cba:b52:aa1b' }
it { expect(ip_address.to_s).to eq string }
end
context 'with suffix' do
let(:string) { '2607:f700:8000:12e:b3d9:1cba:b52:aa1b%wlp1s0' }
let(:result) { '2607:f700:8000:12e:b3d9:1cba:b52:aa1b' }
it { expect(ip_address.to_s).to eq result }
end
end
end
describe '#to_browser' do
context 'when ip_address' do
it { expect(ip_address.to_browser).to eq string }
end
context 'when ipv4' do
let(:string) { '127.0.0.1' }
it { expect(ip_address.to_browser).to eq string }
end
context 'when ipv6' do
let(:string) { '2607:f700:8000:12e:b3d9:1cba:b52:aa1b' }
it { expect(ip_address.to_browser).to eq "[#{string}]" }
end
end
end

View file

@ -3,3 +3,29 @@ SimpleCov.root(File.expand_path(File.dirname(__FILE__) + '/..'))
require 'coveralls' require 'coveralls'
Coveralls.wear! Coveralls.wear!
require 'aruba/api'
RSpec.configure do |config|
config.include Aruba::Api
end
# encoding: utf-8
RSpec.configure do |config|
config.filter_run :focus
config.run_all_when_everything_filtered = true
config.default_formatter = 'doc' if config.files_to_run.one?
# config.profile_examples = 10
config.order = :random
Kernel.srand config.seed
config.expect_with :rspec do |expectations|
expectations.syntax = :expect
end
config.mock_with :rspec do |mocks|
mocks.syntax = :expect
mocks.verify_partial_doubles = true
end
end