+
+var errUserinfoInRedirectTarget = errors.New("redirect target rejected because it contains userinfo")
+
+func validateLoginRedirectTarget(cluster *arvados.Cluster, returnTo string) error {
+ u, err := url.Parse(returnTo)
+ if err != nil {
+ return err
+ }
+ u, err = u.Parse("/")
+ if err != nil {
+ return err
+ }
+ if u.User != nil {
+ return errUserinfoInRedirectTarget
+ }
+ target := origin(*u)
+ for trusted := range cluster.Login.TrustedClients {
+ trustedOrigin := origin(url.URL(trusted))
+ if trustedOrigin == target {
+ return nil
+ }
+ // If TrustedClients has https://*.bar.example, we
+ // trust https://foo.bar.example. Note origin() has
+ // already stripped the incoming Path, so we won't
+ // accidentally trust
+ // https://attacker.example/pwn.bar.example here. See
+ // tests.
+ if strings.HasPrefix(trustedOrigin, u.Scheme+"://*.") && strings.HasSuffix(target, trustedOrigin[len(u.Scheme)+4:]) {
+ return nil
+ }
+ }
+ if target == origin(url.URL(cluster.Services.Workbench1.ExternalURL)) ||
+ target == origin(url.URL(cluster.Services.Workbench2.ExternalURL)) {
+ return nil
+ }
+ if cluster.Login.TrustPrivateNetworks {
+ if u.Hostname() == "localhost" {
+ return nil
+ }
+ if ip := net.ParseIP(u.Hostname()); len(ip) > 0 {
+ for _, n := range privateNetworks {
+ if n.Contains(ip) {
+ return nil
+ }
+ }
+ }
+ }
+ return fmt.Errorf("requesting site is not listed in TrustedClients config")
+}
+
+// origin returns the canonical origin of a URL, e.g.,
+// origin("https://example:443/foo") returns "https://example/"
+func origin(u url.URL) string {
+ origin := url.URL{
+ Scheme: u.Scheme,
+ Host: u.Host,
+ Path: "/",
+ }
+ if origin.Port() == "80" && origin.Scheme == "http" {
+ origin.Host = origin.Hostname()
+ } else if origin.Port() == "443" && origin.Scheme == "https" {
+ origin.Host = origin.Hostname()
+ }
+ return origin.String()
+}