Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion redisinsight/api/src/__mocks__/custom-tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const mockCustomTutorialId2 = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id

export const mockCustomTutorialTmpPath = '/tmp/path';

export const mockCustomTutorialsHttpLink = 'https://somesime.com/archive.zip';
export const mockCustomTutorialsHttpLink = 'https://github.com/archive.zip';
export const mockCustomTutorialsHttpLink2 = 'https://raw.githubusercontent.com/archive.zip';

export const mockCustomTutorial = Object.assign(new CustomTutorial(), {
id: mockCustomTutorialId,
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default {
PLUGIN_STATE_NOT_FOUND: 'Plugin state was not found.',
CUSTOM_TUTORIAL_NOT_FOUND: 'Custom Tutorial was not found.',
CUSTOM_TUTORIAL_UNABLE_TO_FETCH_FROM_EXTERNAL: 'Unable fetch zip file from external source.',
CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN: 'Unsupported origin for tutorial.',
UNDEFINED_INSTANCE_ID: 'Undefined redis database instance id.',
NO_CONNECTION_TO_REDIS_DB: 'No connection to the Redis Database.',
WRONG_DATABASE_TYPE: 'Wrong database type.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import {
HasMimeType, IsFile, MaxFileSize, MemoryStoredFile,
} from 'nestjs-form-data';
import { IsGitHubLink } from 'src/common/decorators';

export class UploadCustomTutorialDto {
@ApiPropertyOptional({
Expand All @@ -24,6 +23,5 @@ export class UploadCustomTutorialDto {
@IsOptional()
@IsString()
@IsNotEmpty()
@IsGitHubLink()
link?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Test, TestingModule } from '@nestjs/testing';
import {
mockCustomTutorial,
mockCustomTutorialAdmZipEntry,
mockCustomTutorialMacosxAdmZipEntry, mockCustomTutorialsHttpLink,
mockCustomTutorialMacosxAdmZipEntry, mockCustomTutorialsHttpLink, mockCustomTutorialsHttpLink2,
mockCustomTutorialTmpPath,
mockCustomTutorialZipFile, mockCustomTutorialZipFileAxiosResponse,
} from 'src/__mocks__';
import * as fs from 'fs-extra';
import axios from 'axios';
import { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';
import { InternalServerErrorException } from '@nestjs/common';
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
import AdmZip from 'adm-zip';
import ERROR_MESSAGES from 'src/constants/error-messages';
import config from 'src/utils/config';
Expand Down Expand Up @@ -80,12 +80,24 @@ describe('CustomTutorialFsProvider', () => {
});

describe('unzipFromExternalLink', () => {
it('should unzip data from external link', async () => {
const result = await service.unzipFromExternalLink(mockCustomTutorialsHttpLink);
it.each([
mockCustomTutorialsHttpLink,
mockCustomTutorialsHttpLink2,
])('should unzip data from external link', async (url) => {
const result = await service.unzipFromExternalLink(url);
expect(result).toEqual(mockCustomTutorialTmpPath);
});

it('should throw InternalServerError when 4incorrect external link provided', async () => {
it.each([
'http://hithub.com',
'http://raw.githubusercontent.com',
'http://raw.amy.other.com',
])('should unzip data from external link', async (url) => {
await expect(service.unzipFromExternalLink(url))
.rejects.toThrow(new BadRequestException(ERROR_MESSAGES.CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN));
});

it('should throw InternalServerError when incorrect external link provided', async () => {
const responsePayload = {
response: {
status: 404,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import {
BadRequestException, Injectable, InternalServerErrorException, Logger,
} from '@nestjs/common';
import { MemoryStoredFile } from 'nestjs-form-data';
import { join } from 'path';
import { v4 as uuidv4 } from 'uuid';
Expand All @@ -13,6 +15,11 @@ const PATH_CONFIG = config.get('dir_path');

const TMP_FOLDER = `${PATH_CONFIG.tmpDir}/RedisInsight/custom-tutorials`;

const UPLOAD_FROM_REMOTE_ORIGINS_WHITELIST = [
'https://github.com',
'https://raw.githubusercontent.com',
];

@Injectable()
export class CustomTutorialFsProvider {
private logger = new Logger('CustomTutorialFsProvider');
Expand Down Expand Up @@ -73,6 +80,12 @@ export class CustomTutorialFsProvider {
*/
public async unzipFromExternalLink(link: string): Promise<string> {
try {
const url = new URL(link);

if (!UPLOAD_FROM_REMOTE_ORIGINS_WHITELIST.includes(url.origin)) {
return Promise.reject(new BadRequestException(ERROR_MESSAGES.CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN));
}

const { data } = await axios.get(link, {
responseType: 'arraybuffer',
});
Expand Down
21 changes: 17 additions & 4 deletions redisinsight/api/src/utils/hosting-provider-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,39 @@ import { IP_ADDRESS_REGEX, PRIVATE_IP_ADDRESS_REGEX } from 'src/constants';
import { HostingProvider } from 'src/modules/database/entities/database.entity';
import { RedisClient } from 'src/modules/redis/client';

const PROVIDER_HOST_REGEX = {
RLCP: /\.rlrcp\.com$/,
REDISLABS: /\.redislabs\.com$/,
REDISCLOUD: /\.redis-cloud\.com$/,
CACHE_AMAZONAWS: /cache\.amazonaws\.com$/,
CACHE_WINDOWS: /cache\.windows\.net$/,
RE_CACHE_AZURE: /redisenterprise\.cache\.azure\.net$/,
};

// Because we do not bind potentially dangerous logic to this.
// We define a hosting provider for telemetry only.
export const getHostingProvider = async (client: RedisClient, databaseHost: string): Promise<HostingProvider> => {
try {
const host = databaseHost.toLowerCase();

// Tries to detect the hosting provider from the hostname.
if (host.endsWith('rlrcp.com') || host.endsWith('redislabs.com') || host.endsWith('redis-cloud.com')) {
if (
PROVIDER_HOST_REGEX.RLCP.test(host)
|| PROVIDER_HOST_REGEX.REDISLABS.test(host)
|| PROVIDER_HOST_REGEX.REDISCLOUD.test(host)
) {
return HostingProvider.RE_CLOUD;
}
if (host.endsWith('cache.amazonaws.com')) {
if (PROVIDER_HOST_REGEX.CACHE_AMAZONAWS.test(host)) {
return HostingProvider.AWS_ELASTICACHE;
}
if (host.includes('memorydb')) {
return HostingProvider.AWS_MEMORYDB;
}
if (host.endsWith('cache.windows.net')) {
if (PROVIDER_HOST_REGEX.CACHE_WINDOWS.test(host)) {
return HostingProvider.AZURE_CACHE;
}
if (host.endsWith('redisenterprise.cache.azure.net')) {
if (PROVIDER_HOST_REGEX.RE_CACHE_AZURE.test(host)) {
return HostingProvider.AZURE_CACHE_REDIS_ENTERPRISE;
}

Expand Down