mirror of https://github.com/mx-space/core
fix: nestjs middleware bug, use interceptor
This commit is contained in:
parent
c4cf9b3eb0
commit
57f04aa669
|
@ -13,10 +13,10 @@
|
|||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
"material-icon-theme.activeIconPack": "nest",
|
||||
"cSpell.words": [
|
||||
"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)
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
Logger,
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common'
|
||||
import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
|
||||
import { GraphQLModule } from '@nestjs/graphql'
|
||||
|
@ -14,15 +8,13 @@ import { AppController } from './app.controller'
|
|||
import { AppResolver } from './app.resolver'
|
||||
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
|
||||
import { RolesGuard } from './common/guard/roles.guard'
|
||||
import { AnalyzeInterceptor } from './common/interceptors/analyze.interceptor'
|
||||
import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
|
||||
import { CountingInterceptor } from './common/interceptors/counting.interceptor'
|
||||
import {
|
||||
JSONSerializeInterceptor,
|
||||
ResponseInterceptor,
|
||||
} 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 {
|
||||
ASSET_DIR,
|
||||
DATA_DIR,
|
||||
|
@ -130,6 +122,10 @@ mkdirs()
|
|||
provide: APP_INTERCEPTOR,
|
||||
useClass: HttpCacheInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AnalyzeInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: CountingInterceptor,
|
||||
|
@ -142,6 +138,7 @@ mkdirs()
|
|||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AllExceptionsFilter,
|
||||
|
@ -154,10 +151,11 @@ mkdirs()
|
|||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(AnalyzeMiddleware)
|
||||
.forRoutes({ path: '(.*?)', method: RequestMethod.GET })
|
||||
.apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
||||
.forRoutes({ path: '(.*?)', method: RequestMethod.ALL })
|
||||
// FIXME: nestjs 8 middleware bug
|
||||
// consumer
|
||||
// .apply(AnalyzeMiddleware)
|
||||
// .forRoutes({ path: '(.*?)', method: RequestMethod.GET })
|
||||
// .apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
||||
// .forRoutes({ path: '(.*?)', method: RequestMethod.ALL })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,31 @@ app.register(FastifyMultipart, {
|
|||
})
|
||||
|
||||
app.getInstance().addHook('onRequest', (request, reply, done) => {
|
||||
// set undefined origin
|
||||
const origin = request.headers.origin
|
||||
if (!origin) {
|
||||
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()
|
||||
})
|
||||
|
||||
|
|
|
@ -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 { tap } from 'rxjs/operators'
|
||||
import { HTTP_REQUEST_TIME } from '~/constants/meta.constant'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
private logger: Logger
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger(LoggingInterceptor.name)
|
||||
}
|
||||
|
@ -32,9 +30,6 @@ export class LoggingInterceptor implements NestInterceptor {
|
|||
next: CallHandler<any>,
|
||||
): Observable<any> {
|
||||
const call$ = next.handle()
|
||||
if (!isDev) {
|
||||
return call$
|
||||
}
|
||||
const request = this.getRequest(context)
|
||||
const content = request.method + ' -> ' + request.url
|
||||
Logger.debug('+++ 收到请求:' + content, LoggingInterceptor.name)
|
||||
|
|
|
@ -40,7 +40,10 @@ async function bootstrap() {
|
|||
app.setGlobalPrefix(isDev ? '' : `api/v${API_VERSION}`, {
|
||||
exclude: [{ path: '/qaqdmin', method: RequestMethod.GET }],
|
||||
})
|
||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
||||
if (isDev) {
|
||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
||||
}
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
|
|
|
@ -48,4 +48,25 @@ describe('AppController (e2e)', () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue