Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add os api config, update csp, minor improvements #167

Merged
merged 9 commits into from
Dec 14, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/test.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x]
node-version: [18.x, 19.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
2 changes: 1 addition & 1 deletion app_template.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
runtime: nodejs16 # or another supported version
runtime: nodejs18 # or another supported version

instance_class: F2

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
},
"dependencies": {
"@adraffy/ens-normalize": "^1.9.0",
"@ensdomains/ens-avatar": "^0.2.5",
"@ensdomains/ensjs": "^3.0.0-alpha.61",
"@ensdomains/ens-avatar": "^0.3.2",
"@ensdomains/ensjs": "^3.0.0-alpha.67",
"@types/lodash": "^4.14.170",
"btoa": "^1.2.1",
"canvas": "^2.11.2",
Expand Down Expand Up @@ -80,6 +80,6 @@
]
},
"volta": {
"node": "16.15.0"
"node": "18.15.0"
}
}
Binary file removed src/assets/DejaVuSans-Bold.ttf
Binary file not shown.
4 changes: 3 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const INAMEWRAPPER = process.env.INAMEWRAPPER || '0xd82c42d8';

const IPFS_GATEWAY = process.env.IPFS_GATEWAY || 'https://ipfs.io';
const INFURA_API_KEY = process.env.INFURA_API_KEY || '';
const OPENSEA_API_KEY = process.env.OPENSEA_API_KEY || '';
const NODE_PROVIDER = process.env.NODE_PROVIDER || 'geth';
const NODE_PROVIDER_URL = process.env.NODE_PROVIDER_URL || 'http://localhost:8545';

Expand All @@ -32,7 +33,7 @@ const ETH_REGISTRY_ABI = [
];

// response timeout: 1 min
const RESPONSE_TIMEOUT = 10 * 1000;
const RESPONSE_TIMEOUT = 15 * 1000;

export {
ADDRESS_ETH_REGISTRAR,
Expand All @@ -44,6 +45,7 @@ export {
INAMEWRAPPER,
IPFS_GATEWAY,
INFURA_API_KEY,
OPENSEA_API_KEY,
REDIS_URL,
NODE_PROVIDER,
NODE_PROVIDER_URL,
Expand Down
10 changes: 6 additions & 4 deletions src/controller/ensImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ export async function ensImage(req: Request, res: Response) {
error instanceof NamehashMismatchError ||
error instanceof UnsupportedNetwork
) {
res.status(errCode).json({
message: error.message,
});
return;
if (!res.headersSent) {
res.status(errCode).json({
message: error.message,
});
return;
}
}

/* #swagger.responses[404] = {
Expand Down
10 changes: 6 additions & 4 deletions src/controller/ensMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ export async function ensMetadata(req: Request, res: Response) {
error instanceof NamehashMismatchError ||
error instanceof UnsupportedNetwork
) {
res.status(errCode).json({
message: error.message,
});
return;
if (!res.headersSent) {
res.status(errCode).json({
message: error.message,
});
return;
}
}

try {
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ app.use(
'https://unpkg.com/redoc@latest/bundles/redoc.standalone.js'
],
imgSrc: ['*', 'data:'],
styleSrc: ["'self'", "'unsafe-inline'"],
fontSrc: ["'self'", 'data:'],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
connectSrc: ['*', 'data:'],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
frameSrc: ["'none'"],
childSrc: ["'none'"],
workerSrc: ["'none'"],
workerSrc: ['blob:'],
baseUri: ["'none'"],
formAction: ["'none'"],
upgradeInsecureRequests: [],
Expand Down
13 changes: 11 additions & 2 deletions src/service/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
RetrieveURIFailed,
TextRecordNotFound,
} from '../base';
import { IPFS_GATEWAY } from '../config';
import {
IPFS_GATEWAY,
OPENSEA_API_KEY
} from '../config';
import { abortableFetch } from '../utils/abortableFetch';

const window = new JSDOM('').window;
Expand Down Expand Up @@ -46,7 +49,13 @@ export class AvatarMetadata {
avtResolver: AvatarResolver;
constructor(provider: ethers.providers.BaseProvider, uri: string) {
this.defaultProvider = provider;
this.avtResolver = new AvatarResolver(provider, { ipfs: IPFS_GATEWAY });
this.avtResolver = new AvatarResolver(provider,
{
ipfs: IPFS_GATEWAY,
apiKey: { opensea: OPENSEA_API_KEY },
urlDenyList: [ 'metadata.ens.domains' ]
}
);
this.uri = uri;
}

Expand Down
28 changes: 28 additions & 0 deletions src/service/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import avaTest, { ExecutionContext, TestFn } from 'ava';
import { TestContext } from '../../mock/interface';
import { Metadata } from './metadata';
import { Version } from '../base';

const test = avaTest as TestFn<TestContext>;

test('should compute metadata correctly', async (t: ExecutionContext<TestContext>) => {
const nickMetadataObj = {
name: 'nick.eth',
description: 'nick.eth, an ENS name.',
created_date: 1571924851000,
tokenId: '0x5d5727cb0fb76e4944eafb88ec9a3cf0b3c9025a4b2f947729137c5d7f84f68f',
version: Version.v1,
last_request_date: Date.now()
};
const testMetadata = new Metadata(nickMetadataObj);

t.is(testMetadata.name, nickMetadataObj.name);
t.is(testMetadata.description, nickMetadataObj.description);
t.is(testMetadata.attributes[0].value, nickMetadataObj.created_date * 1000);
t.is(testMetadata.version, Version.v1);
});

test('should return correct font size', async (t: ExecutionContext<TestContext>) => {
const textSize = Metadata._getFontSize('nick.eth');
t.is(textSize, 32);
});
69 changes: 45 additions & 24 deletions src/service/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {ens_normalize} from '@adraffy/ens-normalize';
import {
CanvasRenderingContext2D,
createCanvas,
registerFont
} from 'canvas';
import { Version } from '../base';
import {
CANVAS_FONT_PATH,
Expand All @@ -9,19 +14,13 @@ import base64EncodeUnicode from '../utils/base64encode';
import { isASCII, findCharacterSet } from '../utils/characterSet';
import { getCodePointLength, getSegmentLength } from '../utils/charLength';

// no ts declaration files
const { createCanvas, registerFont } = require('canvas');


try {
registerFont(CANVAS_FONT_PATH, { family: 'Satoshi' });
registerFont(CANVAS_EMOJI_FONT_PATH, { family: 'Noto Color Emoji' });
} catch(error) {
console.warn("Font registeration is failed.");
console.warn(error);
interface Attribute {
trait_type: string,
display_type: string,
value: any
}


export interface MetadataInit {
name : string;
description? : string;
Expand All @@ -36,7 +35,7 @@ export interface MetadataInit {
export interface Metadata {
name : string;
description : string;
attributes : object[];
attributes : Attribute[];
name_length? : number;
segment_length? : number;
image : string;
Expand All @@ -51,20 +50,24 @@ export interface Metadata {

export class Metadata {
static MAX_CHAR = 60;
static ctx: CanvasRenderingContext2D;

constructor({
name,
description,
created_date,
tokenId,
version,
last_request_date
last_request_date,
}: MetadataInit) {
const label = this.getLabel(name);
this.is_normalized = this._checkNormalized(name);
this.name = this.formatName(name, tokenId);
this.description = this.formatDescription(name, description);
this.attributes = this.initializeAttributes(created_date, label);
this.url = this.is_normalized ? `https://app.ens.domains/name/${name}` : null;
this.url = this.is_normalized
? `https://app.ens.domains/name/${name}`
: null;
this.last_request_date = last_request_date;
this.version = version;
}
Expand All @@ -84,19 +87,23 @@ export class Metadata {

formatDescription(name: string, description?: string) {
const baseDescription = description || `${this.name}, an ENS name.`;
const normalizedNote = !this.is_normalized ? ` (${name} is not in normalized form)` : '';
const normalizedNote = !this.is_normalized
? ` (${name} is not in normalized form)`
: '';
const asciiWarning = this.generateAsciiWarning(this.getLabel(name));
return `${baseDescription}${normalizedNote}${asciiWarning}`;
}

generateAsciiWarning(label: string) {
if (!isASCII(label)) {
return ' ⚠️ ATTENTION: This name contains non-ASCII characters as shown above. ' +
return (
' ⚠️ ATTENTION: This name contains non-ASCII characters as shown above. ' +
'Please be aware that there are characters that look identical or very ' +
'similar to English letters, especially characters from Cyrillic and Greek. ' +
'Also, traditional Chinese characters can look identical or very similar to ' +
'simplified variants. For more information: ' +
'https://en.wikipedia.org/wiki/IDN_homograph_attack';
'https://en.wikipedia.org/wiki/IDN_homograph_attack'
);
}
return '';
}
Expand Down Expand Up @@ -129,7 +136,7 @@ export class Metadata {
];
}

addAttribute(attribute: object) {
addAttribute(attribute: Attribute) {
this.attributes.push(attribute);
}

Expand All @@ -152,7 +159,12 @@ export class Metadata {

const { domain, subdomainText } = this.processSubdomain(name, isSubdomain);
const { processedDomain, domainFontSize } = this.processDomain(domain);
const svg = this._generateByVersion(domainFontSize, subdomainText, isSubdomain, processedDomain);
const svg = this._generateByVersion(
domainFontSize,
subdomainText,
isSubdomain,
processedDomain
);

try {
this.setImage('data:image/svg+xml;base64,' + base64EncodeUnicode(svg));
Expand All @@ -165,7 +177,7 @@ export class Metadata {
processSubdomain(name: string, isSubdomain: boolean) {
let subdomainText;
let domain = name;

if (isSubdomain && !name.includes('...')) {
const labels = name.split('.');
let subdomain = labels.slice(0, labels.length - 2).join('.') + '.';
Expand Down Expand Up @@ -241,11 +253,20 @@ export class Metadata {
}

static _getFontSize(name: string): number {
const canvas = createCanvas(270, 270, 'svg');
const ctx = canvas.getContext('2d');
ctx.font =
'20px Satoshi, Noto Color Emoji, Apple Color Emoji, sans-serif';
const fontMetrics = ctx.measureText(name);
if (!this.ctx) {
try {
registerFont(CANVAS_FONT_PATH, { family: 'Satoshi' });
registerFont(CANVAS_EMOJI_FONT_PATH, { family: 'Noto Color Emoji' });
} catch (error) {
console.warn('Font registration is failed.');
console.warn(error);
}
const canvas = createCanvas(270, 270, 'svg');
this.ctx = canvas.getContext('2d');
this.ctx.font =
'20px Satoshi, Noto Color Emoji, Apple Color Emoji, sans-serif';
}
const fontMetrics = this.ctx.measureText(name);
const fontSize = Math.floor(20 * (200 / fontMetrics.width));
return fontSize < 34 ? fontSize : 32;
}
Expand Down
19 changes: 12 additions & 7 deletions src/utils/blockRecursiveCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ export function blockRecursiveCalls(
const requestOrigin = req.get('origin') || req.get('referer');

if (requestOrigin) {
const parsedRequestOrigin = new URL(requestOrigin);
try {
const parsedRequestOrigin = new URL(requestOrigin);

if (
parsedRequestOrigin.hostname === req.hostname &&
parsedRequestOrigin.protocol.includes('http')
) {
console.warn(`Recursive call detected`);
res.status(403).json({ message: 'Recursive calls are not allowed.' });
if (
parsedRequestOrigin.hostname === req.hostname &&
parsedRequestOrigin.protocol.includes('http')
) {
console.warn(`Recursive call detected`);
res.status(403).json({ message: 'Recursive calls are not allowed.' });
return;
}
} catch (error) {
console.warn('Error parsing URL', error);
}
}
next();
Expand Down
4 changes: 2 additions & 2 deletions src/utils/rateLimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ if (REDIS_URL) {

const opts = {
storeClient: redisClient,
points: 100, // Number of total points
duration: 5, // Per second(s)
points: 10, // Number of total points
duration: 2, // Per second(s)
execEvenly: false, // Do not delay actions evenly
blockDuration: 0, // Do not block the caller if consumed more than points
keyPrefix: 'ensrl', // Assign unique keys for each limiters with different purposes
Expand Down
Loading