1// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg
2import * as NodeHttp from "node:http";
3
4import { NodeHttpServer, NodeRuntime, NodeServices } from "@effect/platform-node";
5import * as Config from "effect/Config";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
11
12interface MockUpdateServerConfig {
13 readonly port: number;
14 readonly rootRealPath: string;
15}
16
17const resolveMockUpdateServerConfig = Effect.gen(function* () {
18 const fileSystem = yield* FileSystem.FileSystem;
19 const path = yield* Path.Path;
20 const config = yield* Config.all({
21 port: Config.Port("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.withDefault(3000)),
22 root: Config.String("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe(
23 Config.withDefault("../release-mock"),
24 ),
25 });
26
27 const resolvedRoot = path.resolve(import.meta.dirname, config.root);
28
29 return {
30 port: config.port,
31 rootRealPath: yield* fileSystem.realPath(resolvedRoot),
32 } satisfies MockUpdateServerConfig;
33});
34
35const isOutsideRoot = (rootRealPath: string, filePath: string) =>
36 Effect.gen(function* () {
37 const path = yield* Path.Path;
38 const relativePath = path.relative(rootRealPath, filePath);
39 return (
40 relativePath === ".." || relativePath.startsWith("../") || relativePath.startsWith("..\\")
41 );
42 });
43
44const isWithinRoot = (rootRealPath: string, filePath: string) =>
45 Effect.gen(function* () {
46 const fileSystem = yield* FileSystem.FileSystem;
47 const resolvedFilePath = yield* fileSystem.realPath(filePath).pipe(
48 Effect.match({
49 onFailure: () => undefined,
50 onSuccess: (resolvedPath) => resolvedPath,
51 }),
52 );
53
54 return (
55 resolvedFilePath !== undefined && !(yield* isOutsideRoot(rootRealPath, resolvedFilePath))
56 );
57 });
58
59const resolveRequestedFilePath = (rootRealPath: string, requestUrl: string | undefined) =>
60 Effect.gen(function* () {
61 const path = yield* Path.Path;
62 const rawPath = (requestUrl ?? "/").split("?", 1)[0] ?? "/";
63 const decodedPath = yield* Effect.try({
64 try: () => decodeURIComponent(rawPath),
65 catch: () => null,
66 }).pipe(
67 Effect.match({
68 onFailure: () => undefined,
69 onSuccess: (value) => value,
70 }),
71 );
72
73 if (!decodedPath) {
74 return undefined;
75 }
76
77 if (decodedPath.includes("\0")) {
78 return undefined;
79 }
80
81 const filePath = path.resolve(
82 rootRealPath,
83 `.${decodedPath.startsWith("/") ? decodedPath : `/${decodedPath}`}`,
84 );
85
86 return (yield* isOutsideRoot(rootRealPath, filePath)) ? undefined : filePath;
87 });
88
89const isServableFile = (rootRealPath: string, filePath: string) =>
90 Effect.gen(function* () {
91 const fileSystem = yield* FileSystem.FileSystem;
92 const stat = yield* fileSystem.stat(filePath).pipe(
93 Effect.match({
94 onFailure: () => undefined,
95 onSuccess: (info) => info,
96 }),
97 );
98
99 if (stat?.type !== "File") {
100 return false;
101 }
102
103 return yield* isWithinRoot(rootRealPath, filePath);
104 });
105
106export const makeMockUpdateRouteLayer = (rootRealPath: string) => {
107 return HttpRouter.add(
108 "*",
109 "*",
110 Effect.gen(function* () {
111 const request = yield* HttpServerRequest.HttpServerRequest;
112 const requestPath = (request.url ?? "/").split("?", 1)[0] ?? "/";
113 yield* Effect.logInfo(`Request received for path: ${requestPath}`);
114
115 const filePath = yield* resolveRequestedFilePath(rootRealPath, request.url);
116 if (!filePath) {
117 yield* Effect.logWarning(`Attempted to access file outside of root: ${request.url ?? "/"}`);
118 return HttpServerResponse.text("Not Found", { status: 404 });
119 }
120
121 if (!(yield* isServableFile(rootRealPath, filePath))) {
122 yield* Effect.logWarning(`Attempted to access invalid file: ${filePath}`);
123 return HttpServerResponse.text("Not Found", { status: 404 });
124 }
125
126 yield* Effect.logInfo(`Serving file: ${filePath}`);
127 return yield* HttpServerResponse.file(filePath, { status: 200 });
128 }).pipe(
129 Effect.catchCause((cause) =>
130 Effect.gen(function* () {
131 yield* Effect.logError(`Unhandled mock update request failure: ${cause}`);
132 return HttpServerResponse.text("Internal Server Error", { status: 500 });
133 }),
134 ),
135 ),
136 );
137};
138
139const makeMockUpdateServerLayer = (config: MockUpdateServerConfig) =>
140 HttpRouter.serve(makeMockUpdateRouteLayer(config.rootRealPath)).pipe(
141 Layer.provideMerge(
142 NodeHttpServer.layer(NodeHttp.createServer, {
143 host: "localhost",
144 port: config.port,
145 }),
146 ),
147 Layer.provideMerge(NodeServices.layer),
148 );
149
150if (import.meta.main) {
151 resolveMockUpdateServerConfig.pipe(
152 Effect.map(makeMockUpdateServerLayer),
153 Layer.unwrap,
154 Layer.launch,
155 Effect.provide(NodeServices.layer),
156 NodeRuntime.runMain,
157 );
158}