import fastify, {FastifyRequest, FastifyServerOptions} from 'fastify'; import {fastifyErrorHandler} from './packages/fastify-compliant-http-errors'; import fastifySendData from './packages/fastify-send-data'; import fastifyServiceSession from './packages/fastify-service-session'; import {Session, SessionService, SessionServiceImpl} from './modules/auth'; import {Uuid} from '@theoryofnekomata/uuid-buffer'; import fastifyHomeRoute from './packages/fastify-home-route'; import {User} from './modules/user'; import {IncomingMessage} from 'http'; import {createDeflate, createGunzip} from 'zlib'; import {PassThrough, Transform} from 'stream'; interface RequestLike extends Pick {} declare module 'fastify' { interface FastifyRequest { session?: Session & { user: User }, } } enum AvailableDecoder { GZIP = 'gzip', DEFLATE = 'deflate', } type DecoderFunction = () => Transform; const DECODERS: Record = { [AvailableDecoder.GZIP]:() => createGunzip(), [AvailableDecoder.DEFLATE]: () => createDeflate(), } const DEFAULT_DECODER: DecoderFunction = () => new PassThrough(); class PackRequestTransformStream extends Transform { private data: Record = { last: null, commit: null, ref: null, } constructor() { super(); const headerRegExp = /([0-9a-fA-F]+) ([0-9a-fA-F]+) (.+?)( |00|\u0000)|^(0000)$/gi; let bufferedData = Buffer.from(''); this.on('data', (chunk) => { const isHeaderRead = this.data.last && this.data.commit; if (!isHeaderRead) { bufferedData = Buffer.concat([bufferedData, chunk]); const bufferAsString = bufferedData.toString('utf-8'); const bufferHeaderMatch = bufferAsString.match(headerRegExp); if (bufferHeaderMatch) { const [ _header, last, commit, ref, ] = Array.from(bufferHeaderMatch); this.data = { last, commit, ref, } } } this.push(chunk); }) } } const receivePackRequestParser = ( request: FastifyRequest, payload: IncomingMessage, done: Function, ) => { console.log(request.headers); const encoding = request.headers['content-encoding']; let theDecoder: DecoderFunction = DEFAULT_DECODER; if (encoding) { const { [encoding as AvailableDecoder]: decoder } = DECODERS; if (!decoder) { done(new Error(`Unknown encoding: ${encoding}`)); return; } theDecoder = decoder; } const requestParserStream = new PackRequestTransformStream(); const pipeline = payload .pipe(theDecoder()) .pipe(requestParserStream) pipeline.on('end', () => { done(null, pipeline); }); }; export const createServer = (opts?: FastifyServerOptions) => { const server = fastify(opts); server.setErrorHandler(fastifyErrorHandler) server.register(fastifyHomeRoute); server.addContentTypeParser('application/x-git-receive-pack-request', receivePackRequestParser); server.addContentTypeParser('application/x-git-upload-pack-request', receivePackRequestParser); // TODO server.register(fastifyServiceSession, { sessionRequestKey: 'session', getSession: async (sessionId: string) => { const sessionService: SessionService = new SessionServiceImpl() return sessionService.get(Uuid.from(sessionId)) }, isSessionValid: async (sessionId: string) => { const sessionService: SessionService = new SessionServiceImpl() return sessionService.isValid(Uuid.from(sessionId)) }, extractSessionId: (request: RequestLike) => { const sessionIdKey = 'sessionId' const query = request.query as Record const querySessionId = query[sessionIdKey] if (typeof querySessionId === 'string') { return querySessionId } const { authorization } = request.headers if (typeof authorization === 'string') { const [authType, headerSessionId] = authorization.split(' ') if (authType === 'Bearer') { return headerSessionId } } const body = request.body as Record ?? {} const bodySessionId = body[sessionIdKey] if (typeof bodySessionId === 'string') { return bodySessionId } return undefined } }) server.register(fastifySendData); return server; }