hostname.go 19 KB
Newer Older
1 2 3 4 5 6 7 8
package corehttp

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
9
	"regexp"
10 11 12 13
	"strings"

	isd "github.com/jbenet/go-is-domain"
	mbase "github.com/multiformats/go-multibase"
tavit ohanian's avatar
tavit ohanian committed
14 15 16 17 18 19 20 21 22 23
	cid "gitlab.dms3.io/dms3/go-cid"
	core "gitlab.dms3.io/dms3/go-dms3/core"
	coreapi "gitlab.dms3.io/dms3/go-dms3/core/coreapi"
	namesys "gitlab.dms3.io/dms3/go-dms3/namesys"
	"gitlab.dms3.io/p2p/go-p2p-core/peer"

	config "gitlab.dms3.io/dms3/go-dms3-config"
	iface "gitlab.dms3.io/dms3/interface-go-dms3-core"
	options "gitlab.dms3.io/dms3/interface-go-dms3-core/options"
	nsopts "gitlab.dms3.io/dms3/interface-go-dms3-core/options/namesys"
24 25
)

tavit ohanian's avatar
tavit ohanian committed
26
var defaultPaths = []string{"/dms3/", "/dms3ns/", "/api/", "/p2p/", "/version"}
27

28
var pathGatewaySpec = &config.GatewaySpec{
29 30 31 32
	Paths:         defaultPaths,
	UseSubdomains: false,
}

33
var subdomainGatewaySpec = &config.GatewaySpec{
34 35 36 37
	Paths:         defaultPaths,
	UseSubdomains: true,
}

38
var defaultKnownGateways = map[string]*config.GatewaySpec{
39
	"localhost":       subdomainGatewaySpec,
tavit ohanian's avatar
tavit ohanian committed
40 41
	"dms3.io":         pathGatewaySpec,
	"gateway.dms3.io": pathGatewaySpec,
42 43 44
	"dweb.link":       subdomainGatewaySpec,
}

45 46 47
// Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7)
const dnsLabelMaxLength int = 63

48 49
// HostnameOption rewrites an incoming request based on the Host header.
func HostnameOption() ServeOption {
tavit ohanian's avatar
tavit ohanian committed
50
	return func(n *core.Dms3Node, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
51 52
		childMux := http.NewServeMux()

53
		coreAPI, err := coreapi.NewCoreAPI(n)
54 55 56 57 58 59 60 61
		if err != nil {
			return nil, err
		}

		cfg, err := n.Repo.Config()
		if err != nil {
			return nil, err
		}
62 63

		knownGateways := prepareKnownGateways(cfg.Gateway.PublicGateways)
64 65

		mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tavit ohanian's avatar
tavit ohanian committed
66
			// Unfortunately, many (well, dms3.io) gateways use
67
			// DNSLink so if we blindly rewrite with DNSLink, we'll
tavit ohanian's avatar
tavit ohanian committed
68
			// break /dms3 links.
69 70 71 72 73
			//
			// We fix this by maintaining a list of known gateways
			// and the paths that they serve "gateway" content on.
			// That way, we can use DNSLink for everything else.

74 75 76 77 78 79 80
			// Support X-Forwarded-Host if added by a reverse proxy
			// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
			host := r.Host
			if xHost := r.Header.Get("X-Forwarded-Host"); xHost != "" {
				host = xHost
			}

81
			// HTTP Host & Path check: is this one of our  "known gateways"?
82
			if gw, ok := isKnownHostname(host, knownGateways); ok {
83 84 85 86 87 88 89 90 91 92
				// This is a known gateway but request is not using
				// the subdomain feature.

				// Does this gateway _handle_ this path?
				if hasPrefix(r.URL.Path, gw.Paths...) {
					// It does.

					// Should this gateway use subdomains instead of paths?
					if gw.UseSubdomains {
						// Yes, redirect if applicable
tavit ohanian's avatar
tavit ohanian committed
93
						// Example: dweb.link/dms3/{cid} → {cid}.dms3.dweb.link
94
						newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI)
95 96 97 98 99
						if err != nil {
							http.Error(w, err.Error(), http.StatusBadRequest)
							return
						}
						if newURL != "" {
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
							// Just to be sure single Origin can't be abused in
							// web browsers that ignored the redirect for some
							// reason, Clear-Site-Data header clears browsing
							// data (cookies, storage etc) associated with
							// hostname's root Origin
							// Note: we can't use "*" due to bug in Chromium:
							// https://bugs.chromium.org/p/chromium/issues/detail?id=898503
							w.Header().Set("Clear-Site-Data", "\"cookies\", \"storage\"")

							// Set "Location" header with redirect destination.
							// It is ignored by curl in default mode, but will
							// be respected by user agents that follow
							// redirects by default, namely web browsers
							w.Header().Set("Location", newURL)

							// Note: we continue regular gateway processing:
							// HTTP Status Code http.StatusMovedPermanently
							// will be set later, in statusResponseWriter
118 119 120 121
						}
					}

					// Not a subdomain resource, continue with path processing
tavit ohanian's avatar
tavit ohanian committed
122
					// Example: 127.0.0.1:8080/dms3/{CID}, dms3.io/dms3/{CID} etc
123 124 125 126 127 128
					childMux.ServeHTTP(w, r)
					return
				}
				// Not a whitelisted path

				// Try DNSLink, if it was not explicitly disabled for the hostname
129
				if !gw.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
130
					// rewrite path and handle as DNSLink
tavit ohanian's avatar
tavit ohanian committed
131
					r.URL.Path = "/dms3ns/" + stripPort(host) + r.URL.Path
132
					childMux.ServeHTTP(w, withHostnameContext(r, host))
133 134 135 136 137 138 139 140 141
					return
				}

				// If not, resource does not exist on the hostname, return 404
				http.NotFound(w, r)
				return
			}

			// HTTP Host check: is this one of our subdomain-based "known gateways"?
tavit ohanian's avatar
tavit ohanian committed
142 143 144
			// DMS3 details extracted from the host: {rootID}.{ns}.{gwHostname}
			// /dms3/ example: {cid}.dms3.localhost:8080, {cid}.dms3.dweb.link
			// /dms3ns/ example: {p2p-key}.dms3ns.localhost:8080, {inlined-dnslink-fqdn}.dms3ns.dweb.link
145
			if gw, gwHostname, ns, rootID, ok := knownSubdomainDetails(host, knownGateways); ok {
Marcin Rataj's avatar
Marcin Rataj committed
146
				// Looks like we're using a known gateway in subdomain mode.
147 148 149 150

				// Assemble original path prefix.
				pathPrefix := "/" + ns + "/" + rootID

Marcin Rataj's avatar
Marcin Rataj committed
151
				// Does this gateway _handle_ subdomains AND this path?
152 153 154 155 156 157
				if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
					// If not, resource does not exist, return 404
					http.NotFound(w, r)
					return
				}

158 159 160
				// Check if rootID is a valid CID
				if rootCID, err := cid.Decode(rootID); err == nil {
					// Do we need to redirect root CID to a canonical DNS representation?
161
					dnsCID, err := toDNSLabel(rootID, rootCID)
162 163 164 165 166 167
					if err != nil {
						http.Error(w, err.Error(), http.StatusBadRequest)
						return
					}
					if !strings.HasPrefix(r.Host, dnsCID) {
						dnsPrefix := "/" + ns + "/" + dnsCID
168
						newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI)
169 170 171 172 173 174 175
						if err != nil {
							http.Error(w, err.Error(), http.StatusBadRequest)
							return
						}
						if newURL != "" {
							// Redirect to deterministic CID to ensure CID
							// always gets the same Origin on the web
176 177 178 179
							http.Redirect(w, r, newURL, http.StatusMovedPermanently)
							return
						}
					}
180 181 182

					// Do we need to fix multicodec in PeerID represented as CIDv1?
					if isPeerIDNamespace(ns) {
tavit ohanian's avatar
tavit ohanian committed
183
						if rootCID.Type() != cid.P2pKey {
184
							newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI)
185 186 187 188 189 190 191 192 193 194 195
							if err != nil {
								http.Error(w, err.Error(), http.StatusBadRequest)
								return
							}
							if newURL != "" {
								// Redirect to CID fixed inside of toSubdomainURL()
								http.Redirect(w, r, newURL, http.StatusMovedPermanently)
								return
							}
						}
					}
196 197 198 199 200 201 202 203
				} else { // rootID is not a CID..

					// Check if rootID is a single DNS label with an inlined
					// DNSLink FQDN a single DNS label. We support this so
					// loading DNSLink names over TLS "just works" on public
					// HTTP gateways.
					//
					// Rationale for doing this can be found under "Option C"
tavit ohanian's avatar
tavit ohanian committed
204
					// at: https://gitlab.dms3.io/dms3/in-web-browsers/issues/169
205 206
					//
					// TLDR is:
tavit ohanian's avatar
tavit ohanian committed
207
					// https://dweb.link/dms3ns/my.v-long.example.com
208 209
					// can be loaded from a subdomain gateway with a wildcard
					// TLS cert if represented as a single DNS label:
tavit ohanian's avatar
tavit ohanian committed
210 211
					// https://my-v--long-example-com.dms3ns.dweb.link
					if ns == "dms3ns" && !strings.Contains(rootID, ".") {
212 213 214 215 216 217
						// if there is no TXT recordfor rootID
						if !isDNSLinkName(r.Context(), coreAPI, rootID) {
							// my-v--long-example-com → my.v-long.example.com
							dnslinkFQDN := toDNSLinkFQDN(rootID)
							if isDNSLinkName(r.Context(), coreAPI, dnslinkFQDN) {
								// update path prefix to use real FQDN with DNSLink
tavit ohanian's avatar
tavit ohanian committed
218
								pathPrefix = "/dms3ns/" + dnslinkFQDN
219
							}
220 221
						}
					}
222 223 224 225 226 227
				}

				// Rewrite the path to not use subdomains
				r.URL.Path = pathPrefix + r.URL.Path

				// Serve path request
228
				childMux.ServeHTTP(w, withHostnameContext(r, gwHostname))
229 230 231 232 233 234 235 236
				return
			}
			// We don't have a known gateway. Fallback on DNSLink lookup

			// Wildcard HTTP Host check:
			// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
			// 2. does Host header include a fully qualified domain name (FQDN)?
			// 3. does DNSLink record exist in DNS?
237
			if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) {
238
				// rewrite path and handle as DNSLink
tavit ohanian's avatar
tavit ohanian committed
239
				r.URL.Path = "/dms3ns/" + stripPort(host) + r.URL.Path
240
				childMux.ServeHTTP(w, withHostnameContext(r, host))
241 242 243 244 245 246 247 248 249 250
				return
			}

			// else, treat it as an old school gateway, I guess.
			childMux.ServeHTTP(w, r)
		})
		return childMux, nil
	}
}

251 252 253 254 255 256 257 258 259 260
type gatewayHosts struct {
	exact    map[string]*config.GatewaySpec
	wildcard []wildcardHost
}

type wildcardHost struct {
	re   *regexp.Regexp
	spec *config.GatewaySpec
}

261 262 263 264 265 266
// Extends request context to include hostname of a canonical gateway root
// (subdomain root or dnslink fqdn)
func withHostnameContext(r *http.Request, hostname string) *http.Request {
	// This is required for links on directory listing pages to work correctly
	// on subdomain and dnslink gateways. While DNSlink could read value from
	// Host header, subdomain gateways have more comples rules (knownSubdomainDetails)
tavit ohanian's avatar
tavit ohanian committed
267
	// More: https://gitlab.dms3.io/dms3/dir-index-html/issues/42
268 269 270 271
	ctx := context.WithValue(r.Context(), "gw-hostname", hostname)
	return r.WithContext(ctx)
}

272 273 274
func prepareKnownGateways(publicGateways map[string]*config.GatewaySpec) gatewayHosts {
	var hosts gatewayHosts

275
	hosts.exact = make(map[string]*config.GatewaySpec, len(publicGateways)+len(defaultKnownGateways))
276

277 278 279 280
	// First, implicit defaults such as subdomain gateway on localhost
	for hostname, gw := range defaultKnownGateways {
		hosts.exact[hostname] = gw
	}
281

282
	// Then apply values from Gateway.PublicGateways, if present in the config
283 284
	for hostname, gw := range publicGateways {
		if gw == nil {
285 286 287
			// Remove any implicit defaults, if present. This is useful when one
			// wants to disable subdomain gateway on localhost etc.
			delete(hosts.exact, hostname)
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
			continue
		}
		if strings.Contains(hostname, "*") {
			// from *.domain.tld, construct a regexp that match any direct subdomain
			// of .domain.tld.
			//
			// Regexp will be in the form of ^[^.]+\.domain.tld(?::\d+)?$

			escaped := strings.ReplaceAll(hostname, ".", `\.`)
			regexed := strings.ReplaceAll(escaped, "*", "[^.]+")

			re, err := regexp.Compile(fmt.Sprintf(`^%s(?::\d+)?$`, regexed))
			if err != nil {
				log.Warn("invalid wildcard gateway hostname \"%s\"", hostname)
			}

			hosts.wildcard = append(hosts.wildcard, wildcardHost{re: re, spec: gw})
		} else {
			hosts.exact[hostname] = gw
		}
	}

	return hosts
}

313 314
// isKnownHostname checks Gateway.PublicGateways and returns matching
// GatewaySpec with gracefull fallback to version without port
315
func isKnownHostname(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, ok bool) {
316
	// Try hostname (host+optional port - value from Host header as-is)
317 318 319 320 321
	if gw, ok := knownGateways.exact[hostname]; ok {
		return gw, ok
	}
	// Also test without port
	if gw, ok = knownGateways.exact[stripPort(hostname)]; ok {
322 323
		return gw, ok
	}
324 325 326 327 328 329 330 331

	// Wildcard support. Test both with and without port.
	for _, host := range knownGateways.wildcard {
		if host.re.MatchString(hostname) {
			return host.spec, true
		}
	}

Marcin Rataj's avatar
Marcin Rataj committed
332
	return nil, false
333 334
}

Marcin Rataj's avatar
Marcin Rataj committed
335
// Parses Host header and looks for a known gateway matching subdomain host.
336 337
// If found, returns GatewaySpec and subdomain components extracted from Host
// header: {rootID}.{ns}.{gwHostname}
338
// Note: hostname is host + optional port
339
func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw *config.GatewaySpec, gwHostname, ns, rootID string, ok bool) {
340 341
	labels := strings.Split(hostname, ".")
	// Look for FQDN of a known gateway hostname.
tavit ohanian's avatar
tavit ohanian committed
342
	// Example: given "dist.dms3.io.dms3ns.dweb.link":
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
	// 1. Lookup "link" TLD in knownGateways: negative
	// 2. Lookup "dweb.link" in knownGateways: positive
	//
	// Stops when we have 2 or fewer labels left as we need at least a
	// rootId and a namespace.
	for i := len(labels) - 1; i >= 2; i-- {
		fqdn := strings.Join(labels[i:], ".")
		gw, ok := isKnownHostname(fqdn, knownGateways)
		if !ok {
			continue
		}

		ns := labels[i-1]
		if !isSubdomainNamespace(ns) {
			break
		}

		// Merge remaining labels (could be a FQDN with DNSLink)
		rootID := strings.Join(labels[:i-1], ".")
		return gw, fqdn, ns, rootID, true
	}
Marcin Rataj's avatar
Marcin Rataj committed
364 365
	// no match
	return nil, "", "", "", false
366 367
}

368
// isDNSLinkName returns bool if a valid DNS TXT record exist for provided host
tavit ohanian's avatar
tavit ohanian committed
369
func isDNSLinkName(ctx context.Context, dms3 iface.CoreAPI, host string) bool {
370
	fqdn := stripPort(host)
371 372 373
	if len(fqdn) == 0 && !isd.IsDomain(fqdn) {
		return false
	}
tavit ohanian's avatar
tavit ohanian committed
374
	name := "/dms3ns/" + fqdn
375 376
	// check if DNSLink exists
	depth := options.Name.ResolveOption(nsopts.Depth(1))
tavit ohanian's avatar
tavit ohanian committed
377
	_, err := dms3.Name().Resolve(ctx, name, depth)
378 379 380 381 382
	return err == nil || err == namesys.ErrResolveRecursion
}

func isSubdomainNamespace(ns string) bool {
	switch ns {
tavit ohanian's avatar
tavit ohanian committed
383
	case "dms3", "dms3ns", "p2p", "ld":
384 385 386 387 388 389 390 391
		return true
	default:
		return false
	}
}

func isPeerIDNamespace(ns string) bool {
	switch ns {
tavit ohanian's avatar
tavit ohanian committed
392
	case "dms3ns", "p2p":
393 394 395 396 397 398
		return true
	default:
		return false
	}
}

399 400
// Converts a CID to DNS-safe representation that fits in 63 characters
func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) {
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
	// Return as-is if things fit
	if len(rootID) <= dnsLabelMaxLength {
		return rootID, nil
	}

	// Convert to Base36 and see if that helped
	rootID, err = cid.NewCidV1(rootCID.Type(), rootCID.Hash()).StringOfBase(mbase.Base36)
	if err != nil {
		return "", err
	}
	if len(rootID) <= dnsLabelMaxLength {
		return rootID, nil
	}

	// Can't win with DNS at this point, return error
	return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID)
}

419
// Returns true if HTTP request involves TLS certificate.
tavit ohanian's avatar
tavit ohanian committed
420
// See https://gitlab.dms3.io/dms3/in-web-browsers/issues/169 to understand how it
421 422 423 424 425 426
// impacts DNSLink websites on public gateways.
func isHTTPSRequest(r *http.Request) bool {
	// X-Forwarded-Proto if added by a reverse proxy
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
	xproto := r.Header.Get("X-Forwarded-Proto")
	// Is request a native TLS (not used atm, but future-proofing)
tavit ohanian's avatar
tavit ohanian committed
427
	// or a proxied HTTPS (eg. go-dms3 behind nginx at a public gw)?
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
	return r.URL.Scheme == "https" || xproto == "https"
}

// Converts a FQDN to DNS-safe representation that fits in 63 characters:
// my.v-long.example.com → my-v--long-example-com
func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) {
	dnsLabel = strings.ReplaceAll(fqdn, "-", "--")
	dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-")
	if len(dnsLabel) > dnsLabelMaxLength {
		return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel)
	}
	return dnsLabel, nil
}

// Converts a DNS-safe representation of DNSLink FQDN to real FQDN:
// my-v--long-example-com → my.v-long.example.com
func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
	fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels
	fqdn = strings.ReplaceAll(fqdn, "-", ".")
	fqdn = strings.ReplaceAll(fqdn, "@", "-")
	return fqdn
}

451
// Converts a hostname/path to a subdomain-based URL, if applicable.
tavit ohanian's avatar
tavit ohanian committed
452
func toSubdomainURL(hostname, path string, r *http.Request, dms3 iface.CoreAPI) (redirURL string, err error) {
453 454 455 456
	var scheme, ns, rootID, rest string

	query := r.URL.RawQuery
	parts := strings.SplitN(path, "/", 4)
457
	isHTTPS := isHTTPSRequest(r)
458
	safeRedirectURL := func(in string) (out string, err error) {
459 460
		safeURI, err := url.ParseRequestURI(in)
		if err != nil {
461
			return "", err
462
		}
463
		return safeURI.String(), nil
464 465
	}

466
	if isHTTPS {
467 468 469 470 471 472 473 474 475 476 477 478 479
		scheme = "https:"
	} else {
		scheme = "http:"
	}

	switch len(parts) {
	case 4:
		rest = parts[3]
		fallthrough
	case 3:
		ns = parts[1]
		rootID = parts[2]
	default:
480
		return "", nil
481 482 483
	}

	if !isSubdomainNamespace(ns) {
484
		return "", nil
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
	}

	// add prefix if query is present
	if query != "" {
		query = "?" + query
	}

	// Normalize problematic PeerIDs (eg. ed25519+identity) to CID representation
	if isPeerIDNamespace(ns) && !isd.IsDomain(rootID) {
		peerID, err := peer.Decode(rootID)
		// Note: PeerID CIDv1 with protobuf multicodec will fail, but we fix it
		// in the next block
		if err == nil {
			rootID = peer.ToCid(peerID).String()
		}
	}

	// If rootID is a CID, ensure it uses DNS-friendly text representation
503 504 505 506
	if rootCID, err := cid.Decode(rootID); err == nil {
		multicodec := rootCID.Type()
		var base mbase.Encoding = mbase.Base32

tavit ohanian's avatar
tavit ohanian committed
507
		// Normalizations specific to /dms3ns/{p2p-key}
508
		if isPeerIDNamespace(ns) {
tavit ohanian's avatar
tavit ohanian committed
509 510
			// Using Base36 for /dms3ns/ for consistency
			// Context: https://gitlab.dms3.io/dms3/go-dms3/pull/7441#discussion_r452372828
511 512
			base = mbase.Base36

tavit ohanian's avatar
tavit ohanian committed
513 514
			// PeerIDs represented as CIDv1 are expected to have p2p-key
			// multicodec (https://gitlab.dms3.io/p2p/specs/pull/209).
515
			// We ease the transition by fixing multicodec on the fly:
tavit ohanian's avatar
tavit ohanian committed
516 517 518
			// https://gitlab.dms3.io/dms3/go-dms3/issues/5287#issuecomment-492163929
			if multicodec != cid.P2pKey {
				multicodec = cid.P2pKey
519
			}
520 521
		}

522 523 524 525 526 527
		// Ensure CID text representation used in subdomain is compatible
		// with the way DNS and URIs are implemented in user agents.
		//
		// 1. Switch to CIDv1 and enable case-insensitive Base encoding
		//    to avoid issues when user agent force-lowercases the hostname
		//    before making the request
tavit ohanian's avatar
tavit ohanian committed
528
		//    (https://gitlab.dms3.io/dms3/in-web-browsers/issues/89)
529 530 531 532 533 534
		rootCID = cid.NewCidV1(multicodec, rootCID.Hash())
		rootID, err = rootCID.StringOfBase(base)
		if err != nil {
			return "", err
		}
		// 2. Make sure CID fits in a DNS label, adjust encoding if needed
tavit ohanian's avatar
tavit ohanian committed
535
		//    (https://gitlab.dms3.io/dms3/go-dms3/issues/7318)
536
		rootID, err = toDNSLabel(rootID, rootCID)
537
		if err != nil {
538
			return "", err
539
		}
540 541 542 543 544
	} else { // rootID is not a CID

		// Check if rootID is a FQDN with DNSLink and convert it to TLS-safe
		// representation that fits in a single DNS label.  We support this so
		// loading DNSLink names over TLS "just works" on public HTTP gateways
tavit ohanian's avatar
tavit ohanian committed
545
		// that pass 'https' in X-Forwarded-Proto to go-dms3.
546 547
		//
		// Rationale can be found under "Option C"
tavit ohanian's avatar
tavit ohanian committed
548
		// at: https://gitlab.dms3.io/dms3/in-web-browsers/issues/169
549 550
		//
		// TLDR is:
tavit ohanian's avatar
tavit ohanian committed
551
		// /dms3ns/my.v-long.example.com
552 553
		// can be loaded from a subdomain gateway with a wildcard TLS cert if
		// represented as a single DNS label:
tavit ohanian's avatar
tavit ohanian committed
554 555 556
		// https://my-v--long-example-com.dms3ns.dweb.link
		if isHTTPS && ns == "dms3ns" && strings.Contains(rootID, ".") {
			if isDNSLinkName(r.Context(), dms3, rootID) {
557 558 559 560 561 562 563 564 565
				// my.v-long.example.com → my-v--long-example-com
				dnsLabel, err := toDNSLinkDNSLabel(rootID)
				if err != nil {
					return "", err
				}
				// update path prefix to use real FQDN with DNSLink
				rootID = dnsLabel
			}
		}
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
	}

	return safeRedirectURL(fmt.Sprintf(
		"%s//%s.%s.%s/%s%s",
		scheme,
		rootID,
		ns,
		hostname,
		rest,
		query,
	))
}

func hasPrefix(path string, prefixes ...string) bool {
	for _, prefix := range prefixes {
		// Assume people are creative with trailing slashes in Gateway config
		p := strings.TrimSuffix(prefix, "/")
tavit ohanian's avatar
tavit ohanian committed
583
		// Support for both /version and /dms3/$cid
584 585 586 587 588 589 590 591 592 593 594 595 596 597
		if p == path || strings.HasPrefix(path, p+"/") {
			return true
		}
	}
	return false
}

func stripPort(hostname string) string {
	host, _, err := net.SplitHostPort(hostname)
	if err == nil {
		return host
	}
	return hostname
}