fix: nestjs middleware bug, use interceptor
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -13,10 +13,10 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": true
|
"source.organizeImports": true,
|
||||||
},
|
},
|
||||||
"material-icon-theme.activeIconPack": "nest",
|
"material-icon-theme.activeIconPack": "nest",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"qaqdmin"
|
"qaqdmin"
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ docker-compose up -d
|
|||||||
- 拦截器流向
|
- 拦截器流向
|
||||||
|
|
||||||
```
|
```
|
||||||
ResponseInterceptor -> JSONSerializeInterceptor -> CountingInterceptor -> HttpCacheInterceptor
|
ResponseInterceptor -> JSONSerializeInterceptor -> CountingInterceptor -> AnalyzeInterceptor -> HttpCacheInterceptor
|
||||||
```
|
```
|
||||||
|
|
||||||
- [业务逻辑模块](https://github.com/mx-space/server-next/tree/master/src/modules)
|
- [业务逻辑模块](https://github.com/mx-space/server-next/tree/master/src/modules)
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
|
||||||
Logger,
|
|
||||||
MiddlewareConsumer,
|
|
||||||
Module,
|
|
||||||
NestModule,
|
|
||||||
RequestMethod,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { ConfigModule } from '@nestjs/config'
|
import { ConfigModule } from '@nestjs/config'
|
||||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
|
||||||
import { GraphQLModule } from '@nestjs/graphql'
|
import { GraphQLModule } from '@nestjs/graphql'
|
||||||
@@ -14,15 +8,13 @@ import { AppController } from './app.controller'
|
|||||||
import { AppResolver } from './app.resolver'
|
import { AppResolver } from './app.resolver'
|
||||||
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
|
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
|
||||||
import { RolesGuard } from './common/guard/roles.guard'
|
import { RolesGuard } from './common/guard/roles.guard'
|
||||||
|
import { AnalyzeInterceptor } from './common/interceptors/analyze.interceptor'
|
||||||
import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
|
import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
|
||||||
import { CountingInterceptor } from './common/interceptors/counting.interceptor'
|
import { CountingInterceptor } from './common/interceptors/counting.interceptor'
|
||||||
import {
|
import {
|
||||||
JSONSerializeInterceptor,
|
JSONSerializeInterceptor,
|
||||||
ResponseInterceptor,
|
ResponseInterceptor,
|
||||||
} from './common/interceptors/response.interceptors'
|
} from './common/interceptors/response.interceptors'
|
||||||
import { AnalyzeMiddleware } from './common/middlewares/analyze.middleware'
|
|
||||||
import { SkipBrowserDefaultRequestMiddleware } from './common/middlewares/favicon.middleware'
|
|
||||||
import { SecurityMiddleware } from './common/middlewares/security.middleware'
|
|
||||||
import {
|
import {
|
||||||
ASSET_DIR,
|
ASSET_DIR,
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
@@ -130,6 +122,10 @@ mkdirs()
|
|||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: HttpCacheInterceptor,
|
useClass: HttpCacheInterceptor,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: AnalyzeInterceptor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: CountingInterceptor,
|
useClass: CountingInterceptor,
|
||||||
@@ -142,6 +138,7 @@ mkdirs()
|
|||||||
provide: APP_INTERCEPTOR,
|
provide: APP_INTERCEPTOR,
|
||||||
useClass: ResponseInterceptor,
|
useClass: ResponseInterceptor,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
provide: APP_FILTER,
|
provide: APP_FILTER,
|
||||||
useClass: AllExceptionsFilter,
|
useClass: AllExceptionsFilter,
|
||||||
@@ -154,10 +151,11 @@ mkdirs()
|
|||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer
|
// FIXME: nestjs 8 middleware bug
|
||||||
.apply(AnalyzeMiddleware)
|
// consumer
|
||||||
.forRoutes({ path: '(.*?)', method: RequestMethod.GET })
|
// .apply(AnalyzeMiddleware)
|
||||||
.apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
// .forRoutes({ path: '(.*?)', method: RequestMethod.GET })
|
||||||
.forRoutes({ path: '(.*?)', method: RequestMethod.ALL })
|
// .apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
||||||
|
// .forRoutes({ path: '(.*?)', method: RequestMethod.ALL })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,31 @@ app.register(FastifyMultipart, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.getInstance().addHook('onRequest', (request, reply, done) => {
|
app.getInstance().addHook('onRequest', (request, reply, done) => {
|
||||||
|
// set undefined origin
|
||||||
const origin = request.headers.origin
|
const origin = request.headers.origin
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
request.headers.origin = request.headers.host
|
request.headers.origin = request.headers.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forbidden php
|
||||||
|
|
||||||
|
const url = request.url
|
||||||
|
|
||||||
|
if (url.endsWith('.php')) {
|
||||||
|
reply.raw.statusMessage =
|
||||||
|
'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'
|
||||||
|
|
||||||
|
return reply.code(418).send()
|
||||||
|
} else if (url.match(/\/(adminer|admin|wp-login)$/g)) {
|
||||||
|
reply.raw.statusMessage = 'Hey, What the fuck are you doing!'
|
||||||
|
return reply.code(200).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip favicon request
|
||||||
|
if (url.match(/favicon.ico$/) || url.match(/manifest.json$/)) {
|
||||||
|
return reply.code(204).send()
|
||||||
|
}
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
169
src/common/interceptors/analyze.interceptor.ts
Normal file
169
src/common/interceptors/analyze.interceptor.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Analyze interceptor.
|
||||||
|
* @file 数据分析拦截器
|
||||||
|
* @module interceptor/analyze
|
||||||
|
* @author Innei <https://github.com/Innei>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common'
|
||||||
|
import { ReturnModelType } from '@typegoose/typegoose'
|
||||||
|
import { FastifyRequest } from 'fastify'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { InjectModel } from 'nestjs-typegoose'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import UAParser from 'ua-parser-js'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import { RedisKeys } from '~/constants/cache.constant'
|
||||||
|
import { LOCAL_BOT_LIST_DATA_FILE_PATH } from '~/constants/path.constant'
|
||||||
|
import { AnalyzeModel } from '~/modules/analyze/analyze.model'
|
||||||
|
import { OptionModel } from '~/modules/configs/configs.model'
|
||||||
|
import { CacheService } from '~/processors/cache/cache.service'
|
||||||
|
import { CronService } from '~/processors/helper/helper.cron.service'
|
||||||
|
import { TaskQueueService } from '~/processors/helper/helper.tq.service'
|
||||||
|
import { getIp } from '~/utils/ip.util'
|
||||||
|
import { getRedisKey } from '~/utils/redis.util'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnalyzeInterceptor implements NestInterceptor {
|
||||||
|
private parser: UAParser
|
||||||
|
private botListData: RegExp[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectModel(AnalyzeModel)
|
||||||
|
private readonly model: ReturnModelType<typeof AnalyzeModel>,
|
||||||
|
@InjectModel(OptionModel)
|
||||||
|
private readonly options: ReturnModelType<typeof OptionModel>,
|
||||||
|
private readonly cronService: CronService,
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
|
private readonly taskService: TaskQueueService,
|
||||||
|
) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.parser = new UAParser()
|
||||||
|
this.botListData = this.getLocalBotList()
|
||||||
|
this.taskService.add(this.cronService.updateBotList.name, async () =>
|
||||||
|
this.cronService.updateBotList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalBotList() {
|
||||||
|
try {
|
||||||
|
return this.pickPattern2Regexp(
|
||||||
|
JSON.parse(
|
||||||
|
readFileSync(LOCAL_BOT_LIST_DATA_FILE_PATH, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pickPattern2Regexp(data: any): RegExp[] {
|
||||||
|
return data.map((item) => new RegExp(item.pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
async intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<any>,
|
||||||
|
): Promise<Observable<any>> {
|
||||||
|
const call$ = next.handle()
|
||||||
|
const request = this.getRequest(context)
|
||||||
|
if (!request) {
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
const method = request.routerMethod.toUpperCase()
|
||||||
|
if (method !== 'GET') {
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
const ip = getIp(request)
|
||||||
|
|
||||||
|
// if req from SSR server, like 127.0.0.1, skip
|
||||||
|
if (['127.0.0.1', 'localhost', '::-1'].includes(ip)) {
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
// if login
|
||||||
|
if (request.user) {
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user agent is in bot list, skip
|
||||||
|
if (this.botListData.some((rg) => rg.test(request.headers['user-agent']))) {
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = request.url
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.parser.setUA(request.headers['user-agent'])
|
||||||
|
|
||||||
|
const ua = this.parser.getResult()
|
||||||
|
|
||||||
|
await this.model.create({
|
||||||
|
ip,
|
||||||
|
ua,
|
||||||
|
path: new URL('http://a.com' + url).pathname,
|
||||||
|
})
|
||||||
|
const apiCallTimeRecord = await this.options.findOne({
|
||||||
|
name: 'apiCallTime',
|
||||||
|
})
|
||||||
|
if (!apiCallTimeRecord) {
|
||||||
|
await this.options.create({
|
||||||
|
name: 'apiCallTime',
|
||||||
|
value: 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.options.updateOne(
|
||||||
|
{ name: 'apiCallTime' },
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// ip access in redis
|
||||||
|
const client = this.cacheService.getClient()
|
||||||
|
|
||||||
|
const count = await client.sadd(getRedisKey(RedisKeys.Access, 'ips'), ip)
|
||||||
|
if (count) {
|
||||||
|
// record uv to db
|
||||||
|
process.nextTick(async () => {
|
||||||
|
const uvRecord = await this.options.findOne({ name: 'uv' })
|
||||||
|
if (uvRecord) {
|
||||||
|
await uvRecord.updateOne({
|
||||||
|
$inc: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.options.create({
|
||||||
|
name: 'uv',
|
||||||
|
value: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return call$
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequest(context: ExecutionContext) {
|
||||||
|
const req = context.switchToHttp().getRequest<KV>()
|
||||||
|
if (req) {
|
||||||
|
return req as FastifyRequest & { user?: any }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,12 +18,10 @@ import { GqlExecutionContext } from '@nestjs/graphql'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { tap } from 'rxjs/operators'
|
import { tap } from 'rxjs/operators'
|
||||||
import { HTTP_REQUEST_TIME } from '~/constants/meta.constant'
|
import { HTTP_REQUEST_TIME } from '~/constants/meta.constant'
|
||||||
import { isDev } from '~/utils/index.util'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggingInterceptor implements NestInterceptor {
|
export class LoggingInterceptor implements NestInterceptor {
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logger = new Logger(LoggingInterceptor.name)
|
this.logger = new Logger(LoggingInterceptor.name)
|
||||||
}
|
}
|
||||||
@@ -32,9 +30,6 @@ export class LoggingInterceptor implements NestInterceptor {
|
|||||||
next: CallHandler<any>,
|
next: CallHandler<any>,
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
const call$ = next.handle()
|
const call$ = next.handle()
|
||||||
if (!isDev) {
|
|
||||||
return call$
|
|
||||||
}
|
|
||||||
const request = this.getRequest(context)
|
const request = this.getRequest(context)
|
||||||
const content = request.method + ' -> ' + request.url
|
const content = request.method + ' -> ' + request.url
|
||||||
Logger.debug('+++ 收到请求:' + content, LoggingInterceptor.name)
|
Logger.debug('+++ 收到请求:' + content, LoggingInterceptor.name)
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ async function bootstrap() {
|
|||||||
app.setGlobalPrefix(isDev ? '' : `api/v${API_VERSION}`, {
|
app.setGlobalPrefix(isDev ? '' : `api/v${API_VERSION}`, {
|
||||||
exclude: [{ path: '/qaqdmin', method: RequestMethod.GET }],
|
exclude: [{ path: '/qaqdmin', method: RequestMethod.GET }],
|
||||||
})
|
})
|
||||||
|
if (isDev) {
|
||||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
app.useGlobalInterceptors(new LoggingInterceptor())
|
||||||
|
}
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
transform: true,
|
transform: true,
|
||||||
|
|||||||
@@ -48,4 +48,25 @@ describe('AppController (e2e)', () => {
|
|||||||
expect(res.payload).toBeDefined()
|
expect(res.payload).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('GET /admin', () => {
|
||||||
|
return app.inject({ url: '/admin' }).then((res) => {
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(res.payload).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /wp.php', () => {
|
||||||
|
return app.inject({ url: '/wp.php' }).then((res) => {
|
||||||
|
console.log(res.payload)
|
||||||
|
expect(res.statusCode).toBe(418)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /favicon.ico', () => {
|
||||||
|
return app.inject({ url: '/favicon.ico' }).then((res) => {
|
||||||
|
expect(res.payload).toBe('')
|
||||||
|
expect(res.statusCode).toBe(204)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user