├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── open_an_issue.md ├── config.yml └── workflows │ ├── generated-pr.yml │ ├── go-check.yml │ ├── go-test.yml │ ├── release-check.yml │ ├── releaser.yml │ ├── stale.yml │ └── tagpush.yml ├── LICENSE ├── README.md ├── dnslink.go ├── dnslink ├── .gitignore ├── README.md └── main.go ├── dnslink_test.go ├── go.mod ├── go.sum └── version.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Getting Help on IPFS 4 | url: https://ipfs.io/help 5 | about: All information about how and where to get help on IPFS. 6 | - name: IPFS Official Forum 7 | url: https://discuss.ipfs.io 8 | about: Please post general questions, support requests, and discussions here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open_an_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open an issue 3 | about: Only for actionable issues relevant to this repository. 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 20 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | # Comment to be posted to on first time issues 5 | newIssueWelcomeComment: > 6 | Thank you for submitting your first issue to this repository! A maintainer 7 | will be here shortly to triage and review. 8 | 9 | In the meantime, please double-check that you have provided all the 10 | necessary information to make this process easy! Any information that can 11 | help save additional round trips is useful! We currently aim to give 12 | initial feedback within **two business days**. If this does not happen, feel 13 | free to leave a comment. 14 | 15 | Please keep an eye on how this issue will be labeled, as labels give an 16 | overview of priorities, assignments and additional actions requested by the 17 | maintainers: 18 | 19 | - "Priority" labels will show how urgent this is for the team. 20 | - "Status" labels will show if this is ready to be worked on, blocked, or in progress. 21 | - "Need" labels will indicate if additional input or analysis is required. 22 | 23 | Finally, remember to use https://discuss.ipfs.io if you just need general 24 | support. 25 | 26 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 27 | # Comment to be posted to on PRs from first time contributors in your repository 28 | newPRWelcomeComment: > 29 | Thank you for submitting this PR! 30 | 31 | A maintainer will be here shortly to review it. 32 | 33 | We are super grateful, but we are also overloaded! Help us by making sure 34 | that: 35 | 36 | * The context for this PR is clear, with relevant discussion, decisions 37 | and stakeholders linked/mentioned. 38 | 39 | * Your contribution itself is clear (code comments, self-review for the 40 | rest) and in its best form. Follow the [code contribution 41 | guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) 42 | if they apply. 43 | 44 | Getting other community members to do a review would be great help too on 45 | complex PRs (you can ask in the chats/forums). If you are unsure about 46 | something, just leave us a comment. 47 | 48 | Next steps: 49 | 50 | * A maintainer will triage and assign priority to this PR, commenting on 51 | any missing things and potentially assigning a reviewer for high 52 | priority items. 53 | 54 | * The PR gets reviews, discussed and approvals as needed. 55 | 56 | * The PR is merged by maintainers when it has been approved and comments addressed. 57 | 58 | We currently aim to provide initial feedback/triaging within **two business 59 | days**. Please keep an eye on any labelling actions, as these will indicate 60 | priorities and status of your contribution. 61 | 62 | We are very grateful for your contribution! 63 | 64 | 65 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 66 | # Comment to be posted to on pull requests merged by a first time user 67 | # Currently disabled 68 | #firstPRMergeComment: "" 69 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/go-check.yml: -------------------------------------------------------------------------------- 1 | name: Go Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-check: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0 19 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-test: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v1.0 19 | secrets: 20 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/release-check.yml: -------------------------------------------------------------------------------- 1 | name: Release Checker 2 | 3 | on: 4 | pull_request_target: 5 | paths: [ 'version.json' ] 6 | types: [ opened, synchronize, reopened, labeled, unlabeled ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | release-check: 19 | uses: ipdxco/unified-github-workflows/.github/workflows/release-check.yml@v1.0 20 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser 2 | 3 | on: 4 | push: 5 | paths: [ 'version.json' ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.sha }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | releaser: 17 | uses: ipdxco/unified-github-workflows/.github/workflows/releaser.yml@v1.0 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/tagpush.yml: -------------------------------------------------------------------------------- 1 | name: Tag Push Checker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: read 10 | issues: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | releaser: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/tagpush.yml@v1.0 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Juan Batiz-Benet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Deprecated in favor of [dnslink-std/go](https://github.com/dnslink-std/go) 2 | 3 | - 👉 this project is no longer maintained, greenfield projects should use [dnslink-std/go](https://github.com/dnslink-std/go) 4 | - we will convert this repo to a thin arapper around [dnslink-std/go](https://github.com/dnslink-std/go) at some point – see [#15](https://github.com/ipfs/go-dnslink/issues/15) 5 | 6 | # go-dnslink 7 | 8 | [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) 9 | [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) 10 | [![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) 11 | 12 | > dnslink resolution in go-ipfs 13 | 14 | ## Table of Contents 15 | 16 | - [Background](#background) 17 | - [Install](#install) 18 | - [Usage](#usage) 19 | - [As a library](#as-a-library) 20 | - [As a commandline tool](#as-a-commandline-tool) 21 | - [Contribute](#contribute) 22 | - [Want to hack on IPFS?](#want-to-hack-on-ipfs) 23 | - [License](#license) 24 | 25 | ## Background 26 | 27 | Package dnslink implements a DNS link resolver. dnslink is a basic 28 | standard for placing traversable links in DNS itself. See dnslink.info 29 | 30 | A dnslink is a path link in a DNS TXT record, like this: 31 | 32 | ``` 33 | dnslink=/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 34 | ``` 35 | 36 | For example: 37 | 38 | ``` 39 | > dig TXT ipfs.io 40 | ipfs.io. 120 IN TXT dnslink=/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 41 | ``` 42 | 43 | This package eases resolving and working with thse DNS links. For example: 44 | 45 | ```go 46 | import ( 47 | dnslink "github.com/ipfs/go-dnslink" 48 | ) 49 | 50 | link, err := dnslink.Resolve("ipfs.io") 51 | // link = "/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy" 52 | ``` 53 | 54 | It even supports recursive resolution. Suppose you have three domains with 55 | dnslink records like these: 56 | 57 | ``` 58 | > dig TXT foo.com 59 | foo.com. 120 IN TXT dnslink=/ipns/bar.com/f/o/o 60 | > dig TXT bar.com 61 | bar.com. 120 IN TXT dnslink=/ipns/long.test.baz.it/b/a/r 62 | > dig TXT long.test.baz.it 63 | long.test.baz.it. 120 IN TXT dnslink=/b/a/z 64 | ``` 65 | 66 | Expect these resolutions: 67 | 68 | ```go 69 | dnslink.ResolveN("long.test.baz.it", 0) // "/ipns/long.test.baz.it" 70 | dnslink.Resolve("long.test.baz.it") // "/b/a/z" 71 | 72 | dnslink.ResolveN("bar.com", 1) // "/ipns/long.test.baz.it/b/a/r" 73 | dnslink.Resolve("bar.com") // "/b/a/z/b/a/r" 74 | 75 | dnslink.ResolveN("foo.com", 1) // "/ipns/bar.com/f/o/o/" 76 | dnslink.ResolveN("foo.com", 2) // "/ipns/long.test.baz.it/b/a/r/f/o/o/" 77 | dnslink.Resolve("foo.com") // "/b/a/z/b/a/r/f/o/o" 78 | ``` 79 | 80 | ## Install 81 | 82 | ```sh 83 | go get github.com/ipfs/go-dnslink 84 | ``` 85 | 86 | ## Usage 87 | 88 | ### As a library 89 | 90 | ```go 91 | import ( 92 | log 93 | fmt 94 | 95 | dnslink "github.com/ipfs/go-dnslink" 96 | ) 97 | 98 | func main() { 99 | link, err := dnslink.Resolve("ipfs.io") 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | fmt.Println(link) // string path 105 | } 106 | ``` 107 | 108 | ### As a commandline tool 109 | 110 | Check out [the commandline tool](dnslink/), which works like this: 111 | 112 | ```sh 113 | > dnslink ipfs.io 114 | /ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 115 | ``` 116 | 117 | ## Contribute 118 | 119 | Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/go-dnslink/issues)! 120 | 121 | This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 122 | 123 | ### Want to hack on IPFS? 124 | 125 | [![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) 126 | 127 | ## License 128 | 129 | [MIT](LICENSE) © Juan Benet-Batiz 130 | 131 | -------------------------------------------------------------------------------- /dnslink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dnslink implements a dns link resolver. dnslink is a basic 3 | standard for placing traversable links in dns itself. See dnslink.info 4 | 5 | A dnslink is a path link in a dns TXT record, like this: 6 | 7 | dnslink=/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 8 | 9 | For example: 10 | 11 | > dig TXT ipfs.io 12 | ipfs.io. 120 IN TXT dnslink=/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 13 | 14 | This package eases resolving and working with thse dns links. For example: 15 | 16 | import ( 17 | dnslink "github.com/ipfs/go-dnslink" 18 | ) 19 | 20 | link, err := dnslink.Resolve("ipfs.io") 21 | // link = "/ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy" 22 | 23 | It even supports recursive resolution. Suppose you have three domains with 24 | dnslink records like these: 25 | 26 | > dig TXT foo.com 27 | foo.com. 120 IN TXT dnslink=/dns/bar.com/f/o/o 28 | > dig TXT bar.com 29 | bar.com. 120 IN TXT dnslink=/dns/long.test.baz.it/b/a/r 30 | > dig TXT long.test.baz.it 31 | long.test.baz.it. 120 IN TXT dnslink=/b/a/z 32 | 33 | Expect these resolutions: 34 | 35 | dnslink.ResolveN("long.test.baz.it", 0) // "/dns/long.test.baz.it" 36 | dnslink.Resolve("long.test.baz.it") // "/b/a/z" 37 | 38 | dnslink.ResolveN("bar.com", 1) // "/dns/long.test.baz.it/b/a/r" 39 | dnslink.Resolve("bar.com") // "/b/a/z/b/a/r" 40 | 41 | dnslink.ResolveN("foo.com", 1) // "/dns/bar.com/f/o/o/" 42 | dnslink.ResolveN("foo.com", 2) // "/dns/long.test.baz.it/b/a/r/f/o/o/" 43 | dnslink.Resolve("foo.com") // "/b/a/z/b/a/r/f/o/o" 44 | */ 45 | package dnslink 46 | 47 | import ( 48 | "errors" 49 | "net" 50 | "path" 51 | "strings" 52 | 53 | isd "github.com/jbenet/go-is-domain" 54 | ) 55 | 56 | // DefaultDepthLimit controls how many dns links to resolve through before 57 | // returning. Users can override this default. 58 | const DefaultDepthLimit = 16 59 | 60 | // MaximumDepthLimit governs the max number of recursive resolutions. 61 | const MaximumDepthLimit = 256 62 | 63 | var ( 64 | // ErrInvalidDomain is returned when a string representing a domain name 65 | // is not actually a valid domain. 66 | ErrInvalidDomain = errors.New("not a valid domain name") 67 | 68 | // ErrInvalidDnslink is returned when the dnslink entry in a TXT record 69 | // does not follow the proper dnslink format ("dnslink=") 70 | ErrInvalidDnslink = errors.New("not a valid dnslink entry") 71 | 72 | // ErrResolveFailed is returned when a resolution failed, most likely 73 | // due to a network error. 74 | ErrResolveFailed = errors.New("link resolution failed") 75 | 76 | // ErrResolveLimit is returned when a recursive resolution goes over 77 | // the limit. 78 | ErrResolveLimit = errors.New("resolve depth exceeded") 79 | ) 80 | 81 | // LookupTXTFunc is a function that looks up a TXT record in some dns resovler. 82 | // This is useful for testing or passing your own dns resolution process, which 83 | // could take into account non-standard TLDs like .bit, .onion, .ipfs, etc. 84 | type LookupTXTFunc func(name string) (txt []string, err error) 85 | 86 | // Resolve is the simplest way to use this package. It simply resolves the 87 | // dnslink at a particular domain. It will recursively keep resolving until 88 | // reaching the DefaultDepthLimit. If the depth is reached, Resolve will return 89 | // the last value retrieved, and ErrResolveLimit. 90 | // If TXT records are found but are not valid dnslink records, Resolve will 91 | // return ErrInvalidDnslink. Resolve will check every TXT record returned. 92 | // If resolution fails otherwise, Resolve will return ErrResolveFailed 93 | func Resolve(domain string) (string, error) { 94 | return defaultResolver.Resolve(domain) 95 | } 96 | 97 | // ResolveN is just like Resolve, with the option to specify a maximum 98 | // resolution depth. 99 | func ResolveN(domain string, depth int) (string, error) { 100 | return defaultResolver.ResolveN(domain, depth) 101 | } 102 | 103 | // Resolver implements a dnslink Resolver on DNS domains. 104 | // This struct is here for composing dnslink resolution with other 105 | // types of resolvers. 106 | type Resolver struct { 107 | lookupTXT LookupTXTFunc 108 | depthLimit int 109 | // TODO: maybe some sort of caching? 110 | // cache would need a timeout 111 | } 112 | 113 | // defaultResolver is a resolver used by the main package-level functions. 114 | var defaultResolver = &Resolver{} 115 | 116 | // NewResolver constructs a new dnslink resolver. The given defaultDepth 117 | // will be the maximum depth used by the Resolve function. 118 | func NewResolver(defaultDepth int) *Resolver { 119 | return &Resolver{net.LookupTXT, defaultDepth} 120 | } 121 | 122 | func (r *Resolver) setDefaults() { 123 | // check internal params 124 | if r.lookupTXT == nil { 125 | r.lookupTXT = net.LookupTXT 126 | } 127 | if r.depthLimit < 1 { 128 | r.depthLimit = DefaultDepthLimit 129 | } 130 | if r.depthLimit > MaximumDepthLimit { 131 | r.depthLimit = MaximumDepthLimit 132 | } 133 | } 134 | 135 | // Resolve resolves the dnslink at a particular domain. It will recursively 136 | // keep resolving until reaching the defaultDepth of Resolver. If the depth 137 | // is reached, Resolve will return the last value retrieved, and ErrResolveLimit. 138 | // If TXT records are found but are not valid dnslink records, Resolve will 139 | // return ErrInvalidDnslink. Resolve will check every TXT record returned. 140 | // If resolution fails otherwise, Resolve will return ErrResolveFailed 141 | func (r *Resolver) Resolve(domain string) (string, error) { 142 | return r.ResolveN(domain, DefaultDepthLimit) 143 | } 144 | 145 | // ResolveN is just like Resolve, with the option to specify a maximum 146 | // resolution depth. 147 | func (r *Resolver) ResolveN(domain string, depth int) (link string, err error) { 148 | tail := "" 149 | for i := 0; i < depth; i++ { 150 | link, err = r.resolveOnce(domain) 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | // if does not have /dns/ as a prefix, done. 156 | if !strings.HasPrefix(link, "/dns/") { 157 | return link + tail, nil // done 158 | } 159 | 160 | // keep resolving 161 | d, rest, err := ParseLinkDomain(link) 162 | if err != nil { 163 | return "", err 164 | } 165 | 166 | domain = d 167 | tail = rest + tail 168 | } 169 | return "/dns/" + domain + tail, ErrResolveLimit 170 | } 171 | 172 | // resolveOnce implements resolver. 173 | func (r *Resolver) resolveOnce(domain string) (p string, err error) { 174 | r.setDefaults() 175 | 176 | if !isd.IsDomain(domain) { 177 | return "", ErrInvalidDomain 178 | } 179 | 180 | txt, err := r.lookupTXT(domain) 181 | if err != nil { 182 | return "", err 183 | } 184 | 185 | err = ErrResolveFailed 186 | for _, t := range txt { 187 | p, err = ParseTXT(t) 188 | if err == nil { 189 | return p, nil 190 | } 191 | } 192 | 193 | return "", err 194 | } 195 | 196 | // ParseTXT parses a TXT record value for a dnslink value. 197 | // The TXT record must follow the dnslink format: 198 | // 199 | // TXT dnslink= 200 | // TXT dnslink=/foo/bar/baz 201 | // 202 | // ParseTXT will return ErrInvalidDnslink if parsing fails. 203 | func ParseTXT(txt string) (string, error) { 204 | parts := strings.SplitN(txt, "=", 2) 205 | if len(parts) == 2 && parts[0] == "dnslink" && strings.HasPrefix(parts[1], "/") { 206 | return path.Clean(parts[1]), nil 207 | } 208 | 209 | return "", ErrInvalidDnslink 210 | } 211 | 212 | // ParseLinkDomain parses a domain from a dnslink path. 213 | // The link path must follow the dnslink format: 214 | // 215 | // /dns// 216 | // /dns/ipfs.io 217 | // /dns/ipfs.io/blog/0-hello-worlds 218 | // 219 | // ParseLinkDomain will return ErrInvalidDnslink if parsing fails, 220 | // and ErrInvalidDomain if the domain is not valid. 221 | func ParseLinkDomain(txt string) (string, string, error) { 222 | parts := strings.SplitN(txt, "/", 4) 223 | if len(parts) < 3 || parts[0] != "" || parts[1] != "dns" { 224 | return "", "", ErrInvalidDnslink 225 | } 226 | 227 | domain := parts[2] 228 | if !isd.IsDomain(domain) { 229 | return "", "", ErrInvalidDomain 230 | } 231 | 232 | rest := "" 233 | if len(parts) > 3 { 234 | rest = "/" + parts[3] 235 | } 236 | return domain, rest, nil 237 | } 238 | -------------------------------------------------------------------------------- /dnslink/.gitignore: -------------------------------------------------------------------------------- 1 | dnslink 2 | -------------------------------------------------------------------------------- /dnslink/README.md: -------------------------------------------------------------------------------- 1 | # dnslink - resolve dns links in TXT records 2 | 3 | This is a simple commandline tool to resolve dnslink records. It is built with the [go-dnslink](../) package. 4 | 5 | For more information about dnslink, see 6 | 7 | - This note: https://github.com/jbenet/random-ideas/issues/28 8 | 9 | ## Install 10 | 11 | Compile with Go 12 | 13 | ```sh 14 | go get -u github.com/ipfs/go-dnslink/dnslink 15 | ``` 16 | 17 | ## Usage 18 | 19 | ``` 20 | > dnslink --help 21 | dnslink - resolve dns links in TXT records 22 | 23 | USAGE 24 | dnslink 25 | 26 | EXAMPLE 27 | > dnslink blog.ipfs.io 28 | /ipns/ipfs.io/blog 29 | 30 | > dnslink ipfs.io blog.ipfs.io 31 | ipfs.io: /ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 32 | blog.ipfs.io: /ipns/ipfs.io/blog 33 | 34 | > dnslink foo.bar 35 | error: lookup foo.bar on 10.0.1.1:53: no such host 36 | ``` 37 | 38 | ## Examples 39 | 40 | Resolve a single domain 41 | 42 | ```sh 43 | > dnslink blog.ipfs.io 44 | /ipns/ipfs.io/blog 45 | ``` 46 | 47 | Resolve multiple domains 48 | 49 | ```sh 50 | > dnslink ipfs.io blog.ipfs.io 51 | ipfs.io: /ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 52 | blog.ipfs.io: /ipns/ipfs.io/blog 53 | ``` 54 | 55 | Error handling 56 | 57 | ```sh 58 | > dnslink foo.bar 59 | error: lookup foo.bar on 10.0.1.1:53: no such host 60 | ``` 61 | -------------------------------------------------------------------------------- /dnslink/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | dnslink "github.com/ipfs/go-dnslink" 8 | ) 9 | 10 | var usage = `dnslink - resolve dns links in TXT records 11 | 12 | USAGE 13 | dnslink 14 | 15 | EXAMPLE 16 | > dnslink blog.ipfs.io 17 | /ipns/ipfs.io/blog 18 | 19 | > dnslink ipfs.io blog.ipfs.io 20 | ipfs.io: /ipfs/QmR7tiySn6vFHcEjBeZNtYGAFh735PJHfEMdVEycj9jAPy 21 | blog.ipfs.io: /ipns/ipfs.io/blog 22 | 23 | > dnslink foo.bar 24 | error: lookup foo.bar on 10.0.1.1:53: no such host 25 | ` 26 | 27 | func main() { 28 | err := run(os.Args[1:]) 29 | if err != nil { 30 | fmt.Fprintln(os.Stderr, "error:", err) 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func run(args []string) error { 36 | if len(args) < 1 || hasHelp(args) { 37 | fmt.Print(usage) 38 | return nil 39 | } 40 | 41 | if len(args) == 1 { 42 | return printLink(args[0]) 43 | } 44 | return printLinks(args) 45 | } 46 | 47 | func hasHelp(args []string) bool { 48 | for _, arg := range args { 49 | if arg == "--help" || arg == "-h" { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | // print a single link 57 | func printLink(domain string) error { 58 | link, err := dnslink.Resolve(domain) 59 | if err != nil { 60 | return err 61 | } 62 | fmt.Println(link) 63 | return nil 64 | } 65 | 66 | // print multiple links. 67 | // errors printed as output, and do not fail the entire process. 68 | func printLinks(domains []string) error { 69 | for _, domain := range domains { 70 | fmt.Print(domain, ": ") 71 | 72 | result, err := dnslink.Resolve(domain) 73 | if result != "" { 74 | fmt.Print(result) 75 | } 76 | if err != nil { 77 | fmt.Print("error: ", err) 78 | } 79 | fmt.Println() 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /dnslink_test.go: -------------------------------------------------------------------------------- 1 | package dnslink 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type mockDNS struct { 9 | entries map[string][]string 10 | } 11 | 12 | func (m *mockDNS) lookupTXT(name string) (txt []string, err error) { 13 | txt, ok := m.entries[name] 14 | if !ok { 15 | return nil, fmt.Errorf("No TXT entry for %s", name) 16 | } 17 | return txt, nil 18 | } 19 | 20 | func TestDnsLinkParsing(t *testing.T) { 21 | goodEntries := [][]string{ 22 | {"/dns/foo.com", "foo.com", ""}, 23 | {"/dns/foo.com/bar/baz", "foo.com", "/bar/baz"}, 24 | {"/dns/bar.com", "bar.com", ""}, 25 | {"/dns/baz.test.it/bar/baz", "baz.test.it", "/bar/baz"}, 26 | } 27 | 28 | badEntries := []string{ 29 | "/foo/foo.com", 30 | "/baz/foo.com/bar/baz", 31 | "/foo.com/bar/baz", 32 | "foo.com/bar/baz", 33 | "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 34 | "QmYhE8xgFCjGcz6PHgnvJz5NOTCORRECT", 35 | "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 36 | "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/bar", 37 | } 38 | 39 | for _, e := range goodEntries { 40 | a, b, err := ParseLinkDomain(e[0]) 41 | if err != nil { 42 | t.Fatal("expected entry to parse correctly:", e, "got:", err) 43 | } 44 | if a != e[1] { 45 | t.Fatal("expected entry to parse domain correctly:", e[0], e[1], "got:", a) 46 | } 47 | if b != e[2] { 48 | t.Fatal("expected entry to parse rest correctly:", e[0], e[2], "got:", b) 49 | } 50 | } 51 | 52 | for _, e := range badEntries { 53 | _, _, err := ParseLinkDomain(e) 54 | if err == nil { 55 | t.Fatal("expected entry parse to fail:", e) 56 | } 57 | } 58 | } 59 | 60 | func TestDnsEntryParsing(t *testing.T) { 61 | goodEntries := []string{ 62 | "dnslink=/dns/foo.com", 63 | "dnslink=/dns/foo.com/bar/baz", 64 | "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 65 | "dnslink=/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 66 | "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo", 67 | "dnslink=/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/bar", 68 | "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo/bar/baz", 69 | "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 70 | "dnslink=/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo", 71 | } 72 | 73 | badEntries := []string{ 74 | "/dns/foo.com", 75 | "/dns/foo.com/bar/baz", 76 | "foo.com", 77 | "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", // we dont support this one here. 78 | "QmYhE8xgFCjGcz6PHgnvJz5NOTCORRECT", 79 | "quux=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 80 | "dnslink=", 81 | "dnslink=ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/bar", 82 | } 83 | 84 | for _, e := range goodEntries { 85 | _, err := ParseTXT(e) 86 | if err != nil { 87 | t.Fatal("expected entry to parse correctly:", e, "got:", err) 88 | } 89 | } 90 | 91 | for _, e := range badEntries { 92 | _, err := ParseTXT(e) 93 | if err == nil { 94 | t.Fatal("expected entry parse to fail:", e) 95 | } 96 | } 97 | } 98 | 99 | func newMockDNS() *mockDNS { 100 | return &mockDNS{ 101 | entries: map[string][]string{ 102 | "foo.com": {"dnslink=/dns/bar.com/foo/f/o/o"}, 103 | "bar.com": {"dnslink=/dns/test.it.baz.com/bar/b/a/r"}, 104 | "test.it.baz.com": {"dnslink=/baz/b/a/z"}, 105 | "ipfs.example.com": {"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD"}, 106 | "dns1.example.com": {"dnslink=/dns/ipfs.example.com"}, 107 | "dns2.example.com": {"dnslink=/dns/dns1.example.com"}, 108 | "equals.example.com": {"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals"}, 109 | "loop1.example.com": {"dnslink=/dns/loop2.example.com"}, 110 | "loop2.example.com": {"dnslink=/dns/loop1.example.com"}, 111 | "bad.example.com": {"dnslink="}, 112 | "multi.example.com": { 113 | "some stuff", 114 | "dnslink=/dns/dns1.example.com", 115 | "masked dnslink=/dns/example.invalid", 116 | }, 117 | }, 118 | } 119 | } 120 | 121 | func TestResolution(t *testing.T) { 122 | mock := newMockDNS() 123 | r := &Resolver{lookupTXT: mock.lookupTXT} 124 | testResolution(t, r, "foo.com", DefaultDepthLimit, "/baz/b/a/z/bar/b/a/r/foo/f/o/o", nil) 125 | testResolution(t, r, "bar.com", DefaultDepthLimit, "/baz/b/a/z/bar/b/a/r", nil) 126 | testResolution(t, r, "test.it.baz.com", 1, "/baz/b/a/z", nil) 127 | testResolution(t, r, "foo.com", 0, "/dns/foo.com", ErrResolveLimit) 128 | testResolution(t, r, "foo.com", 1, "/dns/bar.com/foo/f/o/o", ErrResolveLimit) 129 | testResolution(t, r, "foo.com", 2, "/dns/test.it.baz.com/bar/b/a/r/foo/f/o/o", ErrResolveLimit) 130 | testResolution(t, r, "bar.com", 0, "/dns/bar.com", ErrResolveLimit) 131 | testResolution(t, r, "bar.com", 1, "/dns/test.it.baz.com/bar/b/a/r", ErrResolveLimit) 132 | testResolution(t, r, "test.it.baz.com", 0, "/dns/test.it.baz.com", ErrResolveLimit) 133 | testResolution(t, r, "ipfs.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) 134 | testResolution(t, r, "dns1.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) 135 | testResolution(t, r, "dns1.example.com", 1, "/dns/ipfs.example.com", ErrResolveLimit) 136 | testResolution(t, r, "dns2.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) 137 | testResolution(t, r, "dns2.example.com", 1, "/dns/dns1.example.com", ErrResolveLimit) 138 | testResolution(t, r, "dns2.example.com", 2, "/dns/ipfs.example.com", ErrResolveLimit) 139 | testResolution(t, r, "multi.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) 140 | testResolution(t, r, "multi.example.com", 1, "/dns/dns1.example.com", ErrResolveLimit) 141 | testResolution(t, r, "multi.example.com", 2, "/dns/ipfs.example.com", ErrResolveLimit) 142 | testResolution(t, r, "equals.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals", nil) 143 | testResolution(t, r, "loop1.example.com", 1, "/dns/loop2.example.com", ErrResolveLimit) 144 | testResolution(t, r, "loop1.example.com", 2, "/dns/loop1.example.com", ErrResolveLimit) 145 | testResolution(t, r, "loop1.example.com", 3, "/dns/loop2.example.com", ErrResolveLimit) 146 | testResolution(t, r, "loop1.example.com", DefaultDepthLimit, "/dns/loop1.example.com", ErrResolveLimit) 147 | testResolution(t, r, "bad.example.com", DefaultDepthLimit, "", ErrInvalidDnslink) 148 | } 149 | 150 | func testResolution(t *testing.T, r *Resolver, name string, depth int, expected string, expError error) { 151 | p, err := r.ResolveN(name, depth) 152 | if err != expError { 153 | t.Fatal(fmt.Errorf( 154 | "Expected %s with a depth of %d to have a '%s' error, but got '%s'", 155 | name, depth, expError, err)) 156 | } 157 | if p != expected { 158 | t.Fatal(fmt.Errorf( 159 | "%s with depth %d resolved to %s != %s", 160 | name, depth, p, expected)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ipfs/go-dnslink 2 | 3 | go 1.23 4 | 5 | require github.com/jbenet/go-is-domain v1.0.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jbenet/go-is-domain v1.0.5 h1:r92uiHbMEJo9Fkey5pMBtZAzjPQWic0ieo7Jw1jEuQQ= 2 | github.com/jbenet/go-is-domain v1.0.5/go.mod h1:xbRLRb0S7FgzDBTJlguhDVwLYM/5yNtvktxj2Ttfy7Q= 3 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "" 3 | } 4 | --------------------------------------------------------------------------------