├── .document ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── data └── public_suffix_list.dat ├── domain_name.gemspec ├── lib ├── domain_name.rb └── domain_name │ ├── etld_data.rb │ ├── etld_data.rb.erb │ ├── punycode.rb │ └── version.rb ├── test ├── helper.rb ├── test_domain_name-punycode.rb └── test_domain_name.rb └── tool └── gen_etld_data.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 2.7 15 | 16 | test: 17 | needs: ruby-versions 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu] 22 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 23 | 24 | name: >- 25 | ${{matrix.os}}:ruby-${{matrix.ruby}} 26 | runs-on: ${{matrix.os}}-latest 27 | continue-on-error: ${{matrix.ruby == 'head' || matrix.ruby == 'jruby'}} 28 | 29 | steps: 30 | - name: Check out 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up ruby and bundle 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{matrix.ruby}} 37 | bundler-cache: true 38 | 39 | - name: Run rake 40 | run: | 41 | bundle exec rake 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.6.20240107](https://github.com/knu/ruby-domain_name/tree/v0.6.20240107) (2024-01-09) 4 | 5 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.6.20231109...v0.6.20240107) 6 | 7 | - Update the eTLD database to 2024-01-07 11:25:45 UTC 8 | 9 | **Closed issues:** 10 | 11 | - git tag v0.6.20231109 is missing [\#25](https://github.com/knu/ruby-domain_name/issues/25) 12 | - Unicode Normalization not appropriate for ASCII-8BIT [\#23](https://github.com/knu/ruby-domain_name/issues/23) 13 | 14 | **Merged pull requests:** 15 | 16 | - Fix regression: Allow DomainName.normalize to accept ASCII-only, non-unicode encoded input [\#27](https://github.com/knu/ruby-domain_name/pull/27) ([tisba](https://github.com/tisba)) 17 | 18 | ## [v0.6.20231109](https://github.com/knu/ruby-domain_name/tree/v0.6.20231109) (2023-11-14) 19 | 20 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20190701...v0.6.20231109) 21 | 22 | - Require Ruby >=2.7 and drop the dependency on unf 23 | - Update the eTLD database to 2023-11-09 00:10:10 UTC 24 | 25 | **Closed issues:** 26 | 27 | - "web.app" domain is not considered canonical [\#21](https://github.com/knu/ruby-domain_name/issues/21) 28 | 29 | ## [v0.5.20190701](https://github.com/knu/ruby-domain_name/tree/v0.5.20190701) (2019-07-05) 30 | 31 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20180417...v0.5.20190701) 32 | 33 | - Update the eTLD database to 2019-07-01 18:45:50 UTC 34 | 35 | ## [v0.5.20180417](https://github.com/knu/ruby-domain_name/tree/v0.5.20180417) (2018-04-18) 36 | 37 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20170404...v0.5.20180417) 38 | 39 | - Update the eTLD database to 2018-04-17T23:50:25Z 40 | 41 | ## [v0.5.20170404](https://github.com/knu/ruby-domain_name/tree/v0.5.20170404) (2017-04-06) 42 | 43 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20170223...v0.5.20170404) 44 | 45 | - Update the eTLD database to 2017-04-04T20:20:25Z 46 | 47 | ## [v0.5.20170223](https://github.com/knu/ruby-domain_name/tree/v0.5.20170223) (2017-02-23) 48 | 49 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20161129...v0.5.20170223) 50 | 51 | - Update the eTLD database to 2017-02-23T00:52:11Z 52 | 53 | **Closed issues:** 54 | 55 | - Changelog doesn't mention the version '0.5.20161021' [\#10](https://github.com/knu/ruby-domain_name/issues/10) 56 | 57 | ## [v0.5.20161129](https://github.com/knu/ruby-domain_name/tree/v0.5.20161129) (2016-11-29) 58 | 59 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20161021...v0.5.20161129) 60 | 61 | - Update the eTLD database to 2016-11-29T01:22:03Z 62 | 63 | ## [v0.5.20161021](https://github.com/knu/ruby-domain_name/tree/v0.5.20161021) (2016-10-27) 64 | 65 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160826...v0.5.20161021) 66 | 67 | - Update the eTLD database to 2016-10-21T20:52:10Z 68 | 69 | ## [v0.5.20160826](https://github.com/knu/ruby-domain_name/tree/v0.5.20160826) (2016-09-01) 70 | 71 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160615...v0.5.20160826) 72 | 73 | - Update the license for the eTLD database 74 | - Update the eTLD database to 2016-08-26T16:52:03Z 75 | 76 | **Closed issues:** 77 | 78 | - Please add a changelog [\#5](https://github.com/knu/ruby-domain_name/issues/5) 79 | 80 | ## [v0.5.20160615](https://github.com/knu/ruby-domain_name/tree/v0.5.20160615) (2016-06-16) 81 | 82 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160310...v0.5.20160615) 83 | 84 | - Always set `@domain` to avoid a warning when `$VERBOSE` is on 85 | - Update the eTLD database to 2016-06-15T16:22:11Z 86 | 87 | ## [v0.5.20160310](https://github.com/knu/ruby-domain_name/tree/v0.5.20160310) (2016-03-17) 88 | 89 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160309...v0.5.20160310) 90 | 91 | - Update development dependencies for obsolete rubies 92 | - Update the eTLD database to 2016-03-10T21:22:02Z 93 | 94 | ## [v0.5.20160309](https://github.com/knu/ruby-domain_name/tree/v0.5.20160309) (2016-03-09) 95 | 96 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160216...v0.5.20160309) 97 | 98 | - Fix support for Ruby 1.8 99 | - Update the eTLD database to 2016-03-09T09:52:02Z 100 | 101 | ## [v0.5.20160216](https://github.com/knu/ruby-domain_name/tree/v0.5.20160216) (2016-02-24) 102 | 103 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20160128...v0.5.20160216) 104 | 105 | - Update the eTLD database to 2016-02-16T19:22:02Z 106 | 107 | ## [v0.5.20160128](https://github.com/knu/ruby-domain_name/tree/v0.5.20160128) (2016-01-29) 108 | 109 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.25...v0.5.20160128) 110 | 111 | - Use the date as part of VERSION 112 | - Update the eTLD database to 2016-01-28T23:22:02Z 113 | 114 | ## [v0.5.25](https://github.com/knu/ruby-domain_name/tree/v0.5.25) (2015-10-06) 115 | 116 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.24...v0.5.25) 117 | 118 | - Restrict i18n < 0.7.0 on ruby 1.8. 119 | - Update the eTLD database to 2015-09-29T17:22:03Z 120 | 121 | **Closed issues:** 122 | 123 | - Bad link on rubygems.org? [\#4](https://github.com/knu/ruby-domain_name/issues/4) 124 | 125 | ## [v0.5.24](https://github.com/knu/ruby-domain_name/tree/v0.5.24) (2015-04-16) 126 | 127 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.23...v0.5.24) 128 | 129 | - Update the eTLD database to 2015-04-07T20:26:05Z 130 | 131 | ## [v0.5.23](https://github.com/knu/ruby-domain_name/tree/v0.5.23) (2014-12-19) 132 | 133 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.22...v0.5.23) 134 | 135 | - Update the eTLD database to 2014-12-18T02:26:03Z 136 | 137 | ## [v0.5.22](https://github.com/knu/ruby-domain_name/tree/v0.5.22) (2014-10-28) 138 | 139 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.21...v0.5.22) 140 | 141 | - Update the eTLD database to 2014-10-27T15:26:07Z 142 | 143 | ## [v0.5.21](https://github.com/knu/ruby-domain_name/tree/v0.5.21) (2014-09-09) 144 | 145 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.20...v0.5.21) 146 | 147 | - Update the eTLD database to 2014-09-05T01:56:10Z 148 | 149 | ## [v0.5.20](https://github.com/knu/ruby-domain_name/tree/v0.5.20) (2014-08-18) 150 | 151 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.19...v0.5.20) 152 | 153 | - Update the eTLD database to 2014-08-14T00:56:09Z 154 | 155 | ## [v0.5.19](https://github.com/knu/ruby-domain_name/tree/v0.5.19) (2014-06-12) 156 | 157 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.18...v0.5.19) 158 | 159 | - Update the eTLD database to 2014-06-11T15:26:13Z 160 | 161 | ## [v0.5.18](https://github.com/knu/ruby-domain_name/tree/v0.5.18) (2014-03-27) 162 | 163 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.17...v0.5.18) 164 | 165 | - Update the eTLD database to 2014-03-27T03:00:59Z 166 | 167 | ## [v0.5.17](https://github.com/knu/ruby-domain_name/tree/v0.5.17) (2014-03-21) 168 | 169 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.16...v0.5.17) 170 | 171 | - Update the eTLD database to 2014-03-20T15:01:09Z 172 | 173 | ## [v0.5.16](https://github.com/knu/ruby-domain_name/tree/v0.5.16) (2014-02-12) 174 | 175 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.15...v0.5.16) 176 | 177 | - Update the eTLD database to 2014-02-11T16:01:07Z 178 | 179 | ## [v0.5.15](https://github.com/knu/ruby-domain_name/tree/v0.5.15) (2013-11-15) 180 | 181 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.14...v0.5.15) 182 | 183 | - Update the eTLD database to 2013-11-15T16:01:28Z 184 | - Merge IDN tests from mozilla-central/netwerk/test/unit/data/test_psl.txt 185 | 186 | ## [v0.5.14](https://github.com/knu/ruby-domain_name/tree/v0.5.14) (2013-10-16) 187 | 188 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.13...v0.5.14) 189 | 190 | - Update the eTLD database to 2013-10-16T07:01:24Z 191 | 192 | ## [v0.5.13](https://github.com/knu/ruby-domain_name/tree/v0.5.13) (2013-08-18) 193 | 194 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.12...v0.5.13) 195 | 196 | - Update the eTLD database to 2013-08-15T11:01:26Z 197 | - Adjust dependencies for Ruby 1.8 198 | 199 | ## [v0.5.12](https://github.com/knu/ruby-domain_name/tree/v0.5.12) (2013-06-07) 200 | 201 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.11...v0.5.12) 202 | 203 | - Update the eTLD database to 2013-06-06T23:00:56Z 204 | - Add *_idn methods that do ToUnicode conversion 205 | 206 | ## [v0.5.11](https://github.com/knu/ruby-domain_name/tree/v0.5.11) (2013-04-12) 207 | 208 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.10...v0.5.11) 209 | 210 | - Add DomainName#superdomain 211 | - Update the database to 2013-04-05T23:00:49Z 212 | 213 | ## [v0.5.10](https://github.com/knu/ruby-domain_name/tree/v0.5.10) (2013-04-01) 214 | 215 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.9...v0.5.10) 216 | 217 | - Update the eTLD database to that of 2013-03-31T03:02:39Z 218 | 219 | ## [v0.5.9](https://github.com/knu/ruby-domain_name/tree/v0.5.9) (2013-03-17) 220 | 221 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.8...v0.5.9) 222 | 223 | - Support unf 0.1.0 224 | 225 | ## [v0.5.8](https://github.com/knu/ruby-domain_name/tree/v0.5.8) (2013-03-14) 226 | 227 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.7...v0.5.8) 228 | 229 | - Update the eTLD database to the version as of 2013-02-18T20:02:07Z 230 | 231 | ## [v0.5.7](https://github.com/knu/ruby-domain_name/tree/v0.5.7) (2013-01-07) 232 | 233 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.6...v0.5.7) 234 | 235 | - Update the eTLD list 236 | 237 | ## [v0.5.6](https://github.com/knu/ruby-domain_name/tree/v0.5.6) (2012-12-06) 238 | 239 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.5...v0.5.6) 240 | 241 | - Update the eTLD list 242 | 243 | ## [v0.5.5](https://github.com/knu/ruby-domain_name/tree/v0.5.5) (2012-12-06) 244 | 245 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.4...v0.5.5) 246 | 247 | - Add an optional host_only flag to DomainName#cookie_domain? 248 | - Migrate from jeweler to bundle gem 249 | 250 | ## [v0.5.4](https://github.com/knu/ruby-domain_name/tree/v0.5.4) (2012-09-18) 251 | 252 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.3...v0.5.4) 253 | 254 | - Update the eTLD list 255 | - Import updated test cases suggested by Mozilla developers 256 | 257 | ## [v0.5.3](https://github.com/knu/ruby-domain_name/tree/v0.5.3) (2012-04-06) 258 | 259 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.2...v0.5.3) 260 | 261 | - Implement Punycode decoder 262 | 263 | **Closed issues:** 264 | 265 | - Running DomainName multi-threaded leads to Stack Errors [\#2](https://github.com/knu/ruby-domain_name/issues/2) 266 | 267 | ## [v0.5.2](https://github.com/knu/ruby-domain_name/tree/v0.5.2) (2012-01-18) 268 | 269 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.1...v0.5.2) 270 | 271 | - Update the eTLD list 272 | 273 | ## [v0.5.1](https://github.com/knu/ruby-domain_name/tree/v0.5.1) (2011-11-09) 274 | 275 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.5.0...v0.5.1) 276 | 277 | - DomainName.new calls #to_str if a non-string object is given 278 | - Fix support for IPv6 addresses enclosed in square brackets 279 | 280 | **Merged pull requests:** 281 | 282 | - Fixed DomainName\#\<=\> for use with Ruby 1.8.7 [\#1](https://github.com/knu/ruby-domain_name/pull/1) ([drbrain](https://github.com/drbrain)) 283 | 284 | ## [v0.5.0](https://github.com/knu/ruby-domain_name/tree/v0.5.0) (2011-11-04) 285 | 286 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v0.0.0...v0.5.0) 287 | 288 | - Implement DomainName comparison and fix cookie_domain?() 289 | - Avoid warnings about uninitialized instance variables 290 | 291 | ## [v0.0.0](https://github.com/knu/ruby-domain_name/tree/v0.0.0) (2011-10-29) 292 | 293 | - Initial release 294 | 295 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 296 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in domain_name.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017 Akinori MUSHA 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | SUCH DAMAGE. 25 | 26 | * lib/domain_name/punycode.rb 27 | 28 | This file is derived from the implementation of punycode available at 29 | here: 30 | 31 | https://www.verisign.com/en_US/channel-resources/domain-registry-products/idn-sdks/index.xhtml 32 | 33 | Copyright (C) 2000-2002 Verisign Inc., All rights reserved. 34 | 35 | Redistribution and use in source and binary forms, with or 36 | without modification, are permitted provided that the following 37 | conditions are met: 38 | 39 | 1) Redistributions of source code must retain the above copyright 40 | notice, this list of conditions and the following disclaimer. 41 | 42 | 2) Redistributions in binary form must reproduce the above copyright 43 | notice, this list of conditions and the following disclaimer in 44 | the documentation and/or other materials provided with the 45 | distribution. 46 | 47 | 3) Neither the name of the VeriSign Inc. nor the names of its 48 | contributors may be used to endorse or promote products derived 49 | from this software without specific prior written permission. 50 | 51 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 52 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 53 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 54 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 55 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 56 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 57 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 58 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 59 | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 60 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 61 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 62 | POSSIBILITY OF SUCH DAMAGE. 63 | 64 | This software is licensed under the BSD open source license. For more 65 | information visit www.opensource.org. 66 | 67 | Authors: 68 | John Colosi (VeriSign) 69 | Srikanth Veeramachaneni (VeriSign) 70 | Nagesh Chigurupati (Verisign) 71 | Praveen Srinivasan(Verisign) 72 | 73 | * lib/domain_name/etld_data.rb 74 | 75 | This file is generated from the Public Suffix List 76 | (https://publicsuffix.org/), which is licensed under MPL 2.0: 77 | 78 | https://mozilla.org/MPL/2.0/ 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | domain_name 2 | =========== 3 | 4 | Synopsis 5 | -------- 6 | 7 | Domain Name manipulation library for Ruby 8 | 9 | Description 10 | ----------- 11 | 12 | * Parses a domain name ready for extracting the registered domain and 13 | TLD. 14 | 15 | require "domain_name" 16 | 17 | host = DomainName("a.b.example.co.uk") 18 | host.domain #=> "example.co.uk" 19 | host.tld #=> "uk" 20 | host.cookie_domain?("example.co.uk") #=> true 21 | host.cookie_domain?("co.uk") #=> false 22 | 23 | host = DomainName("[::1]") # IP addresses like "192.168.1.1" and "::1" are also acceptable 24 | host.ipaddr? #=> true 25 | host.cookie_domain?("0:0:0:0:0:0:0:1") #=> true 26 | 27 | * Implements rudimental IDNA support. 28 | 29 | To-do's 30 | ------- 31 | 32 | * Implement IDNA 2008 (and/or 2003) including the domain label 33 | validation and mapping defined in RFC 5891-5895 and UTS #46. 34 | (work in progress) 35 | 36 | * Define a compact YAML serialization format. 37 | 38 | Installation 39 | ------------ 40 | 41 | gem install domain_name 42 | 43 | References 44 | ---------- 45 | 46 | * [RFC 3492](http://tools.ietf.org/html/rfc3492) (Obsolete; just for test cases) 47 | 48 | * [RFC 5890](http://tools.ietf.org/html/rfc5890) 49 | 50 | * [RFC 5891](http://tools.ietf.org/html/rfc5891) 51 | 52 | * [RFC 5892](http://tools.ietf.org/html/rfc5892) 53 | 54 | * [RFC 5893](http://tools.ietf.org/html/rfc5892) 55 | 56 | * [Public Suffix List](https://publicsuffix.org/list/) 57 | 58 | License 59 | ------- 60 | 61 | Copyright (c) 2011-2017 Akinori MUSHA 62 | 63 | Licensed under the 2-clause BSD license. 64 | 65 | Some portion of this library is copyrighted by third parties and 66 | licensed under MPL 2.0 or 3-clause BSD license, 67 | See `LICENSE.txt` for details. 68 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'uri' 3 | ETLD_DATA_URI = URI('https://publicsuffix.org/list/public_suffix_list.dat') 4 | ETLD_DATA_FILE = 'data/public_suffix_list.dat' 5 | ETLD_DATA_RB = 'lib/domain_name/etld_data.rb' 6 | VERSION_RB = 'lib/domain_name/version.rb' 7 | 8 | task :default => :test 9 | 10 | task :test => ETLD_DATA_RB 11 | 12 | task :import => :etld_data 13 | 14 | # 15 | # eTLD Database 16 | # 17 | 18 | task :etld_data do 19 | require 'open-uri' 20 | require 'time' 21 | 22 | begin 23 | begin 24 | load File.join('.', ETLD_DATA_RB) 25 | data = ETLD_DATA_URI.read( 26 | 'If-Modified-Since' => Time.parse(DomainName::ETLD_DATA_DATE).rfc2822 27 | ) 28 | rescue LoadError, NameError 29 | data = ETLD_DATA_URI.read 30 | end 31 | puts 'eTLD database is modified.' 32 | date = data.last_modified 33 | File.write(ETLD_DATA_FILE, data) 34 | File.utime Time.now, date, ETLD_DATA_FILE 35 | if new_version = DomainName::VERSION.dup.sub!(/\b\d{8}\b/, date.strftime('%Y%m%d')) 36 | File.open(VERSION_RB, 'r+') { |rb| 37 | content = rb.read 38 | rb.rewind 39 | rb.write(content.sub(/(?<=^ VERSION = ')#{Regexp.quote(DomainName::VERSION)}(?='$)/, new_version)) 40 | rb.truncate(rb.tell) 41 | } 42 | end 43 | Rake::Task[ETLD_DATA_RB].execute 44 | rescue OpenURI::HTTPError => e 45 | if e.io.status.first == '304' # Not Modified 46 | puts 'eTLD database is up-to-date.' 47 | else 48 | raise 49 | end 50 | end 51 | end 52 | 53 | namespace :etld_data do 54 | task :commit do 55 | if system('git', 'diff', '--exit-code', '--quiet', ETLD_DATA_FILE) 56 | warn "Nothing to commit." 57 | exit 58 | end 59 | 60 | prev = `ruby -e "$(git cat-file -p @:lib/domain_name/version.rb); puts DomainName::VERSION"`.chomp 61 | curr = `ruby -e "load 'lib/domain_name/version.rb'; puts DomainName::VERSION"`.chomp 62 | timestamp = File.mtime(ETLD_DATA_FILE).utc 63 | 64 | File.open('CHANGELOG.md', 'r+') do |f| 65 | lines = f.readlines 66 | lines.insert(2, <<-EOF) 67 | ## [v#{curr}](https://github.com/knu/ruby-domain_name/tree/v#{curr}) (#{Time.now.strftime('%F')}) 68 | [Full Changelog](https://github.com/knu/ruby-domain_name/compare/v#{prev}...v#{curr}) 69 | 70 | - Update the eTLD database to #{timestamp} 71 | 72 | EOF 73 | f.rewind 74 | f.puts lines 75 | end 76 | 77 | sh 'git', 'commit', 78 | 'CHANGELOG.md', 79 | ETLD_DATA_FILE, 80 | ETLD_DATA_RB, 81 | VERSION_RB, 82 | '-m', 'Update the eTLD database to %s.' % timestamp 83 | 84 | sh 'git', 'tag', "v#{curr}" 85 | end 86 | end 87 | 88 | file ETLD_DATA_RB => [ 89 | ETLD_DATA_FILE, 90 | ETLD_DATA_RB + '.erb', 91 | 'tool/gen_etld_data.rb' 92 | ] do 93 | ruby 'tool/gen_etld_data.rb' 94 | end 95 | 96 | require 'rake/testtask' 97 | Rake::TestTask.new(:test) do |test| 98 | test.libs << 'test' 99 | test.pattern = 'test/**/test_*.rb' 100 | test.verbose = true 101 | end 102 | 103 | require 'rdoc/task' 104 | Rake::RDocTask.new do |rdoc| 105 | version = DomainName::VERSION 106 | 107 | rdoc.rdoc_dir = 'rdoc' 108 | rdoc.title = "domain_name #{version}" 109 | rdoc.rdoc_files.include('lib/**/*.rb') 110 | rdoc.rdoc_files.include(Bundler::GemHelper.gemspec.extra_rdoc_files) 111 | end 112 | -------------------------------------------------------------------------------- /domain_name.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'domain_name/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "domain_name" 8 | gem.version = DomainName::VERSION 9 | gem.required_ruby_version = ">= 2.7.0" 10 | gem.authors = ["Akinori MUSHA"] 11 | gem.email = ["knu@idaemons.org"] 12 | gem.description = <<-'EOS' 13 | This is a Domain Name manipulation library for Ruby. 14 | 15 | It can also be used for cookie domain validation based on the Public 16 | Suffix List. 17 | EOS 18 | gem.summary = %q{Domain Name manipulation library for Ruby} 19 | gem.homepage = "https://github.com/knu/ruby-domain_name" 20 | gem.licenses = ["BSD-2-Clause", "BSD-3-Clause", "MPL-2.0"] 21 | 22 | gem.files = `git ls-files`.split($/) 23 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 24 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 25 | gem.require_paths = ["lib"] 26 | 27 | gem.extra_rdoc_files = [ 28 | "LICENSE.txt", 29 | "README.md" 30 | ] 31 | 32 | gem.add_development_dependency("test-unit") 33 | gem.add_development_dependency("bundler", [">= 1.2.0"]) 34 | gem.add_development_dependency("rake") 35 | gem.add_development_dependency("rdoc", [">= 2.4.2"]) 36 | end 37 | -------------------------------------------------------------------------------- /lib/domain_name.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # domain_name.rb - Domain Name manipulation library for Ruby 4 | # 5 | # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved. 6 | # 7 | 8 | require 'domain_name/version' 9 | require 'domain_name/punycode' 10 | require 'domain_name/etld_data' 11 | require 'ipaddr' 12 | 13 | # Represents a domain name ready for extracting its registered domain 14 | # and TLD. 15 | class DomainName 16 | # The full host name normalized, ASCII-ized and downcased using the 17 | # Unicode NFC rules and the Punycode algorithm. If initialized with 18 | # an IP address, the string representation of the IP address 19 | # suitable for opening a connection to. 20 | attr_reader :hostname 21 | 22 | # The Unicode representation of the #hostname property. 23 | # 24 | # :attr_reader: hostname_idn 25 | 26 | # The least "universally original" domain part of this domain name. 27 | # For example, "example.co.uk" for "www.sub.example.co.uk". This 28 | # may be nil if the hostname does not have one, like when it is an 29 | # IP address, an effective TLD or higher itself, or of a 30 | # non-canonical domain. 31 | attr_reader :domain 32 | 33 | # The Unicode representation of the #domain property. 34 | # 35 | # :attr_reader: domain_idn 36 | 37 | # The TLD part of this domain name. For example, if the hostname is 38 | # "www.sub.example.co.uk", the TLD part is "uk". This property is 39 | # nil only if +ipaddr?+ is true. This may be nil if the hostname 40 | # does not have one, like when it is an IP address or of a 41 | # non-canonical domain. 42 | attr_reader :tld 43 | 44 | # The Unicode representation of the #tld property. 45 | # 46 | # :attr_reader: tld_idn 47 | 48 | # Returns an IPAddr object if this is an IP address. 49 | attr_reader :ipaddr 50 | 51 | # Returns true if this is an IP address, such as "192.168.0.1" and 52 | # "[::1]". 53 | def ipaddr? 54 | @ipaddr ? true : false 55 | end 56 | 57 | # Returns a host name representation suitable for use in the host 58 | # name part of a URI. A host name, an IPv4 address, or a IPv6 59 | # address enclosed in square brackets. 60 | attr_reader :uri_host 61 | 62 | # Returns true if this domain name has a canonical TLD. 63 | def canonical_tld? 64 | @canonical_tld_p 65 | end 66 | 67 | # Returns true if this domain name has a canonical registered 68 | # domain. 69 | def canonical? 70 | @canonical_tld_p && (@domain ? true : false) 71 | end 72 | 73 | DOT = '.'.freeze # :nodoc: 74 | 75 | # Parses _hostname_ into a DomainName object. An IP address is also 76 | # accepted. An IPv6 address may be enclosed in square brackets. 77 | def initialize(hostname) 78 | hostname.is_a?(String) or 79 | (hostname.respond_to?(:to_str) && (hostname = hostname.to_str).is_a?(String)) or 80 | raise TypeError, "#{hostname.class} is not a String" 81 | if hostname.start_with?(DOT) 82 | raise ArgumentError, "domain name must not start with a dot: #{hostname}" 83 | end 84 | case hostname 85 | when /\A([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\z/ 86 | @ipaddr = IPAddr.new($1) 87 | @uri_host = @hostname = @ipaddr.to_s 88 | @domain = @tld = nil 89 | return 90 | when /\A([0-9A-Fa-f:]*:[0-9A-Fa-f:]*:[0-9A-Fa-f:]*)\z/, 91 | /\A\[([0-9A-Fa-f:]*:[0-9A-Fa-f:]*:[0-9A-Fa-f:]*)\]\z/ 92 | @ipaddr = IPAddr.new($1) 93 | @hostname = @ipaddr.to_s 94 | @uri_host = "[#{@hostname}]" 95 | @domain = @tld = nil 96 | return 97 | end 98 | @ipaddr = nil 99 | @hostname = DomainName.normalize(hostname) 100 | @uri_host = @hostname 101 | if last_dot = @hostname.rindex(DOT) 102 | @tld = @hostname[(last_dot + 1)..-1] 103 | else 104 | @tld = @hostname 105 | end 106 | etld_data = DomainName.etld_data 107 | if @canonical_tld_p = etld_data.key?(@tld) 108 | subdomain = domain = nil 109 | parent = @hostname 110 | loop { 111 | case etld_data[parent] 112 | when 0 113 | @domain = domain 114 | return 115 | when -1 116 | @domain = subdomain 117 | return 118 | when 1 119 | @domain = parent 120 | return 121 | end 122 | subdomain = domain 123 | domain = parent 124 | pos = @hostname.index(DOT, -domain.length) or break 125 | parent = @hostname[(pos + 1)..-1] 126 | } 127 | else 128 | # unknown/local TLD 129 | if last_dot 130 | # fallback - accept cookies down to second level 131 | # cf. http://www.dkim-reputation.org/regdom-libs/ 132 | if penultimate_dot = @hostname.rindex(DOT, last_dot - 1) 133 | @domain = @hostname[(penultimate_dot + 1)..-1] 134 | else 135 | @domain = @hostname 136 | end 137 | else 138 | # no domain part - must be a local hostname 139 | @domain = @tld 140 | end 141 | end 142 | end 143 | 144 | # Checks if the server represented by this domain is qualified to 145 | # send and receive cookies with a domain attribute value of 146 | # _domain_. A true value given as the second argument represents 147 | # cookies without a domain attribute value, in which case only 148 | # hostname equality is checked. 149 | def cookie_domain?(domain, host_only = false) 150 | # RFC 6265 #5.3 151 | # When the user agent "receives a cookie": 152 | return self == domain if host_only 153 | 154 | domain = DomainName.new(domain) unless DomainName === domain 155 | if ipaddr? 156 | # RFC 6265 #5.1.3 157 | # Do not perform subdomain matching against IP addresses. 158 | @hostname == domain.hostname 159 | else 160 | # RFC 6265 #4.1.1 161 | # Domain-value must be a subdomain. 162 | @domain && self <= domain && domain <= @domain ? true : false 163 | end 164 | end 165 | 166 | # Returns the superdomain of this domain name. 167 | def superdomain 168 | return nil if ipaddr? 169 | pos = @hostname.index(DOT) or return nil 170 | self.class.new(@hostname[(pos + 1)..-1]) 171 | end 172 | 173 | def ==(other) 174 | other = DomainName.new(other) unless DomainName === other 175 | other.hostname == @hostname 176 | end 177 | 178 | def <=>(other) 179 | other = DomainName.new(other) unless DomainName === other 180 | othername = other.hostname 181 | if othername == @hostname 182 | 0 183 | elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT 184 | # The other is higher 185 | -1 186 | elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT 187 | # The other is lower 188 | 1 189 | else 190 | nil 191 | end 192 | end 193 | 194 | def <(other) 195 | case self <=> other 196 | when -1 197 | true 198 | when nil 199 | nil 200 | else 201 | false 202 | end 203 | end 204 | 205 | def >(other) 206 | case self <=> other 207 | when 1 208 | true 209 | when nil 210 | nil 211 | else 212 | false 213 | end 214 | end 215 | 216 | def <=(other) 217 | case self <=> other 218 | when -1, 0 219 | true 220 | when nil 221 | nil 222 | else 223 | false 224 | end 225 | end 226 | 227 | def >=(other) 228 | case self <=> other 229 | when 1, 0 230 | true 231 | when nil 232 | nil 233 | else 234 | false 235 | end 236 | end 237 | 238 | def to_s 239 | @hostname 240 | end 241 | 242 | alias to_str to_s 243 | 244 | def hostname_idn 245 | @hostname_idn ||= 246 | if @ipaddr 247 | @hostname 248 | else 249 | DomainName::Punycode.decode_hostname(@hostname) 250 | end 251 | end 252 | 253 | alias idn hostname_idn 254 | 255 | def domain_idn 256 | @domain_idn ||= 257 | if @ipaddr 258 | @domain 259 | else 260 | DomainName::Punycode.decode_hostname(@domain) 261 | end 262 | end 263 | 264 | def tld_idn 265 | @tld_idn ||= 266 | if @ipaddr 267 | @tld 268 | else 269 | DomainName::Punycode.decode_hostname(@tld) 270 | end 271 | end 272 | 273 | def inspect 274 | str = '#<%s:%s' % [self.class.name, @hostname] 275 | if @ipaddr 276 | str << ' (ipaddr)' 277 | else 278 | str << ' domain=' << @domain if @domain 279 | str << ' tld=' << @tld if @tld 280 | end 281 | str << '>' 282 | end 283 | 284 | class << self 285 | # Normalizes a _domain_ using the Punycode algorithm as necessary. 286 | # Input must be strictly ASCII-only or unicode. 287 | # The result will be a downcased, ASCII-only string. 288 | def normalize(domain) 289 | chomped = domain.chomp(DOT) 290 | normalized = chomped.ascii_only? ? chomped : chomped.unicode_normalize(:nfc) 291 | 292 | DomainName::Punycode.encode_hostname(normalized).downcase 293 | end 294 | end 295 | end 296 | 297 | # Short hand for DomainName.new(). 298 | def DomainName(hostname) 299 | DomainName.new(hostname) 300 | end 301 | -------------------------------------------------------------------------------- /lib/domain_name/etld_data.rb.erb: -------------------------------------------------------------------------------- 1 | class DomainName 2 | ETLD_DATA_DATE = '<%= etld_data_date.utc.strftime('%Y-%m-%dT%H:%M:%SZ') %>' 3 | 4 | ETLD_DATA = { 5 | <% etld_data.each_pair { |key, value| %> <%= key.inspect %> => <%= value.inspect %>, 6 | <% } %> } 7 | 8 | def self.etld_data 9 | ETLD_DATA 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/domain_name/punycode.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #-- 3 | # punycode.rb - PunyCode encoder for the Domain Name library 4 | # 5 | # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved. 6 | # 7 | # Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN 8 | # Library. 9 | # 10 | # Copyright (C) 2000-2002 Verisign Inc., All rights reserved. 11 | # 12 | # Redistribution and use in source and binary forms, with or 13 | # without modification, are permitted provided that the following 14 | # conditions are met: 15 | # 16 | # 1) Redistributions of source code must retain the above copyright 17 | # notice, this list of conditions and the following disclaimer. 18 | # 19 | # 2) Redistributions in binary form must reproduce the above copyright 20 | # notice, this list of conditions and the following disclaimer in 21 | # the documentation and/or other materials provided with the 22 | # distribution. 23 | # 24 | # 3) Neither the name of the VeriSign Inc. nor the names of its 25 | # contributors may be used to endorse or promote products derived 26 | # from this software without specific prior written permission. 27 | # 28 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 29 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 30 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 31 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 32 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 33 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 34 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 35 | # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 36 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 37 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 38 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 39 | # POSSIBILITY OF SUCH DAMAGE. 40 | # 41 | # This software is licensed under the BSD open source license. For more 42 | # information visit www.opensource.org. 43 | # 44 | # Authors: 45 | # John Colosi (VeriSign) 46 | # Srikanth Veeramachaneni (VeriSign) 47 | # Nagesh Chigurupati (Verisign) 48 | # Praveen Srinivasan(Verisign) 49 | #++ 50 | 51 | class DomainName 52 | module Punycode 53 | BASE = 36 54 | TMIN = 1 55 | TMAX = 26 56 | SKEW = 38 57 | DAMP = 700 58 | INITIAL_BIAS = 72 59 | INITIAL_N = 0x80 60 | DELIMITER = '-'.freeze 61 | 62 | MAXINT = (1 << 32) - 1 63 | 64 | LOBASE = BASE - TMIN 65 | CUTOFF = LOBASE * TMAX / 2 66 | 67 | RE_NONBASIC = /[^\x00-\x7f]/ 68 | 69 | # Returns the numeric value of a basic code point (for use in 70 | # representing integers) in the range 0 to base-1, or nil if cp 71 | # is does not represent a value. 72 | DECODE_DIGIT = {}.tap { |map| 73 | # ASCII A..Z map to 0..25 74 | # ASCII a..z map to 0..25 75 | (0..25).each { |i| map[65 + i] = map[97 + i] = i } 76 | # ASCII 0..9 map to 26..35 77 | (26..35).each { |i| map[22 + i] = i } 78 | } 79 | 80 | # Returns the basic code point whose value (when used for 81 | # representing integers) is d, which must be in the range 0 to 82 | # BASE-1. The lowercase form is used unless flag is true, in 83 | # which case the uppercase form is used. The behavior is 84 | # undefined if flag is nonzero and digit d has no uppercase 85 | # form. 86 | ENCODE_DIGIT = proc { |d, flag| 87 | (d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr 88 | # 0..25 map to ASCII a..z or A..Z 89 | # 26..35 map to ASCII 0..9 90 | } 91 | 92 | DOT = '.'.freeze 93 | PREFIX = 'xn--'.freeze 94 | 95 | # Most errors we raise are basically kind of ArgumentError. 96 | class ArgumentError < ::ArgumentError; end 97 | class BufferOverflowError < ArgumentError; end 98 | 99 | class << self 100 | # Encode a +string+ in Punycode 101 | def encode(string) 102 | input = string.unpack('U*') 103 | output = '' 104 | 105 | # Initialize the state 106 | n = INITIAL_N 107 | delta = 0 108 | bias = INITIAL_BIAS 109 | 110 | # Handle the basic code points 111 | input.each { |cp| output << cp.chr if cp < 0x80 } 112 | 113 | h = b = output.length 114 | 115 | # h is the number of code points that have been handled, b is the 116 | # number of basic code points, and out is the number of characters 117 | # that have been output. 118 | 119 | output << DELIMITER if b > 0 120 | 121 | # Main encoding loop 122 | 123 | while h < input.length 124 | # All non-basic code points < n have been handled already. Find 125 | # the next larger one 126 | 127 | m = MAXINT 128 | input.each { |cp| 129 | m = cp if (n...m) === cp 130 | } 131 | 132 | # Increase delta enough to advance the decoder's state to 133 | # , but guard against overflow 134 | 135 | delta += (m - n) * (h + 1) 136 | raise BufferOverflowError if delta > MAXINT 137 | n = m 138 | 139 | input.each { |cp| 140 | # AMC-ACE-Z can use this simplified version instead 141 | if cp < n 142 | delta += 1 143 | raise BufferOverflowError if delta > MAXINT 144 | elsif cp == n 145 | # Represent delta as a generalized variable-length integer 146 | q = delta 147 | k = BASE 148 | loop { 149 | t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias 150 | break if q < t 151 | q, r = (q - t).divmod(BASE - t) 152 | output << ENCODE_DIGIT[t + r, false] 153 | k += BASE 154 | } 155 | 156 | output << ENCODE_DIGIT[q, false] 157 | 158 | # Adapt the bias 159 | delta = h == b ? delta / DAMP : delta >> 1 160 | delta += delta / (h + 1) 161 | bias = 0 162 | while delta > CUTOFF 163 | delta /= LOBASE 164 | bias += BASE 165 | end 166 | bias += (LOBASE + 1) * delta / (delta + SKEW) 167 | 168 | delta = 0 169 | h += 1 170 | end 171 | } 172 | 173 | delta += 1 174 | n += 1 175 | end 176 | 177 | output 178 | end 179 | 180 | # Encode a hostname using IDN/Punycode algorithms 181 | def encode_hostname(hostname) 182 | hostname.match(RE_NONBASIC) or return hostname 183 | 184 | hostname.split(DOT).map { |name| 185 | if name.match(RE_NONBASIC) 186 | PREFIX + encode(name) 187 | else 188 | name 189 | end 190 | }.join(DOT) 191 | end 192 | 193 | # Decode a +string+ encoded in Punycode 194 | def decode(string) 195 | # Initialize the state 196 | n = INITIAL_N 197 | i = 0 198 | bias = INITIAL_BIAS 199 | 200 | if j = string.rindex(DELIMITER) 201 | b = string[0...j] 202 | 203 | b.match(RE_NONBASIC) and 204 | raise ArgumentError, "Illegal character is found in basic part: #{string.inspect}" 205 | 206 | # Handle the basic code points 207 | 208 | output = b.unpack('U*') 209 | u = string[(j + 1)..-1] 210 | else 211 | output = [] 212 | u = string 213 | end 214 | 215 | # Main decoding loop: Start just after the last delimiter if any 216 | # basic code points were copied; start at the beginning 217 | # otherwise. 218 | 219 | input = u.unpack('C*') 220 | input_length = input.length 221 | h = 0 222 | out = output.length 223 | 224 | while h < input_length 225 | # Decode a generalized variable-length integer into delta, 226 | # which gets added to i. The overflow checking is easier 227 | # if we increase i as we go, then subtract off its starting 228 | # value at the end to obtain delta. 229 | 230 | oldi = i 231 | w = 1 232 | k = BASE 233 | 234 | loop { 235 | digit = DECODE_DIGIT[input[h]] or 236 | raise ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}" 237 | h += 1 238 | i += digit * w 239 | raise BufferOverflowError if i > MAXINT 240 | t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias 241 | break if digit < t 242 | w *= BASE - t 243 | raise BufferOverflowError if w > MAXINT 244 | k += BASE 245 | h < input_length or raise ArgumentError, "Malformed input given: #{string.inspect}" 246 | } 247 | 248 | # Adapt the bias 249 | delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1 250 | delta += delta / (out + 1) 251 | bias = 0 252 | while delta > CUTOFF 253 | delta /= LOBASE 254 | bias += BASE 255 | end 256 | bias += (LOBASE + 1) * delta / (delta + SKEW) 257 | 258 | # i was supposed to wrap around from out+1 to 0, incrementing 259 | # n each time, so we'll fix that now: 260 | 261 | q, i = i.divmod(out + 1) 262 | n += q 263 | raise BufferOverflowError if n > MAXINT 264 | 265 | # Insert n at position i of the output: 266 | 267 | output[i, 0] = n 268 | 269 | out += 1 270 | i += 1 271 | end 272 | output.pack('U*') 273 | end 274 | 275 | # Decode a hostname using IDN/Punycode algorithms 276 | def decode_hostname(hostname) 277 | hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) { 278 | $1 << decode($2) 279 | } 280 | end 281 | end 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /lib/domain_name/version.rb: -------------------------------------------------------------------------------- 1 | class DomainName 2 | VERSION = '0.6.20240107' 3 | end 4 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | 12 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 13 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 14 | require 'domain_name' 15 | 16 | class Test::Unit::TestCase 17 | end 18 | -------------------------------------------------------------------------------- /test/test_domain_name-punycode.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestDomainName < Test::Unit::TestCase 4 | test "encode labels just as listed in RFC 3492 #7.1 (slightly modified)" do 5 | [ 6 | ['(A) Arabic (Egyptian)', 7 | [0x0644, 0x064A, 0x0647, 0x0645, 0x0627, 0x0628, 0x062A, 0x0643, 0x0644, 8 | 0x0645, 0x0648, 0x0634, 0x0639, 0x0631, 0x0628, 0x064A, 0x061F], 9 | 'egbpdaj6bu4bxfgehfvwxn'], 10 | ['(B) Chinese (simplified)', 11 | [0x4ED6, 0x4EEC, 0x4E3A, 0x4EC0, 0x4E48, 0x4E0D, 0x8BF4, 0x4E2D, 0x6587], 12 | 'ihqwcrb4cv8a8dqg056pqjye'], 13 | ['(C) Chinese (traditional)', 14 | [0x4ED6, 0x5011, 0x7232, 0x4EC0, 0x9EBD, 0x4E0D, 0x8AAA, 0x4E2D, 0x6587], 15 | 'ihqwctvzc91f659drss3x8bo0yb'], 16 | ['(D) Czech: Proprostnemluvesky', 17 | [0x0050, 0x0072, 0x006F, 0x010D, 0x0070, 0x0072, 0x006F, 0x0073, 0x0074, 18 | 0x011B, 0x006E, 0x0065, 0x006D, 0x006C, 0x0075, 0x0076, 0x00ED, 0x010D, 19 | 0x0065, 0x0073, 0x006B, 0x0079], 20 | 'Proprostnemluvesky-uyb24dma41a'], 21 | ['(E) Hebrew', 22 | [0x05DC, 0x05DE, 0x05D4, 0x05D4, 0x05DD, 0x05E4, 0x05E9, 0x05D5, 0x05D8, 23 | 0x05DC, 0x05D0, 0x05DE, 0x05D3, 0x05D1, 0x05E8, 0x05D9, 0x05DD, 0x05E2, 24 | 0x05D1, 0x05E8, 0x05D9, 0x05EA], 25 | '4dbcagdahymbxekheh6e0a7fei0b'], 26 | ['(F) Hindi (Devanagari)', 27 | [0x092F, 0x0939, 0x0932, 0x094B, 0x0917, 0x0939, 0x093F, 0x0928, 0x094D, 28 | 0x0926, 0x0940, 0x0915, 0x094D, 0x092F, 0x094B, 0x0902, 0x0928, 0x0939, 29 | 0x0940, 0x0902, 0x092C, 0x094B, 0x0932, 0x0938, 0x0915, 0x0924, 0x0947, 30 | 0x0939, 0x0948, 0x0902], 31 | 'i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd'], 32 | ['(G) Japanese (kanji and hiragana)', 33 | [0x306A, 0x305C, 0x307F, 0x3093, 0x306A, 0x65E5, 0x672C, 0x8A9E, 0x3092, 34 | 0x8A71, 0x3057, 0x3066, 0x304F, 0x308C, 0x306A, 0x3044, 0x306E, 0x304B], 35 | 'n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa'], 36 | ['(H) Korean (Hangul syllables)', 37 | [0xC138, 0xACC4, 0xC758, 0xBAA8, 0xB4E0, 0xC0AC, 0xB78C, 0xB4E4, 0xC774, 38 | 0xD55C, 0xAD6D, 0xC5B4, 0xB97C, 0xC774, 0xD574, 0xD55C, 0xB2E4, 0xBA74, 39 | 0xC5BC, 0xB9C8, 0xB098, 0xC88B, 0xC744, 0xAE4C], 40 | '989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5j' << 41 | 'psd879ccm6fea98c'], 42 | ['(I) Russian (Cyrillic)', 43 | [0x043F, 0x043E, 0x0447, 0x0435, 0x043C, 0x0443, 0x0436, 0x0435, 0x043E, 44 | 0x043D, 0x0438, 0x043D, 0x0435, 0x0433, 0x043E, 0x0432, 0x043E, 0x0440, 45 | 0x044F, 0x0442, 0x043F, 0x043E, 0x0440, 0x0443, 0x0441, 0x0441, 0x043A, 46 | 0x0438], 47 | 'b1abfaaepdrnnbgefbadotcwatmq2g4l'], 48 | ['(J) Spanish: PorqunopuedensimplementehablarenEspaol', 49 | [0x0050, 0x006F, 0x0072, 0x0071, 0x0075, 0x00E9, 0x006E, 0x006F, 0x0070, 50 | 0x0075, 0x0065, 0x0064, 0x0065, 0x006E, 0x0073, 0x0069, 0x006D, 0x0070, 51 | 0x006C, 0x0065, 0x006D, 0x0065, 0x006E, 0x0074, 0x0065, 0x0068, 0x0061, 52 | 0x0062, 0x006C, 0x0061, 0x0072, 0x0065, 0x006E, 0x0045, 0x0073, 0x0070, 53 | 0x0061, 0x00F1, 0x006F, 0x006C], 54 | 'PorqunopuedensimplementehablarenEspaol-fmd56a'], 55 | ['(K) Vietnamese: Tisaohkhngthch' << 56 | 'nitingVit', 57 | [0x0054, 0x1EA1, 0x0069, 0x0073, 0x0061, 0x006F, 0x0068, 0x1ECD, 0x006B, 58 | 0x0068, 0x00F4, 0x006E, 0x0067, 0x0074, 0x0068, 0x1EC3, 0x0063, 0x0068, 59 | 0x1EC9, 0x006E, 0x00F3, 0x0069, 0x0074, 0x0069, 0x1EBF, 0x006E, 0x0067, 60 | 0x0056, 0x0069, 0x1EC7, 0x0074], 61 | 'TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g'], 62 | ['(L) 3B', 63 | [0x0033, 0x5E74, 0x0042, 0x7D44, 0x91D1, 0x516B, 0x5148, 0x751F], 64 | '3B-ww4c5e180e575a65lsy2b'], 65 | ['(M) -with-SUPER-MONKEYS', 66 | [0x5B89, 0x5BA4, 0x5948, 0x7F8E, 0x6075, 0x002D, 0x0077, 0x0069, 0x0074, 67 | 0x0068, 0x002D, 0x0053, 0x0055, 0x0050, 0x0045, 0x0052, 0x002D, 0x004D, 68 | 0x004F, 0x004E, 0x004B, 0x0045, 0x0059, 0x0053], 69 | '-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n'], 70 | ['(N) Hello-Another-Way-', 71 | [0x0048, 0x0065, 0x006C, 0x006C, 0x006F, 0x002D, 0x0041, 0x006E, 0x006F, 72 | 0x0074, 0x0068, 0x0065, 0x0072, 0x002D, 0x0057, 0x0061, 0x0079, 0x002D, 73 | 0x305D, 0x308C, 0x305E, 0x308C, 0x306E, 0x5834, 0x6240], 74 | 'Hello-Another-Way--fc4qua05auwb3674vfr0b'], 75 | ['(O) 2', 76 | [0x3072, 0x3068, 0x3064, 0x5C4B, 0x6839, 0x306E, 0x4E0B, 0x0032], 77 | '2-u9tlzr9756bt3uc0v'], 78 | ['(P) MajiKoi5', 79 | [0x004D, 0x0061, 0x006A, 0x0069, 0x3067, 0x004B, 0x006F, 0x0069, 0x3059, 80 | 0x308B, 0x0035, 0x79D2, 0x524D], 81 | 'MajiKoi5-783gue6qz075azm5e'], 82 | ['(Q) de', 83 | [0x30D1, 0x30D5, 0x30A3, 0x30FC, 0x0064, 0x0065, 0x30EB, 0x30F3, 0x30D0], 84 | 'de-jg4avhby1noc0d'], 85 | ['(R) ', 86 | [0x305D, 0x306E, 0x30B9, 0x30D4, 0x30FC, 0x30C9, 0x3067], 87 | 'd9juau41awczczp'], 88 | ['(S) -> $1.00 <-', 89 | [0x002D, 0x003E, 0x0020, 0x0024, 0x0031, 0x002E, 0x0030, 0x0030, 0x0020, 90 | 0x003C, 0x002D], 91 | '-> $1.00 <--'] 92 | ].each { |title, cps, punycode| 93 | assert_equal punycode, DomainName::Punycode.encode(cps.pack('U*')), title 94 | assert_equal cps.pack('U*').unicode_normalize(:nfc), DomainName::Punycode.decode(punycode), title 95 | } 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/test_domain_name.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'helper' 3 | require 'ipaddr' 4 | 5 | class TestDomainName < Test::Unit::TestCase 6 | test "raise ArgumentError if hostname starts with a dot" do 7 | [ 8 | # Leading dot. 9 | '.com', 10 | '.example', 11 | '.example.com', 12 | '.example.example', 13 | ].each { |hostname| 14 | assert_raises(ArgumentError) { DomainName.new(hostname) } 15 | } 16 | end 17 | 18 | test "accept a String-alike for initialization" do 19 | Object.new.tap { |obj| 20 | def obj.to_str 21 | "Example.org" 22 | end 23 | assert_equal "example.org", DomainName.new(obj).hostname 24 | } 25 | 26 | Object.new.tap { |obj| 27 | def obj.to_str 28 | 123 29 | end 30 | assert_raises(TypeError) { DomainName.new(obj) } 31 | } 32 | 33 | Object.new.tap { |obj| 34 | assert_raises(TypeError) { DomainName.new(obj) } 35 | } 36 | end 37 | 38 | test "accept ASCII-only 'binary' encoded hostnames" do 39 | assert_equal "example.com", DomainName.new("example.com".force_encoding("ASCII-8BIT")).hostname 40 | end 41 | 42 | test "parse canonical domain names correctly" do 43 | [ 44 | # Mixed case. 45 | ['COM', nil, false, 'com', true], 46 | ['example.COM', 'example.com', true, 'com', true], 47 | ['WwW.example.COM', 'example.com', true, 'com', true], 48 | # Unlisted TLD. 49 | ['example', 'example', false, 'example', false], 50 | ['example.example', 'example.example', false, 'example', false], 51 | ['b.example.example', 'example.example', false, 'example', false], 52 | ['a.b.example.example', 'example.example', false, 'example', false], 53 | # Listed, but non-Internet, TLD. 54 | ['local', 'local', false, 'local', false], 55 | ['example.local', 'example.local', false, 'local', false], 56 | ['b.example.local', 'example.local', false, 'local', false], 57 | ['a.b.example.local', 'example.local', false, 'local', false], 58 | # TLD with only 1 rule. 59 | ['biz', nil, false, 'biz', true], 60 | ['domain.biz', 'domain.biz', true, 'biz', true], 61 | ['b.domain.biz', 'domain.biz', true, 'biz', true], 62 | ['a.b.domain.biz', 'domain.biz', true, 'biz', true], 63 | # TLD with some 2-level rules. 64 | ['com', nil, false, 'com', true], 65 | ['example.com', 'example.com', true, 'com', true], 66 | ['b.example.com', 'example.com', true, 'com', true], 67 | ['a.b.example.com', 'example.com', true, 'com', true], 68 | ['uk.com', nil, false, 'com', true], 69 | ['example.uk.com', 'example.uk.com', true, 'com', true], 70 | ['b.example.uk.com', 'example.uk.com', true, 'com', true], 71 | ['a.b.example.uk.com', 'example.uk.com', true, 'com', true], 72 | ['test.ac', 'test.ac', true, 'ac', true], 73 | # TLD with only 1 (wildcard) rule. 74 | ['bd', nil, false, 'bd', true], 75 | ['c.bd', nil, false, 'bd', true], 76 | ['b.c.bd', 'b.c.bd', true, 'bd', true], 77 | ['a.b.c.bd', 'b.c.bd', true, 'bd', true], 78 | # More complex TLD. 79 | ['jp', nil, false, 'jp', true], 80 | ['test.jp', 'test.jp', true, 'jp', true], 81 | ['www.test.jp', 'test.jp', true, 'jp', true], 82 | ['ac.jp', nil, false, 'jp', true], 83 | ['test.ac.jp', 'test.ac.jp', true, 'jp', true], 84 | ['www.test.ac.jp', 'test.ac.jp', true, 'jp', true], 85 | ['kyoto.jp', nil, false, 'jp', true], 86 | ['test.kyoto.jp', 'test.kyoto.jp', true, 'jp', true], 87 | ['ide.kyoto.jp', nil, false, 'jp', true], 88 | ['b.ide.kyoto.jp', 'b.ide.kyoto.jp', true, 'jp', true], 89 | ['a.b.ide.kyoto.jp', 'b.ide.kyoto.jp', true, 'jp', true], 90 | ['c.kobe.jp', nil, false, 'jp', true], 91 | ['b.c.kobe.jp', 'b.c.kobe.jp', true, 'jp', true], 92 | ['a.b.c.kobe.jp', 'b.c.kobe.jp', true, 'jp', true], 93 | ['city.kobe.jp', 'city.kobe.jp', true, 'jp', true], 94 | ['www.city.kobe.jp', 'city.kobe.jp', true, 'jp', true], 95 | # TLD with a wildcard rule and exceptions. 96 | ['ck', nil, false, 'ck', true], 97 | ['test.ck', nil, false, 'ck', true], 98 | ['b.test.ck', 'b.test.ck', true, 'ck', true], 99 | ['a.b.test.ck', 'b.test.ck', true, 'ck', true], 100 | ['www.ck', 'www.ck', true, 'ck', true], 101 | ['www.www.ck', 'www.ck', true, 'ck', true], 102 | # US K12. 103 | ['us', nil, false, 'us', true], 104 | ['test.us', 'test.us', true, 'us', true], 105 | ['www.test.us', 'test.us', true, 'us', true], 106 | ['ak.us', nil, false, 'us', true], 107 | ['test.ak.us', 'test.ak.us', true, 'us', true], 108 | ['www.test.ak.us', 'test.ak.us', true, 'us', true], 109 | ['k12.ak.us', nil, false, 'us', true], 110 | ['test.k12.ak.us', 'test.k12.ak.us', true, 'us', true], 111 | ['www.test.k12.ak.us', 'test.k12.ak.us', true, 'us', true], 112 | # IDN labels. (modified; currently DomainName always converts U-labels to A-labels) 113 | ['食狮.com.cn', 'xn--85x722f.com.cn', true, 'cn', true], 114 | ['食狮.公司.cn', 'xn--85x722f.xn--55qx5d.cn', true, 'cn', true], 115 | ['www.食狮.公司.cn', 'xn--85x722f.xn--55qx5d.cn', true, 'cn', true], 116 | ['shishi.公司.cn', 'shishi.xn--55qx5d.cn', true, 'cn', true], 117 | ['公司.cn', nil, false, 'cn', true], 118 | ['食狮.中国', 'xn--85x722f.xn--fiqs8s', true, 'xn--fiqs8s', true], 119 | ['www.食狮.中国', 'xn--85x722f.xn--fiqs8s', true, 'xn--fiqs8s', true], 120 | ['shishi.中国', 'shishi.xn--fiqs8s', true, 'xn--fiqs8s', true], 121 | ['中国', nil, false, 'xn--fiqs8s', true], 122 | # Same as above, but punycoded. 123 | ['xn--85x722f.com.cn', 'xn--85x722f.com.cn', true, 'cn', true], 124 | ['xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn', true, 'cn', true], 125 | ['www.xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn', true, 'cn', true], 126 | ['shishi.xn--55qx5d.cn', 'shishi.xn--55qx5d.cn', true, 'cn', true], 127 | ['xn--55qx5d.cn', nil, false, 'cn', true], 128 | ['xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s', true, 'xn--fiqs8s', true], 129 | ['www.xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s', true, 'xn--fiqs8s', true], 130 | ['shishi.xn--fiqs8s', 'shishi.xn--fiqs8s', true, 'xn--fiqs8s', true], 131 | ['xn--fiqs8s', nil, false, 'xn--fiqs8s', true], 132 | ].each { |hostname, domain, canonical, tld, canonical_tld| 133 | dn = DomainName.new(hostname) 134 | assert_equal(domain, dn.domain, hostname + ':domain') 135 | assert_equal(canonical, dn.canonical?, hostname + ':canoninal?') 136 | assert_equal(tld, dn.tld, hostname + ':tld') 137 | assert_equal(canonical_tld, dn.canonical_tld?, hostname + ':canoninal_tld?') 138 | } 139 | end 140 | 141 | test "compare hostnames correctly" do 142 | [ 143 | ["foo.com", "abc.foo.com", 1], 144 | ["COM", "abc.foo.com", 1], 145 | ["abc.def.foo.com", "foo.com", -1], 146 | ["abc.def.foo.com", "ABC.def.FOO.com", 0], 147 | ["abc.def.foo.com", "bar.com", nil], 148 | ].each { |x, y, v| 149 | dx, dy = DomainName(x), DomainName(y) 150 | [ 151 | [dx, y, v], 152 | [dx, dy, v], 153 | [dy, x, v ? -v : v], 154 | [dy, dx, v ? -v : v], 155 | ].each { |a, b, expected| 156 | assert_equal expected, a <=> b 157 | case expected 158 | when 1 159 | assert_equal(true, a > b) 160 | assert_equal(true, a >= b) 161 | assert_equal(false, a == b) 162 | assert_equal(false, a <= b) 163 | assert_equal(false, a < b) 164 | when -1 165 | assert_equal(true, a < b) 166 | assert_equal(true, a <= b) 167 | assert_equal(false, a == b) 168 | assert_equal(false, a >= b) 169 | assert_equal(false, a > b) 170 | when 0 171 | assert_equal(false, a < b) 172 | assert_equal(true, a <= b) 173 | assert_equal(true, a == b) 174 | assert_equal(true, a >= b) 175 | assert_equal(false, a > b) 176 | when nil 177 | assert_equal(nil, a < b) 178 | assert_equal(nil, a <= b) 179 | assert_equal(false, a == b) 180 | assert_equal(nil, a >= b) 181 | assert_equal(nil, a > b) 182 | end 183 | } 184 | } 185 | end 186 | 187 | test "check cookie domain correctly" do 188 | { 189 | 'com' => [ 190 | ['com', false], 191 | ['example.com', false], 192 | ['foo.example.com', false], 193 | ['bar.foo.example.com', false], 194 | ], 195 | 196 | 'example.com' => [ 197 | ['com', false], 198 | ['example.com', true], 199 | ['foo.example.com', false], 200 | ['bar.foo.example.com', false], 201 | ], 202 | 203 | 'foo.example.com' => [ 204 | ['com', false], 205 | ['example.com', true], 206 | ['foo.example.com', true], 207 | ['foo.Example.com', true], 208 | ['bar.foo.example.com', false], 209 | ['bar.Foo.Example.com', false], 210 | ], 211 | 212 | 'b.sapporo.jp' => [ 213 | ['jp', false], 214 | ['sapporo.jp', false], 215 | ['b.sapporo.jp', false], 216 | ['a.b.sapporo.jp', false], 217 | ], 218 | 219 | 'b.c.sapporo.jp' => [ 220 | ['jp', false], 221 | ['sapporo.jp', false], 222 | ['c.sapporo.jp', false], 223 | ['b.c.sapporo.jp', true], 224 | ['a.b.c.sapporo.jp', false], 225 | ], 226 | 227 | 'b.c.d.sapporo.jp' => [ 228 | ['jp', false], 229 | ['sapporo.jp', false], 230 | ['d.sapporo.jp', false], 231 | ['c.d.sapporo.jp', true], 232 | ['b.c.d.sapporo.jp', true], 233 | ['a.b.c.d.sapporo.jp', false], 234 | ], 235 | 236 | 'city.sapporo.jp' => [ 237 | ['jp', false], 238 | ['sapporo.jp', false], 239 | ['city.sapporo.jp', true], 240 | ['a.city.sapporo.jp', false], 241 | ], 242 | 243 | 'b.city.sapporo.jp' => [ 244 | ['jp', false], 245 | ['sapporo.jp', false], 246 | ['city.sapporo.jp', true], 247 | ['b.city.sapporo.jp', true], 248 | ['a.b.city.sapporo.jp', false], 249 | ], 250 | }.each_pair { |host, pairs| 251 | dn = DomainName(host) 252 | assert_equal(true, dn.cookie_domain?(host.upcase, true), dn.to_s) 253 | assert_equal(true, dn.cookie_domain?(host.downcase, true), dn.to_s) 254 | assert_equal(false, dn.cookie_domain?("www." << host, true), dn.to_s) 255 | pairs.each { |domain, expected| 256 | assert_equal(expected, dn.cookie_domain?(domain), "%s - %s" % [dn.to_s, domain]) 257 | assert_equal(expected, dn.cookie_domain?(DomainName(domain)), "%s - %s" % [dn.to_s, domain]) 258 | } 259 | } 260 | end 261 | 262 | test "parse IPv4 addresseses" do 263 | a = '192.168.10.20' 264 | dn = DomainName(a) 265 | assert_equal(a, dn.hostname) 266 | assert_equal(true, dn.ipaddr?) 267 | assert_equal(IPAddr.new(a), dn.ipaddr) 268 | assert_equal(true, dn.cookie_domain?(a)) 269 | assert_equal(true, dn.cookie_domain?(a, true)) 270 | assert_equal(true, dn.cookie_domain?(dn)) 271 | assert_equal(true, dn.cookie_domain?(dn, true)) 272 | assert_equal(false, dn.cookie_domain?('168.10.20')) 273 | assert_equal(false, dn.cookie_domain?('20')) 274 | assert_equal(nil, dn.superdomain) 275 | end 276 | 277 | test "parse IPv6 addresseses" do 278 | a = '2001:200:dff:fff1:216:3eff:feb1:44d7' 279 | b = '2001:0200:0dff:fff1:0216:3eff:feb1:44d7' 280 | [b, b.upcase, "[#{b}]", "[#{b.upcase}]"].each { |host| 281 | dn = DomainName(host) 282 | assert_equal("[#{a}]", dn.uri_host) 283 | assert_equal(a, dn.hostname) 284 | assert_equal(true, dn.ipaddr?) 285 | assert_equal(IPAddr.new(a), dn.ipaddr) 286 | assert_equal(true, dn.cookie_domain?(host)) 287 | assert_equal(true, dn.cookie_domain?(host, true)) 288 | assert_equal(true, dn.cookie_domain?(dn)) 289 | assert_equal(true, dn.cookie_domain?(dn, true)) 290 | assert_equal(true, dn.cookie_domain?(a)) 291 | assert_equal(true, dn.cookie_domain?(a, true)) 292 | assert_equal(nil, dn.superdomain) 293 | } 294 | end 295 | 296 | test "get superdomain" do 297 | [ 298 | %w[www.sub.example.local sub.example.local example.local local], 299 | %w[www.sub.example.com sub.example.com example.com com], 300 | ].each { |domain, *superdomains| 301 | dn = DomainName(domain) 302 | superdomains.each { |superdomain| 303 | sdn = DomainName(superdomain) 304 | assert_equal sdn, dn.superdomain 305 | dn = sdn 306 | } 307 | assert_equal nil, dn.superdomain 308 | } 309 | end 310 | 311 | test "have idn methods" do 312 | dn = DomainName("金八先生.B組.3年.日本語ドメイン名Example.日本") 313 | 314 | assert_equal "xn--44q1cv48kq8x.xn--b-gf6c.xn--3-pj3b.xn--example-6q4fyliikhk162btq3b2zd4y2o.xn--wgv71a", dn.hostname 315 | assert_equal "金八先生.b組.3年.日本語ドメイン名example.日本", dn.hostname_idn 316 | assert_equal "xn--example-6q4fyliikhk162btq3b2zd4y2o.xn--wgv71a", dn.domain 317 | assert_equal "日本語ドメイン名example.日本", dn.domain_idn 318 | assert_equal "xn--wgv71a", dn.tld 319 | assert_equal "日本", dn.tld_idn 320 | end 321 | end 322 | -------------------------------------------------------------------------------- /tool/gen_etld_data.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'pathname' 5 | $basedir = Pathname.new(__FILE__).dirname.parent 6 | $LOAD_PATH.unshift $basedir + 'lib' 7 | require 'domain_name' 8 | require 'set' 9 | require 'erb' 10 | 11 | def main 12 | dat_file = $basedir + 'data' + 'public_suffix_list.dat' 13 | dir = $basedir + 'lib' + 'domain_name' 14 | erb_file = dir + 'etld_data.rb.erb' 15 | rb_file = dir + 'etld_data.rb' 16 | 17 | etld_data_date = File.mtime(dat_file) 18 | 19 | File.open(dat_file, 'r:utf-8') { |dat| 20 | etld_data = parse(dat) 21 | File.open(rb_file, 'w:utf-8') { |rb| 22 | File.open(erb_file, 'r:utf-8') { |erb| 23 | rb.print ERB.new(erb.read).result(binding) 24 | } 25 | } 26 | } 27 | end 28 | 29 | def normalize_hostname(domain) 30 | DomainName.normalize(domain) 31 | end 32 | 33 | def parse(f) 34 | {}.tap { |table| 35 | tlds = Set[] 36 | f.each_line { |line| 37 | line.sub!(%r{//.*}, '') 38 | line.strip! 39 | next if line.empty? 40 | case line 41 | when /^local$/ 42 | # ignore .local 43 | next 44 | when /^([^!*]+)$/ 45 | domain = normalize_hostname($1) 46 | value = 0 47 | when /^\*\.([^!*]+)$/ 48 | domain = normalize_hostname($1) 49 | value = -1 50 | when /^\!([^!*]+)$/ 51 | domain = normalize_hostname($1) 52 | value = 1 53 | else 54 | raise "syntax error: #{line}" 55 | end 56 | tld = domain.match(/(?:^|\.)([^.]+)$/)[1] 57 | table[tld] ||= 1 58 | table[domain] = value 59 | } 60 | } 61 | end 62 | 63 | main() 64 | --------------------------------------------------------------------------------