mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
311 lines
9.6 KiB
TypeScript
311 lines
9.6 KiB
TypeScript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
|
|
import { Request, Response } from 'express'
|
|
import { inject, injectable } from 'inversify'
|
|
import { Logger } from 'winston'
|
|
|
|
import TYPES from '../../Bootstrap/Types'
|
|
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
|
|
import { HttpServiceInterface } from './HttpServiceInterface'
|
|
|
|
@injectable()
|
|
export class HttpService implements HttpServiceInterface {
|
|
constructor(
|
|
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
|
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
|
@inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
|
|
@inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
|
|
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
|
|
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
|
|
@inject(TYPES.REVISIONS_SERVER_URL) private revisionsServerUrl: string,
|
|
@inject(TYPES.EMAIL_SERVER_URL) private emailServerUrl: string,
|
|
@inject(TYPES.PROXY_SERVER_URL) private proxyServerUrl: string,
|
|
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
|
|
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
|
@inject(TYPES.Logger) private logger: Logger,
|
|
) {}
|
|
|
|
async callSyncingServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
await this.callServer(this.syncingServerJsUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callRevisionsServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
if (!this.revisionsServerUrl) {
|
|
response.status(400).send({ message: 'Revisions Server not configured' })
|
|
|
|
return
|
|
}
|
|
await this.callServer(this.revisionsServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callLegacySyncingServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
await this.callServerWithLegacyFormat(this.syncingServerJsUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callAuthServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
await this.callServer(this.authServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callEmailServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
if (!this.emailServerUrl) {
|
|
response.status(400).send({ message: 'Email Server not configured' })
|
|
|
|
return
|
|
}
|
|
|
|
await this.callServer(this.emailServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callWebSocketServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
if (!this.webSocketServerUrl) {
|
|
this.logger.debug('Websockets Server URL not defined. Skipped request to WebSockets API.')
|
|
|
|
return
|
|
}
|
|
|
|
await this.callServer(this.webSocketServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callProxyServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
if (!this.proxyServerUrl) {
|
|
this.logger.debug('Proxy Server URL not defined. Skipped request to Proxy.')
|
|
|
|
return
|
|
}
|
|
|
|
await this.callServer(this.proxyServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callPaymentsServer(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
if (!this.paymentsServerUrl) {
|
|
this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
|
|
|
|
return
|
|
}
|
|
await this.callServerWithLegacyFormat(this.paymentsServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
async callAuthServerWithLegacyFormat(
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
await this.callServerWithLegacyFormat(this.authServerUrl, request, response, endpoint, payload)
|
|
}
|
|
|
|
private async getServerResponse(
|
|
serverUrl: string,
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<AxiosResponse | undefined> {
|
|
try {
|
|
const headers: Record<string, string> = {}
|
|
for (const headerName of Object.keys(request.headers)) {
|
|
headers[headerName] = request.headers[headerName] as string
|
|
}
|
|
|
|
delete headers.host
|
|
delete headers['content-length']
|
|
|
|
if (response.locals.authToken) {
|
|
headers['X-Auth-Token'] = response.locals.authToken
|
|
}
|
|
|
|
if (response.locals.offlineAuthToken) {
|
|
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
|
|
}
|
|
|
|
this.logger.debug(`Calling [${request.method}] ${serverUrl}/${endpoint},
|
|
headers: ${JSON.stringify(headers)},
|
|
query: ${JSON.stringify(request.query)},
|
|
payload: ${JSON.stringify(payload)}`)
|
|
|
|
const serviceResponse = await this.httpClient.request({
|
|
method: request.method as Method,
|
|
headers,
|
|
url: `${serverUrl}/${endpoint}`,
|
|
data: this.getRequestData(payload),
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity,
|
|
params: request.query,
|
|
timeout: this.httpCallTimeout,
|
|
validateStatus: (status: number) => {
|
|
return status >= 200 && status < 500
|
|
},
|
|
})
|
|
|
|
if (serviceResponse.headers['x-invalidate-cache']) {
|
|
const userUuid = serviceResponse.headers['x-invalidate-cache']
|
|
await this.crossServiceTokenCache.invalidate(userUuid)
|
|
}
|
|
|
|
return serviceResponse
|
|
} catch (error) {
|
|
const errorMessage = (error as AxiosError).isAxiosError
|
|
? JSON.stringify((error as AxiosError).response?.data)
|
|
: (error as Error).message
|
|
|
|
this.logger.error(`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${errorMessage}`)
|
|
|
|
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
|
|
|
if ((error as AxiosError).response?.headers['content-type']) {
|
|
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
|
}
|
|
|
|
const errorCode =
|
|
(error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
|
|
? +((error as AxiosError).code as string)
|
|
: 500
|
|
|
|
response.status(errorCode).send(errorMessage)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
private async callServer(
|
|
serverUrl: string,
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
|
|
|
this.logger.debug(`Response from underlying server: ${JSON.stringify(serviceResponse?.data)},
|
|
headers: ${JSON.stringify(serviceResponse?.headers)}`)
|
|
|
|
if (!serviceResponse) {
|
|
return
|
|
}
|
|
|
|
this.applyResponseHeaders(serviceResponse, response)
|
|
|
|
if (this.responseShouldNotBeDecorated(serviceResponse)) {
|
|
response.status(serviceResponse.status).send(serviceResponse.data)
|
|
|
|
return
|
|
}
|
|
|
|
response.status(serviceResponse.status).send({
|
|
meta: {
|
|
auth: {
|
|
userUuid: response.locals.userUuid,
|
|
roles: response.locals.roles,
|
|
},
|
|
server: {
|
|
filesServerUrl: this.filesServerUrl,
|
|
},
|
|
},
|
|
data: serviceResponse.data,
|
|
})
|
|
}
|
|
|
|
private async callServerWithLegacyFormat(
|
|
serverUrl: string,
|
|
request: Request,
|
|
response: Response,
|
|
endpoint: string,
|
|
payload?: Record<string, unknown> | string,
|
|
): Promise<void> {
|
|
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
|
|
|
if (!serviceResponse) {
|
|
return
|
|
}
|
|
|
|
this.applyResponseHeaders(serviceResponse, response)
|
|
|
|
if (serviceResponse.request._redirectable._redirectCount > 0) {
|
|
response.status(302).redirect(serviceResponse.request.res.responseUrl)
|
|
} else {
|
|
response.status(serviceResponse.status).send(serviceResponse.data)
|
|
}
|
|
}
|
|
|
|
private getRequestData(
|
|
payload: Record<string, unknown> | string | undefined,
|
|
): Record<string, unknown> | string | undefined {
|
|
if (
|
|
payload === '' ||
|
|
payload === null ||
|
|
payload === undefined ||
|
|
(typeof payload === 'object' && Object.keys(payload).length === 0)
|
|
) {
|
|
return undefined
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
private responseShouldNotBeDecorated(serviceResponse: AxiosResponse): boolean {
|
|
return (
|
|
serviceResponse.headers['content-type'] !== undefined &&
|
|
serviceResponse.headers['content-type'].toLowerCase().includes('text/html')
|
|
)
|
|
}
|
|
|
|
private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void {
|
|
const returnedHeadersFromUnderlyingService = [
|
|
'access-control-allow-methods',
|
|
'access-control-allow-origin',
|
|
'access-control-expose-headers',
|
|
'authorization',
|
|
'content-type',
|
|
'x-ssjs-version',
|
|
'x-auth-version',
|
|
]
|
|
|
|
returnedHeadersFromUnderlyingService.map((headerName) => {
|
|
const headerValue = serviceResponse.headers[headerName]
|
|
if (headerValue) {
|
|
response.setHeader(headerName, headerValue)
|
|
}
|
|
})
|
|
}
|
|
}
|