diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts index 3f01e4a900b180..ca3a9f3aa731e0 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts @@ -5,7 +5,7 @@ */ import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb"; -import { deduplicateAndFilterRepositories } from "./unified-repositories-search-query"; +import { deduplicateAndFilterRepositories, isValidGitUrl } from "./unified-repositories-search-query"; function repo(name: string, project?: string): SuggestedRepository { return new SuggestedRepository({ @@ -95,3 +95,27 @@ test("it should return all repositories without duplicates when excludeProjects expect(deduplicated[0].repoName).toEqual("foo"); expect(deduplicated[1].repoName).toEqual("bar"); }); + +test("should perform weak validation for git URLs", () => { + expect(isValidGitUrl("a:")).toEqual(false); + expect(isValidGitUrl("a:b")).toEqual(false); + expect(isValidGitUrl("https://b")).toEqual(false); + expect(isValidGitUrl("https://b/repo.git")).toEqual(false); + expect(isValidGitUrl("https://b.com/repo.git")).toEqual(true); + expect(isValidGitUrl("git@a.b:")).toEqual(false); + expect(isValidGitUrl("blib@a.b:")).toEqual(false); + expect(isValidGitUrl("blib@a.b:22:")).toEqual(false); + expect(isValidGitUrl("blib@a.b:g/g")).toEqual(true); + + // some "from the wild" cases + expect(isValidGitUrl("https://github.com/gitpod-io/gitpod/pull/20281")).toEqual(true); + expect(isValidGitUrl("https://gitlab.com/filiptronicek/gitpod.git")).toEqual(true); + expect(isValidGitUrl("git@github.com:gitpod-io/gitpod.git")).toEqual(true); + expect(isValidGitUrl("git@gitlab.com:filiptronicek/gitpod.git")).toEqual(true); + expect(isValidGitUrl("ssh://login@server.com:12345/~/repository.git")).toBe(true); + expect(isValidGitUrl("https://bitbucket.gitpod-dev.com/scm/~geropl/test-user-repo.git")).toBe(true); + expect(isValidGitUrl("git://gitlab.com/gitpod/spring-petclinic")).toBe(true); + expect(isValidGitUrl("git@ssh.dev.azure.com:v3/services-azure/open-to-edit-project2/open-to-edit-project2")).toBe( + true, + ); +}); diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 3d46703dc64a4d..1b8da312660c5a 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -11,6 +11,7 @@ import { useMemo } from "react"; import { useListConfigurations } from "../configurations/configuration-queries"; import type { UseInfiniteQueryResult } from "@tanstack/react-query"; import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; +import { parseUrl } from "../../utils"; export const flattenPagedConfigurations = ( data: UseInfiniteQueryResult<{ configurations: Configuration[] }>["data"], @@ -125,17 +126,62 @@ export function deduplicateAndFilterRepositories( } if (results.length === 0) { - try { - // If the searchString is a URL, and it's not present in the proposed results, "artificially" add it here. - new URL(searchString); + // If the searchString is a URL, and it's not present in the proposed results, "artificially" add it here. + if (isValidGitUrl(searchString)) { + console.log("It's valid man"); results.push( new SuggestedRepository({ url: searchString, }), ); - } catch {} + } + + console.log("Valid after man"); } // Limit what we show to 200 results return results.slice(0, 200); } + +const ALLOWED_GIT_PROTOCOLS = ["ssh:", "git:", "http:", "https:"]; +/** + * An opionated git URL validator + * + * Assumptions: + * - Git hosts are not themselves TLDs (like .com) or reserved names like `localhost` + * - Git clone URLs can operate over ssh://, git:// and http(s):// + * - Git clone URLs (both SSH and HTTP ones) must have a nonempty path + */ +export const isValidGitUrl = (input: string): boolean => { + const url = parseUrl(input); + if (!url) { + // SSH URLs with no protocol, such as git@github.com:gitpod-io/gitpod.git + const sshMatch = input.match(/^\w+@([^:]+):(.+)$/); + if (!sshMatch) return false; + + const [, host, path] = sshMatch; + + // Check if the path is not empty + if (!path || path.trim().length === 0) return false; + + if (path.includes(":")) return false; + + return isHostValid(host); + } + + if (!url) return false; + + if (!ALLOWED_GIT_PROTOCOLS.includes(url.protocol)) return false; + if (url.pathname.length <= 1) return false; // make sure we have some path + + return isHostValid(url.host); +}; + +const isHostValid = (input?: string): boolean => { + if (!input) return false; + + const hostSegments = input.split("."); + if (hostSegments.length < 2 || hostSegments.some((chunk) => chunk === "")) return false; // check that there are no consecutive periods as well as no leading or trailing ones + + return true; +}; diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 4787e6fbd446d1..a47759e0aa2f3e 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -214,7 +214,7 @@ export class ReplayableEventEmitter extends EventEm } } -function parseUrl(url: string): URL | null { +export function parseUrl(url: string): URL | null { try { return new URL(url); } catch (_) {