Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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("[email protected]:")).toEqual(false);
expect(isValidGitUrl("[email protected]:")).toEqual(false);
expect(isValidGitUrl("[email protected]:22:")).toEqual(false);
expect(isValidGitUrl("[email protected]: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("[email protected]:gitpod-io/gitpod.git")).toEqual(true);
expect(isValidGitUrl("[email protected]:filiptronicek/gitpod.git")).toEqual(true);
expect(isValidGitUrl("ssh://[email protected]: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("[email protected]:v3/services-azure/open-to-edit-project2/open-to-edit-project2")).toBe(
true,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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 [email protected]: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;
};
2 changes: 1 addition & 1 deletion components/dashboard/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEm
}
}

function parseUrl(url: string): URL | null {
export function parseUrl(url: string): URL | null {
try {
return new URL(url);
} catch (_) {
Expand Down
Loading