├── .github ├── dependabot.yml └── workflows │ ├── actions-updater.yaml │ ├── ci.yml │ ├── codeql.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CHANGELOG.yaml ├── Ctl ├── VERSION └── config.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── RIR.md ├── api.md └── develop.md ├── examples └── get_domain.py ├── pyproject.toml ├── rdap ├── __init__.py ├── assignment.py ├── bootstrap.py ├── cli.py ├── client.py ├── config.py ├── context.py ├── exceptions.py ├── normalize │ ├── __init__.py │ ├── afrinic.py │ ├── apnic.py │ ├── arin.py │ ├── base.py │ ├── geo.py │ ├── lacnic.py │ └── ripe.py ├── objects.py └── schema │ ├── __init__.py │ ├── normalized.py │ ├── rdap.py │ └── source.py ├── script └── mock_update_autnum.py ├── tests ├── conftest.py ├── data │ ├── assignment │ │ ├── delegated-afrinic-extended-latest │ │ ├── delegated-apnic-extended-latest │ │ ├── delegated-arin-extended-latest │ │ ├── delegated-lacnic-extended-latest │ │ └── delegated-ripencc-extended-latest │ ├── iana │ │ ├── bootstrap │ │ │ └── asn.json │ │ └── config.yml │ ├── normalize │ │ ├── autnum │ │ │ ├── 63311.expected │ │ │ ├── 63311.input │ │ │ ├── 8283.expected │ │ │ └── 8283.input │ │ ├── domain │ │ │ ├── 20c.com.expected │ │ │ └── 20c.com.input │ │ ├── entity │ │ │ ├── CLUE1-RIPE.expected │ │ │ ├── CLUE1-RIPE.input │ │ │ ├── DJVG.expected │ │ │ └── DJVG.input │ │ └── ip │ │ │ ├── 206.41.110.0.expected │ │ │ └── 206.41.110.0.input │ └── rdap │ │ ├── autnum │ │ ├── 205697.expected │ │ ├── 205697.input │ │ ├── 205726.expected │ │ ├── 205726.input │ │ ├── 206050.expected │ │ ├── 206050.input │ │ ├── 2515.expected │ │ ├── 2515.input │ │ ├── 2914.expected │ │ ├── 2914.input │ │ ├── 37271.expected │ │ ├── 37271.input │ │ ├── 49037.expected │ │ ├── 49037.input │ │ ├── 53170.expected │ │ ├── 53170.input │ │ ├── 61399.expected │ │ ├── 61399.input │ │ ├── 63311.expected │ │ ├── 63311.input │ │ ├── 8283.expected │ │ ├── 8283.input │ │ ├── 9269.expected │ │ └── 9269.input │ │ ├── domain │ │ └── 20c.com.input │ │ ├── entity │ │ ├── AMS346-RIPE.input │ │ ├── APR41-RIPE.input │ │ ├── AS5496JP.input │ │ ├── BRI2.input │ │ ├── CLUE1-RIPE.input │ │ ├── DJVG.input │ │ ├── EK6175JP.input │ │ ├── GJM3.input │ │ ├── HH11825JP.input │ │ ├── HKBN-HK.input │ │ ├── JK11944-RIPE.input │ │ ├── JNIC1-AP.input │ │ ├── MM47295-RIPE.input │ │ ├── MO5920JP.input │ │ ├── MP31159-RIPE.input │ │ ├── PEERI-ARIN.input │ │ ├── SD12478-RIPE.input │ │ ├── WA2477-RIPE.input │ │ └── YK11438JP.input │ │ ├── ip │ │ └── 206.41.110.0.input │ │ ├── rdap │ │ └── entity │ │ │ ├── PP17-AFRINIC.input │ │ │ └── WOL-AFRINIC.input │ │ └── registry │ │ └── entity │ │ └── PEERI-ARIN.input ├── test_asn.py ├── test_assignment.py ├── test_bootstrap.py ├── test_cli.py ├── test_client.py ├── test_domain.py ├── test_geo.py ├── test_init.py ├── test_ip.py ├── test_normalize.py └── test_objects.py ├── tox.ini └── uv.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/actions-updater.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Version Updater 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | # Automatically run on every Sunday 7 | - cron: '0 0 * * 0' 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | # [Required] Access token with `workflow` scope. 19 | token: ${{ secrets.WORKFLOW_SECRET }} 20 | 21 | - name: Run GitHub Actions Version Updater 22 | uses: saadmk11/github-actions-version-updater@v0.8.1 23 | with: 24 | # [Required] Access token with `workflow` scope. 25 | token: ${{ secrets.WORKFLOW_SECRET }} 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out repository 11 | uses: actions/checkout@v4 12 | - name: Install 13 | uses: astral-sh/setup-uv@v3 14 | #- name: deploy mkdocs gh-pages site 15 | # run: | 16 | # poetry run mkdocs gh-deploy --force 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "22 6 * * 4" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.x" 16 | - name: Install pypa/build 17 | run: >- 18 | python3 -m 19 | pip install 20 | build 21 | --user 22 | - name: Build a binary wheel and a source tarball 23 | run: python3 -m build 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: python-package-distributions 28 | path: dist/ 29 | 30 | publish-to-pypi: 31 | name: >- 32 | Publish Python 🐍 distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags/1') # only publish to PyPI on tag pushes 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/rdap # Replace with your PyPI project name 40 | permissions: 41 | id-token: write # IMPORTANT: mandatory for trusted publishing 42 | 43 | steps: 44 | - name: Download all the dists 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: python-package-distributions 48 | path: dist/ 49 | - name: Publish distribution 📦 to PyPI 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | 52 | github-release: 53 | name: >- 54 | Sign the Python 🐍 distribution 📦 with Sigstore 55 | and upload them to GitHub Release 56 | needs: 57 | - publish-to-pypi 58 | runs-on: ubuntu-latest 59 | 60 | permissions: 61 | contents: write # IMPORTANT: mandatory for making GitHub Releases 62 | id-token: write # IMPORTANT: mandatory for sigstore 63 | 64 | steps: 65 | - name: Download all the dists 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: python-package-distributions 69 | path: dist/ 70 | - name: Sign the dists with Sigstore 71 | uses: sigstore/gh-action-sigstore-python@v3.0.0 72 | with: 73 | inputs: >- 74 | ./dist/*.tar.gz 75 | ./dist/*.whl 76 | - name: Create GitHub Release 77 | env: 78 | GITHUB_TOKEN: ${{ github.token }} 79 | run: >- 80 | gh release create 81 | '${{ github.ref_name }}' 82 | --repo '${{ github.repository }}' 83 | --notes "" 84 | - name: Upload artifact signatures to GitHub Release 85 | env: 86 | GITHUB_TOKEN: ${{ github.token }} 87 | # Upload to GitHub Release using the `gh` CLI. 88 | # `dist/` contains the built packages, and the 89 | # sigstore-produced signatures and certificates. 90 | run: >- 91 | gh release upload 92 | '${{ github.ref_name }}' dist/** 93 | --repo '${{ github.repository }}' 94 | 95 | publish-to-testpypi: 96 | name: Publish Python 🐍 distribution 📦 to TestPyPI 97 | needs: 98 | - build 99 | runs-on: ubuntu-latest 100 | 101 | environment: 102 | name: testpypi 103 | url: https://test.pypi.org/p/rdap 104 | 105 | permissions: 106 | id-token: write # IMPORTANT: mandatory for trusted publishing 107 | 108 | steps: 109 | - name: Download all the dists 110 | uses: actions/download-artifact@v4 111 | with: 112 | name: python-package-distributions 113 | path: dist/ 114 | - name: Publish distribution 📦 to TestPyPI 115 | uses: pypa/gh-action-pypi-publish@release/v1 116 | with: 117 | repository-url: https://test.pypi.org/legacy/ 118 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | linting: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install uv 12 | uses: astral-sh/setup-uv@v3 13 | - name: install project 14 | run: uv sync --all-extras --dev 15 | - name: Run linters 16 | run: | 17 | uv run pre-commit run --all-files 18 | 19 | test: 20 | needs: linting 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ "ubuntu-latest", "macos-latest" ] 25 | python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Check out repository 29 | uses: actions/checkout@v4 30 | - name: Install uv 31 | uses: astral-sh/setup-uv@v3 32 | - name: Install python 33 | run: uv python install ${{ matrix.python-version }} 34 | - name: install tox 35 | # run: uv tool install tox --with tox-uv 36 | run: uv sync --all-extras --dev 37 | - name: Run tests 38 | run: uv run tox 39 | - name: Upload coverage 40 | uses: codecov/codecov-action@v5 41 | with: 42 | fail_ci_if_error: true 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg 3 | *.egg-info 4 | .cache/ 5 | .coverage 6 | .pytest_cache/ 7 | .rdap/ 8 | .tox 9 | .venv 10 | Ctl/tmp 11 | coverage.xml 12 | dist/ 13 | site/ 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | exclude: | 3 | (?x)^( 4 | tests/data/.* 5 | )$ 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.6.0 9 | hooks: 10 | - id: check-yaml 11 | - id: trailing-whitespace 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | # Ruff version. 14 | rev: v0.6.8 15 | hooks: 16 | # Run the linter. 17 | - id: ruff 18 | args: [ --fix ] 19 | # Run the formatter. 20 | - id: ruff-format 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## Unreleased 5 | 6 | 7 | ## 1.6.0 8 | ### Added 9 | - add regctl normalization schema 10 | - python 3.12 support 11 | ### Changed 12 | - moved from poetry to uv 13 | 14 | 15 | ## 1.5.2 16 | ### Fixed 17 | - issue introduced in 1.5.1 that could cause some contact points to be lost 18 | 19 | 20 | ## 1.5.1 21 | ### Fixed 22 | - fixes problem where ASN lookup would sometimes return incorrect organization names through maintainer objects. 23 | 24 | 25 | ## 1.5.0 26 | ### Added 27 | - get ripe org name out of remarks 28 | - add get_rir() 29 | - add cli `--rir` 30 | 31 | 32 | ## 1.4.0 33 | ### Added 34 | - python 3.11 35 | - more recursive tests 36 | ### Fixed 37 | - entity lookup following redirect for ip lookups #39 38 | - fixed parsing of norid.no vcards #41 39 | ### Removed 40 | - python 3.7 41 | 42 | 43 | ## 1.3.1 44 | ### Fixed 45 | - rir assignment status lookup missing some asns 46 | 47 | 48 | ## 1.3.0 49 | ### Added 50 | - rir assignment status lookup 51 | - python 3.10 52 | ### Removed 53 | - python 3.6 54 | 55 | 56 | ## 1.2.2 57 | ### Fixed 58 | - correctly find domains that start with "as", 59 | 60 | 61 | ## 1.2.1 62 | ### Fixed 63 | - bootstrap not found errors throw RdapNotFoundError 64 | ### Changed 65 | - poetry lock 66 | 67 | 68 | ## 1.2.0 69 | ### Added 70 | - option to ignore recurse errors 71 | - option to self bootstrap 72 | - add config option for bootstrap_dir 73 | ### Removed 74 | - python 2 cruft 75 | 76 | 77 | ## 1.1.0 78 | ### Added 79 | - python 3.9 80 | ### Fixed 81 | - ignore not found on nested handle lookups 82 | ### Changed 83 | - improved exception message for unallocated ASNs 84 | ### Removed 85 | - python 3.5 86 | 87 | 88 | ## 1.0.1 89 | ### Fixed 90 | - better range check for unallocated ASNs 91 | 92 | 93 | ## 1.0.0 94 | ### Added 95 | - black formatting 96 | - more object types (#4) 97 | - python 3.8 tests 98 | ### Fixed 99 | - sort lists for consistent tests (#7) 100 | ### Changed 101 | - default bootstrap_url to rdap.org 102 | ### Removed 103 | - python 2.7 support 104 | - python 3.4 tests 105 | 106 | 107 | ## 0.5.1 108 | ### Fixed 109 | - changed MANIFEST file to not include tmp dirs, #3 110 | 111 | 112 | ## 0.5.0 113 | ### Added 114 | - add config for request timeout, default to 0.5 seconds 115 | ### Changed 116 | - bump pytest-filedata to 0.4.0 for pytest4 117 | 118 | 119 | ## 0.4.0 120 | ### Added 121 | - add --parse option to display the parsed output 122 | - RdapObject::handle 123 | - ip address lookup 124 | - domain name lookup 125 | ### Changed 126 | - default CLI to display full data 127 | - rename `raw` to `data` on RdapObjects 128 | 129 | 130 | ## 0.2.1 131 | ### Fixed 132 | - long_description content type 133 | 134 | 135 | ## 0.2.0 136 | ### Added 137 | - moved recurse_roles to config 138 | - added support for LACNIC apikeys 139 | ### Changed 140 | - RdapNotFoundError now inherits from RdapHTTPError instead of LookupError 141 | - updated User-Agent 142 | - converted to requests.Session() -------------------------------------------------------------------------------- /CHANGELOG.yaml: -------------------------------------------------------------------------------- 1 | Unreleased: 2 | added: [] 3 | fixed: [] 4 | changed: [] 5 | deprecated: [] 6 | removed: [] 7 | security: [] 8 | 1.6.0: 9 | added: 10 | - add regctl normalization schema 11 | - python 3.12 support 12 | changed: 13 | - moved from poetry to uv 14 | 1.5.2: 15 | fixed: 16 | - issue introduced in 1.5.1 that could cause some contact points to be lost 17 | 1.5.1: 18 | fixed: 19 | - fixes problem where ASN lookup would sometimes return incorrect organization names 20 | through maintainer objects. 21 | 1.5.0: 22 | added: 23 | - get ripe org name out of remarks 24 | - add get_rir() 25 | - add cli `--rir` 26 | 1.4.0: 27 | added: 28 | - python 3.11 29 | - more recursive tests 30 | fixed: 31 | - 'entity lookup following redirect for ip lookups #39' 32 | - 'fixed parsing of norid.no vcards #41' 33 | removed: 34 | - python 3.7 35 | 1.3.1: 36 | fixed: 37 | - rir assignment status lookup missing some asns 38 | 1.3.0: 39 | added: 40 | - rir assignment status lookup 41 | - python 3.10 42 | removed: 43 | - python 3.6 44 | 1.2.2: 45 | fixed: 46 | - correctly find domains that start with "as", 47 | 1.2.1: 48 | changed: 49 | - poetry lock 50 | fixed: 51 | - bootstrap not found errors throw RdapNotFoundError 52 | 1.2.0: 53 | added: 54 | - option to ignore recurse errors 55 | - option to self bootstrap 56 | - add config option for bootstrap_dir 57 | removed: 58 | - python 2 cruft 59 | 1.1.0: 60 | added: 61 | - python 3.9 62 | changed: 63 | - improved exception message for unallocated ASNs 64 | fixed: 65 | - ignore not found on nested handle lookups 66 | removed: 67 | - python 3.5 68 | 1.0.1: 69 | fixed: 70 | - better range check for unallocated ASNs 71 | 1.0.0: 72 | added: 73 | - black formatting 74 | - more object types (#4) 75 | - python 3.8 tests 76 | changed: 77 | - default bootstrap_url to rdap.org 78 | fixed: 79 | - sort lists for consistent tests (#7) 80 | removed: 81 | - python 2.7 support 82 | - python 3.4 tests 83 | 0.5.1: 84 | fixed: 85 | - 'changed MANIFEST file to not include tmp dirs, #3' 86 | 0.5.0: 87 | added: 88 | - add config for request timeout, default to 0.5 seconds 89 | changed: 90 | - bump pytest-filedata to 0.4.0 for pytest4 91 | 0.4.0: 92 | added: 93 | - add --parse option to display the parsed output 94 | - RdapObject::handle 95 | - ip address lookup 96 | - domain name lookup 97 | changed: 98 | - default CLI to display full data 99 | - rename `raw` to `data` on RdapObjects 100 | 0.2.1: 101 | fixed: 102 | - long_description content type 103 | 0.2.0: 104 | added: 105 | - moved recurse_roles to config 106 | - added support for LACNIC apikeys 107 | changed: 108 | - RdapNotFoundError now inherits from RdapHTTPError instead of LookupError 109 | - updated User-Agent 110 | - converted to requests.Session() 111 | -------------------------------------------------------------------------------- /Ctl/VERSION: -------------------------------------------------------------------------------- 1 | 1.6.0 -------------------------------------------------------------------------------- /Ctl/config.yml: -------------------------------------------------------------------------------- 1 | ctl: 2 | permissions: 3 | - namespace: "ctl" 4 | permission: "r" 5 | 6 | plugins: 7 | - name: this_repo 8 | type: git 9 | config: 10 | branch: main 11 | repo_url: git@github.com:20c/rdap.git 12 | 13 | - name: changelog 14 | type: changelog 15 | 16 | - name: pypi 17 | type: pypi 18 | config: 19 | config_file: ~/.pypirc 20 | sign: true 21 | 22 | - name: version 23 | type: semver2 24 | config: 25 | branch_dev: main 26 | branch_release: main 27 | repositories: 28 | - this_repo 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-include rdap *.py 3 | include Ctl/VERSION 4 | include Ctl/requirements.txt 5 | include Ctl/requirements-test.txt 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # rdap 3 | 4 | [![PyPI](https://img.shields.io/pypi/v/rdap.svg?maxAge=3600)](https://pypi.python.org/pypi/rdap) 5 | [![PyPI](https://img.shields.io/pypi/pyversions/rdap.svg?maxAge=3600)](https://pypi.python.org/pypi/rdap) 6 | [![tests](https://github.com/20c/rdap/actions/workflows/tests.yml/badge.svg)](https://github.com/20c/rdap/actions/workflows/tests.yml) 7 | [![Codecov](https://img.shields.io/codecov/c/github/20c/rdap/master.svg?maxAge=3600)](https://codecov.io/github/20c/rdap) 8 | 9 | 10 | Registration Data Access Protocol tools 11 | 12 | ## Installation 13 | 14 | ```sh 15 | pip install rdap 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | ``` 22 | usage: rdap [-h] [--debug] [--home HOME] [--verbose] [--quiet] [--version] [--output-format OUTPUT_FORMAT] [--show-requests] [--parse] [--rir] [--write-bootstrap-data] query [query ...] 23 | 24 | rdap 25 | 26 | positional arguments: 27 | query 28 | 29 | options: 30 | -h, --help show this help message and exit 31 | --debug enable extra debug output 32 | --home HOME specify the home directory, by default will check in order: $RDAP_HOME, ./.rdap, /home/grizz/.rdap, /home/grizz/.config/rdap 33 | --verbose enable more verbose output 34 | --quiet no output at all 35 | --version show program's version number and exit 36 | --output-format OUTPUT_FORMAT 37 | output format (yaml, json, text) 38 | --show-requests show all requests 39 | --parse parse data into object before display 40 | --rir display rir 41 | --write-bootstrap-data 42 | write bootstrap data for type (as query) 43 | ``` 44 | 45 | 46 | ## Config file 47 | 48 | The client uses the `--home` option to point to a directory, by default will check in order: `$RDAP_HOME`, `./.rdap`, `~/.rdap`, `~/.config/rdap` 49 | 50 | The directory should have a `config.yaml` file in it, defaults shown below. 51 | 52 | ```yaml 53 | rdap: 54 | # URL to bootstrap the initial request off 55 | bootstrap_url: https://rdap.db.ripe.net/ 56 | # boolean to use data from bootstrap_data_url instead of a bootstrap server 57 | self_bootstrap: False 58 | # url to load bootstrap data from 59 | bootstrap_data_url: "https://data.iana.org/rdap/" 60 | # length of time in hours to keep bootstrap data 61 | bootstrap_cache_ttl: 25 62 | # how to format the output 63 | output_format: yaml 64 | # API key for use at rdap.lacnic.net 65 | lacnic_apikey: None 66 | # role types to recursively query when processing 67 | recurse_roles: ["administrative", "technical"] 68 | # HTTP request timeout in seconds, used for both connect and read 69 | timeout: 0.5 70 | ``` 71 | 72 | 73 | ### License 74 | 75 | Copyright 2016-2024 20C, LLC 76 | 77 | Licensed under the Apache License, Version 2.0 (the "License"); 78 | you may not use this softare except in compliance with the License. 79 | You may obtain a copy of the License at 80 | 81 | http://www.apache.org/licenses/LICENSE-2.0 82 | 83 | Unless required by applicable law or agreed to in writing, software 84 | distributed under the License is distributed on an "AS IS" BASIS, 85 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 86 | See the License for the specific language governing permissions and 87 | limitations under the License. 88 | -------------------------------------------------------------------------------- /docs/RIR.md: -------------------------------------------------------------------------------- 1 | 2 | ### ARIN: https://rdap.arin.net/ 3 | 4 | - org data in nested vcard under `roles`: "registrant" 5 | - has email addresses on entities 6 | 7 | 8 | ### RIPE: https://rdap.db.ripe.net/ 9 | 10 | - no org handle on rdap autnums 11 | * emailed 12 | - has email addresses and roles 13 | 14 | 15 | ### AFRNIC: https://rdap.afrinic.net/rdap/ 16 | 17 | - no role information on any entities 18 | - no name on org entity: https://rdap.afrinic.net/rdap/entity/ORG-WCL1-AFRINIC 19 | 20 | 21 | ### APNIC: https://rdap.apnic.net/autnum/7521 22 | - org name in remarks 23 | - no registrant role 24 | - no email addresses 25 | - appears all entities are 404 not found 26 | * https://rdap.apnic.net/autnum/7521 to> http://rdap.apnic.net/entity/JP00001394 is a 404 not found. 27 | 28 | * changed while I was looking at it 29 | * check why the entity recurse went to http 30 | 31 | 32 | ### LACNIC: https://rdap.lacnic.net/bootstrap/ 33 | 34 | NIC.br: 35 | email addresses on IPv4 server, no email addresses on IPv6 server 36 | * emailed 37 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Retrieve objects 4 | 5 | ```python 6 | import rdap 7 | 8 | # instantiate client 9 | client = rdap.RdapClient() 10 | 11 | # asn 12 | as63311 = client.get_asn(63311) 13 | 14 | # domain 15 | domain = client.get_domain('example.com') 16 | 17 | # ip 18 | ip = client.get_ip('206.41.110.0') 19 | 20 | # entity 21 | entity = client.get_entity('NETWO7047-ARIN') 22 | ``` 23 | 24 | ## Output normalized data (ASN example) 25 | 26 | ```python 27 | import json 28 | import rdap 29 | 30 | # instantiate client 31 | client = rdap.RdapClient() 32 | 33 | # request asn 34 | as63311 = client.get_asn(63311) 35 | 36 | # normalized dict 37 | print(json.dumps(as63311.normalized, indent=2)) 38 | ``` 39 | 40 | Output: 41 | ```json 42 | { 43 | "created": "2014-11-17T14:28:43-05:00", 44 | "updated": "2018-10-24T22:58:16-04:00", 45 | "asn": 63311, 46 | "name": "20C", 47 | "organization": { 48 | "name": "20C, LLC" 49 | }, 50 | "locations": [ 51 | { 52 | "updated": "2014-08-05T15:21:11-04:00", 53 | "country": null, 54 | "city": null, 55 | "postal_code": null, 56 | "address": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States", 57 | "geo": null, 58 | "floor": null, 59 | "suite": null 60 | }, 61 | { 62 | "updated": "2023-08-02T14:15:09-04:00", 63 | "country": null, 64 | "city": null, 65 | "postal_code": null, 66 | "address": "603 Discovery Dr\nWest Chicago\nIL\n60185\nUnited States", 67 | "geo": null, 68 | "floor": null, 69 | "suite": null 70 | } 71 | ], 72 | "contacts": [ 73 | { 74 | "created": "2014-07-03T23:22:49-04:00", 75 | "updated": "2023-08-02T14:15:09-04:00", 76 | "name": "Network Engineers", 77 | "roles": [ 78 | "abuse", 79 | "admin", 80 | "technical" 81 | ], 82 | "phone": "+1 978-636-0020", 83 | "email": "neteng@20c.com" 84 | } 85 | ], 86 | "sources": [ 87 | { 88 | "created": "2014-11-17T14:28:43-05:00", 89 | "updated": "2018-10-24T22:58:16-04:00", 90 | "handle": "AS63311", 91 | "urls": [ 92 | "https://rdap.org/autnum/63311", 93 | "https://rdap.arin.net/registry/autnum/63311" 94 | ], 95 | "description": null 96 | } 97 | ] 98 | } 99 | ``` 100 | 101 | ## Work with normalized data through pydantic models 102 | 103 | ```python 104 | import rdap 105 | 106 | from rdap.schema.normalized import Network 107 | 108 | # instantiate client 109 | client = rdap.RdapClient() 110 | 111 | # request asn 112 | as63311 = Network(**client.get_asn(63311).normalized) 113 | 114 | for contact in as63311.contacts: 115 | print(contact.name, contact.email) 116 | ``` -------------------------------------------------------------------------------- /docs/develop.md: -------------------------------------------------------------------------------- 1 | 2 | ## Running formatters 3 | 4 | Run black last, it has the final say in formatting (and does not always agree with others). 5 | 6 | ```sh 7 | pre-commit run --all-files 8 | # or, if it's not installed 9 | # uv run pre-commit run --all-files 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/get_domain.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import sys 3 | 4 | from rdap import RdapClient 5 | 6 | 7 | def lookup_domain(domain): 8 | rdapc = RdapClient() 9 | obj = rdapc.get_domain(domain) 10 | pprint.pprint(obj.data) 11 | 12 | 13 | if __name__ == "__main__": 14 | if len(sys.argv) != 2: 15 | print("Usage: get_domain.py ") 16 | sys.exit(1) 17 | lookup_domain(sys.argv[1]) 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rdap" 3 | version = "1.6.0" 4 | description = "Registration Data Access Protocol tools" 5 | authors = [{ name = "20C", email = "code@20c.com" }] 6 | readme = "README.md" 7 | license = "Apache-2.0" 8 | requires-python = ">= 3.8" 9 | 10 | repository = "https://github.com/20c/rdap" 11 | 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Console", 15 | "License :: OSI Approved :: Apache Software License", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Topic :: Internet", 22 | "Topic :: Software Development", 23 | "Topic :: Software Development :: Libraries", 24 | ] 25 | dependencies = [ 26 | "googlemaps >= 4", 27 | "munge[yaml] >= 1", 28 | "phonenumbers >= 8", 29 | "pydantic >= 2", 30 | "requests >= 2", 31 | "setuptools>=75.1.0", 32 | ] 33 | 34 | 35 | [project.scripts] 36 | rdap = "rdap.cli:main" 37 | 38 | 39 | [project.plugins."markdown.extensions"] 40 | pymdgen = "pymdgen.md:Extension" 41 | 42 | 43 | [project.optional-dependencies] 44 | dev = [ 45 | "coverage >= 7", 46 | "pytest >= 8", 47 | "pytest-filedata >= 1", 48 | "pytest-cov >= 5", 49 | "tox >= 4", 50 | "tox-gh-actions >= 3", 51 | "tox-uv>=1.13.0", 52 | "bandit >= 1", 53 | "mypy >= 1", 54 | "pre-commit >= 3", 55 | "ruff >= 0", 56 | # docs 57 | "markdown >= 3", 58 | "markdown-include >= 0", 59 | "mkdocs >= 1", 60 | ] 61 | 62 | [build-system] 63 | requires = ["hatchling"] 64 | build-backend = "hatchling.build" 65 | 66 | [tool.ruff] 67 | select = [ 68 | "I", # isort 69 | "UP", 70 | ] 71 | 72 | [tool.ruff.lint.per-file-ignores] 73 | "tests/**/*.py" = [ 74 | # at least this three should be fine in tests: 75 | "S101", # asserts allowed in tests... 76 | "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... 77 | "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() 78 | # The below are debateable 79 | "PLR2004", # Magic value used in comparison, ... 80 | "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes 81 | ] 82 | -------------------------------------------------------------------------------- /rdap/__init__.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import get_distribution 2 | 3 | __version__ = get_distribution("rdap").version 4 | 5 | # client to base namespace 6 | from rdap.client import RdapClient 7 | 8 | # exceptions to base namespace 9 | from rdap.exceptions import RdapException, RdapNotFoundError 10 | 11 | # objects to base namespace 12 | from rdap.objects import RdapAsn 13 | 14 | __all__ = [ 15 | "RdapClient", 16 | "RdapException", 17 | "RdapNotFoundError", 18 | "RdapAsn", 19 | ] 20 | -------------------------------------------------------------------------------- /rdap/assignment.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import requests 5 | 6 | 7 | class RIRAssignmentLookup: 8 | """Fetch RIR assignement status lists from ripe and lookup 9 | assignment per asn 10 | 11 | Files will be downloaded from https://ftp.ripe.net/pub/stats/{rir}/delegated-{rir}-extended-latest 12 | """ 13 | 14 | rir_lists = ["afrinic", "apnic", "arin", "lacnic", "ripencc"] 15 | 16 | def parse_data(self, line): 17 | """Parses a line from a data file and attempts to return the ASN 18 | and assignment status 19 | 20 | A line can parse multiple asns depending of the value in 5th 21 | column. 22 | 23 | Returns: 24 | - `None` if no asn and status could be parsed 25 | - `list<`dict`>` containing asns and status 26 | 27 | """ 28 | parts = line.split("|") 29 | 30 | try: 31 | if parts[2] != "asn": 32 | return None 33 | except IndexError: 34 | return None 35 | 36 | try: 37 | asn = parts[3] 38 | count = int(parts[4]) 39 | status = parts[6].strip() 40 | asns = None 41 | 42 | asns = [{"asn": int(asn) + i, "status": status} for i in range(count)] 43 | 44 | return asns 45 | 46 | except IndexError: 47 | return None 48 | 49 | def load_data(self, data_path=".", cache_days=1): 50 | """Reads RIR assignment data into memory 51 | 52 | This is called autoamtically by `get_status` 53 | 54 | This will download assignement status files from ripe if they dont 55 | exist yet or have expired. Initial call of this function will 56 | be signicantly slower than successive calls. 57 | 58 | For best performance it is recommended to use one RIRAssignmentLookup instance 59 | for multiple lookups. 60 | 61 | Argrument(s): 62 | 63 | - data_path (`str`): directory path to where downloaded files are to be saved 64 | - cache_days (`int`): maximum age of downloaded files before they will be 65 | downloaded again 66 | """ 67 | if not hasattr(self, "_data_files"): 68 | self._data_files = [] 69 | 70 | for rir in self.rir_lists: 71 | rir_file_path = os.path.join( 72 | data_path, 73 | f"delegated-{rir}-extended-latest", 74 | ) 75 | self.download_data(rir, rir_file_path, cache_days) 76 | self._data_files.append(rir_file_path) 77 | 78 | if not hasattr(self, "_data"): 79 | self._data = {} 80 | 81 | for rir_file_path in self._data_files: 82 | print(rir_file_path) 83 | with open(rir_file_path) as fh: 84 | for line in fh.read().splitlines(): 85 | asns = self.parse_data(line) 86 | 87 | if not asns: 88 | continue 89 | 90 | try: 91 | for data in asns: 92 | self._data[int(data["asn"])] = data["status"] 93 | except (TypeError, ValueError): 94 | pass 95 | 96 | return self._data 97 | 98 | def download_data(self, rir, file_path, cache_days=1): 99 | """Download RIR network assignment status data from RIPE 100 | https://ftp.ripe.net/pub/stats/{rir}/delegated-{rir}-extended-latesy 101 | """ 102 | # Only download/re-download the file if it doesn't exist or the file is older than a day django_settings.RIR_ALLOCATION_DATA_CACHE_DAYS 103 | if ( 104 | not os.path.exists(file_path) 105 | or ( 106 | datetime.datetime.now() 107 | - datetime.datetime.fromtimestamp(os.path.getmtime(file_path)) 108 | ).days 109 | > cache_days 110 | ): 111 | url = ( 112 | f"https://ftp.ripe.net/pub/stats/{rir}/delegated-{rir}-extended-latest" 113 | ) 114 | response = requests.get(url) 115 | 116 | with open(file_path, "w") as file: 117 | file.write(response.text) 118 | 119 | def get_status(self, asn): 120 | """Get RIR assignment status for an ASN""" 121 | if not hasattr(self, "_data"): 122 | self.load_data() 123 | 124 | return self._data.get(asn) 125 | -------------------------------------------------------------------------------- /rdap/bootstrap.py: -------------------------------------------------------------------------------- 1 | from bisect import bisect_left, bisect_right 2 | 3 | 4 | class AsnService: 5 | """Defines a service URL to lookup an ASN from.""" 6 | 7 | def __init__(self, url, start_asn, end_asn=None): 8 | self.url = url 9 | self.start_asn = start_asn 10 | # use start_asn if it's a single value 11 | self.end_asn = end_asn or start_asn 12 | 13 | def __str__(self): 14 | return f"{self.start_asn}-{self.end_asn} {self.url}" 15 | 16 | 17 | class AsnTree: 18 | """Defines a search tree to find service URLs for ASNs.""" 19 | 20 | def __init__(self, data=None): 21 | self._keys = [] 22 | self._items = [] 23 | 24 | if data: 25 | self.load_data(data) 26 | 27 | def __len__(self): 28 | return len(self._items) 29 | 30 | def __iter__(self): 31 | return iter(self._items) 32 | 33 | def load_data(self, data): 34 | """Loads data from iana format.""" 35 | for service in data["services"]: 36 | # only get primary URL 37 | url = service[1][0].rstrip("/") 38 | for asn_range in service[0]: 39 | self.insert(url, asn_range) 40 | 41 | def insert(self, url, asn_range): 42 | """Insert a service locator record.""" 43 | top = None 44 | try: 45 | bottom, top = map(int, asn_range.split("-")) 46 | except ValueError: 47 | bottom = int(asn_range) 48 | service = AsnService(url, bottom, top) 49 | 50 | i = bisect_left(self._keys, bottom) 51 | self._keys.insert(i, bottom) 52 | self._items.insert(i, service) 53 | return service 54 | 55 | def get_service(self, asn): 56 | """Return service for asn. Raise LookupError if not found.""" 57 | i = bisect_right(self._keys, asn) 58 | # correct record will be the previous one 59 | service = self._items[i - 1] 60 | if service and asn >= service.start_asn and asn <= service.end_asn: 61 | return service 62 | raise KeyError(f"No service found for AS{asn}") 63 | 64 | 65 | # def load_asn_db(directory): 66 | # if 67 | -------------------------------------------------------------------------------- /rdap/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | import munge 5 | import munge.click 6 | 7 | import rdap 8 | from rdap.config import Config 9 | from rdap.context import RdapRequestContext 10 | 11 | 12 | def add_options(parser, options): 13 | for opt in options: 14 | name = opt.pop("name") 15 | 16 | # clicks is_flag 17 | if "is_flag" in opt: 18 | del opt["is_flag"] 19 | opt["action"] = "store_true" 20 | 21 | parser.add_argument(name, **opt) 22 | 23 | 24 | class Context(munge.click.Context): 25 | """command line interface context""" 26 | 27 | app_name = "rdap" 28 | config_class = Config 29 | 30 | 31 | def main(argv=None): 32 | if argv is None: 33 | argv = sys.argv[1:] 34 | 35 | ctx = Context() 36 | 37 | parser = argparse.ArgumentParser(description="rdap") 38 | add_options(parser, Context.option_list()) 39 | parser.add_argument( 40 | "--version", 41 | action="version", 42 | version="{}s version {}".format("%(prog)", rdap.__version__), 43 | ) 44 | parser.add_argument("--output-format", help="output format (yaml, json, text)") 45 | parser.add_argument( 46 | "--show-requests", 47 | action="store_true", 48 | help="show all requests", 49 | ) 50 | parser.add_argument( 51 | "--parse", 52 | action="store_true", 53 | help="parse data into object before display", 54 | ) 55 | parser.add_argument( 56 | "--normalize", 57 | action="store_true", 58 | help="normalize data before display", 59 | ) 60 | parser.add_argument("--rir", action="store_true", help="display rir", default=False) 61 | parser.add_argument( 62 | "--write-bootstrap-data", 63 | action="store_true", 64 | help="write bootstrap data for type (as query)", 65 | ) 66 | 67 | parser.add_argument("query", nargs="+") 68 | args = parser.parse_args(argv) 69 | 70 | # get dict of options and update config 71 | argd = vars(args) 72 | ctx.update_options(argd) 73 | 74 | client = rdap.RdapClient(ctx.config) 75 | output_format = argd.get("output_format") 76 | if not output_format: 77 | output_format = ctx.config.get_nested("rdap", "output_format") 78 | 79 | if argd.get("write_bootstrap_data"): 80 | for each in argd["query"]: 81 | client.write_bootstrap_data(each) 82 | return 0 83 | 84 | codec = munge.get_codec(output_format)() 85 | for each in argd["query"]: 86 | with RdapRequestContext(client=client): 87 | obj = client.get(each) 88 | if argd.get("rir", False): 89 | print(f"rir: {obj.get_rir()}") 90 | if argd.get("parse", False): 91 | print(codec.dumps(obj.parsed())) 92 | elif argd.get("normalize", False): 93 | print(codec.dumps(obj.normalized)) 94 | else: 95 | print(codec.dumps(obj.data)) 96 | 97 | if argd.get("show_requests", False): 98 | print("# Requests") 99 | for each in client.history: 100 | print("{} {}".format(*each)) 101 | 102 | return 0 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /rdap/config.py: -------------------------------------------------------------------------------- 1 | import munge 2 | 3 | 4 | class Config(munge.Config): 5 | """command line interface config""" 6 | 7 | defaults = { 8 | "config": { 9 | "rdap": { 10 | "bootstrap_url": "https://rdap.org/", 11 | "self_bootstrap": False, 12 | "bootstrap_data_url": "https://data.iana.org/rdap/", 13 | # bootstrap dir, defaults to config_dir/bootstrap 14 | "bootstrap_dir": None, 15 | "bootstrap_cache_ttl": 25, 16 | "output_format": "yaml", 17 | "ignore_recurse_errors": False, 18 | "recurse_roles": ["administrative", "technical"], 19 | "lacnic_apikey": None, 20 | "timeout": 5, 21 | }, 22 | }, 23 | "codec": "yaml", 24 | } 25 | -------------------------------------------------------------------------------- /rdap/context.py: -------------------------------------------------------------------------------- 1 | """Contact management for RDAP requests.""" 2 | 3 | from contextvars import ContextVar 4 | from datetime import datetime 5 | from typing import List, Optional 6 | 7 | import pydantic 8 | 9 | from rdap.exceptions import RdapHTTPError 10 | 11 | __all__ = [ 12 | "rdap_request", 13 | "RdapRequestContext", 14 | "RdapRequestState", 15 | ] 16 | 17 | 18 | class RdapSource(pydantic.BaseModel): 19 | """Describes a source of RDAP data.""" 20 | 21 | # urls requested for this source 22 | urls: List[str] = pydantic.Field(default_factory=list) 23 | 24 | # rdap object handle 25 | handle: Optional[str] = None 26 | 27 | # source creation date (if available) 28 | created: Optional[datetime] = None 29 | 30 | # source last update date (if available) 31 | updated: Optional[datetime] = None 32 | 33 | 34 | class RdapRequestState(pydantic.BaseModel): 35 | """Describe the current rdap request, tracking sources queried 36 | and entities retrieved. 37 | """ 38 | 39 | # list of sources for the current request 40 | sources: List[RdapSource] = pydantic.Field(default_factory=list) 41 | 42 | # reference to the rdap client instance 43 | client: Optional[object] = None 44 | 45 | # cache of entities (to avoid duplicate requests to the same entity 46 | # within the current request context) 47 | entities: dict = pydantic.Field(default_factory=dict) 48 | 49 | def update_source( 50 | self, 51 | handle: str, 52 | created: Optional[datetime], 53 | updated: Optional[datetime], 54 | ): 55 | """Update the current source with the handle and dates.""" 56 | self.sources[-1].handle = handle 57 | self.sources[-1].created = created 58 | self.sources[-1].updated = updated 59 | 60 | 61 | # context that holds the currently requested rdap url 62 | 63 | rdap_request = ContextVar("rdap_request", default=RdapRequestState()) 64 | 65 | # context manager to set the rdap url 66 | # can be nested 67 | 68 | 69 | class RdapRequestContext: 70 | """Opens a request context 71 | 72 | If no state is present, a new state is created. 73 | 74 | If a state is present, a new source is added to the state. 75 | """ 76 | 77 | def __init__(self, url: str = None, client: object = None): 78 | self.url = url 79 | self.token = None 80 | self.client = client 81 | 82 | def __enter__(self): 83 | # get existing state 84 | 85 | state = rdap_request.get() 86 | 87 | if state and self.url: 88 | state.sources.append(RdapSource(urls=[self.url])) 89 | else: 90 | state = RdapRequestState( 91 | sources=[RdapSource(urls=[self.url] if self.url else [])], 92 | ) 93 | self.token = rdap_request.set(state) 94 | 95 | if self.client: 96 | state.client = self.client 97 | 98 | return self 99 | 100 | def __exit__(self, *exc): 101 | if self.token: 102 | rdap_request.reset(self.token) 103 | 104 | def push_url(self, url: str): 105 | state = rdap_request.get() 106 | state.sources[-1].urls.append(url) 107 | 108 | def get(self, typ: str, handle: str): 109 | state = rdap_request.get() 110 | client = state.client 111 | 112 | if typ not in ["entity", "ip", "domain", "autnum"]: 113 | raise ValueError(f"Invalid type: {typ}") 114 | 115 | if state.entities.get(handle): 116 | return state.entities[handle] 117 | try: 118 | get = getattr(client, f"get_{typ}") 119 | r_entity = get(handle).normalized 120 | state.entities[handle] = r_entity 121 | return r_entity 122 | except RdapHTTPError: 123 | state.entities[handle] = {} 124 | return {} 125 | -------------------------------------------------------------------------------- /rdap/exceptions.py: -------------------------------------------------------------------------------- 1 | class RdapException(Exception): 2 | """Base exception used by this module.""" 3 | 4 | 5 | class RdapHTTPError(RdapException): 6 | """An HTTP error occurred.""" 7 | 8 | 9 | class RdapNotFoundError(RdapHTTPError): 10 | """RDAP query returned 404 Not Found.""" 11 | -------------------------------------------------------------------------------- /rdap/normalize/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains a set of functions to parse various rdap data properties""" 2 | 3 | import json 4 | from typing import List, Union 5 | 6 | import rdap.schema.normalized as schema 7 | from rdap.context import RdapRequestState, rdap_request 8 | from rdap.normalize import afrinic, apnic, arin, base, lacnic, ripe 9 | from rdap.schema.source import ( 10 | autnum_model, 11 | domain_model, 12 | entity_model, 13 | ip_network_model, 14 | ) 15 | 16 | __all__ = [ 17 | "normalize", 18 | "normalize_autnum", 19 | "normalize_entity", 20 | "normalize_ip", 21 | "normalize_domain", 22 | "get_sources", 23 | ] 24 | 25 | HANDLERS = { 26 | "arin": arin.Handler(), 27 | "ripe": ripe.Handler(), 28 | "apnic": apnic.Handler(), 29 | "afrinic": afrinic.Handler(), 30 | "lacnic": lacnic.Handler(), 31 | # other (verisign for domains etc.) 32 | None: base.Handler(), 33 | } 34 | 35 | 36 | def get_sources( 37 | state: RdapRequestState, 38 | handle: str, 39 | entity: Union[schema.Network, schema.IPNetwork, schema.Domain, schema.Entity], 40 | ) -> List[schema.Source]: 41 | sources = [] 42 | 43 | for source in state.sources: 44 | if not source.urls or not source.handle: 45 | continue 46 | 47 | source = schema.Source( 48 | handle=source.handle, 49 | created=source.created, 50 | updated=source.updated, 51 | urls=source.urls, 52 | ) 53 | 54 | sources.append(source) 55 | 56 | return sources 57 | 58 | 59 | def normalize(data: dict, rir: str, typ: str) -> dict: 60 | """Normalize data based on RIR 61 | 62 | Will return a normalized dict based on the RIR 63 | """ 64 | if typ == "autnum": 65 | return normalize_autnum(data, rir) 66 | if typ == "entity": 67 | return normalize_entity(data, rir) 68 | if typ == "ip": 69 | return normalize_ip(data, rir) 70 | if typ == "domain": 71 | return normalize_domain(data, rir) 72 | raise ValueError(f"Type {typ} not supported") 73 | 74 | 75 | def normalize_autnum(data: dict, rir: str) -> dict: 76 | """Normalize data based on RIR: Autnum 77 | 78 | Will return a normalized dict based on the RIR 79 | """ 80 | handler = HANDLERS.get(rir) 81 | if not handler: 82 | raise ValueError(f"RIR {rir} not supported") 83 | 84 | current_rdap_request = rdap_request.get() 85 | 86 | rdap_autnum = autnum_model(rir)(**data) 87 | 88 | current_rdap_request.update_source( 89 | rdap_autnum.handle, 90 | **handler.dates(rdap_autnum.events), 91 | ) 92 | 93 | org_name = handler.org_name(rdap_autnum) 94 | org = schema.Organization(name=org_name) 95 | 96 | net = schema.Network( 97 | name=rdap_autnum.name, 98 | organization=org, 99 | asn=rdap_autnum.startAutnum, 100 | contacts=handler.contacts(rdap_autnum), 101 | locations=handler.locations(rdap_autnum), 102 | **handler.dates(rdap_autnum.events), 103 | ) 104 | 105 | if current_rdap_request: 106 | net.sources = get_sources(current_rdap_request, rdap_autnum.handle, net) 107 | 108 | return json.loads(net.model_dump_json()) 109 | 110 | 111 | def normalize_ip(data: dict, rir: str) -> dict: 112 | """Normalize data based on RIR: IPNetwork 113 | 114 | Will return a normalized dict based on the RIR 115 | """ 116 | handler = HANDLERS.get(rir) 117 | if not handler: 118 | raise ValueError(f"RIR {rir} not supported") 119 | 120 | current_rdap_request = rdap_request.get() 121 | 122 | rdap_ip_network = ip_network_model(rir)(**data) 123 | 124 | current_rdap_request.update_source( 125 | rdap_ip_network.handle, 126 | **handler.dates(rdap_ip_network.events), 127 | ) 128 | 129 | prefix = handler.prefix(rdap_ip_network) 130 | 131 | net = schema.IPNetwork( 132 | name=rdap_ip_network.name, 133 | prefix=prefix, 134 | parent=handler.parent_prefix(rdap_ip_network), 135 | version=handler.ip_version(rdap_ip_network), 136 | type=rdap_ip_network.type, 137 | # TODO: What happens if more than one status is in there? 138 | status=rdap_ip_network.status[0] if rdap_ip_network.status else None, 139 | contacts=handler.contacts(rdap_ip_network), 140 | **handler.dates(rdap_ip_network.events), 141 | ) 142 | 143 | if current_rdap_request: 144 | net.sources = get_sources(current_rdap_request, rdap_ip_network.handle, net) 145 | 146 | return json.loads(net.model_dump_json()) 147 | 148 | 149 | def normalize_domain(data: dict, rir: str) -> dict: 150 | """Normalize data based on RIR: Domain 151 | 152 | Will return a normalized dict based on the RIR 153 | """ 154 | handler = HANDLERS.get(rir) 155 | if not handler: 156 | raise ValueError(f"RIR {rir} not supported") 157 | 158 | current_rdap_request = rdap_request.get() 159 | 160 | rdap_domain = domain_model(rir)(**data) 161 | 162 | current_rdap_request.update_source( 163 | rdap_domain.handle, 164 | **handler.dates(rdap_domain.events), 165 | ) 166 | 167 | net = schema.Domain( 168 | name=rdap_domain.ldhName, 169 | handle=rdap_domain.handle, 170 | dns_sec=handler.secure_dns(rdap_domain), 171 | contacts=handler.contacts(rdap_domain), 172 | nameservers=handler.nameservers(rdap_domain), 173 | **handler.dates(rdap_domain.events), 174 | ) 175 | 176 | if current_rdap_request: 177 | net.sources = get_sources(current_rdap_request, rdap_domain.handle, net) 178 | 179 | return json.loads(net.model_dump_json()) 180 | 181 | 182 | def normalize_entity(data: dict, rir: str) -> dict: 183 | """Normalize data based on RIR: Entity 184 | 185 | Will return a normalized dict based on the RIR 186 | """ 187 | handler = HANDLERS.get(rir) 188 | if not handler: 189 | raise ValueError(f"RIR {rir} not supported") 190 | 191 | current_rdap_request = rdap_request.get() 192 | 193 | rdap_entity = entity_model(rir)(**data) 194 | 195 | current_rdap_request.update_source( 196 | rdap_entity.handle, 197 | **handler.dates(rdap_entity.events), 198 | ) 199 | 200 | org_name = handler.org_name_from_entity(rdap_entity) 201 | 202 | if org_name: 203 | org = schema.Organization(name=org_name) 204 | else: 205 | org = None 206 | 207 | entity = schema.Entity( 208 | name=rdap_entity.handle, 209 | organization=org, 210 | contacts=handler.contacts_from_entity(rdap_entity), 211 | locations=handler.locations_from_entity(rdap_entity), 212 | **handler.dates(rdap_entity.events), 213 | ) 214 | 215 | if current_rdap_request: 216 | entity.sources = get_sources(current_rdap_request, rdap_entity.handle, entity) 217 | 218 | return json.loads(entity.model_dump_json()) 219 | -------------------------------------------------------------------------------- /rdap/normalize/afrinic.py: -------------------------------------------------------------------------------- 1 | """Some case specific normalization functions for AFRINIC data.""" 2 | 3 | from rdap.normalize import base 4 | 5 | __all__ = [ 6 | "Handler", 7 | ] 8 | 9 | 10 | class Handler(base.Handler): 11 | """No known AFRINIC specific normalizations.""" 12 | -------------------------------------------------------------------------------- /rdap/normalize/apnic.py: -------------------------------------------------------------------------------- 1 | """Some case specific normalization functions for APNIC data.""" 2 | 3 | from typing import Union 4 | 5 | import rdap.schema.rdap as schema 6 | from rdap.normalize import base 7 | 8 | __all__ = [ 9 | "Handler", 10 | ] 11 | 12 | 13 | class Handler(base.Handler): 14 | """APNIC sometimes puts org name into the remarks""" 15 | 16 | def org_name( 17 | self, 18 | entity: Union[schema.AutNum, schema.IPNetwork, schema.Domain], 19 | ) -> Union[str, None]: 20 | """If super() return None or equal to entity.name try checking 21 | remarks for an entry where title == "description" 22 | 23 | TODO: Sometimes description just contains an address and no org name 24 | How to handle this? 25 | """ 26 | org_name = super().org_name(entity) 27 | 28 | if org_name is None or org_name == entity.name: 29 | for remark in entity.remarks: 30 | if remark.title == "description": 31 | org_name = remark.description[0] 32 | break 33 | 34 | return org_name 35 | -------------------------------------------------------------------------------- /rdap/normalize/arin.py: -------------------------------------------------------------------------------- 1 | """Some case specific normalization functions for ARIN data.""" 2 | 3 | from rdap.normalize import base 4 | 5 | __all__ = [ 6 | "Handler", 7 | ] 8 | 9 | 10 | class Handler(base.Handler): 11 | """No known ARIN specific normalizations.""" 12 | -------------------------------------------------------------------------------- /rdap/normalize/geo.py: -------------------------------------------------------------------------------- 1 | """Uses googlemaps to resolve address to geolocation 2 | and into fields 3 | """ 4 | 5 | import os 6 | from datetime import datetime 7 | 8 | import googlemaps 9 | 10 | from rdap.context import RdapRequestState, rdap_request 11 | from rdap.schema.normalized import GeoLocation, Location 12 | 13 | GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY") 14 | 15 | 16 | class RequestError(Exception): 17 | pass 18 | 19 | 20 | class Timeout(Exception): 21 | pass 22 | 23 | 24 | class NotFound(KeyError): 25 | pass 26 | 27 | 28 | class GoogleKeyNotSet(Exception): 29 | pass 30 | 31 | 32 | def get_client(key: str = GOOGLE_MAPS_API_KEY): 33 | if not key: 34 | raise GoogleKeyNotSet("Google Maps API Key not set") 35 | 36 | return googlemaps.Client(key) 37 | 38 | 39 | def lookup(formatted_address: str, client=None) -> dict: 40 | """Return the latitude, longitude field values of the specified 41 | location. 42 | """ 43 | request: RdapRequestState = rdap_request.get() 44 | 45 | key = f"geo:{formatted_address}" 46 | 47 | if key in request.entities: 48 | return request.entities[key] 49 | 50 | if not client: 51 | client = get_client() 52 | 53 | try: 54 | result = client.geocode( 55 | formatted_address, 56 | ) 57 | except ( 58 | googlemaps.exceptions.HTTPError, 59 | googlemaps.exceptions.ApiError, 60 | googlemaps.exceptions.TransportError, 61 | ) as exc: 62 | raise RequestError(exc) 63 | except googlemaps.exceptions.Timeout: 64 | raise Timeout 65 | 66 | if not result: 67 | raise NotFound 68 | 69 | # cache to avoid duplicate lookups during the same 70 | # request context 71 | request.entities[key] = result[0] 72 | 73 | return result[0] 74 | 75 | 76 | def normalize(formatted_address: str, date: datetime = None, client=None) -> Location: 77 | """Takes a formatted address and returns a normalized location object""" 78 | try: 79 | result = lookup(formatted_address, client) 80 | except (GoogleKeyNotSet, NotFound): 81 | # If a google maps key is not set, return a location object with 82 | # only the address field set 83 | return Location( 84 | updated=date, 85 | address=formatted_address, 86 | ) 87 | 88 | city = None 89 | postal_code = None 90 | country = None 91 | floor = None 92 | suite = None 93 | 94 | address_components = result.get("address_components", []) 95 | print("Address components:", address_components) 96 | 97 | for component in result.get("address_components", []): 98 | types = component.get("types", []) 99 | print("Component:", component) 100 | if "country" in types: 101 | country = component.get("short_name") 102 | 103 | if "postal_code" in types: 104 | postal_code = component.get("long_name") 105 | 106 | if "locality" in types or "postal_town" in types: 107 | city = component.get("long_name") 108 | 109 | if "floor" in types: 110 | floor = component.get("long_name") 111 | 112 | if "subpremise" in types: 113 | suite = component.get("long_name") 114 | 115 | return Location( 116 | updated=date, 117 | country=country, 118 | city=city, 119 | postal_code=postal_code, 120 | floor=floor, 121 | suite=suite, 122 | address=result.get("formatted_address"), 123 | geo=GeoLocation( 124 | latitude=result.get("geometry").get("location").get("lat"), 125 | longitude=result.get("geometry").get("location").get("lng"), 126 | ), 127 | ) 128 | -------------------------------------------------------------------------------- /rdap/normalize/lacnic.py: -------------------------------------------------------------------------------- 1 | """Some case specific normalization functions for LACNIC data.""" 2 | 3 | from rdap.normalize import base 4 | 5 | __all__ = [ 6 | "Handler", 7 | ] 8 | 9 | 10 | class Handler(base.Handler): 11 | """No known LACNIC specific normalizations.""" 12 | -------------------------------------------------------------------------------- /rdap/normalize/ripe.py: -------------------------------------------------------------------------------- 1 | """Some case specific normalization functions for RIPE data.""" 2 | 3 | from typing import Union 4 | 5 | import rdap.schema.rdap as schema 6 | from rdap.normalize import base 7 | 8 | __all__ = [ 9 | "Handler", 10 | ] 11 | 12 | 13 | class Handler(base.Handler): 14 | """RIPE sometimes puts org name into the remarks""" 15 | 16 | def org_name( 17 | self, 18 | entity: Union[schema.AutNum, schema.IPNetwork, schema.Domain], 19 | ) -> Union[str, None]: 20 | """If super() return None or equal to entity.name try checking 21 | remarks for an entry where title == "description" 22 | """ 23 | org_name = super().org_name(entity) 24 | 25 | if org_name is None or org_name == entity.name: 26 | for remark in entity.remarks: 27 | if remark.title == "description": 28 | org_name = remark.description[0] 29 | break 30 | 31 | return org_name 32 | -------------------------------------------------------------------------------- /rdap/objects.py: -------------------------------------------------------------------------------- 1 | from rdap.exceptions import RdapHTTPError, RdapNotFoundError 2 | from rdap.normalize import normalize 3 | 4 | 5 | def rir_from_domain(domain): 6 | """Gets the RIR from a URL or domain, if possible""" 7 | try: 8 | for rir in ["arin", "apnic", "afrinic", "lacnic", "ripe"]: 9 | if rir in domain: 10 | return rir 11 | 12 | if "nic.br" in domain: 13 | return "lacnic" 14 | 15 | except Exception: 16 | return None 17 | 18 | 19 | class RdapObject: 20 | """RDAP base object, allows for lazy parsing""" 21 | 22 | def __init__(self, data, rdapc=None): 23 | self._rdapc = rdapc 24 | self._data = data 25 | self._parsed = dict() 26 | 27 | @property 28 | def data(self): 29 | return self._data 30 | 31 | @property 32 | def name(self): 33 | return self.parsed()["name"] 34 | 35 | @property 36 | def handle(self): 37 | return self._data["handle"] 38 | 39 | @property 40 | def emails(self): 41 | return self.parsed()["emails"] 42 | 43 | @property 44 | def org_name(self): 45 | return self.parsed()["org_name"] 46 | 47 | @property 48 | def org_address(self): 49 | return self.parsed()["org_address"] 50 | 51 | @property 52 | def kind(self): 53 | return self.parsed()["kind"] 54 | 55 | def parsed(self): 56 | """Returns parsed dict""" 57 | if not self._parsed: 58 | self._parse() 59 | return self._parsed 60 | 61 | def _parse_vcard(self, data): 62 | """Iterates over current level's vcardArray and gets data""" 63 | vcard = dict() 64 | 65 | for row in data.get("vcardArray", [0])[1:]: 66 | for typ in row: 67 | if typ[0] in ["version"]: 68 | continue 69 | if typ[0] == "email": 70 | vcard.setdefault("emails", set()).add(typ[3].strip().lower()) 71 | elif typ[0] == "fn" or typ[0] == "kind": 72 | vcard[typ[0]] = typ[3].strip() 73 | elif typ[0] == "adr": 74 | # WORKAROUND ARIN uses label in the extra field 75 | adr = typ[1].get("label", "").strip() 76 | if not adr: 77 | # rest use the text field 78 | adr = "\n".join([str(item) for item in typ[3]]).strip() 79 | if adr: 80 | vcard["adr"] = adr 81 | return vcard 82 | 83 | def _parse_entity_self_link(self, entity): 84 | for link in entity.get("links", []): 85 | if link["rel"] == "self": 86 | return link["href"] 87 | return None 88 | 89 | def _parse(self): 90 | """Parses data into our format, and use entities for address info ?""" 91 | name = self._data.get("name", "") 92 | # emails done with a set to eat duplicates 93 | emails = set() 94 | org_name = "" 95 | org_address = "" 96 | org_name_final = False 97 | org_address_final = False 98 | 99 | for ent in self._data.get("entities", []): 100 | vcard = self._parse_vcard(ent) 101 | emails |= vcard.get("emails", set()) 102 | roles = ent.get("roles", []) 103 | kind = vcard.get("kind", "") 104 | handle = ent.get("handle", None) 105 | # try for link to 'self', if registry doesn't supply it, fall back to creating it. 106 | handle_url = self._parse_entity_self_link(ent) 107 | if not handle_url: 108 | handle_url = self._rdapc.get_entity_url(handle) 109 | 110 | if "registrant" in roles: 111 | if "fn" in vcard and not org_name_final: 112 | org_name = vcard["fn"] 113 | if "org" in kind: 114 | org_name_final = True 115 | if "adr" in vcard and not org_address_final: 116 | org_address = vcard["adr"] 117 | if "org" in kind: 118 | org_address_final = True 119 | 120 | # check nested entities 121 | for nent in ent.get("entities", []): 122 | vcard = self._parse_vcard(nent) 123 | emails |= vcard.get("emails", set()) 124 | 125 | # if role is in settings to recurse, try to do a lookup 126 | if handle and self._rdapc: 127 | if not self._rdapc.recurse_roles.isdisjoint(roles): 128 | try: 129 | rdata = self._rdapc.get_data(handle_url) 130 | vcard = self._parse_vcard(rdata) 131 | emails |= vcard.get("emails", set()) 132 | 133 | # check for HTTP Errors to ignore 134 | except RdapHTTPError: 135 | if not self._rdapc.config.get("ignore_recurse_errors"): 136 | raise 137 | 138 | # WORKAROUND APNIC keeps org info in remarks 139 | if "apnic" in self._data.get("port43", ""): 140 | try: 141 | for rem in self._data["remarks"]: 142 | if rem["title"] == "description": 143 | if org_name: 144 | org_name += ", " 145 | org_name += rem["description"][0] 146 | break 147 | except KeyError: 148 | pass 149 | 150 | # RIPE keeps org info in remarks 151 | elif "ripe" in self._data.get("port43", ""): 152 | try: 153 | for rem in self._data["remarks"]: 154 | if rem["description"]: 155 | if org_name: 156 | org_name += ", " 157 | org_name += rem["description"][0] 158 | break 159 | except KeyError: 160 | pass 161 | 162 | self._parsed = dict( 163 | name=name, 164 | emails=sorted(emails), 165 | org_name=org_name, 166 | org_address=org_address, 167 | ) 168 | 169 | def get_rir(self): 170 | """Gets the RIR for the object, if possible""" 171 | try: 172 | if "port43" in self._data: 173 | if rir := rir_from_domain(self._data.get("port43")): 174 | return rir 175 | 176 | if self._rdapc and self._rdapc.last_req_url: 177 | if rir := rir_from_domain(self._rdapc.last_req_url): 178 | return rir 179 | 180 | except Exception: 181 | return None 182 | 183 | 184 | class RdapAsn(RdapObject): 185 | """access interface for lazy parsing of RDAP looked up aut-num objects""" 186 | 187 | def __init__(self, data, rdapc=None): 188 | # check for ASN range, meaning it's delegated and unallocated 189 | if data: 190 | start = data.get("startAutnum", None) 191 | end = data.get("endAutnum", None) 192 | if start and end and start != end: 193 | raise RdapNotFoundError( 194 | f"Query returned a block ({start} - {end}), AS is reported not allocated", 195 | ) 196 | 197 | super().__init__(data, rdapc) 198 | 199 | @property 200 | def normalized(self) -> dict: 201 | return normalize(self._data, self.get_rir(), "autnum") 202 | 203 | 204 | class RdapNetwork(RdapObject): 205 | def __init__(self, data, rdapc=None): 206 | super().__init__(data, rdapc) 207 | 208 | @property 209 | def normalized(self) -> dict: 210 | return normalize(self._data, self.get_rir(), "ip") 211 | 212 | 213 | class RdapDomain(RdapObject): 214 | def __init__(self, data, rdapc=None): 215 | super().__init__(data, rdapc) 216 | 217 | @property 218 | def normalized(self) -> dict: 219 | return normalize(self._data, self.get_rir(), "domain") 220 | 221 | 222 | class RdapEntity(RdapObject): 223 | def __init__(self, data, rdapc=None): 224 | super().__init__(data, rdapc) 225 | 226 | @property 227 | def normalized(self) -> dict: 228 | return normalize(self._data, self.get_rir(), "entity") 229 | -------------------------------------------------------------------------------- /rdap/schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/20c/rdap/bc567bfc3270fc8c131121b9e66528570f263070/rdap/schema/__init__.py -------------------------------------------------------------------------------- /rdap/schema/normalized.py: -------------------------------------------------------------------------------- 1 | """Pydantic schemas for normalized RDAP data""" 2 | 3 | import enum 4 | import ipaddress 5 | from datetime import datetime 6 | from typing import Any, List, Optional, Union 7 | 8 | import pydantic 9 | 10 | __all__ = [ 11 | "IP_VERSION", 12 | "STATUS", 13 | "ROLE", 14 | "DNSSEC", 15 | "GeoLocation", 16 | "Location", 17 | "Contact", 18 | "Source", 19 | "Organization", 20 | "Network", 21 | "IPNetwork", 22 | "Entity", 23 | "Nameserver", 24 | "Domain", 25 | ] 26 | 27 | 28 | class IP_VERSION(int, enum.Enum): 29 | """Enum for IP version""" 30 | 31 | ipv4 = 4 32 | ipv6 = 6 33 | 34 | 35 | class STATUS(str, enum.Enum): 36 | active = "active" 37 | inactive = "inactive" 38 | 39 | 40 | NORMALIZED_STATUS = { 41 | "administrative": "active", 42 | "validated": "active", 43 | } 44 | 45 | 46 | class ROLE(str, enum.Enum): 47 | abuse = "abuse" 48 | admin = "admin" 49 | policy = "policy" 50 | technical = "technical" 51 | registrant = "registrant" 52 | billing = "billing" 53 | sponsor = "sponsor" 54 | 55 | 56 | NORMALIZED_ROLES = { 57 | "administrative": "admin", 58 | "noc": "technical", 59 | "registrar": "registrant", 60 | "routing": "technical", 61 | "dns": "technical", 62 | } 63 | 64 | 65 | VALID_ROLES = [str(r) for r in ROLE] 66 | 67 | 68 | class DNSSEC(str, enum.Enum): 69 | secure = "secure" 70 | insecure = "insecure" 71 | unknown = "unknown" 72 | 73 | 74 | class GeoLocation(pydantic.BaseModel): 75 | """Describes geographic coordinates""" 76 | 77 | latitude: float 78 | longitude: float 79 | 80 | 81 | class Location(pydantic.BaseModel): 82 | """Describes a location""" 83 | 84 | updated: Optional[datetime] = None 85 | country: Optional[str] = None 86 | city: Optional[str] = None 87 | postal_code: Optional[str] = None 88 | address: Optional[str] = None 89 | geo: Optional[GeoLocation] = None 90 | floor: Optional[str] = None 91 | suite: Optional[str] = None 92 | 93 | def __hash__(self): 94 | return f"{self.address}-{self.city}-{self.country}-{self.postal_code}-{self.floor}-{self.suite}".__hash__() 95 | 96 | 97 | class Contact(pydantic.BaseModel): 98 | """Describes a point of contact""" 99 | 100 | created: Optional[datetime] = None 101 | updated: Optional[datetime] = None 102 | name: str 103 | roles: List[ROLE] = pydantic.Field(default_factory=list) 104 | phone: Optional[str] = None 105 | email: Optional[str] = None 106 | 107 | @pydantic.model_validator(mode="before") 108 | @classmethod 109 | def normalize_roles(cls, data: Any) -> Any: 110 | roles = [] 111 | 112 | for role in data.get("roles", []): 113 | role = NORMALIZED_ROLES.get(role, role) 114 | roles.append(role) 115 | 116 | data["roles"] = roles 117 | 118 | # drop duplicates 119 | data["roles"] = list(set(data["roles"])) 120 | 121 | # drop any invalid roles 122 | data["roles"] = [ 123 | role for role in data["roles"] if f"ROLE.{role}" in VALID_ROLES 124 | ] 125 | 126 | return data 127 | 128 | def __hash__(self): 129 | return f"{self.name}-{self.email}-{self.phone}: {self.roles}".__hash__() 130 | 131 | 132 | class Source(pydantic.BaseModel): 133 | """Describes a source of rdap data 134 | 135 | Will contain where the data was fetched from and when 136 | """ 137 | 138 | created: Optional[datetime] = None 139 | updated: Optional[datetime] = None 140 | handle: str 141 | urls: List[str] = pydantic.Field(default_factory=list) 142 | description: Optional[str] = None 143 | 144 | 145 | class Organization(pydantic.BaseModel): 146 | """Describes an organization""" 147 | 148 | name: str 149 | 150 | 151 | class Network(pydantic.BaseModel): 152 | """Describes a network""" 153 | 154 | created: Optional[datetime] = None 155 | updated: Optional[datetime] = None 156 | asn: int 157 | name: str 158 | organization: Organization 159 | locations: List[Location] = pydantic.Field(default_factory=list) 160 | contacts: List[Contact] = pydantic.Field(default_factory=list) 161 | sources: List[Source] = pydantic.Field(default_factory=list) 162 | 163 | 164 | class IPNetwork(pydantic.BaseModel): 165 | """Describes an IP network""" 166 | 167 | created: Optional[datetime] = None 168 | updated: Optional[datetime] = None 169 | prefix: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] = None 170 | version: Optional[IP_VERSION] = None 171 | name: Optional[str] = None 172 | type: Optional[str] = None 173 | status: STATUS 174 | parent: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] = None 175 | contacts: List[Contact] = pydantic.Field(default_factory=list) 176 | sources: List[Source] = pydantic.Field(default_factory=list) 177 | 178 | @pydantic.model_validator(mode="before") 179 | @classmethod 180 | def normalize_status(cls, data: Any) -> Any: 181 | status = data.get("status") 182 | 183 | if status: 184 | data["status"] = NORMALIZED_STATUS.get(status, status) 185 | 186 | return data 187 | 188 | 189 | class Entity(pydantic.BaseModel): 190 | """Describes an entity""" 191 | 192 | created: Optional[datetime] = None 193 | updated: Optional[datetime] = None 194 | name: str 195 | organization: Optional[Organization] = None 196 | locations: List[Location] = pydantic.Field(default_factory=list) 197 | contacts: List[Contact] = pydantic.Field(default_factory=list) 198 | sources: List[Source] = pydantic.Field(default_factory=list) 199 | 200 | 201 | class Nameserver(pydantic.BaseModel): 202 | """Describes a nameserver""" 203 | 204 | host: str 205 | 206 | 207 | class Domain(pydantic.BaseModel): 208 | """Describes a domain""" 209 | 210 | created: Optional[datetime] = None 211 | updated: Optional[datetime] = None 212 | name: str 213 | handle: str 214 | dns_sec: DNSSEC 215 | nameservers: List[Nameserver] = pydantic.Field(default_factory=list) 216 | contacts: List[Contact] = pydantic.Field(default_factory=list) 217 | sources: List[Source] = pydantic.Field(default_factory=list) 218 | -------------------------------------------------------------------------------- /rdap/schema/rdap.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, List, Optional, Union 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | __all__ = [ 7 | "Link", 8 | "Event", 9 | "Notice", 10 | "VCardValue", 11 | "Remark", 12 | "Entity", 13 | "IPNetwork", 14 | ] 15 | 16 | 17 | class Link(BaseModel): 18 | """Represents a hyperlink in the RDAP response.""" 19 | 20 | # The label or description of the link 21 | value: Optional[str] = None 22 | # The relationship of the link to the current object 23 | rel: Optional[str] = None 24 | # The MIME type of the target resource 25 | type: Optional[str] = None 26 | # The URL of the link 27 | href: Optional[str] = None 28 | 29 | 30 | class Event(BaseModel): 31 | """Represents a timestamped event in the lifecycle of an RDAP object.""" 32 | 33 | # The type of event (e.g., "registration", "last changed") 34 | eventAction: Optional[str] = None 35 | # The date and time of the event 36 | eventDate: Optional[datetime] = None 37 | 38 | 39 | class Notice(BaseModel): 40 | """Represents a notice or message in the RDAP response.""" 41 | 42 | # The title of the notice 43 | title: Optional[str] = None 44 | # A list of text lines comprising the notice 45 | description: List[str] = Field(default_factory=list) 46 | # Optional links related to the notice 47 | links: List[Link] = Field(default_factory=list) 48 | 49 | 50 | class VCardValue(BaseModel): 51 | """Represents additional properties for vCard values.""" 52 | 53 | # Types associated with the vCard value (e.g., "work", "voice" for telephone) 54 | type: Optional[List[str]] = None 55 | 56 | 57 | class Remark(BaseModel): 58 | """Represents a remark or comment in the RDAP response.""" 59 | 60 | # The title of the remark 61 | title: Optional[str] = None 62 | # A list of text lines comprising the remark 63 | description: List[str] = Field(default_factory=list) 64 | 65 | 66 | class Entity(BaseModel): 67 | """Represents an entity (organization, individual, or role) in the RDAP response.""" 68 | 69 | # A unique identifier for the entity 70 | handle: str = Field(default_factory=str) 71 | # Contact information in vCard format 72 | vcardArray: List[Union[str, List[List[Union[str, dict, list, None]]]]] = Field( 73 | default_factory=list, 74 | ) 75 | # Roles of the entity (e.g., registrant, technical, administrative) 76 | roles: List[str] = Field(default_factory=list) 77 | # Links related to the entity 78 | links: List[Link] = Field(default_factory=list) 79 | # Events associated with the entity 80 | events: List[Event] = Field(default_factory=list) 81 | # Status of the entity 82 | status: List[str] = Field(default_factory=list) 83 | # WHOIS server for the entity 84 | port43: str = Field(default_factory=str) 85 | # Type of the object (always "entity" for Entity) 86 | objectClassName: str 87 | # Additional remarks about the entity 88 | remarks: List[Remark] = Field(default_factory=list) 89 | # Nested entities (e.g., contacts within an organization) 90 | entities: List["Entity"] = Field(default_factory=list) 91 | 92 | @property 93 | def self_link(self) -> Optional[str]: 94 | """Returns the href of the link where rel == 'self'""" 95 | for link in self.links: 96 | if link.rel == "self": 97 | return link.href 98 | return None 99 | 100 | 101 | class IPNetwork(BaseModel): 102 | """Represents an IP network in the RDAP response.""" 103 | 104 | # list of conformance levels 105 | rdapConformance: List[str] = Field(default_factory=list) 106 | # Notices related to the IP network 107 | notices: List[Notice] = Field(default_factory=list) 108 | # A unique identifier for the IP network 109 | handle: Optional[str] = None 110 | # The first IP address in the network range 111 | startAddress: Optional[str] = None 112 | # The last IP address in the network range 113 | endAddress: Optional[str] = None 114 | # IP version (v4 or v6) 115 | ipVersion: Optional[str] = None 116 | # Name of the network 117 | name: Optional[str] = None 118 | # Type of the network allocation 119 | type: Optional[str] = None 120 | # Handle of the parent network 121 | parentHandle: Optional[str] = None 122 | # Additional remarks about the network 123 | remarks: List[Remark] = Field(default_factory=list) 124 | # Events associated with the network 125 | events: List[Event] = Field(default_factory=list) 126 | # Links related to the network 127 | links: List[Link] = Field(default_factory=list) 128 | # Entities associated with the network 129 | entities: List[Entity] = Field(default_factory=list) 130 | # WHOIS server for the network 131 | port43: Optional[str] = None 132 | # Status of the network 133 | status: List[str] = Field(default_factory=list) 134 | # Type of the object (always "ip network" for IPNetwork) 135 | objectClassName: Optional[str] = None 136 | # CIDR notation for the network 137 | cidr0_cidrs: List[Dict] = Field(default_factory=list) 138 | # Origin AS numbers for the network 139 | arin_originas0_originautnums: List = Field(default_factory=list) 140 | 141 | 142 | class DSData(BaseModel): 143 | """Represents DS data for secure DNS in the RDAP response.""" 144 | 145 | # Key tag for the DS record 146 | keyTag: Optional[int] = None 147 | # Algorithm number for the DS record 148 | algorithm: Optional[int] = None 149 | # Digest type for the DS record 150 | digestType: Optional[int] = None 151 | # Digest value for the DS record 152 | digest: Optional[str] = None 153 | 154 | 155 | class SecureDNS(BaseModel): 156 | # true if there are DS records in the parent, false otherwise. 157 | delegationSigned: Optional[bool] = None 158 | # if the zone has been signed, false otherwise. 159 | zeroSigned: Optional[bool] = None 160 | # DS data for secure DNS 161 | dsData: List[DSData] = Field(default_factory=list) 162 | 163 | 164 | class Nameserver(BaseModel): 165 | objectClassName: Optional[str] = None 166 | ldhName: Optional[str] = None 167 | unicodeName: Optional[str] = None 168 | ipAddresses: Dict[str, List[str]] = Field(default_factory=dict) 169 | remarks: List[Remark] = Field(default_factory=list) 170 | port43: Optional[str] = None 171 | events: List[Event] = Field(default_factory=list) 172 | 173 | 174 | class Domain(BaseModel): 175 | """Represents a domain name in the RDAP response.""" 176 | 177 | # list of conformance levels 178 | rdapConformance: List[str] = Field(default_factory=list) 179 | # Notices related to the domain 180 | notices: List[Notice] = Field(default_factory=list) 181 | # A unique identifier for the domain 182 | handle: Optional[str] = None 183 | # The domain name in LDH (Letter Digit Hyphen) format 184 | ldhName: Optional[str] = None 185 | # Events associated with the domain 186 | events: List[Event] = Field(default_factory=list) 187 | # Links related to the domain 188 | links: List[Link] = Field(default_factory=list) 189 | # Entities associated with the domain 190 | entities: List[Entity] = Field(default_factory=list) 191 | # WHOIS server for the domain 192 | port43: str = Field(default_factory=str) 193 | # Network information for the domain 194 | network: Optional[IPNetwork] = None 195 | # Type of the object (always "domain" for Domain) 196 | objectClassName: Optional[str] = None 197 | 198 | secureDNS: Optional[SecureDNS] = None 199 | 200 | nameservers: List[Nameserver] = Field(default_factory=list) 201 | 202 | 203 | class AutNum(BaseModel): 204 | """Represents an Autonomous System Number in the RDAP response.""" 205 | 206 | # list of conformance levels 207 | rdapConformance: List[str] = Field(default_factory=list) 208 | # Notices related to the AS number 209 | notices: List[Notice] = Field(default_factory=list) 210 | # A unique identifier for the AS number 211 | handle: Optional[str] = None 212 | # The starting AS number in the range 213 | startAutnum: Optional[int] = None 214 | # The ending AS number in the range (same as startAutnum for single AS) 215 | endAutnum: Optional[int] = None 216 | # Name of the AS 217 | name: Optional[str] = None 218 | # WHOIS server for the AS number 219 | port43: Optional[str] = None 220 | 221 | # Type of the object (always "autnum" for AutNum) 222 | objectClassName: Optional[str] = None 223 | 224 | # Events associated with the AS number 225 | events: List[Event] = Field(default_factory=list) 226 | # Links related to the AS number 227 | links: List[Link] = Field(default_factory=list) 228 | # Entities associated with the AS number 229 | entities: List[Entity] = Field(default_factory=list) 230 | # Status of the AS number 231 | status: List[str] = Field(default_factory=list) 232 | 233 | # Remarks about the AS number 234 | remarks: List[Remark] = Field(default_factory=list) 235 | 236 | 237 | Entity.model_rebuild() 238 | -------------------------------------------------------------------------------- /rdap/schema/source.py: -------------------------------------------------------------------------------- 1 | from rdap.schema import rdap 2 | 3 | __all__ = [ 4 | "SCHEMAS_BY_RIR", 5 | "ip_network_model", 6 | "domain_model", 7 | "autnum_model", 8 | "entity_model", 9 | ] 10 | 11 | SCHEMAS_BY_RIR = { 12 | "arin": rdap, 13 | "ripe": rdap, 14 | "apnic": rdap, 15 | "afrinic": rdap, 16 | "lacnic": rdap, 17 | None: rdap, 18 | } 19 | 20 | 21 | def ip_network_model(rir: str) -> rdap.IPNetwork: 22 | """Returns pydantic model for IPNetwork for the given RIR 23 | 24 | Arguments: 25 | - rir: str: RIR name (e.g., "arin", "ripe", "apnic", "afrinic", "lacnic") 26 | 27 | """ 28 | return SCHEMAS_BY_RIR[rir].IPNetwork 29 | 30 | 31 | def domain_model(rir: str) -> rdap.Domain: 32 | """Returns pydantic model for Domain for the given RIR 33 | 34 | Arguments: 35 | - rir: str: RIR name (e.g., "arin", "ripe", "apnic", "afrinic", "lacnic") 36 | 37 | """ 38 | return SCHEMAS_BY_RIR[rir].Domain 39 | 40 | 41 | def autnum_model(rir: str) -> rdap.AutNum: 42 | """Returns pydantic model for AutNum for the given RIR 43 | 44 | Arguments: 45 | - rir: str: RIR name (e.g., "arin", "ripe", "apnic", "afrinic", "lacnic") 46 | 47 | """ 48 | return SCHEMAS_BY_RIR[rir].AutNum 49 | 50 | 51 | def entity_model(rir: str) -> rdap.Entity: 52 | """Returns pydantic model for Entity for the given RIR 53 | 54 | Arguments: 55 | - rir: str: RIR name (e.g., "arin", "ripe", "apnic", "afrinic", "lacnic") 56 | 57 | """ 58 | return SCHEMAS_BY_RIR[rir].Entity 59 | -------------------------------------------------------------------------------- /script/mock_update_autnum.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | 6 | from rdap import RdapClient 7 | 8 | 9 | def rdap_autnum_file_list(directory): 10 | input_file_list = [] 11 | expected_file_list = [] 12 | for filename in os.listdir(directory): 13 | if os.path.isfile(os.path.join(directory, filename)) and filename.endswith( 14 | ".input", 15 | ): 16 | input_file_list.append(filename) 17 | elif os.path.isfile(os.path.join(directory, filename)) and filename.endswith( 18 | ".expected", 19 | ): 20 | expected_file_list.append(filename) 21 | return input_file_list, expected_file_list 22 | 23 | 24 | def update_autnum_entries(rdapc, file_name, filepath): 25 | asn = int(file_name.split(".")[0]) 26 | rdap_data = rdapc.get_asn(asn) 27 | full_filepath = os.path.join(filepath, file_name) 28 | 29 | with open(full_filepath, "w") as output_file: 30 | if file_name.split(".")[-1] == "input": 31 | formatted_data = json.dumps(rdap_data.data, indent=2) 32 | output_file.write(str(formatted_data)) 33 | else: 34 | formatted_parsed = json.dumps(rdap_data.parsed(), indent=2) 35 | output_file.write(str(formatted_parsed)) 36 | 37 | print(f"RDAP data for ASN {asn} written to {full_filepath}") 38 | 39 | 40 | def main(include_expected=False): 41 | path_autnum = "./tests/data/rdap/autnum/" 42 | input_file_list, expected_file_list = rdap_autnum_file_list(path_autnum) 43 | rdapc = RdapClient() 44 | for file in input_file_list: 45 | update_autnum_entries(rdapc, file, path_autnum) 46 | if include_expected: 47 | for file in expected_file_list: 48 | update_autnum_entries(rdapc, file, path_autnum) 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser(description="Script Description") 53 | 54 | # Add the --include-expected option 55 | parser.add_argument( 56 | "--include-expected", 57 | action="store_true", 58 | help="Include expected", 59 | ) 60 | 61 | args = parser.parse_args() 62 | 63 | # Call the main function with the value of --include-expected 64 | main(include_expected=args.include_expected) 65 | 66 | sys.exit() 67 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | import pytest_filedata 6 | 7 | import rdap 8 | 9 | 10 | def _this_dir(): 11 | """Returns dirname for location of this file 12 | py.test no longer allows fixtures to be called 13 | directly so we provide a private function that can be 14 | """ 15 | return os.path.dirname(__file__) 16 | 17 | 18 | @pytest.fixture 19 | def this_dir(): 20 | return _this_dir() 21 | 22 | 23 | pytest_filedata.setup(_this_dir()) 24 | 25 | 26 | def pytest_generate_tests(metafunc): 27 | for fixture in metafunc.fixturenames: 28 | if fixture.startswith("data_"): 29 | data = pytest_filedata.get_data(fixture) 30 | metafunc.parametrize(fixture, list(data.values()), ids=list(data.keys())) 31 | 32 | 33 | @pytest.fixture 34 | def iana_asn(): 35 | asn_file = os.path.join(_this_dir(), "data/iana/bootstrap/asn.json") 36 | with open(asn_file) as fh: 37 | data = json.load(fh) 38 | return data 39 | 40 | 41 | @pytest.fixture 42 | def rdapc(): 43 | return rdap.RdapClient({"timeout": 10}) 44 | -------------------------------------------------------------------------------- /tests/data/assignment/delegated-afrinic-extended-latest: -------------------------------------------------------------------------------- 1 | afrinic|ZA|asn|7231|1|19961014|allocated|F36AEAB9 2 | afrinic|ZA|asn|7390|1|19961127|allocated|F368C804 3 | afrinic|ZM|asn|7420|1|19961206|allocated|F367AA74 4 | afrinic|ZA|asn|7460|1|19961218|allocated|F36A27E6 5 | afrinic|ZA|asn|7971|1|19970307|allocated|F3686F76 6 | afrinic|ZA|asn|7972|1|19970307|allocated|F36AB59A 7 | afrinic|ZA|asn|8094|1|19970409|allocated|F36C3DA1 8 | afrinic|EG|asn|8524|1|20070920|allocated|F362AF0D 9 | afrinic|ZZ|asn|8770|1||available| 10 | -------------------------------------------------------------------------------- /tests/data/assignment/delegated-apnic-extended-latest: -------------------------------------------------------------------------------- 1 | apnic||ipv6|240f:d000::|20||available| 2 | apnic||ipv6|240f:e000::|19||available| 3 | apnic|JP|asn|173|1|20020801|allocated|A91E66F2 4 | apnic|NZ|asn|681|1|20020801|allocated|A91D9208 5 | apnic|AU|asn|1221|1|20000131|allocated|A916A983 6 | apnic|JP|asn|1233|1|20020801|allocated|A91E66F2 7 | apnic|KR|asn|1237|1|20020801|allocated|A9149F3E 8 | apnic|SG|asn|1250|1|20020801|allocated|A919DB08 9 | apnic|TW|asn|1659|1|20020801|allocated|A91BDB29 10 | apnic|KR|asn|1704|1|20020801|allocated|A9149F3E 11 | apnic|TW|asn|1768|2|20020801|allocated|A91BDB29 12 | apnic|KR|asn|1781|1|20020801|allocated|A9149F3E 13 | -------------------------------------------------------------------------------- /tests/data/assignment/delegated-arin-extended-latest: -------------------------------------------------------------------------------- 1 | arin|US|asn|63306|1|20141114|assigned|4fc3f6134ecad88aa675d972baace9ea 2 | arin|US|asn|63307|1|20141117|assigned|9ac07fc54e1d68750f403d8a98d89fe4 3 | arin|PR|asn|63308|1|20141117|assigned|15ac6d0042bf51de4b7004765215a969 4 | arin|US|asn|63309|1|20141117|assigned|f27849ed149430f551c780591fd1d5fa 5 | arin|US|asn|63310|1|20141117|assigned|59567d838a43cb370681d16e2cb7d001 6 | arin|US|asn|63311|1|20141117|assigned|cb67e421570e9e1e132ff34313bd22b9 7 | arin|US|asn|63312|1|20141117|assigned|144f56df8b5654099c9b46a0f11106f1 8 | arin|US|asn|63313|1|20141117|assigned|33941ebdd2dedfac82872f14ac76503c 9 | arin|US|asn|63314|1|20141117|assigned|b004b3ecde24c85e32c1923f10d3fb62 10 | arin|US|asn|63315|1|20141126|assigned|fe99af214a090f87be3ca5398e527772 11 | arin|US|asn|63316|1|20141126|assigned|07d0afd7d77334cdfb30266a65f838e2 12 | arin||asn|63317|1||reserved| 13 | arin|US|asn|63360|2|20141126|assigned|07d0afd7d77334cdfb30266a65f838e2 14 | -------------------------------------------------------------------------------- /tests/data/assignment/delegated-lacnic-extended-latest: -------------------------------------------------------------------------------- 1 | lacnic|UY|asn|6057|1|19951026|allocated|20124 2 | lacnic|MX|asn|6063|1|19870101|allocated|76631 3 | lacnic||asn|6064|1||available 4 | lacnic||asn|6065|1||reserved| 5 | lacnic|MX|asn|6084|1|19951108|allocated|76631 6 | lacnic|AR|asn|6121|1|19951117|allocated|45551 7 | lacnic|BR|asn|6125|1|20130401|allocated|130343 8 | lacnic|GT|asn|6133|1|19951122|allocated|11733 9 | lacnic||asn|6135|1||reserved| 10 | lacnic|PE|asn|6147|1|19951208|allocated|21169 11 | lacnic||asn|6148|1||available 12 | lacnic|PA|asn|6193|1|19870101|allocated|44882 13 | lacnic|CL|asn|6240|1|19870101|allocated|1282 14 | -------------------------------------------------------------------------------- /tests/data/assignment/delegated-ripencc-extended-latest: -------------------------------------------------------------------------------- 1 | ripencc||ipv4|193.33.188.0|512||available 2 | ripencc||ipv4|195.35.105.0|256||available 3 | ripencc||ipv4|212.6.36.0|256||available 4 | ripencc|EU|asn|7|1|19930901|allocated|fbf93c3e-7884-422f-860e-75ea4eb3a038 5 | ripencc|DE|asn|28|1|19930901|allocated|9ad07dc7-08eb-41b3-942a-9c246138cf84 6 | ripencc|IT|asn|137|1|19930901|allocated|26d57363-ccbb-458b-96fd-0b8dfa416e28 7 | ripencc|NO|asn|224|1|19930901|allocated|fbf93c3e-7884-422f-860e-75ea4eb3a038 8 | ripencc|EU|asn|248|1|19930901|allocated|fbf93c3e-7884-422f-860e-75ea4eb3a038 9 | ripencc|EU|asn|249|1|19930901|allocated|fbf93c3e-7884-422f-860e-75ea4eb3a038 10 | -------------------------------------------------------------------------------- /tests/data/iana/bootstrap/asn.json: -------------------------------------------------------------------------------- 1 | {"description":"RDAP bootstrap file for Autonomous System Number allocations","publication":"2024-04-10T20:00:01Z","services":[[["36864-37887","327680-328703","328704-329727"],["https://rdap.afrinic.net/rdap/","http://rdap.afrinic.net/rdap/"]],[["4608-4865","7467-7722","9216-10239","17408-18431","23552-24575","37888-38911","45056-46079","55296-56319","58368-59391","63488-63999","64000-64098","64297-64395","131072-132095","132096-133119","133120-133631","133632-134556","134557-135580","135581-136505","136506-137529","137530-138553","138554-139577","139578-140601","140602-141625","141626-142649","142650-143673","143674-144697","144698-145721","145722-146745","146746-147769","147770-148793","148794-149817","149818-150841","150842-151865","151866-152889","152890-153913"],["https://rdap.apnic.net/"]],[["1-1876","1902-2042","2044-2046","2048-2106","2137-2584","2615-2772","2823-2829","2880-3153","3354-4607","4866-5376","5632-6655","6912-7466","7723-8191","10240-12287","13312-15359","16384-17407","18432-20479","21504-23455","23457-23551","25600-26623","26624-27647","29696-30719","31744-32767","32768-33791","35840-36863","39936-40959","46080-47103","53248-54271","54272-55295","62464-63487","64198-64296","393216-394239","394240-395164","395165-396188","396189-397212","397213-398236","398237-399260","399261-400284","400285-401308","401309-402332"],["https://rdap.arin.net/registry/","http://rdap.arin.net/registry/"]],[["1877-1901","2043","2047","2107-2136","2585-2614","2773-2822","2830-2879","3154-3353","5377-5631","6656-6911","8192-9215","12288-13311","15360-16383","20480-21503","24576-25599","28672-29695","30720-31743","33792-34815","34816-35839","38912-39935","40960-41983","41984-43007","43008-44031","44032-45055","47104-48127","48128-49151","49152-50175","50176-51199","51200-52223","56320-57343","57344-58367","59392-60415","60416-61439","61952-62463","64396-64495","196608-197631","197632-198655","198656-199679","199680-200191","200192-201215","201216-202239","202240-203263","203264-204287","204288-205211","205212-206235","206236-207259","207260-208283","208284-209307","209308-210331","210332-211355","211356-212379","212380-213403","213404-214427","214428-215451","215452-216475"],["https://rdap.db.ripe.net/"]],[["27648-28671","52224-53247","61440-61951","64099-64197","262144-263167","263168-263679","263680-264604","264605-265628","265629-266652","266653-267676","267677-268700","268701-269724","269725-270748","270749-271772","271773-272796","272797-273820","273821-274844"],["https://rdap.lacnic.net/rdap/"]]],"version":"1.0"} -------------------------------------------------------------------------------- /tests/data/iana/config.yml: -------------------------------------------------------------------------------- 1 | 2 | rdap: 3 | self_bootstrap: true 4 | -------------------------------------------------------------------------------- /tests/data/normalize/autnum/63311.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2014-11-17T14:28:43-05:00", 3 | "updated": "2018-10-24T22:58:16-04:00", 4 | "asn": 63311, 5 | "name": "20C", 6 | "organization": { 7 | "name": "20C, LLC" 8 | }, 9 | "locations": [ 10 | { 11 | "updated": "2014-08-05T15:21:11-04:00", 12 | "country": "US", 13 | "city": "Mountain View", 14 | "postal_code": "94043", 15 | "address": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States", 16 | "geo": { 17 | "latitude": 37.4224764, 18 | "longitude": -122.0842499 19 | }, 20 | "floor": "4", 21 | "suite": "Suite 200" 22 | }, 23 | { 24 | "updated": "2023-08-02T14:15:09-04:00", 25 | "country": "US", 26 | "city": "Mountain View", 27 | "postal_code": "94043", 28 | "address": "603 Discovery Dr\nWest Chicago\nIL\n60185\nUnited States", 29 | "geo": { 30 | "latitude": 37.4224764, 31 | "longitude": -122.0842499 32 | }, 33 | "floor": "4", 34 | "suite": "Suite 200" 35 | } 36 | ], 37 | "contacts": [ 38 | { 39 | "created": "2014-07-03T23:22:49-04:00", 40 | "updated": "2023-08-02T14:15:09-04:00", 41 | "name": "Network Engineers", 42 | "roles": [ 43 | "abuse", 44 | "admin", 45 | "technical" 46 | ], 47 | "phone": "+1 978-636-0020", 48 | "email": "neteng@20c.com" 49 | } 50 | ], 51 | "sources": [ 52 | { 53 | "created": "2014-11-17T14:28:43-05:00", 54 | "updated": "2018-10-24T22:58:16-04:00", 55 | "handle": "AS63311", 56 | "urls": [ 57 | "https://rdap.org/autnum/63311", 58 | "https://rdap.org/autnum/63311" 59 | ], 60 | "description": null 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /tests/data/normalize/autnum/63311.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance": [ 3 | "nro_rdap_profile_0", 4 | "rdap_level_0", 5 | "nro_rdap_profile_asn_flat_0" 6 | ], 7 | "notices": [ 8 | { 9 | "title": "Terms of Service", 10 | "description": [ 11 | "By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use" 12 | ], 13 | "links": [ 14 | { 15 | "value": "https://rdap.arin.net/registry/autnum/63311", 16 | "rel": "terms-of-service", 17 | "type": "text/html", 18 | "href": "https://www.arin.net/resources/registry/whois/tou/" 19 | } 20 | ] 21 | }, 22 | { 23 | "title": "Whois Inaccuracy Reporting", 24 | "description": [ 25 | "If you see inaccuracies in the results, please visit: " 26 | ], 27 | "links": [ 28 | { 29 | "value": "https://rdap.arin.net/registry/autnum/63311", 30 | "rel": "inaccuracy-report", 31 | "type": "text/html", 32 | "href": "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/" 33 | } 34 | ] 35 | }, 36 | { 37 | "title": "Copyright Notice", 38 | "description": [ 39 | "Copyright 1997-2023, American Registry for Internet Numbers, Ltd." 40 | ] 41 | } 42 | ], 43 | "handle": "AS63311", 44 | "startAutnum": 63311, 45 | "endAutnum": 63311, 46 | "name": "20C", 47 | "events": [ 48 | { 49 | "eventAction": "last changed", 50 | "eventDate": "2018-10-24T22:58:16-04:00" 51 | }, 52 | { 53 | "eventAction": "registration", 54 | "eventDate": "2014-11-17T14:28:43-05:00" 55 | } 56 | ], 57 | "links": [ 58 | { 59 | "value": "https://rdap.arin.net/registry/autnum/63311", 60 | "rel": "self", 61 | "type": "application/rdap+json", 62 | "href": "https://rdap.arin.net/registry/autnum/63311" 63 | }, 64 | { 65 | "value": "https://rdap.arin.net/registry/autnum/63311", 66 | "rel": "alternate", 67 | "type": "application/xml", 68 | "href": "https://whois.arin.net/rest/asn/AS63311" 69 | } 70 | ], 71 | "entities": [ 72 | { 73 | "handle": "CL-672", 74 | "vcardArray": [ 75 | "vcard", 76 | [ 77 | [ 78 | "version", 79 | {}, 80 | "text", 81 | "4.0" 82 | ], 83 | [ 84 | "fn", 85 | {}, 86 | "text", 87 | "20C, LLC" 88 | ], 89 | [ 90 | "adr", 91 | { 92 | "label": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States" 93 | }, 94 | "text", 95 | [ 96 | "", 97 | "", 98 | "", 99 | "", 100 | "", 101 | "", 102 | "" 103 | ] 104 | ], 105 | [ 106 | "kind", 107 | {}, 108 | "text", 109 | "org" 110 | ] 111 | ] 112 | ], 113 | "roles": [ 114 | "registrant" 115 | ], 116 | "links": [ 117 | { 118 | "value": "https://rdap.arin.net/registry/autnum/63311", 119 | "rel": "self", 120 | "type": "application/rdap+json", 121 | "href": "https://rdap.arin.net/registry/entity/CL-672" 122 | }, 123 | { 124 | "value": "https://rdap.arin.net/registry/autnum/63311", 125 | "rel": "alternate", 126 | "type": "application/xml", 127 | "href": "https://whois.arin.net/rest/org/CL-672" 128 | } 129 | ], 130 | "events": [ 131 | { 132 | "eventAction": "last changed", 133 | "eventDate": "2014-08-05T15:21:11-04:00" 134 | }, 135 | { 136 | "eventAction": "registration", 137 | "eventDate": "2014-08-05T15:21:11-04:00" 138 | } 139 | ], 140 | "entities": [ 141 | { 142 | "handle": "NETWO7047-ARIN", 143 | "vcardArray": [ 144 | "vcard", 145 | [ 146 | [ 147 | "version", 148 | {}, 149 | "text", 150 | "4.0" 151 | ], 152 | [ 153 | "adr", 154 | { 155 | "label": "603 Discovery Dr\nWest Chicago\nIL\n60185\nUnited States" 156 | }, 157 | "text", 158 | [ 159 | "", 160 | "", 161 | "", 162 | "", 163 | "", 164 | "", 165 | "" 166 | ] 167 | ], 168 | [ 169 | "fn", 170 | {}, 171 | "text", 172 | "Network Engineers" 173 | ], 174 | [ 175 | "org", 176 | {}, 177 | "text", 178 | "Network Engineers" 179 | ], 180 | [ 181 | "kind", 182 | {}, 183 | "text", 184 | "group" 185 | ], 186 | [ 187 | "email", 188 | {}, 189 | "text", 190 | "neteng@20c.com" 191 | ], 192 | [ 193 | "tel", 194 | { 195 | "type": [ 196 | "work", 197 | "voice" 198 | ] 199 | }, 200 | "text", 201 | "+1-978-636-0020" 202 | ] 203 | ] 204 | ], 205 | "roles": [ 206 | "technical", 207 | "abuse", 208 | "administrative" 209 | ], 210 | "links": [ 211 | { 212 | "value": "https://rdap.arin.net/registry/autnum/63311", 213 | "rel": "self", 214 | "type": "application/rdap+json", 215 | "href": "https://rdap.arin.net/registry/entity/NETWO7047-ARIN" 216 | }, 217 | { 218 | "value": "https://rdap.arin.net/registry/autnum/63311", 219 | "rel": "alternate", 220 | "type": "application/xml", 221 | "href": "https://whois.arin.net/rest/poc/NETWO7047-ARIN" 222 | } 223 | ], 224 | "events": [ 225 | { 226 | "eventAction": "last changed", 227 | "eventDate": "2023-08-02T14:15:09-04:00" 228 | }, 229 | { 230 | "eventAction": "registration", 231 | "eventDate": "2014-07-03T23:22:49-04:00" 232 | } 233 | ], 234 | "status": [ 235 | "validated" 236 | ], 237 | "port43": "whois.arin.net", 238 | "objectClassName": "entity" 239 | } 240 | ], 241 | "port43": "whois.arin.net", 242 | "objectClassName": "entity" 243 | } 244 | ], 245 | "port43": "whois.arin.net", 246 | "status": [ 247 | "active" 248 | ], 249 | "objectClassName": "autnum" 250 | } -------------------------------------------------------------------------------- /tests/data/normalize/autnum/8283.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2003-06-23T14:40:34Z", 3 | "updated": "2023-11-27T23:02:44Z", 4 | "asn": 8283, 5 | "name": "COLOCLUE-AS", 6 | "organization": { 7 | "name": "Netwerkvereniging Coloclue" 8 | }, 9 | "locations": [ 10 | { 11 | "updated": null, 12 | "country": "US", 13 | "city": "Mountain View", 14 | "postal_code": "94043", 15 | "address": "Bijsterveld 1\n4902 ZN Oosterhout\nThe Netherlands", 16 | "geo": { 17 | "latitude": 37.4224764, 18 | "longitude": -122.0842499 19 | }, 20 | "floor": "4", 21 | "suite": "Suite 200" 22 | }, 23 | { 24 | "updated": null, 25 | "country": "US", 26 | "city": "Mountain View", 27 | "postal_code": "94043", 28 | "address": "BytePark\nGruttostraat 63\nNL-2665 EL Bleiswijk\nThe Netherlands", 29 | "geo": { 30 | "latitude": 37.4224764, 31 | "longitude": -122.0842499 32 | }, 33 | "floor": "4", 34 | "suite": "Suite 200" 35 | }, 36 | { 37 | "updated": null, 38 | "country": "US", 39 | "city": "Mountain View", 40 | "postal_code": "94043", 41 | "address": "Frans Duwaerstraat 34\n1318 AC\nAlmere\nNETHERLANDS", 42 | "geo": { 43 | "latitude": 37.4224764, 44 | "longitude": -122.0842499 45 | }, 46 | "floor": "4", 47 | "suite": "Suite 200" 48 | }, 49 | { 50 | "updated": null, 51 | "country": "US", 52 | "city": "Mountain View", 53 | "postal_code": "94043", 54 | "address": "Frans Duwaerstraat 34\n1318 AC ALMERE\nThe Netherlands", 55 | "geo": { 56 | "latitude": 37.4224764, 57 | "longitude": -122.0842499 58 | }, 59 | "floor": "4", 60 | "suite": "Suite 200" 61 | }, 62 | { 63 | "updated": null, 64 | "country": "US", 65 | "city": "Mountain View", 66 | "postal_code": "94043", 67 | "address": "Frans Duwaerstraat 34\n1318AC Almere\nNetherlands", 68 | "geo": { 69 | "latitude": 37.4224764, 70 | "longitude": -122.0842499 71 | }, 72 | "floor": "4", 73 | "suite": "Suite 200" 74 | }, 75 | { 76 | "updated": null, 77 | "country": "US", 78 | "city": "Mountain View", 79 | "postal_code": "94043", 80 | "address": "Kanaalstraat 89-BS", 81 | "geo": { 82 | "latitude": 37.4224764, 83 | "longitude": -122.0842499 84 | }, 85 | "floor": "4", 86 | "suite": "Suite 200" 87 | }, 88 | { 89 | "updated": null, 90 | "country": "US", 91 | "city": "Mountain View", 92 | "postal_code": "94043", 93 | "address": "Kruier 1\n1567LA Assendelft\nNetherlands", 94 | "geo": { 95 | "latitude": 37.4224764, 96 | "longitude": -122.0842499 97 | }, 98 | "floor": "4", 99 | "suite": "Suite 200" 100 | }, 101 | { 102 | "updated": null, 103 | "country": "US", 104 | "city": "Mountain View", 105 | "postal_code": "94043", 106 | "address": "Maasstraat 16\n1442RV Purmerend\nNetherlands", 107 | "geo": { 108 | "latitude": 37.4224764, 109 | "longitude": -122.0842499 110 | }, 111 | "floor": "4", 112 | "suite": "Suite 200" 113 | }, 114 | { 115 | "updated": null, 116 | "country": "US", 117 | "city": "Mountain View", 118 | "postal_code": "94043", 119 | "address": "NETHERLANDS", 120 | "geo": { 121 | "latitude": 37.4224764, 122 | "longitude": -122.0842499 123 | }, 124 | "floor": "4", 125 | "suite": "Suite 200" 126 | }, 127 | { 128 | "updated": null, 129 | "country": "US", 130 | "city": "Mountain View", 131 | "postal_code": "94043", 132 | "address": "Purmer 60, 8244AT Lelystad", 133 | "geo": { 134 | "latitude": 37.4224764, 135 | "longitude": -122.0842499 136 | }, 137 | "floor": "4", 138 | "suite": "Suite 200" 139 | }, 140 | { 141 | "updated": null, 142 | "country": "US", 143 | "city": "Mountain View", 144 | "postal_code": "94043", 145 | "address": "Wamberg 26-1\n1083CV\nAmsterdam\nNETHERLANDS", 146 | "geo": { 147 | "latitude": 37.4224764, 148 | "longitude": -122.0842499 149 | }, 150 | "floor": "4", 151 | "suite": "Suite 200" 152 | }, 153 | { 154 | "updated": null, 155 | "country": "US", 156 | "city": "Mountain View", 157 | "postal_code": "94043", 158 | "address": "Wandeling 8\n4301 JT Zierikzee\nNetherlands", 159 | "geo": { 160 | "latitude": 37.4224764, 161 | "longitude": -122.0842499 162 | }, 163 | "floor": "4", 164 | "suite": "Suite 200" 165 | } 166 | ], 167 | "contacts": [ 168 | { 169 | "created": null, 170 | "updated": null, 171 | "name": "Jasper Backer", 172 | "roles": [ 173 | "technical" 174 | ], 175 | "phone": "+31 6 83708052", 176 | "email": null 177 | }, 178 | { 179 | "created": null, 180 | "updated": null, 181 | "name": "Jelle Luteijn", 182 | "roles": [ 183 | "admin", 184 | "technical" 185 | ], 186 | "phone": null, 187 | "email": "hostmaster@luje.net" 188 | }, 189 | { 190 | "created": null, 191 | "updated": null, 192 | "name": "Jurrian van Iersel", 193 | "roles": [ 194 | "technical" 195 | ], 196 | "phone": "+31 85 876 8785", 197 | "email": "ripe@jurrian.vaniersel.net" 198 | }, 199 | { 200 | "created": null, 201 | "updated": null, 202 | "name": "Mark Scholten", 203 | "roles": [ 204 | "technical" 205 | ], 206 | "phone": "+31 6 42408602", 207 | "email": null 208 | }, 209 | { 210 | "created": null, 211 | "updated": null, 212 | "name": "Menno Wouter Thomas Spaans", 213 | "roles": [ 214 | "admin" 215 | ], 216 | "phone": "+31 6 24206858", 217 | "email": "menno@someones.net" 218 | }, 219 | { 220 | "created": null, 221 | "updated": null, 222 | "name": "Netwerkvereniging Coloclue", 223 | "roles": [ 224 | "abuse", 225 | "admin", 226 | "registrant", 227 | "technical" 228 | ], 229 | "phone": "+31 6 51387718", 230 | "email": "abuse@coloclue.net" 231 | }, 232 | { 233 | "created": null, 234 | "updated": null, 235 | "name": "Niels Raijer", 236 | "roles": [ 237 | "technical" 238 | ], 239 | "phone": "+31 6 54918205", 240 | "email": null 241 | }, 242 | { 243 | "created": null, 244 | "updated": null, 245 | "name": "Paul de Weerd", 246 | "roles": [ 247 | "admin", 248 | "technical" 249 | ], 250 | "phone": "+31 6 51387718", 251 | "email": "weerd@weirdnet.nl" 252 | }, 253 | { 254 | "created": null, 255 | "updated": null, 256 | "name": "Rogier Krieger", 257 | "roles": [ 258 | "technical" 259 | ], 260 | "phone": "+31 15 212 8820", 261 | "email": "hostmaster@bytepark.net" 262 | }, 263 | { 264 | "created": null, 265 | "updated": null, 266 | "name": "Tijn Buijs", 267 | "roles": [ 268 | "admin", 269 | "technical" 270 | ], 271 | "phone": "+31 6 13962562", 272 | "email": "contact@cybertinus.nl" 273 | }, 274 | { 275 | "created": null, 276 | "updated": null, 277 | "name": "Tim de Boer", 278 | "roles": [ 279 | "technical" 280 | ], 281 | "phone": "+31 6 40282615", 282 | "email": null 283 | } 284 | ], 285 | "sources": [ 286 | { 287 | "created": "2014-11-17T14:28:43-05:00", 288 | "updated": "2018-10-24T22:58:16-04:00", 289 | "handle": "AS63311", 290 | "urls": [ 291 | "https://rdap.org/autnum/63311", 292 | "https://rdap.org/autnum/63311" 293 | ], 294 | "description": null 295 | }, 296 | { 297 | "created": "2003-06-23T14:40:34Z", 298 | "updated": "2023-11-27T23:02:44Z", 299 | "handle": "AS8283", 300 | "urls": [ 301 | "https://rdap.org/autnum/8283", 302 | "https://rdap.org/autnum/8283" 303 | ], 304 | "description": null 305 | } 306 | ] 307 | } -------------------------------------------------------------------------------- /tests/data/normalize/domain/20c.com.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2004-06-28T18:28:14Z", 3 | "updated": "2024-06-25T03:31:37Z", 4 | "name": "20C.COM", 5 | "handle": "123664426_DOMAIN_COM-VRSN", 6 | "dns_sec": "insecure", 7 | "nameservers": [ 8 | { 9 | "host": "NS-1468.AWSDNS-55.ORG" 10 | }, 11 | { 12 | "host": "NS-1771.AWSDNS-29.CO.UK" 13 | }, 14 | { 15 | "host": "NS-327.AWSDNS-40.COM" 16 | }, 17 | { 18 | "host": "NS-545.AWSDNS-04.NET" 19 | } 20 | ], 21 | "contacts": [], 22 | "sources": [ 23 | { 24 | "created": "2014-11-17T14:28:43-05:00", 25 | "updated": "2018-10-24T22:58:16-04:00", 26 | "handle": "AS63311", 27 | "urls": [ 28 | "https://rdap.org/autnum/63311", 29 | "https://rdap.org/autnum/63311" 30 | ], 31 | "description": null 32 | }, 33 | { 34 | "created": "2003-06-23T14:40:34Z", 35 | "updated": "2023-11-27T23:02:44Z", 36 | "handle": "AS8283", 37 | "urls": [ 38 | "https://rdap.org/autnum/8283", 39 | "https://rdap.org/autnum/8283" 40 | ], 41 | "description": null 42 | }, 43 | { 44 | "created": null, 45 | "updated": "2017-09-21T23:55:48Z", 46 | "handle": "CLUE1-RIPE", 47 | "urls": [ 48 | "https://rdap.org/entity/CLUE1-RIPE", 49 | "https://rdap.org/entity/CLUE1-RIPE" 50 | ], 51 | "description": null 52 | }, 53 | { 54 | "created": null, 55 | "updated": "2017-06-13T22:21:32Z", 56 | "handle": "DJVG", 57 | "urls": [ 58 | "https://rdap.org/entity/DJVG", 59 | "https://rdap.org/entity/DJVG" 60 | ], 61 | "description": null 62 | }, 63 | { 64 | "created": "2004-06-28T18:28:14Z", 65 | "updated": "2024-06-25T03:31:37Z", 66 | "handle": "123664426_DOMAIN_COM-VRSN", 67 | "urls": [ 68 | "https://rdap.org/domain/20c.com", 69 | "https://rdap.org/domain/20c.com" 70 | ], 71 | "description": null 72 | } 73 | ] 74 | } -------------------------------------------------------------------------------- /tests/data/normalize/domain/20c.com.input: -------------------------------------------------------------------------------- 1 | { 2 | "name": "20C.COM", 3 | "handle": "123664426_DOMAIN_COM-VRSN", 4 | "dns_sec": "insecure", 5 | "contacts": [], 6 | "nameservers": [ 7 | { 8 | "host": "NS-1468.AWSDNS-55.ORG" 9 | }, 10 | { 11 | "host": "NS-1771.AWSDNS-29.CO.UK" 12 | }, 13 | { 14 | "host": "NS-327.AWSDNS-40.COM" 15 | }, 16 | { 17 | "host": "NS-545.AWSDNS-04.NET" 18 | } 19 | ], 20 | "sources": [ 21 | { 22 | "handle": "123664426_DOMAIN_COM-VRSN", 23 | "urls": [ 24 | "https://rdap.org/domain/20c.com", 25 | "https://rdap.verisign.com/com/v1/domain/20c.com" 26 | ], 27 | "created": "2004-06-28T18:28:14Z", 28 | "updated": "2024-06-25T03:31:37Z" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /tests/data/normalize/entity/CLUE1-RIPE.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": null, 3 | "updated": "2017-09-21T23:55:48Z", 4 | "name": "CLUE1-RIPE", 5 | "organization": null, 6 | "locations": [ 7 | { 8 | "updated": "2017-09-21T23:55:48Z", 9 | "country": "US", 10 | "city": "Mountain View", 11 | "postal_code": "94043", 12 | "address": "Frans Duwaerstraat 34\n1318AC Almere\nNetherlands", 13 | "geo": { 14 | "latitude": 37.4224764, 15 | "longitude": -122.0842499 16 | }, 17 | "floor": "4", 18 | "suite": "Suite 200" 19 | } 20 | ], 21 | "contacts": [ 22 | { 23 | "created": null, 24 | "updated": "2017-09-21T23:55:48Z", 25 | "name": "Netwerkvereniging Coloclue", 26 | "roles": [], 27 | "phone": "+31 6 51387718", 28 | "email": "routers@coloclue.net" 29 | } 30 | ], 31 | "sources": [ 32 | { 33 | "created": "2014-11-17T14:28:43-05:00", 34 | "updated": "2018-10-24T22:58:16-04:00", 35 | "handle": "AS63311", 36 | "urls": [ 37 | "https://rdap.org/autnum/63311", 38 | "https://rdap.org/autnum/63311" 39 | ], 40 | "description": null 41 | }, 42 | { 43 | "created": "2003-06-23T14:40:34Z", 44 | "updated": "2023-11-27T23:02:44Z", 45 | "handle": "AS8283", 46 | "urls": [ 47 | "https://rdap.org/autnum/8283", 48 | "https://rdap.org/autnum/8283" 49 | ], 50 | "description": null 51 | }, 52 | { 53 | "created": null, 54 | "updated": "2017-09-21T23:55:48Z", 55 | "handle": "CLUE1-RIPE", 56 | "urls": [ 57 | "https://rdap.org/entity/CLUE1-RIPE", 58 | "https://rdap.org/entity/CLUE1-RIPE" 59 | ], 60 | "description": null 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /tests/data/normalize/entity/CLUE1-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "CLUE1-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Netwerkvereniging Coloclue" ], [ "kind", { }, "text", "group" ], [ "adr", { 4 | "label" : "Frans Duwaerstraat 34\n1318AC Almere\nNetherlands" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+31651387718" ], [ "email", { }, "text", "ops@coloclue.net" ], [ "email", { }, "text", "routers@coloclue.net" ] ] ], 8 | "entities" : [ { 9 | "handle" : "COLOCLUE-MNT", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | }, { 13 | "handle" : "JB17421-RIPE", 14 | "roles" : [ "technical" ], 15 | "objectClassName" : "entity" 16 | }, { 17 | "handle" : "JL9785-RIPE", 18 | "roles" : [ "technical" ], 19 | "objectClassName" : "entity" 20 | }, { 21 | "handle" : "JVI-RIPE", 22 | "roles" : [ "technical" ], 23 | "objectClassName" : "entity" 24 | }, { 25 | "handle" : "MS44437-RIPE", 26 | "roles" : [ "administrative", "technical" ], 27 | "objectClassName" : "entity" 28 | }, { 29 | "handle" : "MWTS1-RIPE", 30 | "roles" : [ "administrative" ], 31 | "objectClassName" : "entity" 32 | }, { 33 | "handle" : "NMR5-RIPE", 34 | "roles" : [ "technical" ], 35 | "objectClassName" : "entity" 36 | }, { 37 | "handle" : "NT1031-RIPE", 38 | "roles" : [ "administrative" ], 39 | "objectClassName" : "entity" 40 | }, { 41 | "handle" : "PDW-RIPE", 42 | "roles" : [ "administrative", "technical" ], 43 | "objectClassName" : "entity" 44 | }, { 45 | "handle" : "PEER-RIPE", 46 | "roles" : [ "technical" ], 47 | "objectClassName" : "entity" 48 | }, { 49 | "handle" : "TIJN-RIPE", 50 | "roles" : [ "administrative", "technical" ], 51 | "objectClassName" : "entity" 52 | } ], 53 | "links" : [ { 54 | "value" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE", 55 | "rel" : "self", 56 | "href" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE" 57 | }, { 58 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 59 | "rel" : "copyright", 60 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 61 | } ], 62 | "events" : [ { 63 | "eventAction" : "last changed", 64 | "eventDate" : "2017-09-21T23:55:48Z" 65 | } ], 66 | "rdapConformance" : [ "rdap_level_0" ], 67 | "notices" : [ { 68 | "title" : "Filtered", 69 | "description" : [ "This output has been filtered." ] 70 | }, { 71 | "title" : "Source", 72 | "description" : [ "Objects returned came from source", "RIPE" ] 73 | }, { 74 | "title" : "Terms and Conditions", 75 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 76 | "links" : [ { 77 | "value" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE", 78 | "rel" : "terms-of-service", 79 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 80 | "type" : "application/pdf" 81 | } ] 82 | } ], 83 | "port43" : "whois.ripe.net", 84 | "objectClassName" : "entity" 85 | } -------------------------------------------------------------------------------- /tests/data/normalize/entity/DJVG.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": null, 3 | "updated": "2017-06-13T22:21:32Z", 4 | "name": "DJVG", 5 | "organization": null, 6 | "locations": [ 7 | { 8 | "updated": "2017-06-13T22:21:32Z", 9 | "country": "US", 10 | "city": "Mountain View", 11 | "postal_code": "94043", 12 | "address": "Postbus 8160\n1180LD Amstelveen\nthe Netherlands", 13 | "geo": { 14 | "latitude": 37.4224764, 15 | "longitude": -122.0842499 16 | }, 17 | "floor": "4", 18 | "suite": "Suite 200" 19 | } 20 | ], 21 | "contacts": [ 22 | { 23 | "created": null, 24 | "updated": "2017-06-13T22:21:32Z", 25 | "name": "Daan van Gorkum", 26 | "roles": [], 27 | "phone": "+31 20 308 0063", 28 | "email": "daan.vangorkum@vusam.com" 29 | } 30 | ], 31 | "sources": [ 32 | { 33 | "created": "2014-11-17T14:28:43-05:00", 34 | "updated": "2018-10-24T22:58:16-04:00", 35 | "handle": "AS63311", 36 | "urls": [ 37 | "https://rdap.org/autnum/63311", 38 | "https://rdap.org/autnum/63311" 39 | ], 40 | "description": null 41 | }, 42 | { 43 | "created": "2003-06-23T14:40:34Z", 44 | "updated": "2023-11-27T23:02:44Z", 45 | "handle": "AS8283", 46 | "urls": [ 47 | "https://rdap.org/autnum/8283", 48 | "https://rdap.org/autnum/8283" 49 | ], 50 | "description": null 51 | }, 52 | { 53 | "created": null, 54 | "updated": "2017-09-21T23:55:48Z", 55 | "handle": "CLUE1-RIPE", 56 | "urls": [ 57 | "https://rdap.org/entity/CLUE1-RIPE", 58 | "https://rdap.org/entity/CLUE1-RIPE" 59 | ], 60 | "description": null 61 | }, 62 | { 63 | "created": null, 64 | "updated": "2017-06-13T22:21:32Z", 65 | "handle": "DJVG", 66 | "urls": [ 67 | "https://rdap.org/entity/DJVG", 68 | "https://rdap.org/entity/DJVG" 69 | ], 70 | "description": null 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /tests/data/normalize/entity/DJVG.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "DJVG", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Daan van Gorkum" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Postbus 8160\n1180LD Amstelveen\nthe Netherlands" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+31203080063" ], [ "email", { }, "text", "daan.vangorkum@vusam.com" ] ] ], 8 | "entities" : [ { 9 | "handle" : "VUSAM", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/DJVG", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/DJVG" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-06-13T22:21:32Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/DJVG", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/normalize/ip/206.41.110.0.expected: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2014-10-17T18:32:20-04:00", 3 | "updated": "2021-12-14T20:28:45-05:00", 4 | "prefix": "206.41.110.0/24", 5 | "version": 4, 6 | "name": "CHIX", 7 | "type": "DIRECT ALLOCATION", 8 | "status": "active", 9 | "parent": "206.0.0.0/8", 10 | "contacts": [ 11 | { 12 | "created": "2022-07-06T18:47:31-04:00", 13 | "updated": "2024-06-04T14:09:08-04:00", 14 | "name": "Jordan Hazen", 15 | "roles": [ 16 | "technical" 17 | ], 18 | "phone": "+1 904-549-7540", 19 | "email": "jhazen@sbaedge.com" 20 | }, 21 | { 22 | "created": "2021-01-22T13:10:25-05:00", 23 | "updated": "2024-01-23T08:35:36-05:00", 24 | "name": "Network Operations", 25 | "roles": [ 26 | "admin", 27 | "technical" 28 | ], 29 | "phone": null, 30 | "email": "netops@sbaedge.com" 31 | }, 32 | { 33 | "created": "2014-11-19T00:39:22-05:00", 34 | "updated": "2023-09-05T13:27:59-04:00", 35 | "name": "United IX Engineers", 36 | "roles": [ 37 | "abuse", 38 | "technical" 39 | ], 40 | "phone": "+1 877-432-2656", 41 | "email": "neteng@unitedix.net" 42 | }, 43 | { 44 | "created": "2024-05-30T10:27:44-04:00", 45 | "updated": "2024-05-30T10:42:04-04:00", 46 | "name": "Vitalii Kukanov", 47 | "roles": [ 48 | "technical" 49 | ], 50 | "phone": "+1 877-432-2656", 51 | "email": "vkukanov@sbaedge.com" 52 | } 53 | ], 54 | "sources": [ 55 | { 56 | "created": "2014-11-17T14:28:43-05:00", 57 | "updated": "2018-10-24T22:58:16-04:00", 58 | "handle": "AS63311", 59 | "urls": [ 60 | "https://rdap.org/autnum/63311", 61 | "https://rdap.org/autnum/63311" 62 | ], 63 | "description": null 64 | }, 65 | { 66 | "created": "2003-06-23T14:40:34Z", 67 | "updated": "2023-11-27T23:02:44Z", 68 | "handle": "AS8283", 69 | "urls": [ 70 | "https://rdap.org/autnum/8283", 71 | "https://rdap.org/autnum/8283" 72 | ], 73 | "description": null 74 | }, 75 | { 76 | "created": null, 77 | "updated": "2017-09-21T23:55:48Z", 78 | "handle": "CLUE1-RIPE", 79 | "urls": [ 80 | "https://rdap.org/entity/CLUE1-RIPE", 81 | "https://rdap.org/entity/CLUE1-RIPE" 82 | ], 83 | "description": null 84 | }, 85 | { 86 | "created": null, 87 | "updated": "2017-06-13T22:21:32Z", 88 | "handle": "DJVG", 89 | "urls": [ 90 | "https://rdap.org/entity/DJVG", 91 | "https://rdap.org/entity/DJVG" 92 | ], 93 | "description": null 94 | }, 95 | { 96 | "created": "2004-06-28T18:28:14Z", 97 | "updated": "2024-06-25T03:31:37Z", 98 | "handle": "123664426_DOMAIN_COM-VRSN", 99 | "urls": [ 100 | "https://rdap.org/domain/20c.com", 101 | "https://rdap.org/domain/20c.com" 102 | ], 103 | "description": null 104 | }, 105 | { 106 | "created": "2014-10-17T18:32:20-04:00", 107 | "updated": "2021-12-14T20:28:45-05:00", 108 | "handle": "NET-206-41-110-0-1", 109 | "urls": [ 110 | "https://rdap.org/ip/206.41.110.0", 111 | "https://rdap.org/ip/206.41.110.0" 112 | ], 113 | "description": null 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/205697.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ALOJALIA", 3 | "emails": [ 4 | "abuse@alojalia.com", 5 | "alejandro@alojalia.com", 6 | "angel@alojalia.com" 7 | ], 8 | "org_name": "ALOJALIA CLOUD S.L., ================================", 9 | "org_address": "Avd. Federico Anaya, 52 3C\n37004\nSalamanca\nSPAIN" 10 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/205726.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vusam", 3 | "emails": [ 4 | "abuse@vusam.com", 5 | "daan.vangorkum@vusam.com", 6 | "support@vusam.com" 7 | ], 8 | "org_name": "Daan Jurriaan van Gorkum trading as Vusam V.O.F", 9 | "org_address": "Postbus 8515\n8934BT\nLeeuwarden\nNETHERLANDS" 10 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/206050.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elitnet-tr", 3 | "emails": [ 4 | "abuse@geoipa.com", 5 | "mehmet@elitnet.net.tr" 6 | ], 7 | "org_name": "ELITNET TELEKOMUNIKASYON INTERNET VE ILETISIM HIZ.TIC LTD.STI.", 8 | "org_address": "15 Temmuz Mah. 142 Sk . B-Blok No:3 / 9" 9 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/2515.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JPNIC", 3 | "emails": [ 4 | "hostmaster@nic.ad.jp" 5 | ], 6 | "org_name": "Japan Network Information Center", 7 | "org_address": "" 8 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/2515.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance": [ 3 | "nro_rdap_profile_0", 4 | "nro_rdap_profile_asn_hierarchical_0", 5 | "cidr0", 6 | "rdap_level_0" 7 | ], 8 | "notices": [ 9 | { 10 | "title": "Source", 11 | "description": [ 12 | "Objects returned came from source", 13 | "JPNIC" 14 | ] 15 | }, 16 | { 17 | "title": "Terms and Conditions", 18 | "description": [ 19 | "This is the APNIC WHOIS Database query service. The objects are in RDAP format.", 20 | "This information has been partially mirrored by APNIC from JPNIC. To obtain more specific information, please use the JPNIC WHOIS Gateway at http://www.nic.ad.jp/en/db/whois/en-gateway.html or whois.nic.ad.jp for WHOIS client. (The WHOIS client defaults to Japanese output, use the /e switch for English output)" 21 | ], 22 | "links": [ 23 | { 24 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 25 | "rel": "terms-of-service", 26 | "href": "http://www.apnic.net/db/dbcopyright.html", 27 | "type": "text/html" 28 | } 29 | ] 30 | }, 31 | { 32 | "title": "Whois Inaccuracy Reporting", 33 | "description": [ 34 | "If you see inaccuracies in the results, please visit: " 35 | ], 36 | "links": [ 37 | { 38 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 39 | "rel": "inaccuracy-report", 40 | "href": "https://www.apnic.net/manage-ip/using-whois/abuse-and-spamming/invalid-contact-form", 41 | "type": "text/html" 42 | } 43 | ] 44 | } 45 | ], 46 | "country": "JP", 47 | "events": [ 48 | { 49 | "eventAction": "last changed", 50 | "eventDate": "2023-06-20T01:29:02Z" 51 | } 52 | ], 53 | "name": "JPNIC", 54 | "remarks": [ 55 | { 56 | "description": [ 57 | "Japan Network Information Center" 58 | ], 59 | "title": "description" 60 | } 61 | ], 62 | "links": [ 63 | { 64 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 65 | "rel": "self", 66 | "href": "https://jpnic.rdap.apnic.net/autnum/2515", 67 | "type": "application/rdap+json" 68 | }, 69 | { 70 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 71 | "rel": "related", 72 | "href": "https://netox.apnic.net/search/AS2515?utm_source=rdap&utm_medium=result&utm_campaign=rdap_result", 73 | "type": "text/html" 74 | } 75 | ], 76 | "status": [ 77 | "active" 78 | ], 79 | "handle": "AS2515", 80 | "endAutnum": 2515, 81 | "startAutnum": 2515, 82 | "objectClassName": "autnum", 83 | "entities": [ 84 | { 85 | "roles": [ 86 | "administrative", 87 | "technical" 88 | ], 89 | "events": [ 90 | { 91 | "eventAction": "last changed", 92 | "eventDate": "2022-01-05T03:04:02Z" 93 | } 94 | ], 95 | "links": [ 96 | { 97 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 98 | "rel": "self", 99 | "href": "https://jpnic.rdap.apnic.net/entity/JNIC1-AP", 100 | "type": "application/rdap+json" 101 | } 102 | ], 103 | "vcardArray": [ 104 | "vcard", 105 | [ 106 | [ 107 | "version", 108 | {}, 109 | "text", 110 | "4.0" 111 | ], 112 | [ 113 | "fn", 114 | {}, 115 | "text", 116 | "Japan Network Information Center" 117 | ], 118 | [ 119 | "kind", 120 | {}, 121 | "text", 122 | "group" 123 | ], 124 | [ 125 | "adr", 126 | { 127 | "label": "Uchikanda OS Bldg 4F, 2-12-6 Uchi-Kanda\nChiyoda-ku, Tokyo 101-0047, Japan" 128 | }, 129 | "text", 130 | [ 131 | "", 132 | "", 133 | "", 134 | "", 135 | "", 136 | "", 137 | "" 138 | ] 139 | ], 140 | [ 141 | "tel", 142 | { 143 | "type": "voice" 144 | }, 145 | "text", 146 | "+81-3-5297-2311" 147 | ], 148 | [ 149 | "tel", 150 | { 151 | "type": "fax" 152 | }, 153 | "text", 154 | "+81-3-5297-2312" 155 | ], 156 | [ 157 | "email", 158 | {}, 159 | "text", 160 | "hostmaster@nic.ad.jp" 161 | ] 162 | ] 163 | ], 164 | "objectClassName": "entity", 165 | "handle": "JNIC1-AP" 166 | }, 167 | { 168 | "roles": [ 169 | "technical" 170 | ], 171 | "links": [ 172 | { 173 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 174 | "rel": "related", 175 | "href": "http://whois.nic.ad.jp/cgi-bin/whois_gw?key=YK11438JP/e", 176 | "type": "text/html", 177 | "hreflang": "en" 178 | }, 179 | { 180 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 181 | "rel": "related", 182 | "href": "http://whois.nic.ad.jp/cgi-bin/whois_gw?key=YK11438JP", 183 | "type": "text/html", 184 | "hreflang": "jp" 185 | } 186 | ], 187 | "objectClassName": "entity", 188 | "handle": "YK11438JP" 189 | }, 190 | { 191 | "roles": [ 192 | "technical" 193 | ], 194 | "links": [ 195 | { 196 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 197 | "rel": "related", 198 | "href": "http://whois.nic.ad.jp/cgi-bin/whois_gw?key=EK6175JP/e", 199 | "type": "text/html", 200 | "hreflang": "en" 201 | }, 202 | { 203 | "value": "https://jpnic.rdap.apnic.net/autnum/2515", 204 | "rel": "related", 205 | "href": "http://whois.nic.ad.jp/cgi-bin/whois_gw?key=EK6175JP", 206 | "type": "text/html", 207 | "hreflang": "jp" 208 | } 209 | ], 210 | "objectClassName": "entity", 211 | "handle": "EK6175JP" 212 | } 213 | ], 214 | "port43": "whois.apnic.net" 215 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/2914.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NTT-LTD-2914", 3 | "emails": [ 4 | "abuse@ntt.net", 5 | "ipr@gin.ntt.net", 6 | "massimo@ntt.net", 7 | "peering@ntt.net", 8 | "support@us.ntt.net" 9 | ], 10 | "org_name": "NTT America, Inc.", 11 | "org_address": "15809 Bear Creek Pkwy\nSuite 320\nRedmond\nWA\n98052\nUnited States" 12 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/37271.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ORG-WCL1-AFRINIC", 3 | "emails": [ 4 | "abuse@workonline.africa", 5 | "communications@workonline.africa", 6 | "noc@workonline.africa", 7 | "peterp@workonline.africa" 8 | ], 9 | "org_name": "Workonline Communications(Pty) Ltd", 10 | "org_address": "114 West St\nJohannesburg 2196" 11 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/49037.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PG19", 3 | "emails": [ 4 | "abuse@webrocket.am", 5 | "mikhail.majorov@gmail.com", 6 | "ripe@webrocket.am" 7 | ], 8 | "org_name": "PROSTIE RESHENIA LLC, ****************** Routing Policy *******************", 9 | "org_address": "ST. MOVSESA HORENACI, 14/20\n1109\nVAGARSHAPAT\nARMENIA" 10 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/53170.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ASN53170", 3 | "emails": [], 4 | "org_name": "Cabangu Internet Ltda", 5 | "org_address": "" 6 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/53170.input: -------------------------------------------------------------------------------- 1 | { 2 | "objectClassName": "autnum", 3 | "handle": "53170", 4 | "startAutnum": 53170, 5 | "endAutnum": 53170, 6 | "name": "ASN53170", 7 | "type": "DIRECT ALLOCATION", 8 | "country": "BR", 9 | "links": [ 10 | { 11 | "value": "https://rdap.registro.br/autnum/53170", 12 | "rel": "self", 13 | "href": "https://rdap.registro.br/autnum/53170", 14 | "type": "application/rdap+json" 15 | }, 16 | { 17 | "value": "https://rdap.registro.br/autnum/53170", 18 | "rel": "related", 19 | "href": "https://rdap.registro.br/ip/186.224.48.0/20", 20 | "type": "application/rdap+json" 21 | }, 22 | { 23 | "value": "https://rdap.registro.br/autnum/53170", 24 | "rel": "related", 25 | "href": "https://rdap.registro.br/ip/2804:3838::/32", 26 | "type": "application/rdap+json" 27 | } 28 | ], 29 | "entities": [ 30 | { 31 | "objectClassName": "entity", 32 | "handle": "17769837000100", 33 | "vcardArray": [ 34 | "vcard", 35 | [ 36 | [ 37 | "version", 38 | {}, 39 | "text", 40 | "4.0" 41 | ], 42 | [ 43 | "kind", 44 | {}, 45 | "text", 46 | "org" 47 | ], 48 | [ 49 | "fn", 50 | {}, 51 | "text", 52 | "Cabangu Internet Ltda" 53 | ] 54 | ] 55 | ], 56 | "roles": [ 57 | "registrant" 58 | ], 59 | "entities": [ 60 | { 61 | "objectClassName": "entity", 62 | "handle": "GJM3", 63 | "vcardArray": [ 64 | "vcard", 65 | [ 66 | [ 67 | "version", 68 | {}, 69 | "text", 70 | "4.0" 71 | ], 72 | [ 73 | "kind", 74 | {}, 75 | "text", 76 | "individual" 77 | ], 78 | [ 79 | "fn", 80 | {}, 81 | "text", 82 | "Geovane Jose Vieira Martins" 83 | ], 84 | [ 85 | "lang", 86 | {}, 87 | "language-tag", 88 | "pt" 89 | ] 90 | ] 91 | ], 92 | "roles": [ 93 | "administrative" 94 | ], 95 | "events": [ 96 | { 97 | "eventAction": "registration", 98 | "eventDate": "1998-11-10T12:00:00Z" 99 | }, 100 | { 101 | "eventAction": "last changed", 102 | "eventDate": "2022-08-01T12:22:55Z" 103 | } 104 | ] 105 | } 106 | ], 107 | "events": [ 108 | { 109 | "eventAction": "registration", 110 | "eventDate": "1998-11-10T12:00:00Z" 111 | }, 112 | { 113 | "eventAction": "last changed", 114 | "eventDate": "2011-08-11T20:18:33Z" 115 | } 116 | ], 117 | "links": [ 118 | { 119 | "value": "https://rdap.registro.br/entity/17769837000100", 120 | "rel": "self", 121 | "href": "https://rdap.registro.br/entity/17769837000100", 122 | "type": "application/rdap+json" 123 | } 124 | ], 125 | "legalRepresentative": "Geovane Jose Vieira Martins" 126 | }, 127 | { 128 | "objectClassName": "entity", 129 | "handle": "BRI2", 130 | "vcardArray": [ 131 | "vcard", 132 | [ 133 | [ 134 | "version", 135 | {}, 136 | "text", 137 | "4.0" 138 | ], 139 | [ 140 | "kind", 141 | {}, 142 | "text", 143 | "individual" 144 | ], 145 | [ 146 | "fn", 147 | {}, 148 | "text", 149 | "Bruno Imbroisi" 150 | ], 151 | [ 152 | "lang", 153 | {}, 154 | "language-tag", 155 | "pt" 156 | ] 157 | ] 158 | ], 159 | "roles": [ 160 | "administrative", 161 | "abuse" 162 | ], 163 | "events": [ 164 | { 165 | "eventAction": "registration", 166 | "eventDate": "1998-02-19T12:00:00Z" 167 | }, 168 | { 169 | "eventAction": "last changed", 170 | "eventDate": "2011-05-17T15:01:16Z" 171 | } 172 | ] 173 | } 174 | ], 175 | "events": [ 176 | { 177 | "eventAction": "registration", 178 | "eventDate": "2010-07-26T14:27:24Z" 179 | }, 180 | { 181 | "eventAction": "last changed", 182 | "eventDate": "2023-02-28T10:29:08Z" 183 | } 184 | ], 185 | "notices": [ 186 | { 187 | "title": "Use and Privacy Policy", 188 | "description": [ 189 | "Copyright (c) NIC.br", 190 | "The use of the data is only permitted as described in", 191 | "full by the Use and Privacy Policy, being prohibited its ", 192 | "distribution, commercialization or reproduction, in ", 193 | "particular, to use it for advertising or any similar ", 194 | "purpose." 195 | ], 196 | "links": [ 197 | { 198 | "value": "https://rdap.registro.br/autnum/53170", 199 | "rel": "alternate", 200 | "href": "https://registro.br/upp", 201 | "type": "text/html" 202 | } 203 | ] 204 | } 205 | ], 206 | "remarks": [ 207 | { 208 | "type": "object truncated due to server policy" 209 | } 210 | ], 211 | "rdapConformance": [ 212 | "rdap_level_0", 213 | "nicbr_level_0" 214 | ], 215 | "port43": "whois.nic.br" 216 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/61399.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KG-IX", 3 | "emails": [ 4 | "lir@ix.kg" 5 | ], 6 | "org_name": "KG-IX LLC", 7 | "org_address": "Razzakova st. 55\n720040\nBishkek\nKYRGYZSTAN" 8 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/63311.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "20C", 3 | "emails": [ 4 | "neteng@20c.com" 5 | ], 6 | "org_name": "20C, LLC", 7 | "org_address": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States" 8 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/63311.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance": [ 3 | "nro_rdap_profile_0", 4 | "rdap_level_0", 5 | "nro_rdap_profile_asn_flat_0" 6 | ], 7 | "notices": [ 8 | { 9 | "title": "Terms of Service", 10 | "description": [ 11 | "By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use" 12 | ], 13 | "links": [ 14 | { 15 | "value": "https://rdap.arin.net/registry/autnum/63311", 16 | "rel": "terms-of-service", 17 | "type": "text/html", 18 | "href": "https://www.arin.net/resources/registry/whois/tou/" 19 | } 20 | ] 21 | }, 22 | { 23 | "title": "Whois Inaccuracy Reporting", 24 | "description": [ 25 | "If you see inaccuracies in the results, please visit: " 26 | ], 27 | "links": [ 28 | { 29 | "value": "https://rdap.arin.net/registry/autnum/63311", 30 | "rel": "inaccuracy-report", 31 | "type": "text/html", 32 | "href": "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/" 33 | } 34 | ] 35 | }, 36 | { 37 | "title": "Copyright Notice", 38 | "description": [ 39 | "Copyright 1997-2023, American Registry for Internet Numbers, Ltd." 40 | ] 41 | } 42 | ], 43 | "handle": "AS63311", 44 | "startAutnum": 63311, 45 | "endAutnum": 63311, 46 | "name": "20C", 47 | "events": [ 48 | { 49 | "eventAction": "last changed", 50 | "eventDate": "2018-10-24T22:58:16-04:00" 51 | }, 52 | { 53 | "eventAction": "registration", 54 | "eventDate": "2014-11-17T14:28:43-05:00" 55 | } 56 | ], 57 | "links": [ 58 | { 59 | "value": "https://rdap.arin.net/registry/autnum/63311", 60 | "rel": "self", 61 | "type": "application/rdap+json", 62 | "href": "https://rdap.arin.net/registry/autnum/63311" 63 | }, 64 | { 65 | "value": "https://rdap.arin.net/registry/autnum/63311", 66 | "rel": "alternate", 67 | "type": "application/xml", 68 | "href": "https://whois.arin.net/rest/asn/AS63311" 69 | } 70 | ], 71 | "entities": [ 72 | { 73 | "handle": "CL-672", 74 | "vcardArray": [ 75 | "vcard", 76 | [ 77 | [ 78 | "version", 79 | {}, 80 | "text", 81 | "4.0" 82 | ], 83 | [ 84 | "fn", 85 | {}, 86 | "text", 87 | "20C, LLC" 88 | ], 89 | [ 90 | "adr", 91 | { 92 | "label": "303 W Ohio #1701\nChicago\nIL\n60654\nUnited States" 93 | }, 94 | "text", 95 | [ 96 | "", 97 | "", 98 | "", 99 | "", 100 | "", 101 | "", 102 | "" 103 | ] 104 | ], 105 | [ 106 | "kind", 107 | {}, 108 | "text", 109 | "org" 110 | ] 111 | ] 112 | ], 113 | "roles": [ 114 | "registrant" 115 | ], 116 | "links": [ 117 | { 118 | "value": "https://rdap.arin.net/registry/autnum/63311", 119 | "rel": "self", 120 | "type": "application/rdap+json", 121 | "href": "https://rdap.arin.net/registry/entity/CL-672" 122 | }, 123 | { 124 | "value": "https://rdap.arin.net/registry/autnum/63311", 125 | "rel": "alternate", 126 | "type": "application/xml", 127 | "href": "https://whois.arin.net/rest/org/CL-672" 128 | } 129 | ], 130 | "events": [ 131 | { 132 | "eventAction": "last changed", 133 | "eventDate": "2014-08-05T15:21:11-04:00" 134 | }, 135 | { 136 | "eventAction": "registration", 137 | "eventDate": "2014-08-05T15:21:11-04:00" 138 | } 139 | ], 140 | "entities": [ 141 | { 142 | "handle": "NETWO7047-ARIN", 143 | "vcardArray": [ 144 | "vcard", 145 | [ 146 | [ 147 | "version", 148 | {}, 149 | "text", 150 | "4.0" 151 | ], 152 | [ 153 | "adr", 154 | { 155 | "label": "603 Discovery Dr\nWest Chicago\nIL\n60185\nUnited States" 156 | }, 157 | "text", 158 | [ 159 | "", 160 | "", 161 | "", 162 | "", 163 | "", 164 | "", 165 | "" 166 | ] 167 | ], 168 | [ 169 | "fn", 170 | {}, 171 | "text", 172 | "Network Engineers" 173 | ], 174 | [ 175 | "org", 176 | {}, 177 | "text", 178 | "Network Engineers" 179 | ], 180 | [ 181 | "kind", 182 | {}, 183 | "text", 184 | "group" 185 | ], 186 | [ 187 | "email", 188 | {}, 189 | "text", 190 | "neteng@20c.com" 191 | ], 192 | [ 193 | "tel", 194 | { 195 | "type": [ 196 | "work", 197 | "voice" 198 | ] 199 | }, 200 | "text", 201 | "+1-978-636-0020" 202 | ] 203 | ] 204 | ], 205 | "roles": [ 206 | "technical", 207 | "abuse", 208 | "administrative" 209 | ], 210 | "links": [ 211 | { 212 | "value": "https://rdap.arin.net/registry/autnum/63311", 213 | "rel": "self", 214 | "type": "application/rdap+json", 215 | "href": "https://rdap.arin.net/registry/entity/NETWO7047-ARIN" 216 | }, 217 | { 218 | "value": "https://rdap.arin.net/registry/autnum/63311", 219 | "rel": "alternate", 220 | "type": "application/xml", 221 | "href": "https://whois.arin.net/rest/poc/NETWO7047-ARIN" 222 | } 223 | ], 224 | "events": [ 225 | { 226 | "eventAction": "last changed", 227 | "eventDate": "2023-08-02T14:15:09-04:00" 228 | }, 229 | { 230 | "eventAction": "registration", 231 | "eventDate": "2014-07-03T23:22:49-04:00" 232 | } 233 | ], 234 | "status": [ 235 | "validated" 236 | ], 237 | "port43": "whois.arin.net", 238 | "objectClassName": "entity" 239 | } 240 | ], 241 | "port43": "whois.arin.net", 242 | "objectClassName": "entity" 243 | } 244 | ], 245 | "port43": "whois.arin.net", 246 | "status": [ 247 | "active" 248 | ], 249 | "objectClassName": "autnum" 250 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/8283.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "COLOCLUE-AS", 3 | "emails": [ 4 | "abuse@coloclue.net", 5 | "contact@cybertinus.nl", 6 | "hostmaster@bytepark.net", 7 | "hostmaster@luje.net", 8 | "menno@someones.net", 9 | "ops@coloclue.net", 10 | "ripe@jurrian.vaniersel.net", 11 | "routers@coloclue.net", 12 | "weerd@weirdnet.nl" 13 | ], 14 | "org_name": "Netwerkvereniging Coloclue, Netwerkvereniging Coloclue, Amsterdam, Netherlands", 15 | "org_address": "Frans Duwaerstraat 34\n1318 AC\nAlmere\nNETHERLANDS" 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/9269.expected: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HKBN-AS-AP", 3 | "emails": [ 4 | "abuse@hkbn.com.hk", 5 | "abuse@hkbn.net", 6 | "hostmaster@hkbn.com.hk", 7 | "nocsn@hkbn.com.hk" 8 | ], 9 | "org_name": "Hong Kong Broadband Network Ltd, Hong Kong Broadband Network Ltd.", 10 | "org_address": "15/F Trans Asia Centre\n18 Kin Hong Street" 11 | } -------------------------------------------------------------------------------- /tests/data/rdap/autnum/9269.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance": [ 3 | "history_version_0", 4 | "nro_rdap_profile_0", 5 | "nro_rdap_profile_asn_hierarchical_0", 6 | "cidr0", 7 | "rdap_level_0" 8 | ], 9 | "notices": [ 10 | { 11 | "title": "Source", 12 | "description": [ 13 | "Objects returned came from source", 14 | "APNIC" 15 | ] 16 | }, 17 | { 18 | "title": "Terms and Conditions", 19 | "description": [ 20 | "This is the APNIC WHOIS Database query service. The objects are in RDAP format." 21 | ], 22 | "links": [ 23 | { 24 | "value": "https://rdap.apnic.net/autnum/9269", 25 | "rel": "terms-of-service", 26 | "href": "http://www.apnic.net/db/dbcopyright.html", 27 | "type": "text/html" 28 | } 29 | ] 30 | }, 31 | { 32 | "title": "Whois Inaccuracy Reporting", 33 | "description": [ 34 | "If you see inaccuracies in the results, please visit: " 35 | ], 36 | "links": [ 37 | { 38 | "value": "https://rdap.apnic.net/autnum/9269", 39 | "rel": "inaccuracy-report", 40 | "href": "https://www.apnic.net/manage-ip/using-whois/abuse-and-spamming/invalid-contact-form", 41 | "type": "text/html" 42 | } 43 | ] 44 | } 45 | ], 46 | "country": "HK", 47 | "events": [ 48 | { 49 | "eventAction": "registration", 50 | "eventDate": "2008-09-04T06:40:30Z" 51 | }, 52 | { 53 | "eventAction": "last changed", 54 | "eventDate": "2020-07-15T13:09:38Z" 55 | } 56 | ], 57 | "name": "HKBN-AS-AP", 58 | "remarks": [ 59 | { 60 | "description": [ 61 | "Hong Kong Broadband Network Ltd." 62 | ], 63 | "title": "description" 64 | } 65 | ], 66 | "links": [ 67 | { 68 | "value": "https://rdap.apnic.net/autnum/9269", 69 | "rel": "self", 70 | "href": "https://rdap.apnic.net/autnum/9269", 71 | "type": "application/rdap+json" 72 | }, 73 | { 74 | "value": "https://rdap.apnic.net/autnum/9269", 75 | "rel": "related", 76 | "href": "https://netox.apnic.net/search/AS9269?utm_source=rdap&utm_medium=result&utm_campaign=rdap_result", 77 | "type": "text/html" 78 | } 79 | ], 80 | "status": [ 81 | "active" 82 | ], 83 | "handle": "AS9269", 84 | "endAutnum": 9269, 85 | "startAutnum": 9269, 86 | "objectClassName": "autnum", 87 | "entities": [ 88 | { 89 | "roles": [ 90 | "abuse" 91 | ], 92 | "events": [ 93 | { 94 | "eventAction": "registration", 95 | "eventDate": "2012-05-16T06:32:40Z" 96 | }, 97 | { 98 | "eventAction": "last changed", 99 | "eventDate": "2023-10-27T01:46:39Z" 100 | } 101 | ], 102 | "remarks": [ 103 | { 104 | "description": [ 105 | "abuse@hkbn.net was validated on 2023-09-13", 106 | "hostmaster@hkbn.com.hk was validated on 2023-10-27" 107 | ], 108 | "title": "remarks" 109 | } 110 | ], 111 | "links": [ 112 | { 113 | "value": "https://rdap.apnic.net/autnum/9269", 114 | "rel": "self", 115 | "href": "https://rdap.apnic.net/entity/IRT-HKBN-HK", 116 | "type": "application/rdap+json" 117 | } 118 | ], 119 | "vcardArray": [ 120 | "vcard", 121 | [ 122 | [ 123 | "version", 124 | {}, 125 | "text", 126 | "4.0" 127 | ], 128 | [ 129 | "fn", 130 | {}, 131 | "text", 132 | "IRT-HKBN-HK" 133 | ], 134 | [ 135 | "kind", 136 | {}, 137 | "text", 138 | "group" 139 | ], 140 | [ 141 | "adr", 142 | { 143 | "label": "15/F Trans Asia Centre\n18 Kin Hong Street, Kwai Chung\nN.T." 144 | }, 145 | "text", 146 | [ 147 | "", 148 | "", 149 | "", 150 | "", 151 | "", 152 | "", 153 | "" 154 | ] 155 | ], 156 | [ 157 | "email", 158 | {}, 159 | "text", 160 | "hostmaster@hkbn.com.hk" 161 | ], 162 | [ 163 | "email", 164 | { 165 | "pref": "1" 166 | }, 167 | "text", 168 | "abuse@hkbn.net" 169 | ] 170 | ] 171 | ], 172 | "objectClassName": "entity", 173 | "handle": "IRT-HKBN-HK" 174 | }, 175 | { 176 | "roles": [ 177 | "registrant" 178 | ], 179 | "events": [ 180 | { 181 | "eventAction": "registration", 182 | "eventDate": "2017-08-21T12:56:42Z" 183 | }, 184 | { 185 | "eventAction": "last changed", 186 | "eventDate": "2023-09-05T02:14:48Z" 187 | } 188 | ], 189 | "links": [ 190 | { 191 | "value": "https://rdap.apnic.net/autnum/9269", 192 | "rel": "self", 193 | "href": "https://rdap.apnic.net/entity/ORG-HKBN1-AP", 194 | "type": "application/rdap+json" 195 | } 196 | ], 197 | "vcardArray": [ 198 | "vcard", 199 | [ 200 | [ 201 | "version", 202 | {}, 203 | "text", 204 | "4.0" 205 | ], 206 | [ 207 | "fn", 208 | {}, 209 | "text", 210 | "Hong Kong Broadband Network Ltd" 211 | ], 212 | [ 213 | "kind", 214 | {}, 215 | "text", 216 | "org" 217 | ], 218 | [ 219 | "adr", 220 | { 221 | "label": "15/F Trans Asia Centre\n18 Kin Hong Street" 222 | }, 223 | "text", 224 | [ 225 | "", 226 | "", 227 | "", 228 | "", 229 | "", 230 | "", 231 | "" 232 | ] 233 | ], 234 | [ 235 | "tel", 236 | { 237 | "type": "voice" 238 | }, 239 | "text", 240 | "+852-3999-3888" 241 | ], 242 | [ 243 | "tel", 244 | { 245 | "type": "fax" 246 | }, 247 | "text", 248 | "+852-8167-7020" 249 | ], 250 | [ 251 | "email", 252 | {}, 253 | "text", 254 | "nocsn@hkbn.com.hk" 255 | ] 256 | ] 257 | ], 258 | "objectClassName": "entity", 259 | "handle": "ORG-HKBN1-AP" 260 | }, 261 | { 262 | "roles": [ 263 | "technical", 264 | "administrative" 265 | ], 266 | "events": [ 267 | { 268 | "eventAction": "registration", 269 | "eventDate": "2012-05-23T03:52:23Z" 270 | }, 271 | { 272 | "eventAction": "last changed", 273 | "eventDate": "2021-03-25T09:01:18Z" 274 | } 275 | ], 276 | "links": [ 277 | { 278 | "value": "https://rdap.apnic.net/autnum/9269", 279 | "rel": "self", 280 | "href": "https://rdap.apnic.net/entity/HKBN-HK", 281 | "type": "application/rdap+json" 282 | } 283 | ], 284 | "vcardArray": [ 285 | "vcard", 286 | [ 287 | [ 288 | "version", 289 | {}, 290 | "text", 291 | "4.0" 292 | ], 293 | [ 294 | "fn", 295 | {}, 296 | "text", 297 | "HKBN Hostmaster" 298 | ], 299 | [ 300 | "kind", 301 | {}, 302 | "text", 303 | "individual" 304 | ], 305 | [ 306 | "adr", 307 | { 308 | "label": "Trans Asia Centre\n18 Kin Hong Street, Kwai Chung\nN.T." 309 | }, 310 | "text", 311 | [ 312 | "", 313 | "", 314 | "", 315 | "", 316 | "", 317 | "", 318 | "" 319 | ] 320 | ], 321 | [ 322 | "tel", 323 | { 324 | "type": "voice" 325 | }, 326 | "text", 327 | "+852-3999-3888" 328 | ], 329 | [ 330 | "tel", 331 | { 332 | "type": "fax" 333 | }, 334 | "text", 335 | "+852-8167-7020" 336 | ], 337 | [ 338 | "email", 339 | {}, 340 | "text", 341 | "hostmaster@hkbn.com.hk" 342 | ], 343 | [ 344 | "email", 345 | { 346 | "pref": "1" 347 | }, 348 | "text", 349 | "abuse@hkbn.com.hk" 350 | ] 351 | ] 352 | ], 353 | "objectClassName": "entity", 354 | "handle": "HKBN-HK" 355 | } 356 | ], 357 | "port43": "whois.apnic.net" 358 | } -------------------------------------------------------------------------------- /tests/data/rdap/domain/20c.com.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance": [ 3 | "rdap_level_0", 4 | "icann_rdap_technical_implementation_guide_0", 5 | "icann_rdap_response_profile_0" 6 | ], 7 | "notices": [ 8 | { 9 | "title": "Terms of Use", 10 | "description": [ 11 | "Service subject to Terms of Use." 12 | ], 13 | "links": [ 14 | { 15 | "value": null, 16 | "rel": null, 17 | "type": "text/html", 18 | "href": "https://www.verisign.com/domain-names/registration-data-access-protocol/terms-service/index.xhtml" 19 | } 20 | ] 21 | }, 22 | { 23 | "title": "Status Codes", 24 | "description": [ 25 | "For more information on domain status codes, please visit https://icann.org/epp" 26 | ], 27 | "links": [ 28 | { 29 | "value": null, 30 | "rel": null, 31 | "type": "text/html", 32 | "href": "https://icann.org/epp" 33 | } 34 | ] 35 | }, 36 | { 37 | "title": "RDDS Inaccuracy Complaint Form", 38 | "description": [ 39 | "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" 40 | ], 41 | "links": [ 42 | { 43 | "value": null, 44 | "rel": null, 45 | "type": "text/html", 46 | "href": "https://icann.org/wicf" 47 | } 48 | ] 49 | } 50 | ], 51 | "handle": "123664426_DOMAIN_COM-VRSN", 52 | "ldhName": "20C.COM", 53 | "events": [ 54 | { 55 | "eventAction": "registration", 56 | "eventDate": "2004-06-28T18:28:14Z" 57 | }, 58 | { 59 | "eventAction": "expiration", 60 | "eventDate": "2025-06-28T18:28:14Z" 61 | }, 62 | { 63 | "eventAction": "last changed", 64 | "eventDate": "2024-06-25T03:31:37Z" 65 | }, 66 | { 67 | "eventAction": "last update of RDAP database", 68 | "eventDate": "2024-07-24T18:48:30Z" 69 | } 70 | ], 71 | "links": [ 72 | { 73 | "value": "https://rdap.verisign.com/com/v1/domain/20C.COM", 74 | "rel": "self", 75 | "type": "application/rdap+json", 76 | "href": "https://rdap.verisign.com/com/v1/domain/20C.COM" 77 | }, 78 | { 79 | "value": "https://rdap.joker.com/domain/20C.COM", 80 | "rel": "related", 81 | "type": "application/rdap+json", 82 | "href": "https://rdap.joker.com/domain/20C.COM" 83 | } 84 | ], 85 | "entities": [ 86 | { 87 | "handle": "113", 88 | "vcardArray": [ 89 | "vcard", 90 | [ 91 | [ 92 | "version", 93 | {}, 94 | "text", 95 | "4.0" 96 | ], 97 | [ 98 | "fn", 99 | {}, 100 | "text", 101 | "CSL Computer Service Langenbach GmbH d/b/a joker.com" 102 | ] 103 | ] 104 | ], 105 | "roles": [ 106 | "registrar" 107 | ], 108 | "links": [], 109 | "events": [], 110 | "status": [], 111 | "port43": "", 112 | "objectClassName": "entity", 113 | "remarks": [], 114 | "entities": [ 115 | { 116 | "handle": "", 117 | "vcardArray": [ 118 | "vcard", 119 | [ 120 | [ 121 | "version", 122 | {}, 123 | "text", 124 | "4.0" 125 | ], 126 | [ 127 | "fn", 128 | {}, 129 | "text", 130 | "" 131 | ], 132 | [ 133 | "tel", 134 | { 135 | "type": "voice" 136 | }, 137 | "uri", 138 | "tel:+49.21186767447" 139 | ], 140 | [ 141 | "email", 142 | {}, 143 | "text", 144 | "abuse@joker.com" 145 | ] 146 | ] 147 | ], 148 | "roles": [ 149 | "abuse" 150 | ], 151 | "links": [], 152 | "events": [], 153 | "status": [], 154 | "port43": "", 155 | "objectClassName": "entity", 156 | "remarks": [], 157 | "entities": [] 158 | } 159 | ] 160 | } 161 | ], 162 | "port43": "", 163 | "network": null, 164 | "objectClassName": "domain", 165 | "secureDNS": { 166 | "delegationSigned": false, 167 | "zeroSigned": null, 168 | "dsData": [] 169 | }, 170 | "nameservers": [ 171 | { 172 | "objectClassName": "nameserver", 173 | "ldhName": "NS-1468.AWSDNS-55.ORG", 174 | "unicodeName": null, 175 | "ipAddresses": {}, 176 | "remarks": [], 177 | "port43": null, 178 | "events": [] 179 | }, 180 | { 181 | "objectClassName": "nameserver", 182 | "ldhName": "NS-1771.AWSDNS-29.CO.UK", 183 | "unicodeName": null, 184 | "ipAddresses": {}, 185 | "remarks": [], 186 | "port43": null, 187 | "events": [] 188 | }, 189 | { 190 | "objectClassName": "nameserver", 191 | "ldhName": "NS-327.AWSDNS-40.COM", 192 | "unicodeName": null, 193 | "ipAddresses": {}, 194 | "remarks": [], 195 | "port43": null, 196 | "events": [] 197 | }, 198 | { 199 | "objectClassName": "nameserver", 200 | "ldhName": "NS-545.AWSDNS-04.NET", 201 | "unicodeName": null, 202 | "ipAddresses": {}, 203 | "remarks": [], 204 | "port43": null, 205 | "events": [] 206 | } 207 | ] 208 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/AMS346-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "AMS346-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Angel Martinez Sanchez" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Avd. Federico Anaya, 52\n37004 Salamanca\nSPAIN" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+34910912131" ], [ "email", { }, "text", "angel@alojalia.com" ], [ "org", { }, "text", "ORG-IYCS4-RIPE" ] ] ], 8 | "entities" : [ { 9 | "handle" : "es-alojalia-1-mnt", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/AMS346-RIPE", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/AMS346-RIPE" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-11-04T02:59:37Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/AMS346-RIPE", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/APR41-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "links" : [ { 3 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 4 | "rel" : "copyright", 5 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 6 | } ], 7 | "rdapConformance" : [ "rdap_level_0" ], 8 | "notices" : [ { 9 | "title" : "Terms and Conditions", 10 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 11 | "links" : [ { 12 | "rel" : "terms-of-service", 13 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 14 | "type" : "application/pdf" 15 | } ] 16 | } ], 17 | "port43" : "whois.ripe.net", 18 | "errorCode" : 400, 19 | "title" : "Invalid syntax." 20 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/AS5496JP.input: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode" : 404, 3 | "title" : "Not Found", 4 | "description" : [ "The server has not found anything matching the Request-URI.", "The syntax used for this request is invalid for this particular server." ], 5 | "rdapConformance" : [ "rdap_level_0" ], 6 | "notices" : [ { 7 | "title" : "Terms and Conditions", 8 | "description" : [ "This is the APNIC WHOIS Database query service. The objects are in RDAP format." ], 9 | "links" : [ { 10 | "value" : "https://rdap.apnic.net/entity//AS5496JP", 11 | "rel" : "terms-of-service", 12 | "href" : "http://www.apnic.net/db/dbcopyright.html", 13 | "type" : "text/html" 14 | } ] 15 | } ] 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/BRI2.input: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/data/rdap/entity/CLUE1-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "CLUE1-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Netwerkvereniging Coloclue" ], [ "kind", { }, "text", "group" ], [ "adr", { 4 | "label" : "Frans Duwaerstraat 34\n1318AC Almere\nNetherlands" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+31651387718" ], [ "email", { }, "text", "ops@coloclue.net" ], [ "email", { }, "text", "routers@coloclue.net" ] ] ], 8 | "entities" : [ { 9 | "handle" : "COLOCLUE-MNT", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | }, { 13 | "handle" : "JB17421-RIPE", 14 | "roles" : [ "technical" ], 15 | "objectClassName" : "entity" 16 | }, { 17 | "handle" : "JL9785-RIPE", 18 | "roles" : [ "technical" ], 19 | "objectClassName" : "entity" 20 | }, { 21 | "handle" : "JVI-RIPE", 22 | "roles" : [ "technical" ], 23 | "objectClassName" : "entity" 24 | }, { 25 | "handle" : "MS44437-RIPE", 26 | "roles" : [ "administrative", "technical" ], 27 | "objectClassName" : "entity" 28 | }, { 29 | "handle" : "MWTS1-RIPE", 30 | "roles" : [ "administrative" ], 31 | "objectClassName" : "entity" 32 | }, { 33 | "handle" : "NMR5-RIPE", 34 | "roles" : [ "technical" ], 35 | "objectClassName" : "entity" 36 | }, { 37 | "handle" : "NT1031-RIPE", 38 | "roles" : [ "administrative" ], 39 | "objectClassName" : "entity" 40 | }, { 41 | "handle" : "PDW-RIPE", 42 | "roles" : [ "administrative", "technical" ], 43 | "objectClassName" : "entity" 44 | }, { 45 | "handle" : "PEER-RIPE", 46 | "roles" : [ "technical" ], 47 | "objectClassName" : "entity" 48 | }, { 49 | "handle" : "TIJN-RIPE", 50 | "roles" : [ "administrative", "technical" ], 51 | "objectClassName" : "entity" 52 | } ], 53 | "links" : [ { 54 | "value" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE", 55 | "rel" : "self", 56 | "href" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE" 57 | }, { 58 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 59 | "rel" : "copyright", 60 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 61 | } ], 62 | "events" : [ { 63 | "eventAction" : "last changed", 64 | "eventDate" : "2017-09-21T23:55:48Z" 65 | } ], 66 | "rdapConformance" : [ "rdap_level_0" ], 67 | "notices" : [ { 68 | "title" : "Filtered", 69 | "description" : [ "This output has been filtered." ] 70 | }, { 71 | "title" : "Source", 72 | "description" : [ "Objects returned came from source", "RIPE" ] 73 | }, { 74 | "title" : "Terms and Conditions", 75 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 76 | "links" : [ { 77 | "value" : "https://rdap.db.ripe.net/entity/CLUE1-RIPE", 78 | "rel" : "terms-of-service", 79 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 80 | "type" : "application/pdf" 81 | } ] 82 | } ], 83 | "port43" : "whois.ripe.net", 84 | "objectClassName" : "entity" 85 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/DJVG.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "DJVG", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Daan van Gorkum" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Postbus 8160\n1180LD Amstelveen\nthe Netherlands" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+31203080063" ], [ "email", { }, "text", "daan.vangorkum@vusam.com" ] ] ], 8 | "entities" : [ { 9 | "handle" : "VUSAM", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/DJVG", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/DJVG" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-06-13T22:21:32Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/DJVG", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/EK6175JP.input: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode" : 404, 3 | "title" : "Not Found", 4 | "description" : [ "The server has not found anything matching the Request-URI.", "The syntax used for this request is invalid for this particular server." ], 5 | "rdapConformance" : [ "rdap_level_0" ], 6 | "notices" : [ { 7 | "title" : "Terms and Conditions", 8 | "description" : [ "This is the APNIC WHOIS Database query service. The objects are in RDAP format." ], 9 | "links" : [ { 10 | "value" : "https://rdap.apnic.net/entity//EK6175JP", 11 | "rel" : "terms-of-service", 12 | "href" : "http://www.apnic.net/db/dbcopyright.html", 13 | "type" : "text/html" 14 | } ] 15 | } ] 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/GJM3.input: -------------------------------------------------------------------------------- 1 | {"objectClassName":"entity","handle":"GJM3","vcardArray":["vcard",[["version",{},"text","4.0"],["kind",{},"text","individual"],["fn",{},"text","Geovane Jose Vieira Martins"],["lang",{},"language-tag","PT"]]],"events":[{"eventAction":"registration","eventDate":"1998-11-10T12:00:00Z"},{"eventAction":"last changed","eventDate":"2016-04-26T11:58:32Z"}],"links":[{"value":"https://rdap.registro.br/entity/GJM3","rel":"self","href":"https://rdap.registro.br/entity/GJM3","type":"application/rdap+json"}],"remarks":[{"type":"object truncated due to server policy","description":["The object does not contain all data due to server policy. Could be excessive total number of queries."]}],"notices":[{"title":"Terms of Use","description":["Copyright (c) NIC.br","The use of the data is only permitted as described in","full by the terms of use, being prohibited its ","distribution, commercialization or reproduction, in ","particular, to use it for advertising or any similar ","purpose."],"links":[{"value":"https://rdap.registro.br/entity/GJM3","rel":"alternate","href":"https://registro.br/termo/en.html","type":"text/html"}]}],"rdapConformance":["rdap_level_0","nicbr_level_0"],"port43":"whois.nic.br"} 2 | -------------------------------------------------------------------------------- /tests/data/rdap/entity/HH11825JP.input: -------------------------------------------------------------------------------- 1 | {"description":["Request could not be understood, malformed syntax"],"errorCode":"400","title":"Bad Request","notices":[{"description":["This is the APNIC RDAP query service. The objects are in RDAP format."],"links":[{"href":"http://www.apnic.net/db/dbcopyright.html","rel":"terms-of-service","type":"text/html"}],"title":"Terms and Conditions"}],"rdapConformance":["rdap_level_0"]} -------------------------------------------------------------------------------- /tests/data/rdap/entity/HKBN-HK.input: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode" : 404, 3 | "title" : "Not Found", 4 | "description" : [ "The server has not found anything matching the Request-URI.", "The syntax used for this request is invalid for this particular server." ], 5 | "rdapConformance" : [ "rdap_level_0" ], 6 | "notices" : [ { 7 | "title" : "Terms and Conditions", 8 | "description" : [ "This is the APNIC WHOIS Database query service. The objects are in RDAP format." ], 9 | "links" : [ { 10 | "value" : "https://rdap.apnic.net/entity//HKBN-HK", 11 | "rel" : "terms-of-service", 12 | "href" : "http://www.apnic.net/db/dbcopyright.html", 13 | "type" : "text/html" 14 | } ] 15 | } ] 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/JK11944-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "JK11944-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Jiri Kreibich" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Piskovcova 736/8\n19000\nPraha 9 - Prosek\nCZECH REPUBLIC" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+420723807560" ], [ "email", { }, "text", "kreibich@freepeer.net" ] ] ], 8 | "entities" : [ { 9 | "handle" : "FeldHost-MNT", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/JK11944-RIPE", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/JK11944-RIPE" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-03-30T21:41:09Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/JK11944-RIPE", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/JNIC1-AP.input: -------------------------------------------------------------------------------- 1 | { 2 | "links" : [ { 3 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 4 | "rel" : "copyright", 5 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 6 | } ], 7 | "rdapConformance" : [ "rdap_level_0" ], 8 | "notices" : [ { 9 | "title" : "Terms and Conditions", 10 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 11 | "links" : [ { 12 | "rel" : "terms-of-service", 13 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 14 | "type" : "application/pdf" 15 | } ] 16 | } ], 17 | "port43" : "whois.ripe.net", 18 | "errorCode" : 404, 19 | "title" : "not found" 20 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/MM47295-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "MM47295-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Mikhail Majorov" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Chehova st. 318\n347900\nTaganrog\nRUSSIAN FEDERATION" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+79054309006" ] ] ], 8 | "entities" : [ { 9 | "handle" : "MNT-ML", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/MM47295-RIPE", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/MM47295-RIPE" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-11-07T12:50:27Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/MM47295-RIPE", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/MO5920JP.input: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode" : 404, 3 | "title" : "Not Found", 4 | "description" : [ "The server has not found anything matching the Request-URI.", "The syntax used for this request is invalid for this particular server." ], 5 | "rdapConformance" : [ "rdap_level_0" ], 6 | "notices" : [ { 7 | "title" : "Terms and Conditions", 8 | "description" : [ "This is the APNIC WHOIS Database query service. The objects are in RDAP format." ], 9 | "links" : [ { 10 | "value" : "https://rdap.apnic.net/entity//MO5920JP", 11 | "rel" : "terms-of-service", 12 | "href" : "http://www.apnic.net/db/dbcopyright.html", 13 | "type" : "text/html" 14 | } ] 15 | } ] 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/MP31159-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "MP31159-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Mikhail Purtov" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "271 Frunze st.\n720065\nBishkek\nKYRGYZSTAN" 5 | }, "text", null ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+996555771311" ] ] ], 8 | "entities" : [ { 9 | "handle" : "kg-ix-1-mnt", 10 | "roles" : [ "registrant" ], 11 | "objectClassName" : "entity" 12 | } ], 13 | "links" : [ { 14 | "value" : "https://rdap.db.ripe.net/entity/MP31159-RIPE", 15 | "rel" : "self", 16 | "href" : "https://rdap.db.ripe.net/entity/MP31159-RIPE" 17 | }, { 18 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 19 | "rel" : "copyright", 20 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 21 | } ], 22 | "events" : [ { 23 | "eventAction" : "last changed", 24 | "eventDate" : "2017-07-03T17:29:24Z" 25 | } ], 26 | "rdapConformance" : [ "rdap_level_0" ], 27 | "notices" : [ { 28 | "title" : "Filtered", 29 | "description" : [ "This output has been filtered." ] 30 | }, { 31 | "title" : "Source", 32 | "description" : [ "Objects returned came from source", "RIPE" ] 33 | }, { 34 | "title" : "Terms and Conditions", 35 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 36 | "links" : [ { 37 | "value" : "https://rdap.db.ripe.net/entity/MP31159-RIPE", 38 | "rel" : "terms-of-service", 39 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 40 | "type" : "application/pdf" 41 | } ] 42 | } ], 43 | "port43" : "whois.ripe.net", 44 | "objectClassName" : "entity" 45 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/PEERI-ARIN.input: -------------------------------------------------------------------------------- 1 | { 2 | "links" : [ { 3 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 4 | "rel" : "copyright", 5 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 6 | } ], 7 | "rdapConformance" : [ "rdap_level_0" ], 8 | "notices" : [ { 9 | "title" : "Terms and Conditions", 10 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 11 | "links" : [ { 12 | "rel" : "terms-of-service", 13 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 14 | "type" : "application/pdf" 15 | } ] 16 | } ], 17 | "port43" : "whois.ripe.net", 18 | "errorCode" : 400, 19 | "title" : "Invalid syntax." 20 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/SD12478-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "SD12478-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "Yavuz Selim MALKOC" ], [ "kind", { }, "text", "individual" ], [ "adr", { 4 | "label" : "Sultan Orhan Mah. Ilyasbey Cad, 1812 Sk.\n41400\nKocaeli\nTURKEY" 5 | }, "text", [ "", "", "", "", "", "", "" ] ], [ "tel", { 6 | "type" : "voice" 7 | }, "text", "+902626446663" ] ] ], 8 | "entities" : [ { 9 | "handle" : "mnt-tr-internetten-1", 10 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "mnt-tr-internetten-1" ], [ "kind", { }, "text", "individual" ] ] ], 11 | "roles" : [ "registrant" ], 12 | "links" : [ { 13 | "value" : "https://rdap.db.ripe.net/entity/SD12478-RIPE", 14 | "rel" : "self", 15 | "href" : "https://rdap.db.ripe.net/entity/mnt-tr-internetten-1" 16 | }, { 17 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 18 | "rel" : "copyright", 19 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 20 | } ], 21 | "objectClassName" : "entity" 22 | } ], 23 | "links" : [ { 24 | "value" : "https://rdap.db.ripe.net/entity/SD12478-RIPE", 25 | "rel" : "self", 26 | "href" : "https://rdap.db.ripe.net/entity/SD12478-RIPE" 27 | }, { 28 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 29 | "rel" : "copyright", 30 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 31 | } ], 32 | "events" : [ { 33 | "eventAction" : "registration", 34 | "eventDate" : "2019-09-27T12:14:59Z" 35 | }, { 36 | "eventAction" : "last changed", 37 | "eventDate" : "2019-09-30T13:41:16Z" 38 | } ], 39 | "rdapConformance" : [ "cidr0", "rdap_level_0", "nro_rdap_profile_0", "redacted" ], 40 | "notices" : [ { 41 | "title" : "Filtered", 42 | "description" : [ "This output has been filtered." ] 43 | }, { 44 | "title" : "Whois Inaccuracy Reporting", 45 | "description" : [ "If you see inaccuracies in the results, please visit:" ], 46 | "links" : [ { 47 | "value" : "https://rdap.db.ripe.net/entity/SD12478-RIPE", 48 | "rel" : "inaccuracy-report", 49 | "href" : "https://www.ripe.net/contact-form?topic=ripe_dbm&show_form=true", 50 | "type" : "text/html" 51 | } ] 52 | }, { 53 | "title" : "Source", 54 | "description" : [ "Objects returned came from source", "RIPE" ] 55 | }, { 56 | "title" : "Terms and Conditions", 57 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 58 | "links" : [ { 59 | "value" : "https://rdap.db.ripe.net/entity/SD12478-RIPE", 60 | "rel" : "terms-of-service", 61 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 62 | "type" : "application/pdf" 63 | } ] 64 | } ], 65 | "port43" : "whois.ripe.net", 66 | "objectClassName" : "entity" 67 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/WA2477-RIPE.input: -------------------------------------------------------------------------------- 1 | { 2 | "handle" : "WA2477-RIPE", 3 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "WEBROCKET SUPPORT TEAM" ], [ "kind", { }, "text", "group" ], [ "adr", { 4 | "label" : "st. Movsesa Horenaci 14/20, Vagharshapat, Armavir, Armenia, 1101" 5 | }, "text", [ "", "", "", "", "", "", "" ] ], [ "email", { 6 | "type" : "abuse" 7 | }, "text", "abuse@webrocket.am" ] ] ], 8 | "entities" : [ { 9 | "handle" : "WEBROCKET-MNT", 10 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "fn", { }, "text", "WEBROCKET-MNT" ], [ "kind", { }, "text", "individual" ] ] ], 11 | "roles" : [ "registrant" ], 12 | "links" : [ { 13 | "value" : "https://rdap.db.ripe.net/entity/WA2477-RIPE", 14 | "rel" : "self", 15 | "href" : "https://rdap.db.ripe.net/entity/WEBROCKET-MNT" 16 | }, { 17 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 18 | "rel" : "copyright", 19 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 20 | } ], 21 | "objectClassName" : "entity" 22 | } ], 23 | "links" : [ { 24 | "value" : "https://rdap.db.ripe.net/entity/WA2477-RIPE", 25 | "rel" : "self", 26 | "href" : "https://rdap.db.ripe.net/entity/WA2477-RIPE" 27 | }, { 28 | "value" : "http://www.ripe.net/data-tools/support/documentation/terms", 29 | "rel" : "copyright", 30 | "href" : "http://www.ripe.net/data-tools/support/documentation/terms" 31 | } ], 32 | "events" : [ { 33 | "eventAction" : "registration", 34 | "eventDate" : "2023-07-01T14:23:57Z" 35 | }, { 36 | "eventAction" : "last changed", 37 | "eventDate" : "2023-07-01T14:33:34Z" 38 | } ], 39 | "rdapConformance" : [ "cidr0", "rdap_level_0", "nro_rdap_profile_0", "redacted" ], 40 | "notices" : [ { 41 | "title" : "Filtered", 42 | "description" : [ "This output has been filtered." ] 43 | }, { 44 | "title" : "Whois Inaccuracy Reporting", 45 | "description" : [ "If you see inaccuracies in the results, please visit:" ], 46 | "links" : [ { 47 | "value" : "https://rdap.db.ripe.net/entity/WA2477-RIPE", 48 | "rel" : "inaccuracy-report", 49 | "href" : "https://www.ripe.net/contact-form?topic=ripe_dbm&show_form=true", 50 | "type" : "text/html" 51 | } ] 52 | }, { 53 | "title" : "Source", 54 | "description" : [ "Objects returned came from source", "RIPE" ] 55 | }, { 56 | "title" : "Terms and Conditions", 57 | "description" : [ "This is the RIPE Database query service. The objects are in RDAP format." ], 58 | "links" : [ { 59 | "value" : "https://rdap.db.ripe.net/entity/WA2477-RIPE", 60 | "rel" : "terms-of-service", 61 | "href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf", 62 | "type" : "application/pdf" 63 | } ] 64 | } ], 65 | "port43" : "whois.ripe.net", 66 | "objectClassName" : "entity", 67 | "redacted" : [ { 68 | "name" : { 69 | "description" : "Personal e-mail information" 70 | }, 71 | "reason" : { 72 | "description" : "Personal data" 73 | }, 74 | "prePath" : "$.vcardArray[1][?(@[0]=='e-mail')]", 75 | "method" : "removal" 76 | } ] 77 | } -------------------------------------------------------------------------------- /tests/data/rdap/entity/YK11438JP.input: -------------------------------------------------------------------------------- 1 | { 2 | "errorCode" : 404, 3 | "title" : "Not Found", 4 | "description" : [ "The server has not found anything matching the Request-URI.", "The syntax used for this request is invalid for this particular server." ], 5 | "rdapConformance" : [ "rdap_level_0" ], 6 | "notices" : [ { 7 | "title" : "Terms and Conditions", 8 | "description" : [ "This is the APNIC WHOIS Database query service. The objects are in RDAP format." ], 9 | "links" : [ { 10 | "value" : "https://rdap.apnic.net/entity//YK11438JP", 11 | "rel" : "terms-of-service", 12 | "href" : "http://www.apnic.net/db/dbcopyright.html", 13 | "type" : "text/html" 14 | } ] 15 | } ] 16 | } -------------------------------------------------------------------------------- /tests/data/rdap/rdap/entity/PP17-AFRINIC.input: -------------------------------------------------------------------------------- 1 | {"rdapConformance":["rdap_level_0","nro_rdap_profile_0"],"notices":[{"title":"ABOUT","description":["This is the AfriNIC RDAP server."],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"describedby","href":"https://www.afrinic.net/support/whois-db/reference-manual","hreflang":["en"],"type":"text/html","title":"AFRINIC Database Reference Manual","media":"screen"}]},{"title":"Terms and Conditions","description":["This is the AFRINIC Database query service. The objects are in RDAP format."],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"terms-of-service","href":"https://afrinic.net/whois/terms","type":"text/html"}]},{"title":"Whois Inaccuracy Reporting","description":["If you see inaccuracies in the results, please visit: "],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"inaccuracy-report","href":"https://www.afrinic.net/support/whois-db/reference-manual","type":"text/html","title":"AFRINIC WHOIS Inaccuracy Report"}]},{"title":"jCard sunset end","description":["2023-12-31T23:59:59Z"],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"related","href":"https://www.ietf.org/id/draft-ietf-regext-rdap-jscontact-12.html","type":"text/html","title":"jCard deprecation and replacement with JSContact Card"},{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"alternate","href":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC?jscard=1","type":"application/rdap+json","title":"JSContact Card version"}]},{"title":"jCard deprecation end","description":["2022-12-31T23:59:59Z"],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"related","href":"https://www.ietf.org/id/draft-ietf-regext-rdap-jscontact-12.html","type":"text/html","title":"jCard deprecation and replacement with JSContact Card"},{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"alternate","href":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC?jscard=1","type":"application/rdap+json","title":"JSContact Card version"}]}],"lang":"en","links":[{"value":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC","rel":"self","href":"https://rdap.afrinic.net/rdap/entity/PP17-AFRINIC"}],"handle":"PP17-AFRINIC","status":["active"],"port43":"whois.afrinic.net","vcardArray":["vcard",[["version",{},"text","4.0"],["kind",{},"text","individual"],["fn",{},"text","Peter Peele"],["tel",{"type":"work"},"text","tel:+27-82-064-3322"],["email",{},"text","peterp@workonline.africa"],["adr",{"label":"114 West Street\nSandton\nSouth Africa\nPostal Code 2066"},"text",["114 West Street","Sandton","South Africa","Postal Code 2066","","",""]]]],"objectClassName":"entity"} -------------------------------------------------------------------------------- /tests/data/rdap/rdap/entity/WOL-AFRINIC.input: -------------------------------------------------------------------------------- 1 | {"rdapConformance":["rdap_level_0","nro_rdap_profile_0"],"notices":[{"title":"ABOUT","description":["This is the AfriNIC RDAP server."],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"describedby","href":"https://www.afrinic.net/support/whois-db/reference-manual","hreflang":["en"],"type":"text/html","title":"AFRINIC Database Reference Manual","media":"screen"}]},{"title":"Terms and Conditions","description":["This is the AFRINIC Database query service. The objects are in RDAP format."],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"terms-of-service","href":"https://afrinic.net/whois/terms","type":"text/html"}]},{"title":"Whois Inaccuracy Reporting","description":["If you see inaccuracies in the results, please visit: "],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"inaccuracy-report","href":"https://www.afrinic.net/support/whois-db/reference-manual","type":"text/html","title":"AFRINIC WHOIS Inaccuracy Report"}]},{"title":"jCard sunset end","description":["2023-12-31T23:59:59Z"],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"related","href":"https://www.ietf.org/id/draft-ietf-regext-rdap-jscontact-12.html","type":"text/html","title":"jCard deprecation and replacement with JSContact Card"},{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"alternate","href":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC?jscard=1","type":"application/rdap+json","title":"JSContact Card version"}]},{"title":"jCard deprecation end","description":["2022-12-31T23:59:59Z"],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"related","href":"https://www.ietf.org/id/draft-ietf-regext-rdap-jscontact-12.html","type":"text/html","title":"jCard deprecation and replacement with JSContact Card"},{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"alternate","href":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC?jscard=1","type":"application/rdap+json","title":"JSContact Card version"}]}],"lang":"en","links":[{"value":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC","rel":"self","href":"https://rdap.afrinic.net/rdap/entity/WOL-AFRINIC"}],"handle":"WOL-AFRINIC","status":["active"],"port43":"whois.afrinic.net","vcardArray":["vcard",[["version",{},"text","4.0"],["kind",{},"text","group"],["fn",{},"text","Workonline NOC"],["tel",{"type":"work"},"text","tel:+27-21-200-9009"],["email",{},"text","noc@workonline.africa"],["adr",{"label":"114 West St\nJohannesburg 2196\nSouth Africa"},"text",["114 West St","Johannesburg 2196","South Africa","","","",""]]]],"entities":[{"lang":"en","remarks":[{"title":"Remark","description":["Ben Maddison"]}],"links":[{"value":"https://rdap.afrinic.net/rdap/entity/BM15-AFRINIC","rel":"self","href":"https://rdap.afrinic.net/rdap/entity/BM15-AFRINIC"}],"handle":"BM15-AFRINIC","status":["active"],"port43":"whois.afrinic.net","vcardArray":["vcard",[["version",{},"text","4.0"],["kind",{},"text","individual"],["fn",{},"text","Ben Maddison"],["tel",{"type":"work"},"text","tel:+27-21-200-9000"],["email",{},"text","benm@workonline.africa"],["adr",{"label":"114 West St\nJohannesburg 2916\nSouth Africa"},"text",["114 West St","Johannesburg 2916","South Africa","","","",""]]]],"roles":["administrative","technical"],"objectClassName":"entity"},{"lang":"en","links":[{"value":"https://rdap.afrinic.net/rdap/entity/ORG-WCL1-AFRINIC","rel":"self","href":"https://rdap.afrinic.net/rdap/entity/ORG-WCL1-AFRINIC"}],"handle":"ORG-WCL1-AFRINIC","status":["active"],"port43":"whois.afrinic.net","vcardArray":["vcard",[["version",{},"text","4.0"],["kind",{},"text","org"],["fn",{},"text","Workonline Communications(Pty) Ltd"],["tel",{"type":"work"},"text","tel:+27-21-200-9000"],["tel",{"type":"work"},"text","tel:+27-21-200-9009"],["email",{},"text","noc@workonline.africa"],["email",{},"text","abuse@workonline.africa"],["email",{},"text","communications@workonline.africa"],["adr",{"label":"114 West St\nJohannesburg 2196"},"text",["114 West St","Johannesburg 2196","","","","",""]]]],"roles":["organisation"],"objectClassName":"entity"}],"objectClassName":"entity"} -------------------------------------------------------------------------------- /tests/data/rdap/registry/entity/PEERI-ARIN.input: -------------------------------------------------------------------------------- 1 | { 2 | "rdapConformance" : [ "rdap_level_0" ], 3 | "notices" : [ { 4 | "title" : "Terms of Service", 5 | "description" : [ "By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use" ], 6 | "links" : [ { 7 | "value" : "https://rdap.arin.net/registry/entity/PEERI-ARIN", 8 | "rel" : "about", 9 | "type" : "text/html", 10 | "href" : "https://www.arin.net/resources/registry/whois/tou/" 11 | } ] 12 | }, { 13 | "title" : "Whois Inaccuracy Reporting", 14 | "description" : [ "If you see inaccuracies in the results, please visit: " ], 15 | "links" : [ { 16 | "value" : "https://rdap.arin.net/registry/entity/PEERI-ARIN", 17 | "rel" : "about", 18 | "type" : "text/html", 19 | "href" : "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/" 20 | } ] 21 | }, { 22 | "title" : "Copyright Notice", 23 | "description" : [ "Copyright 1997-2020, American Registry for Internet Numbers, Ltd." ] 24 | } ], 25 | "handle" : "PEERI-ARIN", 26 | "vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "adr", { 27 | "label" : "101 Park Ave.\r\n41st. floor\nNew York\nNY\n10178\nUnited States" 28 | }, "text", [ "", "", "", "", "", "", "" ] ], [ "fn", { }, "text", "Peering" ], [ "org", { }, "text", "Peering" ], [ "kind", { }, "text", "group" ], [ "email", { }, "text", "peering@ntt.net" ], [ "tel", { 29 | "type" : [ "work", "voice" ] 30 | }, "text", "+1-877-688-6625" ] ] ], 31 | "links" : [ { 32 | "value" : "https://rdap.arin.net/registry/entity/PEERI-ARIN", 33 | "rel" : "self", 34 | "type" : "application/rdap+json", 35 | "href" : "https://rdap.arin.net/registry/entity/PEERI-ARIN" 36 | }, { 37 | "value" : "https://rdap.arin.net/registry/entity/PEERI-ARIN", 38 | "rel" : "alternate", 39 | "type" : "application/xml", 40 | "href" : "https://whois.arin.net/rest/poc/PEERI-ARIN" 41 | } ], 42 | "events" : [ { 43 | "eventAction" : "last changed", 44 | "eventDate" : "2020-01-07T13:05:54-05:00" 45 | }, { 46 | "eventAction" : "registration", 47 | "eventDate" : "2006-09-05T19:36:06-04:00" 48 | } ], 49 | "status" : [ "validated" ], 50 | "port43" : "whois.arin.net", 51 | "objectClassName" : "entity" 52 | } -------------------------------------------------------------------------------- /tests/test_asn.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_filedata 3 | 4 | from rdap import RdapAsn, RdapNotFoundError 5 | 6 | 7 | def assert_parsed(data, parsed): 8 | # dump in json format for easily adding expected 9 | print( 10 | f"echo \\\n'{data.dumps(parsed)}'\\\n > {data.path}/{data.name}.expected", 11 | ) 12 | assert data.expected == parsed 13 | 14 | 15 | def test_rdap_asn_object(rdapc): 16 | data = dict(test="data") 17 | asn = RdapAsn(data, rdapc) 18 | assert rdapc == asn._rdapc 19 | assert data == asn._data 20 | 21 | 22 | def test_rdap_asn_lookup_not_found(rdapc): 23 | with pytest.raises(RdapNotFoundError): 24 | rdapc.get_asn(65535) 25 | 26 | 27 | def test_rdap_asn_lookup_no_client(rdapc): 28 | asn = rdapc.get_asn(63311) 29 | # force null the client 30 | asn._rdapc = None 31 | assert asn.parsed() 32 | 33 | 34 | def test_get_rdap(rdapc): 35 | obj = rdapc.get_rdap("https://rdap.arin.net/registry/autnum/63311") 36 | assert type(obj) is RdapAsn 37 | 38 | 39 | @pytest_filedata.RequestsData("rdap") # XXX , real_http=True) 40 | def test_rdap_asn_lookup(rdapc, data_rdap_autnum): 41 | print(data_rdap_autnum.name) 42 | # asn = rdap.get_asn(205726) 43 | asn = rdapc.get_asn(data_rdap_autnum.name) 44 | assert asn.get_rir() 45 | assert_parsed(data_rdap_autnum, asn.parsed()) 46 | -------------------------------------------------------------------------------- /tests/test_assignment.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from rdap.assignment import RIRAssignmentLookup 4 | 5 | 6 | class TestLookup(RIRAssignmentLookup): 7 | def download_data(self, rir, file_path, cache_days): 8 | setattr(self, "_downloaded_{rir}", file_path) 9 | 10 | 11 | def test_lookup(): 12 | lookup = TestLookup() 13 | path = os.path.join(os.path.dirname(__file__), "data", "assignment") 14 | lookup.load_data(path) 15 | 16 | # no info 17 | assert lookup.get_status(8771) is None 18 | 19 | # afrinic 20 | assert lookup.get_status(8524) == "allocated" 21 | assert lookup.get_status(8770) == "available" 22 | 23 | # apnic 24 | assert lookup.get_status(1781) == "allocated" 25 | 26 | # arin 27 | assert lookup.get_status(63311) == "assigned" 28 | assert lookup.get_status(63317) == "reserved" 29 | assert lookup.get_status(63360) == "assigned" 30 | assert lookup.get_status(63361) == "assigned" 31 | assert lookup.get_status(63362) is None 32 | 33 | # lacnic 34 | assert lookup.get_status(6193) == "allocated" 35 | assert lookup.get_status(6148) == "available" 36 | 37 | # ripe 38 | assert lookup.get_status(7) == "allocated" 39 | -------------------------------------------------------------------------------- /tests/test_bootstrap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import pytest 5 | 6 | import rdap 7 | from rdap import RdapNotFoundError, bootstrap 8 | 9 | 10 | def check_asn_service(asn, service): 11 | assert asn >= service.start_asn 12 | assert asn <= service.end_asn 13 | assert service.url 14 | 15 | 16 | def test_asn_tree(iana_asn): 17 | tree = bootstrap.AsnTree(iana_asn) 18 | empty_tree = bootstrap.AsnTree() 19 | empty_tree.load_data(iana_asn) 20 | assert len(empty_tree) == len(tree) 21 | 22 | 23 | @pytest.mark.parametrize("asn", [1, 1876, 1877, 2403, 63311, 400284, 400285, 401308]) 24 | def test_asn_tree_lookup(iana_asn, asn): 25 | tree = bootstrap.AsnTree(iana_asn) 26 | check_asn_service(asn, tree.get_service(asn)) 27 | 28 | 29 | @pytest.mark.parametrize("asn", [0, 65535, 99999999]) 30 | def test_asn_tree_lookup_notfound(iana_asn, asn): 31 | tree = bootstrap.AsnTree(iana_asn) 32 | with pytest.raises(LookupError): 33 | check_asn_service(asn, tree.get_service(asn)) 34 | 35 | 36 | @pytest.mark.parametrize("asn", [0, 65535, 99999999]) 37 | def test_asn_lookup_notfound(this_dir, asn): 38 | config_dir = os.path.join(this_dir, "data", "iana") 39 | rdapc = rdap.RdapClient(config_dir=config_dir) 40 | 41 | with pytest.raises(RdapNotFoundError) as excinfo: 42 | rdapc.get_asn(asn) 43 | assert "No service found for AS" in str(excinfo.value) 44 | 45 | 46 | @pytest.mark.parametrize("asn", [63311]) 47 | def test_asn_lookup(this_dir, asn, tmpdir): 48 | config_dir = os.path.join(this_dir, "data", "iana") 49 | shutil.copy(os.path.join(config_dir, "config.yml"), tmpdir) 50 | 51 | # check download and cache 52 | rdapc = rdap.RdapClient(config_dir=tmpdir) 53 | rdapc.get_asn(asn) 54 | assert rdapc.history[0][0] == "https://data.iana.org/rdap/asn.json" 55 | 56 | # delete history and get again to test lookup caching 57 | rdapc._history = [] 58 | rdapc.get_asn(asn) 59 | print(rdapc.history) 60 | assert rdapc.history[0][0] != "https://data.iana.org/rdap/asn.json" 61 | assert len(rdapc.history) == 1 62 | 63 | asn_db_file = os.path.join(tmpdir, "bootstrap", "asn.json") 64 | assert os.path.isfile(asn_db_file) 65 | # delete cache and manually write file 66 | os.remove(os.path.join(tmpdir, "bootstrap", "asn.json")) 67 | assert not os.path.isfile(asn_db_file) 68 | 69 | rdapc.write_bootstrap_data("asn") 70 | assert os.path.isfile(asn_db_file) 71 | 72 | rdapc._history = [] 73 | rdapc.get_asn(asn) 74 | print(rdapc.history) 75 | assert rdapc.history[0][0] != "https://data.iana.org/rdap/asn.json" 76 | assert len(rdapc.history) == 1 77 | 78 | 79 | @pytest.mark.parametrize("asn", [63311]) 80 | def test_asn_bootstrap_cache(tmpdir, asn): 81 | config = dict( 82 | self_bootstrap=True, 83 | bootstrap_dir=tmpdir, 84 | ) 85 | asn_db_file = os.path.join(tmpdir, "asn.json") 86 | 87 | # check download and cache 88 | rdapc = rdap.RdapClient(config) 89 | rdapc.get_asn(asn) 90 | assert os.path.isfile(asn_db_file) 91 | assert rdapc.history[0][0] == "https://data.iana.org/rdap/asn.json" 92 | 93 | # delete history and get again to test lookup caching 94 | rdapc._history = [] 95 | rdapc.get_asn(asn) 96 | print(rdapc.history) 97 | assert rdapc.history[0][0] != "https://data.iana.org/rdap/asn.json" 98 | assert len(rdapc.history) == 1 99 | 100 | # delete cache and manually write file 101 | os.remove(asn_db_file) 102 | assert not os.path.isfile(asn_db_file) 103 | 104 | rdapc.write_bootstrap_data("asn") 105 | assert os.path.isfile(asn_db_file) 106 | 107 | rdapc._history = [] 108 | rdapc.get_asn(asn) 109 | print(rdapc.history) 110 | assert rdapc.history[0][0] != "https://data.iana.org/rdap/asn.json" 111 | assert len(rdapc.history) == 1 112 | 113 | 114 | @pytest.mark.parametrize("asn", [63311]) 115 | def test_asn_bootstrap_no_cache(asn): 116 | config = dict( 117 | self_bootstrap=True, 118 | ) 119 | 120 | # check download and cache 121 | rdapc = rdap.RdapClient(config) 122 | rdapc.get_asn(asn) 123 | assert rdapc.history[0][0] == "https://data.iana.org/rdap/asn.json" 124 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from rdap.cli import main 6 | 7 | 8 | def test_usage(): 9 | with pytest.raises(SystemExit) as excinfo: 10 | main() 11 | assert excinfo.value.code == 2 12 | 13 | 14 | def test_lookup(): 15 | args = ["as63311", "--show-requests"] 16 | rv = main(args) 17 | assert rv == 0 18 | 19 | 20 | def test_write_bootstrap_data(tmpdir): 21 | config_file = str(os.path.join(tmpdir, "config.yml")) 22 | with open(config_file, "w") as fh: 23 | fh.write("rdap: {}") 24 | 25 | args = ["--home", str(tmpdir), "--write-bootstrap-data", "asn"] 26 | rv = main(args) 27 | assert os.path.isfile(os.path.join(tmpdir, "bootstrap", "asn.json")) 28 | assert rv == 0 29 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rdap import client 4 | from rdap.exceptions import RdapHTTPError 5 | 6 | 7 | def test_strip_auth_none(): 8 | url = "https://example.com/entity/20C" 9 | 10 | # test no auth 11 | assert url == client.strip_auth(url) 12 | 13 | 14 | def test_strip_auth(): 15 | url = "https://example.com/entity/20C/?key=value" 16 | 17 | # test no auth 18 | assert url == client.strip_auth(url) 19 | 20 | # test only auth 21 | assert url == client.strip_auth(url + "&apikey=12345") 22 | 23 | 24 | @pytest.mark.skip( 25 | "this no longer raises a http error, need to investigate changes on their end", 26 | ) 27 | def test_lacnic_bad_apikey(): 28 | rdapc = client.RdapClient(dict(lacnic_apikey="12345")) 29 | with pytest.raises(RdapHTTPError) as excinfo: 30 | rdapc.get_asn(28001).parsed() 31 | assert "returned 400" in str(excinfo.value) 32 | 33 | 34 | def test_lacnic_no_apikey(rdapc): 35 | assert rdapc.get_asn(28001).parsed() 36 | 37 | 38 | def test_ignore_recurse_errors(): 39 | rdapc = client.RdapClient(dict(lacnic_apikey="12345", ignore_recurse_errors=True)) 40 | rdapc.get_asn(28001).parsed() 41 | 42 | 43 | def test_asdomain_lookup(rdapc): 44 | rdapc.get("as63311.net") 45 | -------------------------------------------------------------------------------- /tests/test_domain.py: -------------------------------------------------------------------------------- 1 | from rdap import RdapClient 2 | 3 | 4 | def test_rdap_domain_lookup(): 5 | rdapc = RdapClient() 6 | rdapc.url = "https://rdap.norid.no" 7 | query = "norid.no" 8 | obj = rdapc.get_domain(query) 9 | assert obj.data 10 | assert obj.parsed() 11 | 12 | # test get duduction 13 | assert obj.data == rdapc.get(query).data 14 | -------------------------------------------------------------------------------- /tests/test_geo.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | from rdap.context import RdapRequestState 7 | 8 | # Import the functions and exceptions from your module 9 | from rdap.normalize.geo import GoogleKeyNotSet, NotFound, normalize 10 | from rdap.schema.normalized import GeoLocation, Location 11 | 12 | # Mock data 13 | MOCK_GEOCODE_RESULT = [ 14 | { 15 | "formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA", 16 | "geometry": {"location": {"lat": 37.4224764, "lng": -122.0842499}}, 17 | "address_components": [ 18 | {"types": ["country"], "short_name": "US"}, 19 | {"types": ["postal_code"], "long_name": "94043"}, 20 | {"types": ["locality"], "long_name": "Mountain View"}, 21 | {"types": ["floor"], "long_name": "4"}, 22 | {"types": ["subpremise"], "long_name": "Suite 200"}, 23 | ], 24 | }, 25 | ] 26 | 27 | 28 | @pytest.fixture 29 | def mock_google_client(): 30 | with patch("googlemaps.Client") as mock_client: 31 | yield mock_client 32 | 33 | 34 | @pytest.fixture 35 | def mock_rdap_request(): 36 | with patch("rdap.context.rdap_request") as mock_request: 37 | mock_request.get.return_value = RdapRequestState() 38 | yield mock_request 39 | 40 | 41 | @pytest.fixture(autouse=True) 42 | def mock_google_api_key(): 43 | with patch.dict("os.environ", {"GOOGLE_MAPS_API_KEY": "fake_api_key"}): 44 | yield 45 | 46 | 47 | def test_normalize_success(mock_google_client): 48 | mock_client = mock_google_client.return_value 49 | mock_client.geocode.return_value = MOCK_GEOCODE_RESULT 50 | 51 | date = datetime.now() 52 | 53 | # Mock the lookup function directly 54 | with patch("rdap.normalize.geo.lookup") as mock_lookup: 55 | # Return the first item of the list 56 | mock_lookup.return_value = MOCK_GEOCODE_RESULT[0] 57 | result = normalize("1600 Amphitheatre Parkway, Mountain View, CA", date) 58 | 59 | assert isinstance(result, Location) 60 | assert result.updated == date 61 | assert result.country == "US" 62 | assert result.city == "Mountain View" 63 | assert result.postal_code == "94043" 64 | assert result.floor == "4" 65 | assert result.suite == "Suite 200" 66 | assert result.address == "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA" 67 | assert isinstance(result.geo, GeoLocation) 68 | assert result.geo.latitude == 37.4224764 69 | assert result.geo.longitude == -122.0842499 70 | 71 | # Verify that lookup was called 72 | mock_lookup.assert_called_once_with( 73 | "1600 Amphitheatre Parkway, Mountain View, CA", 74 | None, 75 | ) 76 | 77 | 78 | def test_normalize_google_key_not_set(): 79 | with patch("rdap.normalize.geo.lookup", side_effect=GoogleKeyNotSet): 80 | date = datetime.now() 81 | result = normalize("1600 Amphitheatre Parkway, Mountain View, CA", date) 82 | 83 | assert isinstance(result, Location) 84 | assert result.updated == date 85 | assert result.address == "1600 Amphitheatre Parkway, Mountain View, CA" 86 | assert result.country is None 87 | assert result.city is None 88 | assert result.postal_code is None 89 | assert result.floor is None 90 | assert result.suite is None 91 | assert result.geo is None 92 | 93 | 94 | def test_normalize_not_found(): 95 | with patch("rdap.normalize.geo.lookup", side_effect=NotFound): 96 | date = datetime.now() 97 | result = normalize("Non-existent Address", date) 98 | 99 | assert isinstance(result, Location) 100 | assert result.updated == date 101 | assert result.address == "Non-existent Address" 102 | assert result.country is None 103 | assert result.city is None 104 | assert result.postal_code is None 105 | assert result.floor is None 106 | assert result.suite is None 107 | assert result.geo is None 108 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | import rdap 2 | 3 | 4 | def test_init(): 5 | rdap.RdapClient() 6 | -------------------------------------------------------------------------------- /tests/test_ip.py: -------------------------------------------------------------------------------- 1 | def sort_data(data): 2 | """Sorts data returned from rdap queries for comparing.""" 3 | for entity in data.get("entities", []): 4 | if "roles" in entity: 5 | entity["roles"].sort() 6 | print(entity["roles"]) 7 | 8 | 9 | def test_rdap_ip_lookup(rdapc): 10 | query = "1.1.1.1" 11 | ip = rdapc.get_ip(query) 12 | assert ip.data 13 | assert ip.parsed() 14 | assert ip.get_rir() 15 | sort_data(ip.data) 16 | 17 | # test get deduction 18 | obj = rdapc.get(query) 19 | assert obj.data 20 | sort_data(obj.data) 21 | 22 | assert type(ip) is type(obj) 23 | assert ip.data == obj.data 24 | -------------------------------------------------------------------------------- /tests/test_normalize.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | import pytest_filedata 8 | 9 | from tests.test_geo import MOCK_GEOCODE_RESULT 10 | 11 | 12 | def dynamic_mock_address(formatted_address, client): 13 | result = copy.deepcopy(MOCK_GEOCODE_RESULT[0]) 14 | result["formatted_address"] = f"{formatted_address}" 15 | return result 16 | 17 | 18 | @pytest.fixture(autouse=True) 19 | def set_google_maps_api_key(): 20 | original_key = os.environ.get("GOOGLE_MAPS_API_KEY") 21 | os.environ["GOOGLE_MAPS_API_KEY"] = "your_test_api_key" 22 | yield 23 | if original_key is not None: 24 | os.environ["GOOGLE_MAPS_API_KEY"] = original_key 25 | else: 26 | del os.environ["GOOGLE_MAPS_API_KEY"] 27 | 28 | 29 | @pytest_filedata.RequestsData("rdap") 30 | def test_rdap_asn_lookup(rdapc, data_normalize_autnum): 31 | with patch("rdap.normalize.geo.lookup") as mock_lookup: 32 | mock_lookup.side_effect = dynamic_mock_address 33 | asn = rdapc.get_asn(data_normalize_autnum.name) 34 | # uncomment to write expected data 35 | # with open(f"tests/data/normalize/autnum/{data_normalize_autnum.name}.expected", "w") as f: 36 | # f.write(json.dumps(asn.normalized, indent=2)) 37 | 38 | assert json.dumps(data_normalize_autnum.expected, indent=2) == json.dumps( 39 | asn.normalized, 40 | indent=2, 41 | ) 42 | 43 | 44 | @pytest_filedata.RequestsData("rdap") 45 | def test_rdap_entity_lookup(rdapc, data_normalize_entity): 46 | with patch("rdap.normalize.geo.lookup") as mock_lookup: 47 | mock_lookup.side_effect = dynamic_mock_address 48 | entity = rdapc.get_entity(data_normalize_entity.name) 49 | # uncomment to write expected data 50 | # with open(f"tests/data/normalize/entity/{data_normalize_entity.name}.expected", "w") as f: 51 | # f.write(json.dumps(entity.normalized, indent=2)) 52 | 53 | assert json.dumps(data_normalize_entity.expected, indent=2) == json.dumps( 54 | entity.normalized, 55 | indent=2, 56 | ) 57 | 58 | 59 | @pytest_filedata.RequestsData("rdap") 60 | def test_rdap_domain_lookup(rdapc, data_normalize_domain): 61 | entity = rdapc.get_domain(data_normalize_domain.name) 62 | # uncomment to write expected data 63 | # with open(f"tests/data/normalize/domain/{data_normalize_domain.name}.expected", "w") as f: 64 | # f.write(json.dumps(entity.normalized, indent=2)) 65 | 66 | assert json.dumps(data_normalize_domain.expected, indent=2) == json.dumps( 67 | entity.normalized, 68 | indent=2, 69 | ) 70 | 71 | 72 | @pytest_filedata.RequestsData("rdap") 73 | def test_rdap_ip_lookup(rdapc, data_normalize_ip): 74 | entity = rdapc.get_ip(data_normalize_ip.name) 75 | # uncomment to write expected data 76 | # with open(f"tests/data/normalize/ip/{data_normalize_ip.name}.expected", "w") as f: 77 | # f.write(json.dumps(entity.normalized, indent=2)) 78 | 79 | assert json.dumps(data_normalize_ip.expected, indent=2) == json.dumps( 80 | entity.normalized, 81 | indent=2, 82 | ) 83 | -------------------------------------------------------------------------------- /tests/test_objects.py: -------------------------------------------------------------------------------- 1 | import rdap.objects 2 | 3 | 4 | def test_rir_lookup(rdapc): 5 | assert rdap.objects.rir_from_domain("arin.net") == "arin" 6 | assert not rdap.objects.rir_from_domain("invalid") 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [gh-actions] 2 | python = 3 | 3.8: py38 4 | 3.9: py39 5 | 3.10: py310 6 | 3.11: py311 7 | 3.12: py312 8 | 9 | [pytest] 10 | norecursedirs = data gen .tox 11 | 12 | [tox] 13 | isolated_build = true 14 | envlist = py{38,39,310,311,312} 15 | 16 | [testenv] 17 | runner = uv-venv-lock-runner 18 | extras = dev 19 | commands = 20 | pytest -vv --cov="{toxinidir}/rdap" --cov-report=term-missing --cov-report=xml tests/ 21 | --------------------------------------------------------------------------------