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
81 changes: 72 additions & 9 deletions src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function fetchStac(
if (response.ok) {
return response
.json()
.then((json) => maybeAddSelfLink(json, href.toString()))
.then((json) => makeStacHrefsAbsolute(json, href.toString()))
.then((json) => maybeAddTypeField(json));
} else {
throw new Error(`${method} ${href}: ${response.statusText}`);
Expand All @@ -33,19 +33,82 @@ export async function fetchStacLink(link: StacLink, href?: string | undefined) {
);
}

// eslint-disable-next-line
function maybeAddSelfLink(value: any, href: string) {
if (!(value as StacValue)?.links?.find((link) => link.rel == "self")) {
const link = { href, rel: "self" };
if (Array.isArray(value.links)) {
value.links.push(link);
} else {
value.links = [link];
/**
* Attempt to convert links and asset URLS to absolute URLs while ensuring a self link exists.
*
* @param value Source stac item, collection, or catalog
* @param baseUrl base location of the STAC document
*/
export function makeStacHrefsAbsolute<T extends StacValue>(
value: T,
baseUrl: string,
): T {
const baseUrlObj = new URL(baseUrl);

if (value.links != null) {
let hasSelf = false;
for (const link of value.links) {
if (link.rel === "self") hasSelf = true;
if (link.href) {
link.href = toAbsoluteUrl(link.href, baseUrlObj);
}
}
if (hasSelf === false) {
value.links.push({ href: baseUrl, rel: "self" });
}
} else {
value.links = [{ href: baseUrl, rel: "self" }];
}

if (value.assets != null) {
for (const asset of Object.values(value.assets)) {
if (asset.href) {
asset.href = toAbsoluteUrl(asset.href, baseUrlObj);
}
}
}
return value;
}

/**
* Determine if the URL is absolute
* @returns true if absolute, false otherwise
*/
function isAbsolute(url: string) {
try {
new URL(url);
return true;
} catch {
return false;
}
}

/**
* Attempt to convert a possibly relative URL to an absolute URL
*
* If the URL is already absolute, it is returned unchanged.
*
* **WARNING**: if the URL is http it will be returned as URL encoded
*
* @param href
* @param baseUrl
* @returns absolute URL
*/
export function toAbsoluteUrl(href: string, baseUrl: URL): string {
if (isAbsolute(href)) return href;

const targetUrl = new URL(href, baseUrl);

if (targetUrl.protocol === "http:" || targetUrl.protocol === "https:") {
return targetUrl.toString();
}

// S3 links should not be encoded
if (targetUrl.protocol === "s3:") return decodeURI(targetUrl.toString());

return targetUrl.toString();
}

// eslint-disable-next-line
function maybeAddTypeField(value: any) {
if (!value.type) {
Expand Down
66 changes: 66 additions & 0 deletions tests/http.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, test } from "vitest";
import { makeStacHrefsAbsolute, toAbsoluteUrl } from "../src/http";
import { StacItem } from "stac-ts";

test("should preserve UTF8 characters while making URLS absolute", async () => {
expect(toAbsoluteUrl("🦄.tiff", new URL("s3://some-bucket"))).equals(
"s3://some-bucket/🦄.tiff",
);
expect(
toAbsoluteUrl("https://foo/bar/🦄.tiff", new URL("s3://some-bucket")),
).equals("https://foo/bar/🦄.tiff");
expect(
toAbsoluteUrl("../../../🦄.tiff", new URL("s3://some-bucket/🌈/path/a/b/")),
).equals("s3://some-bucket/🌈/🦄.tiff");

expect(toAbsoluteUrl("a+🦄.tiff", new URL("s3://some-bucket/🌈/"))).equals(
"s3://some-bucket/🌈/a+🦄.tiff",
);

expect(
toAbsoluteUrl("../../../🦄.tiff", new URL("https://some-url/🌈/path/a/b/")),
).equals("https://some-url/%F0%9F%8C%88/%F0%9F%A6%84.tiff");
expect(
toAbsoluteUrl(
"foo/🦄.tiff?width=1024",
new URL("https://user@[2601:195:c381:3560::f42a]:1234/test"),
),
).equals(
"https://user@[2601:195:c381:3560::f42a]:1234/foo/%F0%9F%A6%84.tiff?width=1024",
);
});

test("should convert relative links to absolute", () => {
expect(
makeStacHrefsAbsolute(
{
links: [
{ href: "a/b/c", rel: "child" },
{ href: "/d/e/f", rel: "child" },
],
} as unknown as StacItem,
"https://example.com/root/item.json",
).links,
).deep.equals([
{ href: "https://example.com/root/a/b/c", rel: "child" },
{ href: "https://example.com/d/e/f", rel: "child" },
{ href: "https://example.com/root/item.json", rel: "self" },
]);
});

test("should convert relative assets to absolute", () => {
expect(
makeStacHrefsAbsolute(
{
assets: {
tiff: { href: "./foo.tiff" },
thumbnail: { href: "../thumbnails/foo.png" },
},
} as unknown as StacItem,
"https://example.com/root/item.json",
).assets,
).deep.equals({
tiff: { href: "https://example.com/root/foo.tiff" },
thumbnail: { href: "https://example.com/thumbnails/foo.png" },
});
});