diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..647618f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor, +# operating system or environment, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +data/*.zone +data/*.txt + +spec/examples.txt + +coverage diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..cceba06 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--color +--order random +--format progress diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f58d70f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: ruby +script: bundle exec rake zones:update && bundle exec rspec spec diff --git a/Gemfile.lock b/Gemfile.lock index d99abb7..345e9dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,23 +2,43 @@ PATH remote: . specs: domain_name_validator (0.5) + zonefile GEM remote: http://rubygems.org/ specs: - diff-lcs (1.2.4) - rspec (2.13.0) - rspec-core (~> 2.13.0) - rspec-expectations (~> 2.13.0) - rspec-mocks (~> 2.13.0) - rspec-core (2.13.1) - rspec-expectations (2.13.0) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.13.1) + diff-lcs (1.2.5) + docile (1.1.5) + json (1.8.3) + rake (10.4.2) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.2) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) + simplecov (0.10.0) + docile (~> 1.1.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + zonefile (1.04) PLATFORMS - x86-mingw32 + ruby DEPENDENCIES domain_name_validator! + rake rspec + simplecov + +BUNDLED WITH + 1.10.6 diff --git a/Rakefile b/Rakefile index 7d2d224..64cb74e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,14 @@ -require 'bundler' -Bundler::GemHelper.install_tasks -# or.... -# require 'bundler/gem_tasks' +$:.unshift(File.expand_path("../lib", __FILE__)) +require 'domain_name_validator/update_zones' + +task default: :prepare + +task :prepare do + Rake::Task['zones:update'].invoke +end + +namespace :zones do + task :update do + DomainNameValidator.update_zones + end +end diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..7adf3b4 --- /dev/null +++ b/bin/setup @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' + +# Bundle +bundle install + +# Update data +rake zones:update diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/domain_name_validator.gemspec b/domain_name_validator.gemspec index 0d570ea..6284486 100644 --- a/domain_name_validator.gemspec +++ b/domain_name_validator.gemspec @@ -1,28 +1,35 @@ # -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) -require "domain_name_validator/version" +$:.push File.expand_path('../lib', __FILE__) +require 'domain_name_validator/version' Gem::Specification.new do |gem| - gem.name = "domain_name_validator" - gem.version = DomainNameValidator::VERSION - gem.platform = Gem::Platform::RUBY - gem.authors = ["David Keener"] - gem.email = ["dkeener@keenertech.com"] - gem.homepage = "http://www.keenertech.com" - gem.summary = %q{Domain Name Validator} - gem.description = %q{Checks the validity of domain names.} - gem.license = 'MIT' - - gem.add_development_dependency "rspec" + gem.name = 'domain_name_validator' + gem.version = DomainNameValidator::VERSION + gem.platform = Gem::Platform::RUBY + gem.authors = ['David Keener'] + gem.email = ['dkeener@keenertech.com'] + gem.homepage = 'http://www.keenertech.com' + gem.summary = %q{Domain Name Validator} + gem.description = %q{Checks the validity of domain names.} + gem.license = 'MIT' + gem.bindir = 'exe' + gem.require_paths = ['lib'] - gem.rubyforge_project = "domain_name_validator" + gem.extensions = ['Rakefile'] + + gem.add_dependency 'zonefile' + gem.add_development_dependency 'rspec' + gem.add_development_dependency 'rake' + gem.add_development_dependency 'simplecov' + + gem.rubyforge_project = 'domain_name_validator' gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + gem.executables = gem.files.grep(%r{^exe/}) { |f| File.basename(f) } #gem.rdoc_options = ["--charset=UTF-8"] #gem.extra_rdoc_files = %w[README.rdoc LICENSE Changelog.rdoc] - gem.require_paths = ["lib"] + gem.require_paths = ['lib'] end diff --git a/exe/update-zones b/exe/update-zones new file mode 100755 index 0000000..fb95481 --- /dev/null +++ b/exe/update-zones @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +$:.unshift(File.expand_path("../../lib", __FILE__)) +require 'domain_name_validator/update_zones' + +DomainNameValidator.update_zones diff --git a/lib/domain_name_validator.rb b/lib/domain_name_validator.rb index 4828e67..a10e060 100644 --- a/lib/domain_name_validator.rb +++ b/lib/domain_name_validator.rb @@ -1,3 +1,4 @@ +require 'domain_name_validator/config' require 'domain_name_validator/validator' require 'domain_name_validator/version' diff --git a/lib/domain_name_validator/config.rb b/lib/domain_name_validator/config.rb new file mode 100644 index 0000000..adfe3b2 --- /dev/null +++ b/lib/domain_name_validator/config.rb @@ -0,0 +1,31 @@ +class DomainNameValidator + ROOT_ZONE_URL = 'http://data.iana.org/TLD/tlds-alpha-by-domain.txt' + TLD_FILE = File.expand_path('../../../data/tlds-alpha-by-domain.txt', __FILE__) + MAX_DOMAIN_LENGTH = 253 + MAX_LABEL_LENGTH = 63 + MAX_LEVELS = 127 + MIN_LEVELS = 2 + + ERRS = { + :bogus_tld => + 'Malformed TLD: Could not possibly match any valid TLD', + :illegal_chars => + 'Domain label contains an illegal character', + :illegal_start => + 'No domain name may start with a period', + :label_dash_begin => + 'No domain label may begin with a dash', + :label_dash_end => + 'No domain label may end with a dash', + :max_domain_size => + 'Maximum domain length of 253 exceeded', + :max_label_size => + 'Maximum domain label length of 63 exceeded', + :max_level_size => + 'Maximum domain level limit of 127 exceeded', + :min_level_size => + 'Minimum domain level limit of 2 not achieved', + :zero_size => + 'Zero-length domain name' + } +end diff --git a/lib/domain_name_validator/update_zones.rb b/lib/domain_name_validator/update_zones.rb new file mode 100644 index 0000000..1c44f57 --- /dev/null +++ b/lib/domain_name_validator/update_zones.rb @@ -0,0 +1,44 @@ +require 'domain_name_validator/config' +require 'zonefile' +require 'net/http' +require 'uri' + +class DomainNameValidator + + def self.update_zones + uri = URI(ROOT_ZONE_URL) + dnv = DomainNameValidator.new + dnv.download_zone_file(uri) + end + + def download_zone_file(uri) + puts "# Downloading zones from #{ROOT_ZONE_URL}" + amount_downloaded = 0 + current_width = 0 + print ' #> 000%' + Net::HTTP.start(uri.host, uri.port) do |http| + request = Net::HTTP::Get.new uri.to_s + http.request request do |response| + file_size = response['content-length'].to_i + open TLD_FILE, 'w' do |io| + response.read_body do |chunk| + io.write chunk + amount_downloaded += chunk.size + diff = status_bar(file_size, amount_downloaded, current_width) + current_width += diff + end + end + end + end + puts ' ok' + end + + def status_bar(file_size, amount_downloaded, current_width) + percentage = (amount_downloaded.to_f / file_size * 100) + diff = (percentage.to_i/2-current_width) + if (current_width+percentage.to_i/2 > current_width+1 ) + print "\b" * 6 + '=' * diff.to_i + "> #{percentage.to_i.to_s.rjust(3, "0")}%" + end + return diff + end +end diff --git a/lib/domain_name_validator/validator.rb b/lib/domain_name_validator/validator.rb index 0742bbc..d97f095 100644 --- a/lib/domain_name_validator/validator.rb +++ b/lib/domain_name_validator/validator.rb @@ -3,39 +3,6 @@ # obscured in other more wide-ranging domain-related gems. class DomainNameValidator - - MAX_DOMAIN_LENGTH = 253 - MAX_LABEL_LENGTH = 63 - MAX_LEVELS = 127 - MAX_TLD_LENGTH = 3 # Except for "aero", "arpa", "info" and "museum" - MIN_LEVELS = 2 - MIN_TLD_LENGTH = 2 - - ERRS = { - :bogus_tld => - 'Malformed TLD: Could not possibly match any valid TLD', - :illegal_chars => - 'Domain label contains an illegal character', - :illegal_start => - 'No domain name may start with a period', - :label_dash_begin => - 'No domain label may begin with a dash', - :label_dash_end => - 'No domain label may end with a dash', - :max_domain_size => - 'Maximum domain length of 253 exceeded', - :max_label_size => - 'Maximum domain label length of 63 exceeded', - :max_level_size => - 'Maximum domain level limit of 127 exceeded', - :min_level_size => - 'Minimum domain level limit of 2 not achieved', - :top_numerical => - 'The top-level domain (TLD) cannot be numerical', - :zero_size => - 'Zero-length domain name' - } - # Validates the proper formatting of a normalized domain name, i.e. - a # domain that is represented in ASCII. Thus, international domain names are # supported and validated, if they have undergone the required IDN @@ -69,27 +36,11 @@ def validate(dn, errs = []) errs << ERRS[:label_dash_end] if p[-1] == '-' errs << ERRS[:illegal_chars] unless p.match(/^[a-z0-9\-\_]+$/) end - errs << ERRS[:top_numerical] if parts.last.match(/^[0-9]+$/) - if parts.last.size < MIN_TLD_LENGTH || parts.last.size > MAX_TLD_LENGTH - unless parts.last == 'arpa' || - parts.last == 'aero' || - parts.last == 'asia' || - parts.last == 'coop' || - parts.last == 'info' || - parts.last == 'jobs' || - parts.last == 'mobi' || - parts.last == 'museum' || - parts.last == 'name' || - parts.last == 'post' || - parts.last == 'travel' || - parts.last.match(/^xn--/) - errs << ERRS[:bogus_tld] - end - end + errs << ERRS[:bogus_tld] unless File.readlines(TLD_FILE).map{ + |line| line.chomp.downcase }.include?(parts.last) errs << ERRS[:illegal_start] if parts.first[0] == '.' end errs.size == 0 # TRUE if valid, FALSE otherwise end - end diff --git a/spec/domain_name_validator_spec.rb b/spec/domain_name_validator_spec.rb index 61770fa..0f0e17a 100644 --- a/spec/domain_name_validator_spec.rb +++ b/spec/domain_name_validator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DomainNameValidator do +RSpec.describe DomainNameValidator do before(:each) do @validator = DomainNameValidator.new @@ -10,68 +10,68 @@ it 'should pass a valid domain name' do response = @validator.validate('keenertech.com') - response.should be == true + expect(response).to be(true) end it 'should pass a valid domain name with uppercase letters' do response = @validator.validate('KeenerTech.COM') - response.should be == true + expect(response).to be(true) end it 'should fail when it finds nil instead of a domain name' do response = @validator.validate(nil) - response.should be == false + expect(response).to be(false) end it 'should fail when it finds an empty string instead of a domain name' do response = @validator.validate("") - response.should be == false + expect(response).to be(false) end it 'should pass when it finds a domain with leading/trainling whitespace' do response = @validator.validate(" www.domain-1.com\n") - response.should be == true + expect(response).to be(true) end it 'should fail when it finds a numeric top-level extension' do response = @validator.validate('keenertech.123') - response.should be == false + expect(response).to be(false) end it 'should fail when the domain name max size is exceeded' do domain = "a"*250 + ".com" # 254 chars; max is 253 response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail when the max number of levels is exceeded' do domain = "a."*127 + "com" # 128 levels; max is 127 response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail when a label exceeds the max label length' do domain = "a"*64 + "b.c.com" # 64 chars; max is 63 for a label response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail when a label begins with a dash' do domain = "a.-b.c.com" response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail when a TLD begins with a dash' do domain = "a.b.c.-com" response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail when a domain name begins with a period' do domain = ".b.c.com" response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end end @@ -80,7 +80,7 @@ it 'should pass with a normalized international domain name' do domain = "xn--kbenhavn-54.eu" response = @validator.validate(domain) - response.should be == true + expect(response).to be(true) end end @@ -90,45 +90,37 @@ it 'should fail if the TLD is too short' do domain = "test.a" response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it 'should fail if the TLD is too long' do domain = "test.domain" response = @validator.validate(domain) - response.should be == false + expect(response).to be(false) end it "should succeed if the TLD is too long but equals 'aero'" do domain = "test.aero" response = @validator.validate(domain) - response.should be == true + expect(response).to be(true) end it "should succeed if the TLD is too long but equals 'info'" do domain = "test.info" response = @validator.validate(domain) - response.should be == true + expect(response).to be(true) end it "should succeed if the TLD is too long but equals 'arpa'" do domain = "test.arpa" response = @validator.validate(domain) - response.should be == true + expect(response).to be(true) end it "should succeed if the TLD is too long but equals 'museum'" do domain = "test.museum" response = @validator.validate(domain) - response.should be == true + expect(response).to be(true) end - - it "should succeed if the TLD is too long but matches 'xn--'" do - domain = "test.xn--really-long-text" - response = @validator.validate(domain) - response.should be == true - end - end - end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 68a362c..cb84b7b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,21 @@ +require 'simplecov' +SimpleCov.start require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib domain_name_validator])) - + RSpec.configure do |config| + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = 'spec/examples.txt' + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + # == Mock Framework # # RSpec uses it's own mocking framework by default. If you prefer to