url.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package urllib // import "miniflux.app/v2/internal/urllib"
  4. import (
  5. "errors"
  6. "fmt"
  7. "net"
  8. "net/url"
  9. "slices"
  10. "strings"
  11. )
  12. // IsRelativePath returns true if the link is a relative path.
  13. func IsRelativePath(link string) bool {
  14. if link == "" {
  15. return false
  16. }
  17. if parsedURL, err := url.Parse(link); err == nil {
  18. // Only allow relative paths (not scheme-relative URLs like //example.org)
  19. // and ensure the URL doesn't have a host component
  20. if !parsedURL.IsAbs() && parsedURL.Host == "" && parsedURL.Scheme == "" {
  21. return true
  22. }
  23. }
  24. return false
  25. }
  26. // IsAbsoluteURL returns true if the link is absolute.
  27. func IsAbsoluteURL(link string) bool {
  28. u, err := url.Parse(link)
  29. if err != nil {
  30. return false
  31. }
  32. return u.IsAbs()
  33. }
  34. // GetAbsoluteURL returns the absolute form of `input` if possible, as well as its parsed form.
  35. func GetAbsoluteURL(input string) (string, *url.URL, error) {
  36. if strings.HasPrefix(input, "//") {
  37. return "https:" + input, nil, nil
  38. }
  39. if strings.HasPrefix(input, "https://") || strings.HasPrefix(input, "http://") {
  40. return input, nil, nil
  41. }
  42. u, err := url.Parse(input)
  43. if err != nil {
  44. return "", nil, fmt.Errorf("unable to parse input URL: %v", err)
  45. }
  46. if u.IsAbs() {
  47. return u.String(), u, nil
  48. }
  49. return "", u, nil
  50. }
  51. // AbsoluteURL converts the input URL as absolute URL if necessary.
  52. func AbsoluteURL(baseURL, input string) (string, error) {
  53. absURL, u, err := GetAbsoluteURL(input)
  54. if err != nil {
  55. return "", err
  56. }
  57. if absURL != "" {
  58. return absURL, nil
  59. }
  60. base, err := url.Parse(baseURL)
  61. if err != nil {
  62. return "", fmt.Errorf("unable to parse base URL: %v", err)
  63. }
  64. return base.ResolveReference(u).String(), nil
  65. }
  66. // RootURL returns absolute URL without the path.
  67. func RootURL(websiteURL string) string {
  68. if strings.HasPrefix(websiteURL, "//") {
  69. websiteURL = "https://" + websiteURL[2:]
  70. }
  71. absoluteURL, err := AbsoluteURL(websiteURL, "")
  72. if err != nil {
  73. return websiteURL
  74. }
  75. u, err := url.Parse(absoluteURL)
  76. if err != nil {
  77. return absoluteURL
  78. }
  79. return u.Scheme + "://" + u.Host + "/"
  80. }
  81. // IsHTTPS returns true if the URL is using HTTPS.
  82. func IsHTTPS(websiteURL string) bool {
  83. parsedURL, err := url.Parse(websiteURL)
  84. if err != nil {
  85. return false
  86. }
  87. return strings.EqualFold(parsedURL.Scheme, "https")
  88. }
  89. // Domain returns only the domain part of the given URL.
  90. func Domain(websiteURL string) string {
  91. parsedURL, err := url.Parse(websiteURL)
  92. if err != nil {
  93. return websiteURL
  94. }
  95. return parsedURL.Host
  96. }
  97. // DomainWithoutWWW returns only the domain part of the given URL, with the "www." prefix removed if present.
  98. func DomainWithoutWWW(websiteURL string) string {
  99. return strings.TrimPrefix(Domain(websiteURL), "www.")
  100. }
  101. // JoinBaseURLAndPath returns a URL string with the provided path elements joined together.
  102. func JoinBaseURLAndPath(baseURL, path string) (string, error) {
  103. if baseURL == "" {
  104. return "", errors.New("empty base URL")
  105. }
  106. if path == "" {
  107. return "", errors.New("empty path")
  108. }
  109. _, err := url.Parse(baseURL)
  110. if err != nil {
  111. return "", fmt.Errorf("invalid base URL: %w", err)
  112. }
  113. finalURL, err := url.JoinPath(baseURL, path)
  114. if err != nil {
  115. return "", fmt.Errorf("unable to join base URL %s and path %s: %w", baseURL, path, err)
  116. }
  117. return finalURL, nil
  118. }
  119. // ResolvesToPrivateIP resolves a hostname and returns true if
  120. // ANY resolved IP address is non-public.
  121. func ResolvesToPrivateIP(host string) (bool, error) {
  122. ips, err := net.LookupIP(host)
  123. if err != nil {
  124. return false, err
  125. }
  126. if slices.ContainsFunc(ips, isNonPublicIP) {
  127. return true, nil
  128. }
  129. return false, nil
  130. }
  131. // isNonPublicIP returns true if the given IP is private, loopback,
  132. // link-local, multicast, or unspecified.
  133. func isNonPublicIP(ip net.IP) bool {
  134. if ip == nil {
  135. return true
  136. }
  137. return ip.IsPrivate() ||
  138. ip.IsLoopback() ||
  139. ip.IsLinkLocalUnicast() ||
  140. ip.IsLinkLocalMulticast() ||
  141. ip.IsMulticast() ||
  142. ip.IsUnspecified()
  143. }