From 232aca91bced48f77efa31b0a0ddf696a17d4cfd Mon Sep 17 00:00:00 2001 From: Max Meyer Date: Thu, 25 Jun 2015 22:26:46 +0200 Subject: [PATCH 1/2] Make testing a little bit easier and require features-directory to make subdirectories in cucumber work --- .rubocop.yml | 1 + Gemfile | 2 +- gem_rake_helper.rb | 2 +- middleman-core/.rspec | 1 + middleman-core/cucumber.yml | 2 ++ 5 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 middleman-core/.rspec create mode 100644 middleman-core/cucumber.yml diff --git a/.rubocop.yml b/.rubocop.yml index 2a518da3..68908de8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,7 @@ AllCops: - 'middleman-core/fixtures/**/*' - 'middleman-core/features/**/*' - 'middleman-core/spec/**/*' + DisplayCopNames: true LineLength: Enabled: false MethodLength: diff --git a/Gemfile b/Gemfile index f34fbaec..6ed44aac 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'pry', '~> 0.10', group: :development gem 'aruba', '~> 0.6' gem 'rspec', '~> 3.0' gem 'fivemat', '~> 1.3' -gem 'cucumber', '~> 1.3' +gem 'cucumber', '~> 2.0' # Optional middleman dependencies, included for tests gem 'less', '2.3', require: false diff --git a/gem_rake_helper.rb b/gem_rake_helper.rb index 5f95c90e..a90ea7dc 100644 --- a/gem_rake_helper.rb +++ b/gem_rake_helper.rb @@ -21,7 +21,7 @@ Cucumber::Rake::Task.new do |t| exempt_tags << '--tags ~@encoding' unless Object.const_defined?(:Encoding) exempt_tags << '--tags ~@nowindows' if Gem.win_platform? 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 Cucumber::Rake::Task.new(:cucumber_wip) do |t| diff --git a/middleman-core/.rspec b/middleman-core/.rspec new file mode 100644 index 00000000..4e1e0d2f --- /dev/null +++ b/middleman-core/.rspec @@ -0,0 +1 @@ +--color diff --git a/middleman-core/cucumber.yml b/middleman-core/cucumber.yml new file mode 100644 index 00000000..a65d694e --- /dev/null +++ b/middleman-core/cucumber.yml @@ -0,0 +1,2 @@ +default: --require features --tags ~@wip +wip: --require features --tags @wip From 6aa7ce741a11db6fc25f3d10a30fb0a79095779e Mon Sep 17 00:00:00 2001 From: Max Meyer Date: Thu, 25 Jun 2015 15:39:56 +0200 Subject: [PATCH 2/2] Refactor preview server to support server_name and bind_address --- Gemfile | 3 + .../features/cli/preview_server.feature | 532 ++++++++++++++++++ .../preview-server-app/bin/dns_server.rb | 33 ++ .../config-complications.rb | 11 + .../preview-server-app/config-empty.rb | 0 .../fixtures/preview-server-app/config.rb | 11 + .../preview-server-app/source/index.html.erb | 1 + .../preview-server-app/source/layout.erb | 9 + .../source/layouts/custom.erb | 8 + .../preview-server-app/source/real.html | 1 + .../source/real/index.html.erb | 5 + .../source/should_be_ignored.html | 1 + .../source/should_be_ignored2.html | 1 + .../source/should_be_ignored3.html | 1 + .../preview-server-app/source/static.html | 1 + .../lib/middleman-core/application.rb | 6 +- .../lib/middleman-core/cli/server.rb | 12 +- .../lib/middleman-core/dns_resolver.rb | 73 +++ .../dns_resolver/basic_network_resolver.rb | 52 ++ .../dns_resolver/hosts_resolver.rb | 63 +++ .../dns_resolver/local_link_resolver.rb | 44 ++ .../dns_resolver/network_resolver.rb | 42 ++ .../lib/middleman-core/preview_server.rb | 94 ++-- .../middleman-core/preview_server/checks.rb | 81 +++ .../preview_server/information.rb | 273 +++++++++ .../network_interface_inventory.rb | 65 +++ .../preview_server/server_hostname.rb | 39 ++ .../preview_server/server_information.rb | 144 +++++ .../server_information_validator.rb | 18 + .../preview_server/server_ip_address.rb | 55 ++ .../preview_server/server_url.rb | 50 ++ .../preview_server/tcp_port_prober.rb | 29 + .../lib/middleman-core/step_definitions.rb | 1 + .../step_definitions/commandline_steps.rb | 88 +++ .../spec/middleman-core/dns_resolver_spec.rb | 118 ++++ .../preview_server/server_hostname_spec.rb | 39 ++ .../preview_server/server_ip_address_spec.rb | 43 ++ middleman-core/spec/spec_helper.rb | 26 + 38 files changed, 2023 insertions(+), 50 deletions(-) create mode 100644 middleman-core/features/cli/preview_server.feature create mode 100755 middleman-core/fixtures/preview-server-app/bin/dns_server.rb create mode 100644 middleman-core/fixtures/preview-server-app/config-complications.rb create mode 100644 middleman-core/fixtures/preview-server-app/config-empty.rb create mode 100644 middleman-core/fixtures/preview-server-app/config.rb create mode 100755 middleman-core/fixtures/preview-server-app/source/index.html.erb create mode 100644 middleman-core/fixtures/preview-server-app/source/layout.erb create mode 100755 middleman-core/fixtures/preview-server-app/source/layouts/custom.erb create mode 100644 middleman-core/fixtures/preview-server-app/source/real.html create mode 100644 middleman-core/fixtures/preview-server-app/source/real/index.html.erb create mode 100644 middleman-core/fixtures/preview-server-app/source/should_be_ignored.html create mode 100644 middleman-core/fixtures/preview-server-app/source/should_be_ignored2.html create mode 100644 middleman-core/fixtures/preview-server-app/source/should_be_ignored3.html create mode 100755 middleman-core/fixtures/preview-server-app/source/static.html create mode 100644 middleman-core/lib/middleman-core/dns_resolver.rb create mode 100644 middleman-core/lib/middleman-core/dns_resolver/basic_network_resolver.rb create mode 100644 middleman-core/lib/middleman-core/dns_resolver/hosts_resolver.rb create mode 100644 middleman-core/lib/middleman-core/dns_resolver/local_link_resolver.rb create mode 100644 middleman-core/lib/middleman-core/dns_resolver/network_resolver.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/checks.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/information.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/network_interface_inventory.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/server_hostname.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/server_information.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/server_information_validator.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/server_ip_address.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/server_url.rb create mode 100644 middleman-core/lib/middleman-core/preview_server/tcp_port_prober.rb create mode 100644 middleman-core/lib/middleman-core/step_definitions/commandline_steps.rb create mode 100644 middleman-core/spec/middleman-core/dns_resolver_spec.rb create mode 100644 middleman-core/spec/middleman-core/preview_server/server_hostname_spec.rb create mode 100644 middleman-core/spec/middleman-core/preview_server/server_ip_address_spec.rb diff --git a/Gemfile b/Gemfile index 6ed44aac..37122c0a 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,9 @@ gem 'sinatra', '>= 1.4', require: false gem 'redcarpet', '>= 3.1', require: false unless RUBY_ENGINE == 'jruby' gem 'asciidoctor', '~> 0.1', require: false +# Dns server to test preview server +gem 'rubydns', '~> 1.0.1', require: false + # To test javascript gem 'poltergeist', '~> 1.6.0', require: false diff --git a/middleman-core/features/cli/preview_server.feature b/middleman-core/features/cli/preview_server.feature new file mode 100644 index 00000000..f82a0a1f --- /dev/null +++ b/middleman-core/features/cli/preview_server.feature @@ -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: + """ + # + 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: + """ + # + 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: + """ + # + ::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: + """ + # + 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: + """ + # + 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: + """ + # + 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" + """ diff --git a/middleman-core/fixtures/preview-server-app/bin/dns_server.rb b/middleman-core/fixtures/preview-server-app/bin/dns_server.rb new file mode 100755 index 00000000..78726f59 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/bin/dns_server.rb @@ -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 diff --git a/middleman-core/fixtures/preview-server-app/config-complications.rb b/middleman-core/fixtures/preview-server-app/config-complications.rb new file mode 100644 index 00000000..14d6c328 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/config-complications.rb @@ -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 diff --git a/middleman-core/fixtures/preview-server-app/config-empty.rb b/middleman-core/fixtures/preview-server-app/config-empty.rb new file mode 100644 index 00000000..e69de29b diff --git a/middleman-core/fixtures/preview-server-app/config.rb b/middleman-core/fixtures/preview-server-app/config.rb new file mode 100644 index 00000000..14d6c328 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/config.rb @@ -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 diff --git a/middleman-core/fixtures/preview-server-app/source/index.html.erb b/middleman-core/fixtures/preview-server-app/source/index.html.erb new file mode 100755 index 00000000..f47aba51 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/index.html.erb @@ -0,0 +1 @@ +

Welcome

\ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/layout.erb b/middleman-core/fixtures/preview-server-app/source/layout.erb new file mode 100644 index 00000000..b72f66e9 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/layout.erb @@ -0,0 +1,9 @@ + + + My Sample Site + + + + <%= yield %> + + \ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/layouts/custom.erb b/middleman-core/fixtures/preview-server-app/source/layouts/custom.erb new file mode 100755 index 00000000..c8a83373 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/layouts/custom.erb @@ -0,0 +1,8 @@ + + + Custom Layout + + + <%= yield %> + + \ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/real.html b/middleman-core/fixtures/preview-server-app/source/real.html new file mode 100644 index 00000000..cb312952 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/real.html @@ -0,0 +1 @@ +I am real \ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/real/index.html.erb b/middleman-core/fixtures/preview-server-app/source/real/index.html.erb new file mode 100644 index 00000000..190d84ec --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/real/index.html.erb @@ -0,0 +1,5 @@ +--- +layout: false +--- + +I am real: <%= @num %> \ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/should_be_ignored.html b/middleman-core/fixtures/preview-server-app/source/should_be_ignored.html new file mode 100644 index 00000000..fb81d5c0 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/should_be_ignored.html @@ -0,0 +1 @@ +

Ignore me!

\ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/should_be_ignored2.html b/middleman-core/fixtures/preview-server-app/source/should_be_ignored2.html new file mode 100644 index 00000000..0940fd7c --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/should_be_ignored2.html @@ -0,0 +1 @@ +

Ignore me! 2

\ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/should_be_ignored3.html b/middleman-core/fixtures/preview-server-app/source/should_be_ignored3.html new file mode 100644 index 00000000..98007c81 --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/should_be_ignored3.html @@ -0,0 +1 @@ +

Ignore me! 3

\ No newline at end of file diff --git a/middleman-core/fixtures/preview-server-app/source/static.html b/middleman-core/fixtures/preview-server-app/source/static.html new file mode 100755 index 00000000..7e50df4e --- /dev/null +++ b/middleman-core/fixtures/preview-server-app/source/static.html @@ -0,0 +1 @@ +Static, no code! \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index f509e943..72c17ee0 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -75,7 +75,11 @@ module Middleman # Which server name should be used # @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. # @return [Boolean] diff --git a/middleman-core/lib/middleman-core/cli/server.rb b/middleman-core/lib/middleman-core/cli/server.rb index 47b61b11..8c7d0d06 100644 --- a/middleman-core/lib/middleman-core/cli/server.rb +++ b/middleman-core/lib/middleman-core/cli/server.rb @@ -14,9 +14,12 @@ module Middleman::Cli method_option :port, aliases: '-p', desc: 'The port Middleman will listen on' - method_option :host, - aliases: '-h', - desc: 'The host name Middleman will use' + method_option :server_name, + aliases: '-s', + 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, type: :boolean, desc: 'Serve the preview server over SSL/TLS' @@ -68,8 +71,9 @@ module Middleman::Cli params = { port: options['port'], + bind_address: options['bind_address'], https: options['https'], - host: options['host'], + server_name: options['server_name'], ssl_certificate: options['ssl_certificate'], ssl_private_key: options['ssl_private_key'], environment: options['environment'], diff --git a/middleman-core/lib/middleman-core/dns_resolver.rb b/middleman-core/lib/middleman-core/dns_resolver.rb new file mode 100644 index 00000000..ab49b11f --- /dev/null +++ b/middleman-core/lib/middleman-core/dns_resolver.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/dns_resolver/basic_network_resolver.rb b/middleman-core/lib/middleman-core/dns_resolver/basic_network_resolver.rb new file mode 100644 index 00000000..11dc3096 --- /dev/null +++ b/middleman-core/lib/middleman-core/dns_resolver/basic_network_resolver.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/dns_resolver/hosts_resolver.rb b/middleman-core/lib/middleman-core/dns_resolver/hosts_resolver.rb new file mode 100644 index 00000000..ba3ff997 --- /dev/null +++ b/middleman-core/lib/middleman-core/dns_resolver/hosts_resolver.rb @@ -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 + # # + # 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 diff --git a/middleman-core/lib/middleman-core/dns_resolver/local_link_resolver.rb b/middleman-core/lib/middleman-core/dns_resolver/local_link_resolver.rb new file mode 100644 index 00000000..e083d8f3 --- /dev/null +++ b/middleman-core/lib/middleman-core/dns_resolver/local_link_resolver.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/dns_resolver/network_resolver.rb b/middleman-core/lib/middleman-core/dns_resolver/network_resolver.rb new file mode 100644 index 00000000..c9db4265 --- /dev/null +++ b/middleman-core/lib/middleman-core/dns_resolver/network_resolver.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server.rb b/middleman-core/lib/middleman-core/preview_server.rb index 1e81b4e6..9076b2fa 100644 --- a/middleman-core/lib/middleman-core/preview_server.rb +++ b/middleman-core/lib/middleman-core/preview_server.rb @@ -1,15 +1,16 @@ require 'webrick' require 'webrick/https' require 'openssl' -require 'socket' require 'middleman-core/meta_pages' require 'middleman-core/logger' +require 'middleman-core/preview_server/server_information' +require 'middleman-core/preview_server/server_url' # rubocop:disable GlobalVars module Middleman - module PreviewServer + class PreviewServer 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 def https? @@ -19,13 +20,31 @@ module Middleman # Start an instance of Middleman::Application # @return [void] def start(opts={}) + # Do not buffer output, otherwise testing of output does not work + $stdout.sync = true + $stderr.sync = true + @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.info "== The Middleman is standing watch at #{uri} (#{uri(public_ip)})" - logger.info "== Inspect your site configuration at #{uri + '__middleman'}" + 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 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 return if @initialized @@ -116,18 +135,26 @@ module Middleman opts[:instrumenting] || false ) - config[:environment] = opts[:environment].to_sym if opts[:environment] - config[:port] = opts[:port] if opts[:port] - config[:host] = opts[:host].presence || Socket.gethostname.tr(' ', '+') - config[:https] = opts[:https] unless opts[:https].nil? + config[:environment] = opts[:environment].to_sym if opts[:environment] + config[:port] = opts[:port] if opts[:port] + config[:bind_address] = opts[:bind_address] + config[:server_name] = opts[:server_name] + config[:https] = opts[:https] unless opts[:https].nil? config[:ssl_certificate] = opts[:ssl_certificate] if opts[:ssl_certificate] config[:ssl_private_key] = opts[:ssl_private_key] if opts[:ssl_private_key] end - @host = @app.config[:host] - @port = @app.config[:port] - @https = @app.config[:https] - @environment = @app.config[:environment] + # store configured port to make a check later on possible + 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] + @environment = @app.config[:environment] @ssl_certificate = @app.config[:ssl_certificate] @ssl_private_key = @app.config[:ssl_private_key] @@ -190,9 +217,10 @@ module Middleman # @return [void] def setup_webrick(is_logging) http_opts = { - Port: port, + Port: server_information.port, AccessLog: [], - ServerName: host, + ServerName: server_information.server_name, + BindAddress: server_information.bind_address.to_s, DoNotReverseLookup: true } @@ -205,7 +233,7 @@ module Middleman http_opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new File.read ssl_private_key else # 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[:SSLPrivateKey] = key end @@ -217,20 +245,17 @@ module Middleman http_opts[:Logger] = ::WEBrick::Log.new(nil, 0) end - attempts_left = 4 - tried_ports = [] begin ::WEBrick::HTTPServer.new(http_opts) 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}" - exit(1) + logger.error %(== Port "#{http_opts[:Port]}" is in use. This should not have happened. Please start "middleman server" again.) end end # 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 # 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) cert = OpenSSL::X509::Certificate.new cert.version = 2 @@ -254,6 +279,8 @@ module Middleman aki = ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always') 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, rsa] @@ -306,29 +333,6 @@ module Middleman 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 class FilteredWebrickLog < ::WEBrick::Log diff --git a/middleman-core/lib/middleman-core/preview_server/checks.rb b/middleman-core/lib/middleman-core/preview_server/checks.rb new file mode 100644 index 00000000..e4302acf --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/checks.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/information.rb b/middleman-core/lib/middleman-core/preview_server/information.rb new file mode 100644 index 00000000..4518cfb2 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/information.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/network_interface_inventory.rb b/middleman-core/lib/middleman-core/preview_server/network_interface_inventory.rb new file mode 100644 index 00000000..d9ba1229 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/network_interface_inventory.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/server_hostname.rb b/middleman-core/lib/middleman-core/preview_server/server_hostname.rb new file mode 100644 index 00000000..9df133cb --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/server_hostname.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/server_information.rb b/middleman-core/lib/middleman-core/preview_server/server_information.rb new file mode 100644 index 00000000..937efdd5 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/server_information.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/server_information_validator.rb b/middleman-core/lib/middleman-core/preview_server/server_information_validator.rb new file mode 100644 index 00000000..ff2eeb2e --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/server_information_validator.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/server_ip_address.rb b/middleman-core/lib/middleman-core/preview_server/server_ip_address.rb new file mode 100644 index 00000000..55a83db5 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/server_ip_address.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/server_url.rb b/middleman-core/lib/middleman-core/preview_server/server_url.rb new file mode 100644 index 00000000..9b7a5b29 --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/server_url.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/preview_server/tcp_port_prober.rb b/middleman-core/lib/middleman-core/preview_server/tcp_port_prober.rb new file mode 100644 index 00000000..c480f91b --- /dev/null +++ b/middleman-core/lib/middleman-core/preview_server/tcp_port_prober.rb @@ -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 diff --git a/middleman-core/lib/middleman-core/step_definitions.rb b/middleman-core/lib/middleman-core/step_definitions.rb index 95245f14..25c4790a 100644 --- a/middleman-core/lib/middleman-core/step_definitions.rb +++ b/middleman-core/lib/middleman-core/step_definitions.rb @@ -3,6 +3,7 @@ require 'aruba/jruby' require 'middleman-core/step_definitions/middleman_steps' require 'middleman-core/step_definitions/builder_steps' require 'middleman-core/step_definitions/server_steps' +require 'middleman-core/step_definitions/commandline_steps' # Monkeypatch for windows support module ArubaMonkeypatch diff --git a/middleman-core/lib/middleman-core/step_definitions/commandline_steps.rb b/middleman-core/lib/middleman-core/step_definitions/commandline_steps.rb new file mode 100644 index 00000000..5a663c32 --- /dev/null +++ b/middleman-core/lib/middleman-core/step_definitions/commandline_steps.rb @@ -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 diff --git a/middleman-core/spec/middleman-core/dns_resolver_spec.rb b/middleman-core/spec/middleman-core/dns_resolver_spec.rb new file mode 100644 index 00000000..8e5b2251 --- /dev/null +++ b/middleman-core/spec/middleman-core/dns_resolver_spec.rb @@ -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 diff --git a/middleman-core/spec/middleman-core/preview_server/server_hostname_spec.rb b/middleman-core/spec/middleman-core/preview_server/server_hostname_spec.rb new file mode 100644 index 00000000..b0dc6a07 --- /dev/null +++ b/middleman-core/spec/middleman-core/preview_server/server_hostname_spec.rb @@ -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 diff --git a/middleman-core/spec/middleman-core/preview_server/server_ip_address_spec.rb b/middleman-core/spec/middleman-core/preview_server/server_ip_address_spec.rb new file mode 100644 index 00000000..a82fec63 --- /dev/null +++ b/middleman-core/spec/middleman-core/preview_server/server_ip_address_spec.rb @@ -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 diff --git a/middleman-core/spec/spec_helper.rb b/middleman-core/spec/spec_helper.rb index a9d71768..32c25a34 100644 --- a/middleman-core/spec/spec_helper.rb +++ b/middleman-core/spec/spec_helper.rb @@ -3,3 +3,29 @@ SimpleCov.root(File.expand_path(File.dirname(__FILE__) + '/..')) require 'coveralls' 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