Skip to content

Commit

Permalink
Merge pull request #125 from boostcampwm-2024/dev-back
Browse files Browse the repository at this point in the history
Dev back merge main
  • Loading branch information
sjy2335 authored Nov 20, 2024
2 parents 63515d7 + d2e8b1b commit 9c5795b
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 46 deletions.
2 changes: 1 addition & 1 deletion backend/name-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "src/index.ts",
"scripts": {
"dev": "tsx watch src/index.ts",
"start:dev": "NODE_ENV=development tsx watch src/index.ts",
"start": "tsx src/index.ts",
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"tmp": "tsx src/index.ts",
Expand Down
12 changes: 6 additions & 6 deletions backend/name-server/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { config } from 'dotenv';
import type { ServerConfig } from './common/utils/validator/configuration.validator';
import { ConfigurationValidator } from './common/utils/validator/configuration.validator';
import { Server } from './server/server';
import { db } from './database/mysql/mysql-database';
import { logger } from './common/utils/logger/console.logger';
import { ProjectQuery } from './database/query/project.query';
import type { ServerConfig } from 'common/utils/validator/configuration.validator';
import { ConfigurationValidator } from 'common/utils/validator/configuration.validator';
import { Server } from 'server/server';
import { db } from 'database/mysql/mysql-database';
import { logger } from 'common/utils/logger/console.logger';
import { ProjectQuery } from 'database/query/project.query';
import { DAURecorder } from 'database/query/dau-recorder';

config();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { ClickHouseClientConfigOptions } from '@clickhouse/client';

export const clickhouseConfig: ClickHouseClientConfigOptions = {
url: process.env.CLICKHOUSE_URL || 'http://localhost:8123',
username: process.env.CLICKHOUSE_USERNAME || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: process.env.CLICKHOUSE_DATABASE,
};
export const clickhouseConfig: ClickHouseClientConfigOptions =
process.env.NODE_ENV === 'development'
? {
url: process.env.DEV_CLICKHOUSE_URL,
username: process.env.DEV_CLICKHOUSE_USERNAME,
password: process.env.DEV_CLICKHOUSE_PASSWORD,
database: process.env.DEV_CLICKHOUSE_DATABASE,
}
: {
url: process.env.CLICKHOUSE_URL || 'http://localhost:8123',
username: process.env.CLICKHOUSE_USERNAME || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: process.env.CLICKHOUSE_DATABASE,
};
23 changes: 16 additions & 7 deletions backend/name-server/src/database/mysql/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import type { PoolOptions } from 'mysql2/promise';

dotenv.config();

export const poolConfig: PoolOptions = {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
};
export const poolConfig: PoolOptions =
process.env.NODE_ENV === 'development'
? {
host: process.env.DEV_DB_HOST,
port: Number(process.env.DEV_DB_PORT),
user: process.env.DEV_DB_USERNAME,
password: process.env.DEV_DB_PASSWORD,
database: process.env.DEV_DB_NAME,
}
: {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
};
3 changes: 2 additions & 1 deletion backend/name-server/src/database/query/dau-recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export interface DAURecorderInterface {

export class DAURecorder implements DAURecorderInterface {
private clickhouseClient = ClickhouseDatabase.getInstance();

public async recordAccess(domain: string): Promise<void> {
const date = new Date().toISOString().slice(0, 10);
const values = [{ domain, date, access: 1 }];
const values = [{ domain: domain.toLowerCase(), date, access: 1 }];
try {
await this.clickhouseClient.insert({
table: 'dau',
Expand Down
4 changes: 2 additions & 2 deletions backend/name-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Application } from './app';
import { logger } from './common/utils/logger/console.logger';
import { Application } from 'app';
import { logger } from 'common/utils/logger/console.logger';

async function main(): Promise<void> {
const initializer = new Application();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const DNSFlags = {
export const DNS_FLAGS = {
AUTHORITATIVE_ANSWER: 0x0400, // 권한 있는 응답 (네임서버가 해당 도메인의 공식 서버일 때)
TRUNCATED_RESPONSE: 0x0200, // 응답이 잘린 경우 (UDP 크기 제한 초과)
RECURSION_DESIRED: 0x0100, // 재귀적 쿼리 요청 (클라이언트가 설정)
Expand All @@ -7,10 +7,10 @@ export const DNSFlags = {
CHECKING_DISABLED: 0x0010, // DNSSEC 검증 비활성화
} as const;

export const ResponseCode = {
export const RESPONSE_CODE = {
NOERROR: 0, // 정상 응답
NXDOMAIN: 3, // 도메인이 존재하지 않음
SERVFAIL: 2, // 서버 에러
} as const;

export type ResponseCodeType = (typeof ResponseCode)[keyof typeof ResponseCode];
export type ResponseCodeType = (typeof RESPONSE_CODE)[keyof typeof RESPONSE_CODE];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const MESSAGE_TYPE = {
DNS: 'DNS',
HEALTH_CHECK: 'HEALTH_CHECK',
} as const;

export const MIN_DNS_MESSAGE_LENGTH = 12;

export type MessageType = (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE];
32 changes: 26 additions & 6 deletions backend/name-server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { Socket } from 'dgram';
import { createSocket, type RemoteInfo } from 'dgram';
import { decode, encode } from 'dns-packet';
import type { DecodedPacket, Question } from 'dns-packet';
import type { ServerConfig } from '../common/utils/validator/configuration.validator';
import type { ServerConfig } from 'common/utils/validator/configuration.validator';
import { PacketValidator } from './utils/packet.validator';
import { DNSResponseBuilder } from './utils/dns-response-builder';
import { ResponseCode } from './constant/dns-packet.constant';
import { logger } from '../common/utils/logger/console.logger';
import { RESPONSE_CODE } from './constant/dns-packet.constant';
import { logger } from 'common/utils/logger/console.logger';
import { ServerError } from './error/server.error';
import type { ProjectQueryInterface } from '../database/query/project.query.interface';
import type { ProjectQueryInterface } from 'database/query/project.query.interface';
import type { DAURecorderInterface } from 'database/query/dau-recorder';
import { MESSAGE_TYPE } from 'server/constant/message-type.constants';

export class Server {
private server: Socket;
Expand All @@ -31,16 +32,25 @@ export class Server {

private async handleMessage(msg: Buffer, remoteInfo: RemoteInfo): Promise<void> {
try {
const messageType = PacketValidator.validateMessageType(msg);

if (messageType === MESSAGE_TYPE.HEALTH_CHECK) {
await this.handleHealthCheck(remoteInfo);
return;
}

const query = decode(msg);
const question = this.parseQuery(query);

logger.logQuery(question.name, remoteInfo);

await this.validateRequest(question.name);
this.dauRecorder.recordAccess(question.name).catch((err) => {
logger.error(`DAU recording failed for ${question.name}: ${err.message}`);
});

const response = new DNSResponseBuilder(this.config, query)
.addAnswer(ResponseCode.NOERROR, question)
.addAnswer(RESPONSE_CODE.NOERROR, question)
.build();
const responseMsg = encode(response);

Expand All @@ -50,6 +60,16 @@ export class Server {
}
}

private async handleHealthCheck(remoteInfo: RemoteInfo): Promise<void> {
try {
const healthCheckResponse = Buffer.from([]);

await this.sendResponse(healthCheckResponse, remoteInfo);
} catch (error) {
logger.error(`Health check response failed: ${(error as Error).message}`);
}
}

private async validateRequest(name: string): Promise<void> {
if (await this.projectQuery.existsByDomain(name)) {
return;
Expand Down Expand Up @@ -89,7 +109,7 @@ export class Server {

const errorMessage = `Failed to process DNS query from ${remoteInfo.address}:${remoteInfo.port}`;
const response = new DNSResponseBuilder(this.config, query)
.addAnswer(ResponseCode.NXDOMAIN)
.addAnswer(RESPONSE_CODE.NXDOMAIN)
.build();

const responseMsg = encode(response);
Expand Down
14 changes: 7 additions & 7 deletions backend/name-server/src/server/utils/dns-response-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Packet, Question, RecordClass } from 'dns-packet';
import type { ServerConfig } from '../../common/utils/validator/configuration.validator';
import { PacketValidator } from './packet.validator';
import type { ResponseCodeType } from '../constant/dns-packet.constant';
import { DNSFlags, ResponseCode } from '../constant/dns-packet.constant';
import { DNS_FLAGS, RESPONSE_CODE } from '../constant/dns-packet.constant';

interface DNSResponse extends Packet {
answers: Array<{
Expand Down Expand Up @@ -30,10 +30,10 @@ export class DNSResponseBuilder {
}

private createFlags(query: Packet): number {
const flags = DNSFlags.AUTHORITATIVE_ANSWER;
const flags = DNS_FLAGS.AUTHORITATIVE_ANSWER;

if (PacketValidator.hasFlags(query) && query.flags & DNSFlags.RECURSION_DESIRED) {
return flags | DNSFlags.RECURSION_DESIRED;
if (PacketValidator.hasFlags(query) && query.flags & DNS_FLAGS.RECURSION_DESIRED) {
return flags | DNS_FLAGS.RECURSION_DESIRED;
}

return flags;
Expand All @@ -42,11 +42,11 @@ export class DNSResponseBuilder {
addAnswer(rcode: ResponseCodeType, question?: Question): this {
this.response.flags = 0x8000;

if (this.response.flags && rcode === ResponseCode.NXDOMAIN) {
this.response.flags |= ResponseCode.NXDOMAIN;
if (this.response.flags && rcode === RESPONSE_CODE.NXDOMAIN) {
this.response.flags |= RESPONSE_CODE.NXDOMAIN;
}

if (rcode === ResponseCode.NOERROR && question) {
if (rcode === RESPONSE_CODE.NOERROR && question) {
this.response.answers = [
{
name: question.name,
Expand Down
21 changes: 21 additions & 0 deletions backend/name-server/src/server/utils/packet.validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { Packet, Question } from 'dns-packet';
import {
MESSAGE_TYPE,
MessageType,
MIN_DNS_MESSAGE_LENGTH,
} from 'server/constant/message-type.constants';

type TypeGuardResult<T> = T extends Packet ? T & { questions: Question[] } : never;

Expand All @@ -14,4 +19,20 @@ export class PacketValidator {
static validatePacket(packet: Packet): boolean {
return this.hasQuestions(packet) && this.hasFlags(packet);
}

static validateMessageType(msg: Buffer): MessageType {
if (msg.length >= MIN_DNS_MESSAGE_LENGTH) {
const flags = msg.readUInt16BE(2);
const isQuery = (flags & 0x8000) === 0;

if (isQuery) {
const questionCount = msg.readUInt16BE(4);
if (questionCount > 0) {
return MESSAGE_TYPE.DNS;
}
}
}

return MESSAGE_TYPE.HEALTH_CHECK;
}
}
10 changes: 5 additions & 5 deletions backend/name-server/test/dns-response-builder.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DNSResponseBuilder } from '../src/server/utils/dns-response-builder';
import type { Packet } from 'dns-packet';
import { PacketValidator } from '../src/server/utils/packet.validator';
import { DNSFlags, ResponseCode } from '../src/server/constant/dns-packet.constant';
import { DNS_FLAGS, RESPONSE_CODE } from '../src/server/constant/dns-packet.constant';

describe('DNSResponseBuilder의', () => {
const mockConfig = {
Expand All @@ -12,7 +12,7 @@ describe('DNSResponseBuilder의', () => {
const mockQuery: Packet = {
type: 'query',
id: 1234,
flags: DNSFlags.RECURSION_DESIRED,
flags: DNS_FLAGS.RECURSION_DESIRED,
questions: [
{
name: 'example.com',
Expand All @@ -29,22 +29,22 @@ describe('DNSResponseBuilder의', () => {

expect(response.id).toBe(mockQuery.id);
expect(response.type).toBe('response');
expect(response.flags).toBe(DNSFlags.AUTHORITATIVE_ANSWER | DNSFlags.RECURSION_DESIRED);
expect(response.flags).toBe(DNS_FLAGS.AUTHORITATIVE_ANSWER | DNS_FLAGS.RECURSION_DESIRED);
});

test('addAnswer()는 올바른 정보를 담은 answer를 추가해야 합니다.', () => {
const builder = new DNSResponseBuilder(mockConfig, mockQuery);

if (!PacketValidator.hasQuestions(mockQuery)) return;
builder.addAnswer(ResponseCode.NOERROR, mockQuery.questions[0]);
builder.addAnswer(RESPONSE_CODE.NOERROR, mockQuery.questions[0]);
const response = builder.build();

expect(response.answers).toHaveLength(1);
expect(response.answers[0]).toEqual({
name: 'example.com',
type: 'A',
class: 'IN',
ttl: 300,
ttl: 86400,
data: '127.0.0.1',
});
});
Expand Down
5 changes: 3 additions & 2 deletions backend/name-server/test/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import dgram from 'dgram';
import * as dgram from 'dgram';
import { encode, decode, Packet } from 'dns-packet';
import { Server } from '../src/server/server';
import { ServerConfig } from '../src/common/utils/validator/configuration.validator';
import { NORMAL_PACKET, NOT_EXIST_DOMAIN_PACKET } from './constant/packet';
import { DAURecorder } from '../src/database/query/dau-recorder';
import { TestProjectQuery } from './database/test-project.query';

describe('DNS 서버는 ', () => {
Expand All @@ -21,7 +22,7 @@ describe('DNS 서버는 ', () => {
beforeAll(async () => {
client = dgram.createSocket('udp4');

server = new Server(config, new TestProjectQuery());
server = new Server(config, new DAURecorder(), new TestProjectQuery());
server.start();

await new Promise((resolve) => setTimeout(resolve, 100));
Expand Down

0 comments on commit 9c5795b

Please sign in to comment.