1import { NodeHttpServer, NodeServices } from "@effect/platform-node";
2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import { HttpClient, HttpRouter } from "effect/http";
8
9import { makeMockUpdateRouteLayer } from "./mock-update-server.ts";
10import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
11
12const withMockUpdateServer = <A, E, R>(rootRealPath: string, effect: Effect.Effect<A, E, R>) =>
13 effect.pipe(
14 Effect.provide(
15 HttpRouter.serve(makeMockUpdateRouteLayer(rootRealPath), {
16 disableListenLog: true,
17 disableLogger: true,
18 }).pipe(Layer.provideMerge(NodeHttpServer.layerTest)),
19 ),
20 );
21
22it.layer(NodeServices.layer)("mock-update-server", (it) => {
23 it.effect("serves files from the configured root", () =>
24 Effect.gen(function* () {
25 const fileSystem = yield* FileSystem.FileSystem;
26 const path = yield* Path.Path;
27 const root = yield* fileSystem.makeTempDirectoryScoped({
28 prefix: "mock-update-server-root-",
29 });
30 const rootRealPath = yield* fileSystem.realPath(root);
31 const filePath = path.join(root, "latest.yml");
32
33 yield* fileSystem.writeFileString(filePath, "version: 0.0.1\n");
34
35 yield* withMockUpdateServer(
36 rootRealPath,
37 Effect.gen(function* () {
38 const client = yield* HttpClient.HttpClient;
39 const response = yield* client.get("/latest.yml");
40
41 assert.equal(response.status, 200);
42 assert.equal(response.headers["content-type"], "text/yaml");
43 assert.equal(yield* response.text, "version: 0.0.1\n");
44 }),
45 );
46 }),
47 );
48
49 it.effect("rejects encoded path traversal outside the configured root", () =>
50 Effect.gen(function* () {
51 const fileSystem = yield* FileSystem.FileSystem;
52 const path = yield* Path.Path;
53 const root = yield* fileSystem.makeTempDirectoryScoped({
54 prefix: "mock-update-server-root-",
55 });
56 const outside = yield* fileSystem.makeTempDirectoryScoped({
57 prefix: "mock-update-server-outside-",
58 });
59 const rootRealPath = yield* fileSystem.realPath(root);
60
61 yield* fileSystem.writeFileString(path.join(outside, "secret.txt"), "nope\n");
62
63 yield* withMockUpdateServer(
64 rootRealPath,
65 Effect.gen(function* () {
66 const client = yield* HttpClient.HttpClient;
67 const response = yield* client.get("/%2e%2e/secret.txt");
68
69 assert.equal(response.status, 404);
70 assert.equal(yield* response.text, "Not Found");
71 }),
72 );
73 }),
74 );
75
76 it.effect.skipIf(!symlinksSupported)(
77 "rejects symlinked files that escape the configured root",
78 () =>
79 Effect.gen(function* () {
80 const fileSystem = yield* FileSystem.FileSystem;
81 const path = yield* Path.Path;
82 const root = yield* fileSystem.makeTempDirectoryScoped({
83 prefix: "mock-update-server-root-",
84 });
85 const outside = yield* fileSystem.makeTempDirectoryScoped({
86 prefix: "mock-update-server-outside-",
87 });
88 const rootRealPath = yield* fileSystem.realPath(root);
89 const outsideFile = path.join(outside, "outside.yml");
90 const linksDir = path.join(root, "links");
91 const symlinkPath = path.join(linksDir, "outside.yml");
92
93 yield* fileSystem.writeFileString(outsideFile, "version: outside\n");
94 yield* fileSystem.makeDirectory(linksDir, { recursive: true });
95 yield* fileSystem.symlink(outsideFile, symlinkPath);
96
97 yield* withMockUpdateServer(
98 rootRealPath,
99 Effect.gen(function* () {
100 const client = yield* HttpClient.HttpClient;
101 const response = yield* client.get("/links/outside.yml");
102
103 assert.equal(response.status, 404);
104 assert.equal(yield* response.text, "Not Found");
105 }),
106 );
107 }),
108 );
109});