summaryrefslogtreecommitdiff
path: root/internal/util/domain.go
blob: c64cc65d74fc8f3bc2f2870ac82e933bb105b194 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package util

import (
	"net/url"
	"strings"

	"golang.org/x/net/idna"
)

var (
	// IDNA (Internationalized Domain Names for Applications)
	// profiles for fast punycode conv and full verification.
	punifyProfile = *idna.Punycode
	verifyProfile = *idna.Lookup
)

// PunifySafely validates the provided domain name,
// and converts unicode chars to ASCII, i.e. punified form.
func PunifySafely(domain string) (string, error) {
	if i := strings.LastIndexByte(domain, ':'); i >= 0 {

		// If there is a port included in domain, we
		// strip it as colon is invalid in a hostname.
		domain, port := domain[:i], domain[i:]
		domain, err := verifyProfile.ToASCII(domain)
		if err != nil {
			return "", err
		}

		// Then rebuild with port after.
		domain = strings.ToLower(domain)
		return domain + port, nil
	} else { //nolint:revive

		// Otherwise we just punify domain as-is.
		domain, err := verifyProfile.ToASCII(domain)
		return strings.ToLower(domain), err
	}
}

// Punify is a faster form of PunifySafely() without validation.
func Punify(domain string) (string, error) {
	domain, err := punifyProfile.ToASCII(domain)
	return strings.ToLower(domain), err
}

// DePunify converts any punycode-encoded unicode characters
// in domain name back to their origin unicode. Please note
// that this performs minimal validation of domain name.
func DePunify(domain string) (string, error) {
	domain = strings.ToLower(domain)
	return punifyProfile.ToUnicode(domain)
}

// URIMatches returns true if the expected URI matches
// any of the given URIs, taking account of punycode.
func URIMatches(expect *url.URL, uris ...*url.URL) (ok bool, err error) {

	// Create new URL to hold
	// punified URI information.
	punyURI := new(url.URL)
	*punyURI = *expect

	// Set punified expected URL host.
	punyURI.Host, err = Punify(expect.Host)
	if err != nil {
		return false, err
	}

	// Calculate expected URI string.
	expectStr := punyURI.String()

	// Use punyURI to iteratively
	// store each punified URI info
	// and generate punified URI
	// strings to check against.
	for _, uri := range uris {
		*punyURI = *uri
		punyURI.Host, err = Punify(uri.Host)
		if err != nil {
			return false, err
		}

		// Check for a match against expect.
		if expectStr == punyURI.String() {
			return true, nil
		}
	}

	// Didn't match.
	return false, nil
}

// PunifyURI returns a new copy of URI with the 'host'
// part converted to punycode with PunifySafely().
// For simple comparisons prefer the faster URIMatches().
func PunifyURI(in *url.URL) (*url.URL, error) {
	punyHost, err := PunifySafely(in.Host)
	if err != nil {
		return nil, err
	}
	out := new(url.URL)
	*out = *in
	out.Host = punyHost
	return out, nil
}

// PunifyURIToStr returns given URI serialized with the
// 'host' part converted to punycode with PunifySafely().
// For simple comparisons prefer the faster URIMatches().
func PunifyURIToStr(in *url.URL) (string, error) {
	punyHost, err := PunifySafely(in.Host)
	if err != nil {
		return "", err
	}
	oldHost := in.Host
	in.Host = punyHost
	str := in.String()
	in.Host = oldHost
	return str, nil
}