@simplysm/service-server 13.0.100 → 14.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -133
- package/dist/auth/auth-token-payload.js +2 -1
- package/dist/auth/auth-token-payload.js.map +1 -6
- package/dist/auth/jwt-manager.js +21 -21
- package/dist/auth/jwt-manager.js.map +1 -6
- package/dist/core/define-service.d.ts +12 -12
- package/dist/core/define-service.d.ts.map +1 -1
- package/dist/core/define-service.js +77 -63
- package/dist/core/define-service.js.map +1 -6
- package/dist/core/service-executor.d.ts.map +1 -1
- package/dist/core/service-executor.js +42 -32
- package/dist/core/service-executor.js.map +1 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -6
- package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
- package/dist/legacy/v1-auto-update-handler.js +42 -35
- package/dist/legacy/v1-auto-update-handler.js.map +1 -6
- package/dist/protocol/protocol-wrapper.d.ts +9 -9
- package/dist/protocol/protocol-wrapper.js +64 -46
- package/dist/protocol/protocol-wrapper.js.map +1 -6
- package/dist/service-server.d.ts +2 -1
- package/dist/service-server.d.ts.map +1 -1
- package/dist/service-server.js +183 -165
- package/dist/service-server.js.map +1 -6
- package/dist/services/auto-update-service.js +35 -34
- package/dist/services/auto-update-service.js.map +1 -6
- package/dist/services/orm-service.js +114 -120
- package/dist/services/orm-service.js.map +1 -6
- package/dist/transport/http/http-request-handler.d.ts.map +1 -1
- package/dist/transport/http/http-request-handler.js +58 -46
- package/dist/transport/http/http-request-handler.js.map +1 -6
- package/dist/transport/http/static-file-handler.js +42 -39
- package/dist/transport/http/static-file-handler.js.map +1 -6
- package/dist/transport/http/upload-handler.d.ts.map +1 -1
- package/dist/transport/http/upload-handler.js +60 -55
- package/dist/transport/http/upload-handler.js.map +1 -6
- package/dist/transport/socket/service-socket.d.ts +13 -13
- package/dist/transport/socket/service-socket.js +132 -108
- package/dist/transport/socket/service-socket.js.map +1 -6
- package/dist/transport/socket/websocket-handler.d.ts +9 -13
- package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
- package/dist/transport/socket/websocket-handler.js +148 -139
- package/dist/transport/socket/websocket-handler.js.map +1 -6
- package/dist/types/server-options.d.ts +1 -1
- package/dist/types/server-options.d.ts.map +1 -1
- package/dist/types/server-options.js +2 -1
- package/dist/types/server-options.js.map +1 -6
- package/dist/utils/config-manager.js +48 -46
- package/dist/utils/config-manager.js.map +1 -6
- package/dist/workers/service-protocol.worker.js +8 -11
- package/dist/workers/service-protocol.worker.js.map +1 -6
- package/docs/auth.md +28 -16
- package/docs/core.md +113 -54
- package/docs/legacy.md +21 -0
- package/docs/main.md +81 -0
- package/docs/protocol.md +31 -0
- package/docs/services.md +49 -44
- package/docs/transport.md +81 -76
- package/docs/types.md +31 -0
- package/docs/utilities.md +27 -0
- package/package.json +12 -13
- package/src/auth/jwt-manager.ts +2 -2
- package/src/core/define-service.ts +19 -19
- package/src/core/service-executor.ts +23 -17
- package/src/index.ts +10 -12
- package/src/legacy/v1-auto-update-handler.ts +10 -10
- package/src/protocol/protocol-wrapper.ts +16 -16
- package/src/service-server.ts +51 -43
- package/src/services/auto-update-service.ts +1 -1
- package/src/services/orm-service.ts +7 -7
- package/src/transport/http/http-request-handler.ts +16 -10
- package/src/transport/http/static-file-handler.ts +8 -8
- package/src/transport/http/upload-handler.ts +16 -9
- package/src/transport/socket/service-socket.ts +22 -22
- package/src/transport/socket/websocket-handler.ts +50 -70
- package/src/types/server-options.ts +1 -1
- package/src/utils/config-manager.ts +11 -11
- package/dist/services/smtp-client-service.d.ts +0 -8
- package/dist/services/smtp-client-service.d.ts.map +0 -1
- package/dist/services/smtp-client-service.js +0 -46
- package/dist/services/smtp-client-service.js.map +0 -6
- package/docs/server.md +0 -126
- package/src/services/smtp-client-service.ts +0 -59
- package/tests/define-service.spec.ts +0 -66
- package/tests/orm-service.spec.ts +0 -83
- package/tests/service-executor.spec.ts +0 -114
|
@@ -5,29 +5,29 @@ import { createServiceProtocol } from "@simplysm/service-common";
|
|
|
5
5
|
import type * as ServiceProtocolWorkerModule from "../workers/service-protocol.worker";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* 프로토콜 래퍼 인터페이스
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
|
|
11
|
+
* 가벼운 작업은 메인 스레드에서 처리한다.
|
|
12
12
|
*/
|
|
13
13
|
export interface ServerProtocolWrapper {
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* 메시지를 인코딩한다 (worker 자동 위임)
|
|
16
16
|
*/
|
|
17
17
|
encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 메시지를 디코딩한다 (worker 자동 위임)
|
|
21
21
|
*/
|
|
22
22
|
decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 프로토콜 리소스를 해제한다
|
|
26
26
|
*/
|
|
27
27
|
dispose(): void;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// 공유 worker 인스턴스 (지연 싱글턴)
|
|
31
31
|
let sharedWorker: WorkerProxy<typeof ServiceProtocolWorkerModule> | undefined;
|
|
32
32
|
|
|
33
33
|
function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
|
|
@@ -43,37 +43,37 @@ function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* 프로토콜 래퍼 인스턴스를 생성한다
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
|
|
49
|
+
* 가벼운 작업은 메인 스레드에서 처리한다.
|
|
50
50
|
*/
|
|
51
51
|
export function createServerProtocolWrapper(): ServerProtocolWrapper {
|
|
52
52
|
// -------------------------------------------------------------------
|
|
53
|
-
//
|
|
53
|
+
// 상태
|
|
54
54
|
// -------------------------------------------------------------------
|
|
55
55
|
|
|
56
56
|
const protocol = createServiceProtocol();
|
|
57
57
|
const SIZE_THRESHOLD = 30 * 1024; // 30KB
|
|
58
58
|
|
|
59
59
|
// -------------------------------------------------------------------
|
|
60
|
-
//
|
|
60
|
+
// 헬퍼
|
|
61
61
|
// -------------------------------------------------------------------
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* 메시지 인코딩에 worker를 사용해야 하는지 확인한다
|
|
65
65
|
*/
|
|
66
66
|
function shouldUseWorkerForEncode(msg: ServiceMessage): boolean {
|
|
67
67
|
if (!("body" in msg)) return false;
|
|
68
68
|
|
|
69
69
|
const body = msg.body;
|
|
70
70
|
|
|
71
|
-
// Uint8Array:
|
|
71
|
+
// Uint8Array: 항상 worker 사용
|
|
72
72
|
if (body instanceof Uint8Array) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// 배열: Uint8Array 요소 존재 여부 확인 (ORM 결과 등)
|
|
77
77
|
if (Array.isArray(body)) {
|
|
78
78
|
return body.length > 0 && body.some((item) => item instanceof Uint8Array);
|
|
79
79
|
}
|
|
@@ -82,7 +82,7 @@ export function createServerProtocolWrapper(): ServerProtocolWrapper {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// -------------------------------------------------------------------
|
|
85
|
-
//
|
|
85
|
+
// 공개 API
|
|
86
86
|
// -------------------------------------------------------------------
|
|
87
87
|
|
|
88
88
|
return {
|
package/src/service-server.ts
CHANGED
|
@@ -31,14 +31,18 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
31
31
|
isOpen = false;
|
|
32
32
|
|
|
33
33
|
private readonly _wsHandler: ReturnType<typeof createWebSocketHandler>;
|
|
34
|
+
private readonly _jwtSecret: string | undefined;
|
|
35
|
+
private _shutdownRegistered = false;
|
|
34
36
|
|
|
35
37
|
readonly fastify: FastifyInstance;
|
|
36
38
|
|
|
37
39
|
constructor(readonly options: ServiceServerOptions) {
|
|
38
40
|
super();
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
this._jwtSecret = options.auth != null && options.auth !== false ? options.auth.jwtSecret : undefined;
|
|
43
|
+
|
|
44
|
+
// SSL 설정 (동기)
|
|
45
|
+
// 참고: Fastify HTTPS는 Buffer 타입이 필요함 (Uint8Array를 직접 사용할 수 없음)
|
|
42
46
|
const httpsConf = options.ssl
|
|
43
47
|
? { pfx: Buffer.from(options.ssl.pfxBytes), passphrase: options.ssl.passphrase }
|
|
44
48
|
: null;
|
|
@@ -47,17 +51,27 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
47
51
|
|
|
48
52
|
this._wsHandler = createWebSocketHandler(
|
|
49
53
|
(def) => executeServiceMethod(this, def),
|
|
50
|
-
|
|
54
|
+
this._jwtSecret,
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
async listen(): Promise<void> {
|
|
55
|
-
logger.info(
|
|
59
|
+
logger.info(`서버 시작 중... ${env.VER ?? ""}`);
|
|
60
|
+
|
|
61
|
+
// auth 설정 검증: auth 미설정(undefined)인데 auth 요구 서비스가 있으면 에러
|
|
62
|
+
if (this.options.auth === undefined) {
|
|
63
|
+
const authRequiredService = this.options.services.find((s) => s.authPermissions != null);
|
|
64
|
+
if (authRequiredService != null) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`auth 설정이 필요합니다: 서비스 [${authRequiredService.name}]에 auth가 설정되어 있습니다.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
56
70
|
|
|
57
|
-
// WebSocket
|
|
71
|
+
// WebSocket 플러그인
|
|
58
72
|
await this.fastify.register(fastifyWebsocket);
|
|
59
73
|
|
|
60
|
-
//
|
|
74
|
+
// 보안 플러그인
|
|
61
75
|
await this.fastify.register(fastifyHelmet, {
|
|
62
76
|
global: true,
|
|
63
77
|
contentSecurityPolicy: {
|
|
@@ -78,17 +92,17 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
78
92
|
originAgentCluster: false,
|
|
79
93
|
});
|
|
80
94
|
|
|
81
|
-
//
|
|
95
|
+
// 업로드 플러그인
|
|
82
96
|
await this.fastify.register(fastifyMultipart);
|
|
83
97
|
|
|
84
|
-
//
|
|
98
|
+
// @fastify/static 등록
|
|
85
99
|
await this.fastify.register(fastifyStatic, {
|
|
86
100
|
root: path.resolve(this.options.rootPath, "www"),
|
|
87
101
|
wildcard: false,
|
|
88
102
|
serve: false,
|
|
89
103
|
});
|
|
90
104
|
|
|
91
|
-
// CORS
|
|
105
|
+
// CORS 설정
|
|
92
106
|
await this.fastify.register(fastifyCors, {
|
|
93
107
|
origin: (_origin, cb) => {
|
|
94
108
|
cb(null, true);
|
|
@@ -97,7 +111,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
97
111
|
exposedHeaders: ["Content-Disposition", "Content-Length"],
|
|
98
112
|
});
|
|
99
113
|
|
|
100
|
-
// JSON
|
|
114
|
+
// JSON 파서
|
|
101
115
|
this.fastify.addContentTypeParser(
|
|
102
116
|
"application/json",
|
|
103
117
|
{ parseAs: "string" },
|
|
@@ -113,22 +127,22 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
113
127
|
},
|
|
114
128
|
);
|
|
115
129
|
|
|
116
|
-
// JSON
|
|
130
|
+
// JSON 직렬화기
|
|
117
131
|
this.fastify.setSerializerCompiler(() => (data) => json.stringify(data));
|
|
118
132
|
|
|
119
|
-
// API
|
|
133
|
+
// API 라우트
|
|
120
134
|
this.fastify.all("/api/:service/:method", async (req, reply) => {
|
|
121
|
-
await handleHttpRequest(req, reply, this.
|
|
135
|
+
await handleHttpRequest(req, reply, this._jwtSecret, (def) =>
|
|
122
136
|
executeServiceMethod(this, def),
|
|
123
137
|
);
|
|
124
138
|
});
|
|
125
139
|
|
|
126
|
-
//
|
|
140
|
+
// 업로드 라우트
|
|
127
141
|
this.fastify.all("/upload", async (req, reply) => {
|
|
128
|
-
await handleUpload(req, reply, this.options.rootPath, this.
|
|
142
|
+
await handleUpload(req, reply, this.options.rootPath, this._jwtSecret);
|
|
129
143
|
});
|
|
130
144
|
|
|
131
|
-
// WebSocket
|
|
145
|
+
// WebSocket 라우트
|
|
132
146
|
const onWebSocketConnected = (socket: WebSocket, req: FastifyRequest) => {
|
|
133
147
|
const { ver, clientId, clientName } = req.query as {
|
|
134
148
|
ver: string | undefined;
|
|
@@ -138,15 +152,15 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
138
152
|
|
|
139
153
|
if (ver === "2") {
|
|
140
154
|
if (clientId == null || clientName == null) {
|
|
141
|
-
socket.close(1008, "
|
|
155
|
+
socket.close(1008, "클라이언트 ID/NAME이 누락되었습니다");
|
|
142
156
|
return;
|
|
143
157
|
}
|
|
144
158
|
this._wsHandler.addSocket(socket, clientId, clientName, req);
|
|
145
159
|
} else {
|
|
146
|
-
// V1
|
|
160
|
+
// V1 레거시 지원 (자동 업데이트 전용)
|
|
147
161
|
const autoUpdateDef = this.options.services.find((s) => s.name === "AutoUpdate");
|
|
148
162
|
if (autoUpdateDef == null) {
|
|
149
|
-
socket.close(1008, "AutoUpdate
|
|
163
|
+
socket.close(1008, "AutoUpdate 서비스가 설정되지 않았습니다");
|
|
150
164
|
return;
|
|
151
165
|
}
|
|
152
166
|
|
|
@@ -163,7 +177,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
163
177
|
this.fastify.get("/", { websocket: true }, onWebSocketConnected.bind(this));
|
|
164
178
|
this.fastify.get("/ws", { websocket: true }, onWebSocketConnected.bind(this));
|
|
165
179
|
|
|
166
|
-
//
|
|
180
|
+
// 정적 파일 와일드카드 핸들러
|
|
167
181
|
this.fastify.route({
|
|
168
182
|
method: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"],
|
|
169
183
|
url: "/*",
|
|
@@ -175,19 +189,19 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
175
189
|
},
|
|
176
190
|
});
|
|
177
191
|
|
|
178
|
-
// HTTP
|
|
192
|
+
// HTTP 서버 수준 에러 핸들링
|
|
179
193
|
this.fastify.server.on("error", (err) => {
|
|
180
|
-
logger.error("HTTP
|
|
194
|
+
logger.error("HTTP 서버 에러", err);
|
|
181
195
|
});
|
|
182
196
|
|
|
183
|
-
//
|
|
197
|
+
// 리슨
|
|
184
198
|
await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
|
|
185
199
|
|
|
186
|
-
//
|
|
200
|
+
// 정상 종료 핸들러 등록
|
|
187
201
|
this._registerGracefulShutdown();
|
|
188
202
|
|
|
189
203
|
this.isOpen = true;
|
|
190
|
-
logger.info(
|
|
204
|
+
logger.info(`서버 시작됨 (포트: ${this.options.port})`);
|
|
191
205
|
this.emit("ready");
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -196,15 +210,10 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
196
210
|
await this.fastify.close();
|
|
197
211
|
|
|
198
212
|
this.isOpen = false;
|
|
199
|
-
logger.debug("
|
|
213
|
+
logger.debug("서버 종료됨");
|
|
200
214
|
this.emit("close");
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>) {
|
|
204
|
-
logger.debug("Broadcasting RELOAD to all server clients");
|
|
205
|
-
await this._wsHandler.broadcastReload(clientName, changedFileSet);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
217
|
async emitEvent<TInfo, TData>(
|
|
209
218
|
eventDef: ServiceEventDef<TInfo, TData>,
|
|
210
219
|
infoSelector: (item: TInfo) => boolean,
|
|
@@ -214,25 +223,24 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
async signAuthToken(payload: AuthTokenPayload<TAuthInfo>) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return signJwt(jwtSecret, payload);
|
|
226
|
+
if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
227
|
+
return signJwt(this._jwtSecret, payload);
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>> {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return verifyJwt(jwtSecret, token);
|
|
231
|
+
if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
232
|
+
return verifyJwt(this._jwtSecret, token);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
private _registerGracefulShutdown() {
|
|
236
|
+
if (this._shutdownRegistered) return;
|
|
237
|
+
this._shutdownRegistered = true;
|
|
238
|
+
|
|
231
239
|
const shutdownHandler = async (signal: string) => {
|
|
232
|
-
logger.info(`${signal}
|
|
240
|
+
logger.info(`${signal} 시그널 수신됨. 서버 종료를 시작합니다...`);
|
|
233
241
|
|
|
234
242
|
const forceExitTimer = setTimeout(() => {
|
|
235
|
-
logger.error("
|
|
243
|
+
logger.error("서버 종료 시간 초과 (10초). 강제 종료합니다.");
|
|
236
244
|
process.exit(1);
|
|
237
245
|
}, 10000);
|
|
238
246
|
|
|
@@ -240,11 +248,11 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
240
248
|
if (this.isOpen) {
|
|
241
249
|
await this.close();
|
|
242
250
|
}
|
|
243
|
-
logger.info("
|
|
251
|
+
logger.info("서버가 정상적으로 종료되었습니다.");
|
|
244
252
|
clearTimeout(forceExitTimer);
|
|
245
253
|
process.exit(0);
|
|
246
254
|
} catch (err) {
|
|
247
|
-
logger.error("
|
|
255
|
+
logger.error("서버 종료 중 에러 발생", err);
|
|
248
256
|
process.exit(1);
|
|
249
257
|
}
|
|
250
258
|
};
|
|
@@ -12,7 +12,7 @@ export const AutoUpdateService = defineService("AutoUpdate", (ctx) => ({
|
|
|
12
12
|
| undefined
|
|
13
13
|
> {
|
|
14
14
|
const clientPath = ctx.clientPath;
|
|
15
|
-
if (clientPath == null) throw new Error("
|
|
15
|
+
if (clientPath == null) throw new Error("클라이언트 경로를 찾을 수 없습니다.");
|
|
16
16
|
|
|
17
17
|
if (!(await fsx.exists(path.resolve(clientPath, platform, "updates")))) return undefined;
|
|
18
18
|
|
|
@@ -15,7 +15,7 @@ import consola from "consola";
|
|
|
15
15
|
|
|
16
16
|
const logger = consola.withTag("service-server:OrmService");
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// 정적 상태는 팩토리 외부에 존재해야 함 (호출 간 공유)
|
|
19
19
|
const socketConns = new WeakMap<ServiceSocket, Map<number, DbConn>>();
|
|
20
20
|
|
|
21
21
|
export const OrmService = defineService(
|
|
@@ -24,7 +24,7 @@ export const OrmService = defineService(
|
|
|
24
24
|
const sock = (): ServiceSocket => {
|
|
25
25
|
const socket = ctx.socket;
|
|
26
26
|
if (socket == null) {
|
|
27
|
-
throw new Error("WebSocket
|
|
27
|
+
throw new Error("WebSocket 연결이 필요합니다. ORM 서비스는 HTTP로 사용할 수 없습니다.");
|
|
28
28
|
}
|
|
29
29
|
return socket;
|
|
30
30
|
};
|
|
@@ -34,7 +34,7 @@ export const OrmService = defineService(
|
|
|
34
34
|
opt.configName
|
|
35
35
|
];
|
|
36
36
|
if (config == null) {
|
|
37
|
-
throw new Error(`ORM
|
|
37
|
+
throw new Error(`ORM 설정을 찾을 수 없습니다: ${opt.configName}`);
|
|
38
38
|
}
|
|
39
39
|
return { ...config, ...opt.config } as DbConnConfig;
|
|
40
40
|
};
|
|
@@ -43,7 +43,7 @@ export const OrmService = defineService(
|
|
|
43
43
|
const myConns = socketConns.get(sock());
|
|
44
44
|
const conn = myConns?.get(connId);
|
|
45
45
|
if (conn == null) {
|
|
46
|
-
throw new Error("
|
|
46
|
+
throw new Error("데이터베이스에 연결되지 않았습니다. (유효하지 않은 연결 ID)");
|
|
47
47
|
}
|
|
48
48
|
return conn;
|
|
49
49
|
};
|
|
@@ -71,7 +71,7 @@ export const OrmService = defineService(
|
|
|
71
71
|
sock().on("close", async () => {
|
|
72
72
|
if (myConns == null) return;
|
|
73
73
|
|
|
74
|
-
logger.debug("
|
|
74
|
+
logger.debug("소켓 종료 감지: 열린 모든 DB 연결을 정리합니다.");
|
|
75
75
|
const conns = Array.from(myConns.values());
|
|
76
76
|
|
|
77
77
|
await Promise.all(
|
|
@@ -81,7 +81,7 @@ export const OrmService = defineService(
|
|
|
81
81
|
await conn.close();
|
|
82
82
|
}
|
|
83
83
|
} catch (err) {
|
|
84
|
-
logger.warn("
|
|
84
|
+
logger.warn("강제 DB 연결 종료 중 에러 무시됨", err);
|
|
85
85
|
}
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
@@ -110,7 +110,7 @@ export const OrmService = defineService(
|
|
|
110
110
|
const conn = getConn(connId);
|
|
111
111
|
await conn.close();
|
|
112
112
|
} catch (err) {
|
|
113
|
-
logger.warn("
|
|
113
|
+
logger.warn("DB 연결 종료 중 에러 무시됨", err);
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
|
|
@@ -16,49 +16,55 @@ export async function handleHttpRequest<TAuthInfo = unknown>(
|
|
|
16
16
|
): Promise<void> {
|
|
17
17
|
const { service, method } = req.params as { service: string; method: string };
|
|
18
18
|
|
|
19
|
-
// ClientName
|
|
19
|
+
// ClientName 헤더
|
|
20
20
|
const clientName = req.headers["x-sd-client-name"] as string | undefined;
|
|
21
|
-
if (clientName == null) throw new Error("ClientName
|
|
21
|
+
if (clientName == null) throw new Error("ClientName 헤더가 필요합니다");
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Authorization 헤더 파싱 및 검증
|
|
24
24
|
let authTokenPayload: AuthTokenPayload<TAuthInfo> | undefined;
|
|
25
25
|
try {
|
|
26
26
|
const authHeader = req.headers.authorization;
|
|
27
27
|
if (authHeader != null) {
|
|
28
|
-
if (jwtSecret == null) throw new Error("JWT Secret
|
|
28
|
+
if (jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
29
29
|
|
|
30
30
|
const token = authHeader.split(" ")[1]; // "Bearer <token>"
|
|
31
31
|
authTokenPayload = await verifyJwt<TAuthInfo>(jwtSecret, token);
|
|
32
32
|
}
|
|
33
33
|
} catch (err) {
|
|
34
34
|
reply.status(401).send({
|
|
35
|
-
error: "
|
|
35
|
+
error: "인증 실패",
|
|
36
36
|
message: err instanceof Error ? err.message : String(err),
|
|
37
37
|
});
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// 매개변수 파싱
|
|
42
42
|
let params: unknown[] | undefined;
|
|
43
43
|
if (req.method === "GET") {
|
|
44
44
|
const query = req.query as { json?: string };
|
|
45
45
|
if (typeof query.json !== "string") {
|
|
46
|
-
throw new Error("JSON
|
|
46
|
+
throw new Error("JSON 쿼리 파라미터가 필요합니다");
|
|
47
47
|
}
|
|
48
48
|
params = json.parse(query.json);
|
|
49
49
|
} else if (req.method === "POST") {
|
|
50
50
|
if (!Array.isArray(req.body)) {
|
|
51
51
|
reply.status(400).send({
|
|
52
|
-
error: "
|
|
53
|
-
message: "
|
|
52
|
+
error: "잘못된 요청",
|
|
53
|
+
message: "요청 본문은 배열이어야 합니다.",
|
|
54
54
|
});
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
params = req.body as unknown[];
|
|
59
|
+
} else {
|
|
60
|
+
reply.status(405).send({
|
|
61
|
+
error: "Method Not Allowed",
|
|
62
|
+
message: `${req.method} 메서드는 지원하지 않습니다.`,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
//
|
|
67
|
+
// 서비스 실행 및 응답 전송
|
|
62
68
|
if (params != null) {
|
|
63
69
|
const serviceResult = await runMethod({
|
|
64
70
|
serviceName: service,
|
|
@@ -14,12 +14,12 @@ export async function handleStaticFile(
|
|
|
14
14
|
let targetFilePath = path.resolve(rootPath, "www", urlPath);
|
|
15
15
|
const allowedRootPath = path.resolve(rootPath, "www");
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// targetPath 보안 가드 (경로 탐색 공격 방지)
|
|
18
18
|
if (targetFilePath !== allowedRootPath && !pathx.isChildPath(targetFilePath, allowedRootPath)) {
|
|
19
|
-
throw new Error("
|
|
19
|
+
throw new Error("접근이 거부되었습니다");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// 디렉토리에 대해 슬래시를 추가하여 리다이렉트 (표준 웹 서버 동작)
|
|
23
23
|
if ((await fsx.exists(targetFilePath)) && (await fsx.stat(targetFilePath)).isDirectory()) {
|
|
24
24
|
if (!urlPath.endsWith("/")) {
|
|
25
25
|
const urlObj = new URL(req.raw.url!, "http://localhost");
|
|
@@ -29,15 +29,15 @@ export async function handleStaticFile(
|
|
|
29
29
|
targetFilePath = path.resolve(targetFilePath, "index.html");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// 권한 확인 (숨김 파일 등)
|
|
33
33
|
if (path.basename(targetFilePath).startsWith(".")) {
|
|
34
|
-
const errorMessage = "
|
|
34
|
+
const errorMessage = "이 파일에 접근할 권한이 없습니다.";
|
|
35
35
|
responseErrorHtml(reply, 403, errorMessage);
|
|
36
36
|
logger.warn(`[403] ${errorMessage} (${targetFilePath})`);
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// 파일 전송
|
|
41
41
|
const filename = path.basename(targetFilePath);
|
|
42
42
|
const directory = path.dirname(targetFilePath);
|
|
43
43
|
|
|
@@ -46,11 +46,11 @@ export async function handleStaticFile(
|
|
|
46
46
|
} catch (err: unknown) {
|
|
47
47
|
const error = err as { code?: string };
|
|
48
48
|
if (error.code === "ENOENT") {
|
|
49
|
-
const errorMessage = "
|
|
49
|
+
const errorMessage = "파일을 찾을 수 없습니다.";
|
|
50
50
|
responseErrorHtml(reply, 404, errorMessage);
|
|
51
51
|
logger.warn(`[404] ${errorMessage} (${targetFilePath})`);
|
|
52
52
|
} else {
|
|
53
|
-
const errorMessage = "
|
|
53
|
+
const errorMessage = "파일 전송 중 에러가 발생했습니다.";
|
|
54
54
|
responseErrorHtml(reply, 500, errorMessage);
|
|
55
55
|
logger.error(`[500] ${errorMessage}`, err);
|
|
56
56
|
}
|
|
@@ -17,24 +17,24 @@ export async function handleUpload(
|
|
|
17
17
|
jwtSecret: string | undefined,
|
|
18
18
|
): Promise<void> {
|
|
19
19
|
if (!req.isMultipart()) {
|
|
20
|
-
reply.status(400).send("Multipart
|
|
20
|
+
reply.status(400).send("Multipart 요청이 필요합니다");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// 인증 확인
|
|
25
25
|
try {
|
|
26
26
|
const authHeader = req.headers.authorization;
|
|
27
27
|
if (authHeader == null) {
|
|
28
|
-
throw new Error("
|
|
28
|
+
throw new Error("인증 토큰이 누락되었습니다.");
|
|
29
29
|
}
|
|
30
30
|
if (jwtSecret == null) {
|
|
31
|
-
throw new Error("JWT Secret
|
|
31
|
+
throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
32
32
|
}
|
|
33
33
|
const token = authHeader.split(" ")[1];
|
|
34
34
|
await verifyJwt(jwtSecret, token);
|
|
35
35
|
} catch (err) {
|
|
36
36
|
reply.status(401).send({
|
|
37
|
-
error: "
|
|
37
|
+
error: "인증 실패",
|
|
38
38
|
message: err instanceof Error ? err.message : String(err),
|
|
39
39
|
});
|
|
40
40
|
return;
|
|
@@ -58,7 +58,7 @@ export async function handleUpload(
|
|
|
58
58
|
await pipeline(part.file, createWriteStream(currentSavePath));
|
|
59
59
|
|
|
60
60
|
if (part.file.truncated) {
|
|
61
|
-
throw new Error(
|
|
61
|
+
throw new Error(`파일 크기 제한 초과: ${originalFilename}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const stats = await fsx.stat(currentSavePath);
|
|
@@ -75,13 +75,20 @@ export async function handleUpload(
|
|
|
75
75
|
|
|
76
76
|
reply.send(result);
|
|
77
77
|
} catch (err) {
|
|
78
|
-
logger.error("
|
|
78
|
+
logger.error("업로드 에러", err);
|
|
79
79
|
|
|
80
80
|
if (currentSavePath != null) {
|
|
81
81
|
await fsx.rm(currentSavePath).catch(() => {});
|
|
82
|
-
logger.warn(
|
|
82
|
+
logger.warn(`불완전한 파일 삭제됨: ${currentSavePath}`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// 이미 저장된 파일 정리
|
|
86
|
+
for (const savedFile of result) {
|
|
87
|
+
const savedPath = path.resolve(rootPath, "www", savedFile.path);
|
|
88
|
+
await fsx.rm(savedPath).catch(() => {});
|
|
89
|
+
logger.warn(`이미 저장된 파일 삭제됨: ${savedPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
reply.code(500).send("업로드 실패");
|
|
86
93
|
}
|
|
87
94
|
}
|