diff --git a/docker-compose.yaml b/docker-compose.yaml index dfb8756a..ba0c3139 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -79,6 +79,7 @@ services: - AWS_ENDPOINT=${AWS_ENDPOINT:-} - AWS_S3_BUCKET=${AWS_S3_BUCKET:-} - AWS_S3_PREFIX=${AWS_S3_PREFIX:-} + - AR_IO_NODE_RELEASE=${AR_IO_NODE_RELEASE:-} depends_on: - redis diff --git a/src/config.ts b/src/config.ts index 77d1c8fa..42459271 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ import { isMainThread } from 'node:worker_threads'; import { createFilter } from './filters.js'; import * as env from './lib/env.js'; import log from './log.js'; +import { release } from './version.js'; dotenv.config(); @@ -154,6 +155,11 @@ export const IO_PROCESS_ID = env.varOrDefault( 'GaQrvEMKBpkjofgnBi_B3IgIDmY_XYelVLB6GcRGrHc', ); +export const AR_IO_NODE_RELEASE = env.varOrDefault( + 'AR_IO_NODE_RELEASE', + release, +); + // // Header caching // diff --git a/src/constants.ts b/src/constants.ts index 5f93c415..59946892 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ const headerNames = { hops: 'X-AR-IO-Hops', origin: 'X-AR-IO-Origin', + originNodeRelease: 'X-AR-IO-Origin-Node-Release', cache: 'X-Cache', arnsTtlSeconds: 'X-ArNS-TTL-Seconds', arnsResolvedId: 'X-ArNS-Resolved-Id', diff --git a/src/data/ar-io-data-source.test.ts b/src/data/ar-io-data-source.test.ts index 3c94da55..8f29897d 100644 --- a/src/data/ar-io-data-source.test.ts +++ b/src/data/ar-io-data-source.test.ts @@ -101,7 +101,11 @@ describe('ArIODataSource', () => { verified: false, sourceContentType: 'application/octet-stream', cached: false, - requestAttributes: { hops: 1, origin: undefined }, + requestAttributes: { + hops: 1, + origin: undefined, + originNodeRelease: undefined, + }, }); }); @@ -130,14 +134,18 @@ describe('ArIODataSource', () => { verified: false, sourceContentType: 'application/octet-stream', cached: false, - requestAttributes: { hops: 1, origin: undefined }, + requestAttributes: { + hops: 1, + origin: undefined, + originNodeRelease: undefined, + }, }); }); it('should increment hops and origin if requestAttributes are provided', async () => { const data = await dataSource.getData({ id: 'dataId', - requestAttributes: { origin: 'node-url', hops: 2 }, + requestAttributes: { hops: 2 }, }); assert.deepEqual(data, { @@ -146,7 +154,11 @@ describe('ArIODataSource', () => { verified: false, sourceContentType: 'application/octet-stream', cached: false, - requestAttributes: { hops: 3, origin: 'node-url' }, + requestAttributes: { + hops: 3, + origin: undefined, + originNodeRelease: undefined, + }, }); }); diff --git a/src/data/ar-io-data-source.ts b/src/data/ar-io-data-source.ts index 8f56f6af..da51a07c 100644 --- a/src/data/ar-io-data-source.ts +++ b/src/data/ar-io-data-source.ts @@ -25,7 +25,10 @@ import { ContiguousDataSource, RequestAttributes, } from '../types.js'; -import { headerNames } from '../constants.js'; +import { + generateRequestAttributes, + parseRequestAttributesHeaders, +} from '../lib/request-attributes.js'; const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; // 10 seconds const DEFAULT_UPDATE_PEERS_REFRESH_INTERVAL_MS = 3_600_000; // 1 hour @@ -169,27 +172,22 @@ export class ArIODataSource implements ContiguousDataSource { let selectedPeer = this.selectPeer(); - const requestOriginAndHopsHeaders: { [key: string]: string } = {}; - let hops = 1; - let origin: string | undefined; - - if (requestAttributes !== undefined) { - hops = requestAttributes.hops + 1; - requestOriginAndHopsHeaders[headerNames.hops] = hops.toString(); - if (requestAttributes.origin !== undefined) { - origin = requestAttributes.origin; - requestOriginAndHopsHeaders[headerNames.origin] = origin; - } - } + const requestAttributesHeaders = + generateRequestAttributes(requestAttributes); try { const response = await this.request({ peerAddress: selectedPeer, id, - headers: requestOriginAndHopsHeaders, + headers: requestAttributesHeaders?.headers || {}, }); - return this.parseResponse(response, hops, origin); + const parsedRequestAttributes = parseRequestAttributesHeaders({ + headers: response.headers as { [key: string]: string }, + currentHops: requestAttributesHeaders?.attributes.hops, + }); + + return this.parseResponse(response, parsedRequestAttributes); } catch (error: any) { log.error('Failed to fetch contiguous data from first random ArIO peer', { message: error.message, @@ -201,10 +199,15 @@ export class ArIODataSource implements ContiguousDataSource { const response = await this.request({ peerAddress: selectedPeer, id, - headers: requestOriginAndHopsHeaders, + headers: requestAttributesHeaders?.headers || {}, }); - return this.parseResponse(response, hops, origin); + const parsedRequestAttributes = parseRequestAttributesHeaders({ + headers: response.headers as { [key: string]: string }, + currentHops: requestAttributesHeaders?.attributes.hops, + }); + + return this.parseResponse(response, parsedRequestAttributes); } catch (error: any) { log.error( 'Failed to fetch contiguous data from second random ArIO peer', @@ -220,8 +223,7 @@ export class ArIODataSource implements ContiguousDataSource { private parseResponse( response: AxiosResponse, - hops: number, - origin?: string, + requestAttributes: RequestAttributes, ): ContiguousData { return { stream: response.data, @@ -229,13 +231,7 @@ export class ArIODataSource implements ContiguousDataSource { verified: false, sourceContentType: response.headers['content-type'], cached: false, - requestAttributes: { - hops: - response.headers[headerNames.hops.toLowerCase()] !== undefined - ? parseInt(response.headers[headerNames.hops.toLowerCase()]) - : hops, - origin, - }, + requestAttributes, }; } } diff --git a/src/data/gateway-data-source.test.ts b/src/data/gateway-data-source.test.ts index 0d63a569..25df9d05 100644 --- a/src/data/gateway-data-source.test.ts +++ b/src/data/gateway-data-source.test.ts @@ -39,6 +39,7 @@ beforeEach(async () => { headers: { 'content-length': '123', 'content-type': 'application/json', + 'X-AR-IO-Origin': 'node-url', }, }), defaults: { @@ -77,6 +78,7 @@ describe('GatewayDataSource', () => { requestAttributes: { hops: requestAttributes.hops + 1, origin: requestAttributes.origin, + originNodeRelease: undefined, }, }); }); @@ -173,7 +175,7 @@ describe('GatewayDataSource', () => { }); assert.equal(data.requestAttributes?.hops, 1); - assert.equal(data.requestAttributes?.origin, undefined); + assert.equal(data.requestAttributes?.origin, 'node-url'); }); it('should increment hops in the response', async () => { @@ -183,7 +185,7 @@ describe('GatewayDataSource', () => { }); assert.equal(data.requestAttributes?.hops, 6); - assert.equal(data.requestAttributes?.origin, undefined); + assert.equal(data.requestAttributes?.origin, 'node-url'); }); }); }); diff --git a/src/data/gateway-data-source.ts b/src/data/gateway-data-source.ts index 44ef7b00..b4b02045 100644 --- a/src/data/gateway-data-source.ts +++ b/src/data/gateway-data-source.ts @@ -17,7 +17,10 @@ */ import { default as axios } from 'axios'; import winston from 'winston'; -import { headerNames } from '../constants.js'; +import { + generateRequestAttributes, + parseRequestAttributesHeaders, +} from '../lib/request-attributes.js'; import { ContiguousData, @@ -61,29 +64,23 @@ export class GatewayDataSource implements ContiguousDataSource { path, }); - const requestOriginAndHopsHeaders: { [key: string]: string } = {}; - let hops; - let origin; - if (requestAttributes !== undefined) { - hops = requestAttributes.hops + 1; - requestOriginAndHopsHeaders[headerNames.hops] = hops.toString(); - - if (requestAttributes.origin !== undefined) { - origin = requestAttributes.origin; - requestOriginAndHopsHeaders[headerNames.origin] = origin; - } - } else { - hops = 1; - } + const requestAttributesHeaders = + generateRequestAttributes(requestAttributes); const response = await this.trustedGatewayAxios.request({ method: 'GET', headers: { 'Accept-Encoding': 'identity', - ...requestOriginAndHopsHeaders, + ...requestAttributesHeaders?.headers, }, url: path, responseType: 'stream', + params: { + 'ar-io-hops': requestAttributesHeaders?.attributes.hops, + 'ar-io-origin': requestAttributesHeaders?.attributes.origin, + 'ar-io-origin-release': + requestAttributesHeaders?.attributes.originNodeRelease, + }, }); if (response.status !== 200) { @@ -98,13 +95,10 @@ export class GatewayDataSource implements ContiguousDataSource { verified: false, sourceContentType: response.headers['content-type'], cached: false, - requestAttributes: { - hops: - response.headers[headerNames.hops.toLowerCase()] !== undefined - ? parseInt(response.headers[headerNames.hops.toLowerCase()]) - : hops, - origin, - }, + requestAttributes: parseRequestAttributesHeaders({ + headers: response.headers as { [key: string]: string }, + currentHops: requestAttributesHeaders?.attributes.hops, + }), }; } } diff --git a/src/data/s3-data-source.ts b/src/data/s3-data-source.ts index e7c0926a..c1fabfbf 100644 --- a/src/data/s3-data-source.ts +++ b/src/data/s3-data-source.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ import winston from 'winston'; -import { headerNames } from '../constants.js'; import { ContiguousData, @@ -26,6 +25,7 @@ import { import { AwsLiteS3 } from '@aws-lite/s3-types'; import { Readable } from 'node:stream'; import { AwsLiteClient } from '@aws-lite/client'; +import { generateRequestAttributes } from '../lib/request-attributes.js'; export class S3DataSource implements ContiguousDataSource { private log: winston.Logger; @@ -121,20 +121,8 @@ export class S3DataSource implements ContiguousDataSource { }, }); - const requestOriginAndHopsHeaders: { [key: string]: string } = {}; - let hops; - let origin; - if (requestAttributes !== undefined) { - hops = requestAttributes.hops + 1; - requestOriginAndHopsHeaders[headerNames.hops] = hops.toString(); - - if (requestAttributes.origin !== undefined) { - origin = requestAttributes.origin; - requestOriginAndHopsHeaders[headerNames.origin] = origin; - } - } else { - hops = 1; - } + const requestAttributesHeaders = + generateRequestAttributes(requestAttributes); if (response.ContentLength === undefined) { throw new Error('Content-Length header missing from S3 response'); @@ -150,10 +138,7 @@ export class S3DataSource implements ContiguousDataSource { verified: false, sourceContentType, cached: false, - requestAttributes: { - hops, - origin, - }, + requestAttributes: requestAttributesHeaders?.attributes, }; } catch (error: any) { log.error('Failed to fetch contiguous data from S3', { diff --git a/src/data/tx-chunks-data-source.ts b/src/data/tx-chunks-data-source.ts index c137c1fe..5dfacc39 100644 --- a/src/data/tx-chunks-data-source.ts +++ b/src/data/tx-chunks-data-source.ts @@ -17,6 +17,7 @@ */ import { Readable } from 'node:stream'; import winston from 'winston'; +import { generateRequestAttributes } from '../lib/request-attributes.js'; import { ChainSource, @@ -115,13 +116,8 @@ export class TxChunksDataSource implements ContiguousDataSource { size, verified: true, cached: false, - requestAttributes: { - hops: - requestAttributes?.hops !== undefined - ? requestAttributes.hops + 1 - : 1, - origin: requestAttributes?.origin, - }, + requestAttributes: + generateRequestAttributes(requestAttributes)?.attributes, }; } } diff --git a/src/lib/request-attributes.test.ts b/src/lib/request-attributes.test.ts new file mode 100644 index 00000000..72da0443 --- /dev/null +++ b/src/lib/request-attributes.test.ts @@ -0,0 +1,145 @@ +/** + * AR.IO Gateway + * Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { strict as assert } from 'node:assert'; +import { describe, it } from 'node:test'; +import { headerNames } from '../constants.js'; +import { RequestAttributes } from '../types.js'; +import { + generateRequestAttributes, + parseRequestAttributesHeaders, +} from './request-attributes.js'; + +describe('Request attributes functions', () => { + describe('generateRequestAttributes', () => { + it('should returns undefined when input is undefined', () => { + const result = generateRequestAttributes(undefined); + assert.strictEqual(result, undefined); + }); + + it('should handles hops, origin, and nodeRelease correctly', () => { + const input: RequestAttributes = { + hops: 2, + origin: 'test-origin', + originNodeRelease: 'v1.0.0', + }; + const expected = { + headers: { + [headerNames.hops]: '3', + [headerNames.origin]: 'test-origin', + [headerNames.originNodeRelease]: 'v1.0.0', + }, + attributes: { + hops: 3, + origin: 'test-origin', + originNodeRelease: 'v1.0.0', + }, + }; + + const result = generateRequestAttributes(input); + assert.deepStrictEqual(result, expected); + }); + + it('should handles missing hops correctly', () => { + const input = { + origin: 'test-origin', + originNodeRelease: 'v1.0.0', + } as RequestAttributes; + const expected = { + headers: { + [headerNames.hops]: '1', + [headerNames.origin]: 'test-origin', + [headerNames.originNodeRelease]: 'v1.0.0', + }, + attributes: { + hops: 1, + origin: 'test-origin', + originNodeRelease: 'v1.0.0', + }, + }; + + const result = generateRequestAttributes(input); + assert.deepStrictEqual(result, expected); + }); + + it('should handles missing origin and nodeVersion correctly', () => { + const input: RequestAttributes = { + hops: 2, + }; + const expected = { + headers: { + [headerNames.hops]: '3', + }, + attributes: { + hops: 3, + }, + }; + + const result = generateRequestAttributes(input); + assert.deepStrictEqual(result, expected); + }); + }); + + describe('parseRequestAttributesHeaders', () => { + it('should parses headers correctly', () => { + const headers = { + [headerNames.hops]: '3', + [headerNames.origin]: 'test-origin', + [headerNames.originNodeRelease]: 'v1.0.0', + }; + const expected: RequestAttributes = { + hops: 3, + origin: headers[headerNames.origin], + originNodeRelease: headers[headerNames.originNodeRelease], + }; + + const result = parseRequestAttributesHeaders({ headers }); + assert.deepStrictEqual(result, expected); + }); + + it('should handles missing hops and currentHops', () => { + const headers = { + [headerNames.origin]: 'test-origin', + [headerNames.originNodeRelease]: 'v1.0.0', + }; + const expected: RequestAttributes = { + hops: 1, + origin: headers[headerNames.origin], + originNodeRelease: headers[headerNames.originNodeRelease], + }; + + const result = parseRequestAttributesHeaders({ headers }); + assert.deepStrictEqual(result, expected); + }); + + it('should use currentHops when hops header is missing', () => { + const headers = { + [headerNames.origin]: 'test-origin', + [headerNames.originNodeRelease]: 'v1.0.0', + }; + const currentHops = 2; + const expected: RequestAttributes = { + hops: currentHops, + origin: headers[headerNames.origin], + originNodeRelease: headers[headerNames.originNodeRelease], + }; + + const result = parseRequestAttributesHeaders({ headers, currentHops }); + assert.deepStrictEqual(result, expected); + }); + }); +}); diff --git a/src/lib/request-attributes.ts b/src/lib/request-attributes.ts new file mode 100644 index 00000000..3e16e541 --- /dev/null +++ b/src/lib/request-attributes.ts @@ -0,0 +1,87 @@ +/** + * AR.IO Gateway + * Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { headerNames } from '../constants.js'; +import { RequestAttributes } from '../types.js'; + +export const generateRequestAttributes = ( + requestAttributes: RequestAttributes | undefined, +): + | { headers: Record; attributes: RequestAttributes } + | undefined => { + if (requestAttributes === undefined) { + return undefined; + } + + const headers: { [key: string]: string } = {}; + const attributes = {} as RequestAttributes; + + if (requestAttributes.hops !== undefined) { + const hops = requestAttributes.hops + 1; + headers[headerNames.hops] = hops.toString(); + attributes.hops = hops; + } else { + headers[headerNames.hops] = '1'; + attributes.hops = 1; + } + + if (requestAttributes.origin !== undefined) { + headers[headerNames.origin] = requestAttributes.origin; + attributes.origin = requestAttributes.origin; + } + + if (requestAttributes.originNodeRelease !== undefined) { + headers[headerNames.originNodeRelease] = + requestAttributes.originNodeRelease; + attributes.originNodeRelease = requestAttributes.originNodeRelease; + } + + return { headers, attributes }; +}; + +export const parseRequestAttributesHeaders = ({ + headers, + currentHops, +}: { + headers: Record; + currentHops?: number; +}): RequestAttributes => { + const headersLowercaseKeys = Object.keys(headers).reduce( + (newHeaders, key) => { + newHeaders[key.toLowerCase()] = headers[key]; + return newHeaders; + }, + {} as Record, + ); + + let hops; + if (headersLowercaseKeys[headerNames.hops.toLowerCase()] !== undefined) { + hops = parseInt(headersLowercaseKeys[headerNames.hops.toLowerCase()], 10); + } else if (currentHops !== undefined) { + hops = currentHops; + } else { + hops = 1; + } + + return { + hops, + origin: headersLowercaseKeys[headerNames.origin.toLowerCase()], + originNodeRelease: + headersLowercaseKeys[headerNames.originNodeRelease.toLowerCase()], + }; +}; diff --git a/src/routes/data/handlers.ts b/src/routes/data/handlers.ts index f6c7415f..7ef06feb 100644 --- a/src/routes/data/handlers.ts +++ b/src/routes/data/handlers.ts @@ -82,6 +82,12 @@ const setDataHeaders = ({ if (data.requestAttributes.origin !== undefined) { res.header(headerNames.origin, data.requestAttributes.origin); } + if (data.requestAttributes.originNodeRelease !== undefined) { + res.header( + headerNames.originNodeRelease, + data.requestAttributes.originNodeRelease, + ); + } } // Use the content type from the L1 or data item index if available @@ -92,20 +98,16 @@ const setDataHeaders = ({ ); }; -const getRequestAttributes = ( - req: Request, - arnsRootHost?: string, -): RequestAttributes => { - let origin: string | undefined; - const originHeader = req.headers[headerNames.origin.toLowerCase()] as string; +const getRequestAttributes = (req: Request): RequestAttributes => { const hopsHeader = req.headers[headerNames.hops.toLowerCase()] as string; - if (originHeader !== undefined) { - origin = originHeader; - } else if (arnsRootHost !== undefined) { - origin = arnsRootHost; - } const hops = parseInt(hopsHeader) || 0; - return { origin, hops }; + return { + hops, + origin: req.headers[headerNames.origin.toLowerCase()] as string | undefined, + originNodeRelease: req.headers[ + headerNames.originNodeRelease.toLowerCase() + ] as string | undefined, + }; }; const handleRangeRequest = ( @@ -226,16 +228,14 @@ export const createRawDataHandler = ({ dataIndex, dataSource, blockListValidator, - arnsRootHost, }: { log: Logger; dataSource: ContiguousDataSource; dataIndex: ContiguousDataIndex; blockListValidator: BlockListValidator; - arnsRootHost?: string; }) => { return asyncHandler(async (req: Request, res: Response) => { - const requestAttributes = getRequestAttributes(req, arnsRootHost); + const requestAttributes = getRequestAttributes(req); const id = req.params[0]; // Return 404 if the data is blocked by ID @@ -289,6 +289,7 @@ export const createRawDataHandler = ({ dataAttributes, requestAttributes, }); + // Check if the request includes a Range header const rangeHeader = req.headers.range; if (rangeHeader !== undefined) { @@ -445,17 +446,15 @@ export const createDataHandler = ({ dataSource, blockListValidator, manifestPathResolver, - arnsRootHost, }: { log: Logger; dataSource: ContiguousDataSource; dataIndex: ContiguousDataIndex; blockListValidator: BlockListValidator; manifestPathResolver: ManifestPathResolver; - arnsRootHost?: string; }) => { return asyncHandler(async (req: Request, res: Response) => { - const requestAttributes = getRequestAttributes(req, arnsRootHost); + const requestAttributes = getRequestAttributes(req); const arnsResolvedId = res.getHeader(headerNames.arnsResolvedId); let id: string | undefined; let manifestPath: string | undefined; diff --git a/src/routes/data/index.ts b/src/routes/data/index.ts index d7dedd2f..0c9c037a 100644 --- a/src/routes/data/index.ts +++ b/src/routes/data/index.ts @@ -20,7 +20,6 @@ import { Router } from 'express'; import log from '../../log.js'; import * as system from '../../system.js'; import { createDataHandler, createRawDataHandler } from './handlers.js'; -import * as config from '../../config.js'; const DATA_PATH_REGEX = /^\/?([a-zA-Z0-9-_]{43})\/?$|^\/?([a-zA-Z0-9-_]{43})\/(.*)$/i; @@ -35,7 +34,6 @@ export const dataHandler = createDataHandler({ dataSource: system.contiguousDataSource, blockListValidator: system.blockListValidator, manifestPathResolver: system.manifestPathResolver, - arnsRootHost: config.ARNS_ROOT_HOST, }); export const dataRouter = Router(); @@ -47,7 +45,6 @@ dataRouter.get( dataIndex: system.contiguousDataIndex, dataSource: system.contiguousDataSource, blockListValidator: system.blockListValidator, - arnsRootHost: config.ARNS_ROOT_HOST, }), ); dataRouter.get(FARCASTER_FRAME_DATA_PATH_REGEX, dataHandler); diff --git a/src/types.d.ts b/src/types.d.ts index 4e6499e7..1370648c 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -324,6 +324,7 @@ interface GqlBlocksResult { interface RequestAttributes { hops: number; origin?: string; + originNodeRelease?: string; } export interface GqlQueryable { diff --git a/test/end-to-end/data.test.ts b/test/end-to-end/data.test.ts index 09fb5d6f..f1935e1a 100644 --- a/test/end-to-end/data.test.ts +++ b/test/end-to-end/data.test.ts @@ -277,7 +277,7 @@ describe('X-Cache header', function () { }); }); -describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { +describe('X-AR-IO headers', function () { describe('with ARNS_ROOT_HOST', function () { let compose: StartedDockerComposeEnvironment; @@ -306,23 +306,26 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { const res = await axios.get(`http://localhost:4000/raw/${tx3}`); assert.equal(res.headers['x-ar-io-hops'], '1'); - assert.equal(res.headers['x-ar-io-origin'], 'ar-io.localhost'); + assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get( `http://localhost:4000/raw/${tx3}`, { headers: { 'X-AR-IO-Hops': '5', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': 'v1.0.0', }, }, ); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '6'); - assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-origin'], undefined); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); - it('Verifying that / for a manifest with a missing index returns no hops and origin', async function () { + it('Verifying that / for a manifest with a missing index returns no hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx1}`, { validateStatus: () => true, headers: { @@ -332,9 +335,10 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { assert.equal(res.headers['x-ar-io-hops'], undefined); assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); }); - it('verifying that / for a manifest with a valid index returns hops and origin', async function () { + it('verifying that / for a manifest with a valid index returns hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx2}/`, { headers: { Host: 'zhtq6zlaiu54cz5ss7vqxlf7yquechmhzcpmwccmrcu7w44f4zbq.ar-io.localhost', @@ -342,21 +346,24 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { }); assert.equal(res.headers['x-ar-io-hops'], '1'); - assert.equal(res.headers['x-ar-io-origin'], 'ar-io.localhost'); + assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get(`http://localhost:4000/${tx2}/`, { headers: { Host: 'zhtq6zlaiu54cz5ss7vqxlf7yquechmhzcpmwccmrcu7w44f4zbq.ar-io.localhost', 'X-AR-IO-Hops': '2', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': 'v2.0.0', }, }); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '3'); assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); - it('Verifying that / for a non-manifest returns hops and origin', async function () { + it('Verifying that / for a non-manifest returns hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx3}`, { headers: { Host: 'sw3yqmkl5ajki5vl5jflcpqy43opvgtpngs6tel3eltuhq73l2jq.ar-io.localhost', @@ -364,18 +371,21 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { }); assert.equal(res.headers['x-ar-io-hops'], '1'); - assert.equal(res.headers['x-ar-io-origin'], 'ar-io.localhost'); + assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get(`http://localhost:4000/${tx3}`, { headers: { 'X-AR-IO-Hops': '5', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': 'v2.0.0', Host: 'sw3yqmkl5ajki5vl5jflcpqy43opvgtpngs6tel3eltuhq73l2jq.ar-io.localhost', }, }); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '6'); assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); }); @@ -407,31 +417,35 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { assert.equal(res.headers['x-ar-io-hops'], '1'); assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get( `http://localhost:4000/raw/${tx3}`, { headers: { 'X-AR-IO-Hops': '5', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': '10', }, }, ); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '6'); - assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-origin'], undefined); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); - it('Verifying that / for a manifest with a missing index returns no hops and origin', async function () { + it('Verifying that / for a manifest with a missing index returns no hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx1}`, { validateStatus: () => true, }); assert.equal(res.headers['x-ar-io-hops'], undefined); assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); }); - it('verifying that / for a manifest with a valid index returns hops and origin', async function () { + it('verifying that / for a manifest with a valid index returns hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx2}/`, { headers: { Host: 'zhtq6zlaiu54cz5ss7vqxlf7yquechmhzcpmwccmrcu7w44f4zbq.ar-io.localhost', @@ -440,20 +454,23 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { assert.equal(res.headers['x-ar-io-hops'], '1'); assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get(`http://localhost:4000/${tx2}/`, { headers: { Host: 'zhtq6zlaiu54cz5ss7vqxlf7yquechmhzcpmwccmrcu7w44f4zbq.ar-io.localhost', 'X-AR-IO-Hops': '2', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': '10', }, }); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '3'); assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); - it('Verifying that / for a non-manifest returns hops and origin', async function () { + it('Verifying that / for a non-manifest returns hops, origin and node release', async function () { const res = await axios.get(`http://localhost:4000/${tx3}`, { headers: { Host: 'sw3yqmkl5ajki5vl5jflcpqy43opvgtpngs6tel3eltuhq73l2jq.ar-io.localhost', @@ -462,17 +479,20 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { assert.equal(res.headers['x-ar-io-hops'], '1'); assert.equal(res.headers['x-ar-io-origin'], undefined); + assert.equal(res.headers['x-ar-io-node-release'], undefined); const resWithHeaders = await axios.get(`http://localhost:4000/${tx3}`, { headers: { 'X-AR-IO-Hops': '5', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': '10', Host: 'sw3yqmkl5ajki5vl5jflcpqy43opvgtpngs6tel3eltuhq73l2jq.ar-io.localhost', }, }); assert.equal(resWithHeaders.headers['x-ar-io-hops'], '6'); assert.equal(resWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(resWithHeaders.headers['x-ar-io-node-release'], undefined); }); }); @@ -491,6 +511,7 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { 'Content-Length': '11', 'X-AR-IO-Hops': hops ? (parseInt(hops) + 1).toString() : '1', 'X-AR-IO-Origin': origin ?? 'fake-gateway', + 'X-AR-IO-Node-Release': '10', }); res.end('hello world'); }); @@ -532,20 +553,23 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { const req = await axios.get(`http://localhost:${corePort}/raw/${tx2}`); assert.equal(req.headers['x-ar-io-hops'], '2'); - assert.equal(req.headers['x-ar-io-origin'], 'ar-io.localhost'); + assert.equal(req.headers['x-ar-io-origin'], 'fake-gateway'); + assert.equal(req.headers['x-ar-io-node-release'], '10'); const reqWithHeaders = await axios.get( `http://localhost:${corePort}/raw/${tx3}`, { headers: { 'X-AR-IO-Hops': '5', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': '20', }, }, ); assert.equal(reqWithHeaders.headers['x-ar-io-hops'], '7'); assert.equal(reqWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(reqWithHeaders.headers['x-ar-io-node-release'], '10'); }); it('Verifying that / returns expected response', async function () { @@ -555,7 +579,8 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { }, }); assert.equal(req.headers['x-ar-io-hops'], '2'); - assert.equal(req.headers['x-ar-io-origin'], 'ar-io.localhost'); + assert.equal(req.headers['x-ar-io-origin'], 'fake-gateway'); + assert.equal(req.headers['x-ar-io-node-release'], '10'); const reqWithHeaders = await axios.get( `http://localhost:${corePort}/${tx3}`, @@ -563,13 +588,15 @@ describe('X-AR-IO-Hops and X-Ar-IO-Origin headers', function () { headers: { Host: 'sw3yqmkl5ajki5vl5jflcpqy43opvgtpngs6tel3eltuhq73l2jq.ar-io.localhost', 'X-AR-IO-Hops': '10', - 'X-Ar-IO-Origin': 'another-host', + 'X-AR-IO-Origin': 'another-host', + 'X-AR-IO-Node-Release': '20', }, }, ); assert.equal(reqWithHeaders.headers['x-ar-io-hops'], '12'); assert.equal(reqWithHeaders.headers['x-ar-io-origin'], 'another-host'); + assert.equal(reqWithHeaders.headers['x-ar-io-node-release'], '10'); }); }); });