Skip to content
Source
Sign inSign up

mock-update-server.ts

161 lines5.1 KB

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;
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 = Effect.fn("mock-update-server.isOutsideRoot")(function* (
36 rootRealPath: string,
37 filePath: string,
38) {
39 const path = yield* Path.Path;
40 const relativePath = path.relative(rootRealPath, filePath);
41 return relativePath === ".." || relativePath.startsWith("../") || relativePath.startsWith("..\\");
42});
43​
44const isWithinRoot = Effect.fn("mock-update-server.isWithinRoot")(function* (
45 rootRealPath: string,
46 filePath: string,
47) {
48 const fileSystem = yield* FileSystem.FileSystem;
49 const resolvedFilePath = yield* fileSystem.realPath(filePath).pipe(
50 Effect.match({
51 onFailure: () => undefined,
52 onSuccess: (resolvedPath) => resolvedPath,
53 }),
54 );
55​
56 return resolvedFilePath !== undefined && !(yield* isOutsideRoot(rootRealPath, resolvedFilePath));
57});
58​
59const resolveRequestedFilePath = Effect.fn("mock-update-server.resolveRequestedFilePath")(
60 function* (rootRealPath: string, requestUrl: string | undefined) {
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);
89​
90const isServableFile = Effect.fn("mock-update-server.isServableFile")(function* (
91 rootRealPath: string,
92 filePath: string,
93) {
94 const fileSystem = yield* FileSystem.FileSystem;
95 const stat = yield* fileSystem.stat(filePath).pipe(
96 Effect.match({
97 onFailure: () => undefined,
98 onSuccess: (info) => info,
99 }),
100 );
101​
102 if (stat?.type !== "File") {
103 return false;
104 }
105​
106 return yield* isWithinRoot(rootRealPath, filePath);
107});
108​
109export const makeMockUpdateRouteLayer = (rootRealPath: string) => {
110 return HttpRouter.add(
111 "*",
112 "*",
113 Effect.gen(function* () {
114 const request = yield* HttpServerRequest.HttpServerRequest;
115 const requestPath = (request.url ?? "/").split("?", 1)[0] ?? "/";
116 yield* Effect.logInfo(`Request received for path: ${requestPath}`);
117​
118 const filePath = yield* resolveRequestedFilePath(rootRealPath, request.url);
119 if (!filePath) {
120 yield* Effect.logWarning(`Attempted to access file outside of root: ${request.url ?? "/"}`);
121 return HttpServerResponse.text("Not Found", { status: 404 });
122 }
123​
124 if (!(yield* isServableFile(rootRealPath, filePath))) {
125 yield* Effect.logWarning(`Attempted to access invalid file: ${filePath}`);
126 return HttpServerResponse.text("Not Found", { status: 404 });
127 }
128​
129 yield* Effect.logInfo(`Serving file: ${filePath}`);
130 return yield* HttpServerResponse.file(filePath, { status: 200 });
131 }).pipe(
132 Effect.catchCause((cause) =>
133 Effect.gen(function* () {
134 yield* Effect.logError(`Unhandled mock update request failure: ${cause}`);
135 return HttpServerResponse.text("Internal Server Error", { status: 500 });
136 }),
137 ),
138 ),
139 );
140};
141​
142const makeMockUpdateServerLayer = (config: MockUpdateServerConfig) =>
143 HttpRouter.serve(makeMockUpdateRouteLayer(config.rootRealPath)).pipe(
144 Layer.provideMerge(
145 NodeHttpServer.layer(NodeHttp.createServer, {
146 host: "localhost",
147 port: config.port,
148 }),
149 ),
150 Layer.provideMerge(NodeServices.layer),
151 );
152​
153if (import.meta.main) {
154 resolveMockUpdateServerConfig.pipe(
155 Effect.map(makeMockUpdateServerLayer),
156 Layer.unwrap,
157 Layer.launch,
158 Effect.provide(NodeServices.layer),
159 NodeRuntime.runMain,
160 );