fix: nestjs middleware bug, use interceptor

This commit is contained in:
Innei 2021-10-01 19:45:20 +08:00
parent c4cf9b3eb0
commit 57f04aa669
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 230 additions and 24 deletions

View File

@ -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"
]
],
}

View File

@ -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)

View File

@ -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 })
}
}

View File

@ -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()
})

View 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
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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)
})
})
})