Skip to content

Commit daece04

Browse files
committed
Introduce basic working version
1 parent e561371 commit daece04

16 files changed

+1446
-79
lines changed

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DB_IP=localhost:9042
2+
DB_DATACENTER=datacenter1
3+
IPFS_IP=http://127.0.0.1:8080
4+
IPFS_API=http://127.0.0.1:5001

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.js

.eslintrc.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module.exports = {
2222
"camelCase": true,
2323
"pascalCase": true
2424
}
25-
}]
25+
}],
26+
"quotes": ["error", "single", { "allowTemplateLiterals": true }]
2627
}
2728
}

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
# IPFS-Signal
2+
3+
## Goals
4+
5+
Bridge web2 to web3
6+
7+
- Push code to IPFS
8+
- Keep track of last few version
9+
10+
- Serve content over HTTP
11+
- Find owner based on domain
12+
- Find location on IPFS node
13+
- Send `x-ipfs-path`: record to user
14+
- Serve content
15+
16+
- Update DNS records to DNSLink ([https://dnslink.io/](https://dnslink.io/))

package.json

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@
1414
"eslint-plugin-node": "^11.1.0",
1515
"eslint-plugin-promise": "^4.2.1",
1616
"eslint-plugin-unicorn": "^39.0.0",
17-
"scyllo": "^0.9.1",
17+
"express": "^4.17.1",
18+
"ipfs-only-hash": "^4.0.0",
19+
"node-fetch": "^3.1.0",
20+
"scyllo": "^0.9.3",
1821
"typescript": "^4.5.2"
1922
},
2023
"scripts": {
2124
"lint": "eslint src --ext .ts",
22-
"lint:fix": "eslint src --ext .ts --fix"
25+
"lint:fix": "eslint src --ext .ts --fix",
26+
"start": "ts-node ./src/index.ts",
27+
"dev": "ts-node-dev ./src/index.ts"
28+
},
29+
"devDependencies": {
30+
"@types/express": "^4.17.13",
31+
"ts-node": "^10.4.0",
32+
"ts-node-dev": "^1.1.8"
2333
}
2434
}

src/Data.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ScylloClient } from "scyllo";
2+
import { EdgeName } from "./types/EdgeName.type";
3+
import { Owner } from "./types/Owner.type";
4+
import { OwnerSiteLookup } from "./types/OwnerSiteLookup.type";
5+
import { Site } from "./types/Site.type";
6+
import { SiteLookup } from "./types/SiteLookup.type";
7+
8+
export const DB = new ScylloClient<{
9+
// Get a list of all the stored data by SiteID
10+
edgenames: EdgeName,
11+
// Get a list of all the owners by OwnerID
12+
owners: Owner,
13+
// Get a list of all the sites by siteID
14+
sites: Site,
15+
// Get the site belonging to a domain
16+
sitelookup: SiteLookup,
17+
// Get the sites belonging to an owner
18+
ownersitelookup: OwnerSiteLookup
19+
}>({
20+
client: {
21+
contactPoints: [process.env.DB_IP || 'localhost:9042'],
22+
localDataCenter: process.env.DB_DATACENTER || 'datacenter1'
23+
},
24+
debug: false
25+
});
26+
27+
export const initDB = async () => {
28+
console.log('Awaiting Connection');
29+
await DB.awaitConnection();
30+
31+
await DB.createKeyspace('ipfssignal');
32+
await DB.useKeyspace('ipfssignal');
33+
34+
console.log('Ensuring Tables');
35+
await DB.createTable('edgenames', true, {
36+
cid: {
37+
type: 'text'
38+
},
39+
site_id: {
40+
type: 'bigint'
41+
}
42+
}, 'site_id');
43+
await DB.createTable('owners', true, {
44+
user_id: {
45+
type: 'bigint'
46+
}
47+
}, 'user_id');
48+
await DB.createTable('sitelookup', true, {
49+
host: {
50+
type: 'text'
51+
},
52+
site_id: {
53+
type: 'bigint'
54+
}
55+
}, 'host', ['site_id']);
56+
await DB.createTable('sites', true, {
57+
host: { type: 'text' },
58+
owner: { type: 'bigint' },
59+
site_id: { type: 'bigint' }
60+
}, 'site_id');
61+
await DB.createTable('ownersitelookup', true, {
62+
owner_id: { type: 'bigint' },
63+
site_id: { type: 'bigint' }
64+
}, 'owner_id', ['site_id']);
65+
};

src/index.ts

+11-55
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,19 @@
1-
import { ScylloClient } from 'scyllo';
2-
import { EdgeName } from './types/EdgeName.type';
1+
import Express from 'express';
32
import { config } from 'dotenv';
4-
import { Owner } from './types/Owner.type';
5-
import { Site } from './types/Site.type';
6-
import { SiteLookup } from './types/SiteLookup.type';
7-
import { OwnerSiteLookup } from './types/OwnerSiteLookup.type';
3+
import { initDB } from './Data';
4+
import { handleRequest } from './lookup/RequestHandler';
85

96
config();
10-
const DB = new ScylloClient<{
11-
// Get a list of all the stored data by SiteID
12-
edgenames: EdgeName,
13-
// Get a list of all the owners by OwnerID
14-
owners: Owner,
15-
// Get a list of all the sites by siteID
16-
sites: Site,
17-
// Get the site belonging to a domain
18-
sitelookup: SiteLookup,
19-
// Get the sites belonging to an owner
20-
ownersitelookup: OwnerSiteLookup
21-
}>({
22-
client: {
23-
contactPoints: [process.env.DB_IP]
24-
}
25-
});
267

278
(async () => {
28-
console.log('Awaiting Connection');
29-
await DB.awaitConnection();
9+
await initDB();
3010

31-
console.log('Ensuring Tables');
32-
await DB.createTable('edgenames', true, {
33-
cid: {
34-
type: 'bigint'
35-
},
36-
site_id: {
37-
type: 'bigint'
38-
}
39-
}, 'site_id');
40-
await DB.createTable('owners', true, {
41-
user_id: {
42-
type: 'bigint'
43-
}
44-
}, 'user_id');
45-
await DB.createTable('sitelookup', true, {
46-
host: {
47-
type: 'text'
48-
},
49-
site_id: {
50-
type: 'bigint'
51-
}
52-
}, 'host', ['site_id']);
53-
await DB.createTable('sites', true, {
54-
host: { type: 'text' },
55-
owner: { type: 'bigint' },
56-
site_id: { type: 'bigint' }
57-
}, 'site_id');
58-
await DB.createTable('ownersitelookup', true, {
59-
owner_id: { type: 'bigint' },
60-
site_id: { type: 'bigint' }
61-
}, 'owner_id', ['site_id']);
11+
console.log('Starting Express');
12+
const app = Express();
6213

14+
app.use(handleRequest);
15+
16+
app.listen(1234, () => {
17+
console.log('Done ✨');
18+
});
6319
})();

src/lookup/NextHandler.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Request, Response, RequestHandler } from 'express';
2+
import { VersionFooter } from '../presets/RejectMessages';
3+
4+
export type RejectReason = {
5+
status: number,
6+
text?: string
7+
};
8+
9+
export const NextHandler: (_function: (request: Request, response: Response) => Promise<void> | Promise<RejectReason>) => RequestHandler = (_function) => {
10+
return async (request, response, next) => {
11+
12+
let result = await _function(request, response);
13+
if (typeof result == 'undefined') return;
14+
15+
console.log('Reject: ' + JSON.stringify(result));
16+
response.status(result.status);
17+
response.type('text/plain');
18+
response.send(result.text + '\n' + VersionFooter);
19+
};
20+
};

src/lookup/RequestHandler.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { DB } from '../Data';
2+
import { DomainNotFound, EmptyDirectory, FileNotFound } from '../presets/RejectMessages';
3+
import { NextHandler } from './NextHandler';
4+
import { request as httpRequest } from 'node:http';
5+
import { join } from 'node:path';
6+
7+
export const handleRequest = NextHandler(async (request, response) => {
8+
console.log('Incomming request at ' + request.hostname + ' ' + request.path);
9+
10+
// Lookup the site by hostname from the database
11+
const a = await DB.selectOneFrom('sitelookup', ['site_id'], { host: request.hostname });
12+
13+
// Reject if the host does not exist
14+
if (!a) return DomainNotFound(request.hostname + request.path);
15+
16+
const b = await DB.selectOneFrom('edgenames', ['cid'], { site_id: a.site_id });
17+
18+
// Verify if file exists on IPFS node
19+
const fileData = await new Promise<Object>((accept, reject) => {
20+
const preparedURL = (process.env.IPFS_API || 'http://127.0.0.1:5001') + '/api/v0/file/ls?arg=' + (join(b.cid, request.path));
21+
var existsRequest = httpRequest({
22+
method: 'post',
23+
host: '127.0.0.1',
24+
port: 5001,
25+
path: '/api/v0/file/ls?arg=' + (join(b.cid, request.path))
26+
}, (incomming) => {
27+
let data = '';
28+
incomming.on('data', (chunk) => {
29+
data += chunk;
30+
});
31+
incomming.on('end', () => {
32+
accept(JSON.parse(data));
33+
});
34+
});
35+
existsRequest.end();
36+
});
37+
38+
// If not exists return
39+
if (fileData['Type'] == 'error') return FileNotFound(request.hostname + request.path);
40+
41+
// If directory
42+
let optionalSuffix = '';
43+
if (fileData['Objects']) {
44+
const localCID = Object.keys(fileData['Objects'])[0];
45+
if (!fileData['Objects'][localCID]) {
46+
return { status: 500, text: 'file not there...' };
47+
}
48+
49+
if (fileData['Objects'][localCID]['Type'] == 'Directory') {
50+
51+
// Find the index.html
52+
let fileFound = false;
53+
for (let item of fileData['Objects'][localCID]['Links']) {
54+
55+
// If name is empty, assume its a spread file
56+
if (item['Name'].length === 0) {
57+
break;
58+
}
59+
60+
// Check if file is index.html
61+
if (item['Name'] == 'index.html' && item['Type'] == 'File') {
62+
optionalSuffix = 'index.html';
63+
fileFound = true;
64+
break;
65+
}
66+
}
67+
68+
// If no index.html found throw
69+
if (!fileFound) return EmptyDirectory(request.hostname + request.path);
70+
}
71+
}
72+
73+
// Fetch file from IPFS Endpoint
74+
var contentRequest = httpRequest(join(process.env.IPFS_IP || 'http://127.0.0.1:8080', 'ipfs', b.cid, request.path, optionalSuffix), (incomming) => {
75+
for (const a of Object.keys(incomming.headers)) {
76+
response.setHeader(a, incomming.headers[a]);
77+
}
78+
incomming.on('data', (chunk) => {
79+
response.write(chunk);
80+
});
81+
incomming.on('end', () => {
82+
response.send();
83+
});
84+
});
85+
contentRequest.on('error', (error) => {
86+
console.log(error);
87+
});
88+
contentRequest.end();
89+
});

src/presets/RejectMessages.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import PackageInfo from '../../package.json';
2+
import { RejectReason } from '../lookup/NextHandler';
3+
4+
export const VersionFooter = `IPFS-Signal v${PackageInfo.version}`;
5+
6+
export const DomainNotFound: (url: string) => RejectReason = (url: string) => ({ status: 404, text: `Domain ${url} not found` });
7+
export const FileNotFound: (url: string) => RejectReason = (url: string) => ({ status: 404, text: `File ${url} not found` });
8+
export const EmptyDirectory: (url: string) => RejectReason = (url: string) => ({ status: 404, text: `Directory ${url} not indexable` });
9+
export const Unknown: (url: string) => RejectReason = (url: string) => ({ status: 404, text: `${url} not found` });

src/types/EdgeName.type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export type EdgeName = {
2-
site_id: string;
2+
site_id: Long;
33
cid: string; // IPFS Location
44
}

src/types/Owner.type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export type Owner = {
2-
user_id: number;
2+
user_id: Long;
33
}

src/types/Site.type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export type Site = {
2-
site_id: number;
2+
site_id: Long;
33
host: string; // HTTP Location
44
owner: number;
55
};

src/types/SiteLookup.type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export type SiteLookup = {
22
host: string
3-
site_id: string;
3+
site_id: Long;
44
};

tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"esModuleInterop": true,
4+
"resolveJsonModule": true
5+
}
6+
}

0 commit comments

Comments
 (0)