nuts-foundation/nuts-node

View on GitHub
core/url.go

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
/*
 * Copyright (C) 2023 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package core

import (
    "errors"
    "fmt"
    "net"
    "net/url"
    "slices"
    "strings"
)

// JoinURLPaths works like path.Join but for URLs; it won't remove double slashes.
// It makes sures there is only one slash between the parts.
func JoinURLPaths(parts ...string) string {
    if len(parts) == 0 {
        return ""
    }
    result := parts[0]
    for i := 1; i < len(parts); i++ {
        if parts[i] == "" {
            continue
        }
        result = strings.TrimSuffix(result, "/") + "/" + strings.TrimPrefix(parts[i], "/")
    }
    return result
}

// ParsePublicURL parses the input URL using ParsePublicURLWithScheme.
// If strictmode is true, no reserved addresses are allowed and the scheme MUST be 'https'
func ParsePublicURL(input string, strictmode bool) (*url.URL, error) {
    if !strictmode {
        return ParsePublicURLWithScheme(input, true, "http", "https")
    }
    return ParsePublicURLWithScheme(input, false, "https")
}

// ParsePublicURLWithScheme parses the given input string as URL and asserts that
// it has a scheme and that it is in the allowedSchemes if provided,
// it is not an IP address, and
// it is not (depending on allowReserved) a reserved address or TLD as described in RFC2606 or https://www.ietf.org/archive/id/draft-chapin-rfc2606bis-00.html.
func ParsePublicURLWithScheme(input string, allowReserved bool, allowedSchemes ...string) (*url.URL, error) {
    parsed, err := url.Parse(input)
    if err != nil {
        return nil, err
    }
    if parsed.Scheme == "" || parsed.Hostname() == "" {
        return nil, errors.New("url must contain scheme and host")
    }
    if len(allowedSchemes) > 0 && !slices.Contains(allowedSchemes, parsed.Scheme) {
        return nil, fmt.Errorf("scheme must be %s", strings.Join(allowedSchemes, " or "))
    }
    if net.ParseIP(parsed.Hostname()) != nil && !allowReserved {
        return nil, errors.New("hostname is IP")
    }
    if !allowReserved && isReserved(parsed) {
        return nil, errors.New("hostname is RFC2606 reserved")
    }
    return parsed, nil
}

// isReserved returns true if URL uses any of the reserved TLDs or addresses
func isReserved(URL *url.URL) bool {
    parts := strings.Split(strings.ToLower(URL.Hostname()), ".")
    tld := parts[len(parts)-1]
    if slices.Contains(reservedTLDs, tld) {
        return true
    }

    if len(parts) > 1 {
        l2address := strings.Join(parts[len(parts)-2:], ".")
        return slices.Contains(reservedAddresses, l2address)
    }

    return false
}

var reservedTLDs = []string{
    "", // no domain specified
    "corp",
    "example",
    "home",
    "host",
    "invalid",
    "lan",
    "local",
    "localdomain",
    "localhost",
    "test",
}
var reservedAddresses = []string{
    "example.com",
    "example.net",
    "example.org",
}