Commit b213dee4 authored by Xunnamius (Luna)'s avatar Xunnamius (Luna)

added hotcrp backend (updated) and a few other updates with the protocol resolution process

parent a9ca18f6
......@@ -11,9 +11,11 @@ import WriteFileWebpackPlugin from 'write-file-webpack-plugin'
require('dotenv').config();
const {
URN_PREFIX,
HASHING_ALGORITHM,
APPLICATION_LABEL,
MAX_REQUEST_HISTORY,
BASE32_URN_LENGTH,
} = process.env;
const paths = {};
......@@ -83,6 +85,8 @@ const configure = (NODE_ENV: ?string) => {
_HASHING_ALGORITHM: JSON.stringify(HASHING_ALGORITHM || 'SHA-256'),
_APPLICATION_LABEL: JSON.stringify(APPLICATION_LABEL || '_haschk'),
_MAX_REQUEST_HISTORY: JSON.stringify(MAX_REQUEST_HISTORY || 1000),
_BASE32_URN_LENGTH: JSON.stringify(BASE32_URN_LENGTH || 112),
_URN_PREFIX: JSON.stringify(URN_PREFIX || '::::'),
}),
new CopyWebpackPlugin([{
......
# The port webpack-dev-server will listen on
WEBPACK_PORT=9000
# The port the hotcrp-backend uses
HOTCRP_BACKEND_PORT=3003
# The host the hotcrp-backend uses for DNS updates
HOTCRP_BACKEND_HOST=haschk.dev
# Where your webpack-dev-server lives
DEV_ENDPOINT=127.0.0.1
......@@ -13,3 +19,9 @@ APPLICATION_LABEL=_haschk
# The maximum number of requests we'll keep around to associate with downloads
MAX_REQUEST_HISTORY=1000
# The correct length of a base32-encoded URN (crockford, uppercase, w/ padding)
BASE32_URN_LENGTH=112
# The prefix used to construct URNs
URN_PREFIX=urn:hash:x:sha256:
// @flow
const Koa = require('koa');
const Router = require('koa-router');
const base32Encode = require('base32-encode');
const hexToArrayBuffer = require('hex-to-array-buffer');
const DigitalOcean = require('do-wrapper').default;
require('dotenv').config();
const api = new DigitalOcean('e6c3d3bce6571c02293d6d1b863491ce9b77f13f663b77a90d5e1c861f52b81f', 1);
const app = new Koa();
const router = new Router();
const outputBuilder = (out, prefix = '') => console[prefix == 'ERR:' ? 'error' : 'log'](`DNSCHK-hotcrp:${prefix}`, out);
const outputErrorBuilder = out => outputBuilder(out, 'ERR:');
const {
DEBUG,
NODE_ENV,
URN_PREFIX,
BASE32_URN_LENGTH,
APPLICATION_LABEL,
HOTCRP_BACKEND_HOST,
HOTCRP_BACKEND_PORT,
} = process.env;
const DEBUG_MODE = DEBUG || ['development', 'debug', 'test'].includes(NODE_ENV);
if(!URN_PREFIX) throw new Error('Bad environment: missing URN_PREFIX');
if(!BASE32_URN_LENGTH) throw new Error('Bad environment: missing BASE32_URN_LENGTH');
if(!APPLICATION_LABEL) throw new Error('Bad environment: missing APPLICATION_LABEL');
if(!HOTCRP_BACKEND_HOST) throw new Error('Bad environment: missing HOTCRP_BACKEND_HOST');
if(!HOTCRP_BACKEND_PORT) throw new Error('Bad environment: missing HOTCRP_BACKEND_PORT');
router.get('primary', '/:contentHash', async ctx => {
ctx.body = 'not ok';
ctx.status = 403;
const contentHash = (ctx.params.contentHash || '').toLowerCase()
if(!contentHash) {
outputBuilder(`(aborted request with empty or missing contentHash parameter)`);
return;
}
if(contentHash.length != 64) {
outputBuilder(`(aborted request with invalid contentHash "${contentHash}" of length ${contentHash.length} !== 64)`);
return;
}
// ? Hash file data with proper algorithm
const base32FileHash = base32Encode(hexToArrayBuffer(contentHash), 'Crockford', {
padding: false
});
DEBUG_MODE && outputBuilder(`base32FileHash: ${base32FileHash}`);
DEBUG_MODE && outputBuilder(`base32FileHash.length: ${base32FileHash.length}`);
// ? Construct BASE32 encoded URN and slice it up to yield C1 and C2
const base32Urn = base32Encode((new TextEncoder()).encode(`${URN_PREFIX}${base32FileHash}`), 'Crockford', {
padding: false
});
DEBUG_MODE && outputBuilder(`base32Urn: ${base32Urn}`);
DEBUG_MODE && outputBuilder(`base32Urn.length: ${base32Urn.length}`);
if(base32Urn.length != BASE32_URN_LENGTH) {
outputErrorBuilder(`(encountered strange base32Urn derived from "${contentHash}" of length ${base32Urn.length} != ${BASE32_URN_LENGTH})`);
return;
}
ctx.body = 'ok';
ctx.status = 200;
const [ C1, C2 ] = [
base32Urn.slice(0, base32Urn.length / 2),
base32Urn.slice(base32Urn.length / 2, base32Urn.length),
];
const recordName = `${C1}.${C2}.${APPLICATION_LABEL}`.toLowerCase();
let res = '<no response object>';
outputBuilder(`adding DNS TXT record to ${HOTCRP_BACKEND_HOST}:\ncontent hash ${ctx.params.contentHash} ==> TXT record ${recordName}`);
try {
await api.domains.getAllRecords(HOTCRP_BACKEND_HOST).then(async data => {
const filtered = JSON.parse(data).domain_records.filter(item => item.type == 'TXT' && item.name == recordName);
DEBUG_MODE && outputBuilder(`Filtered result: [ ${JSON.stringify(filtered)} ]`);
if(filtered.length)
outputBuilder('(skipping adding DNS TXT record as it already exists)');
else {
res = await api.domains.createRecord(HOTCRP_BACKEND_HOST, {
type: 'TXT',
name: recordName,
data: 'OK'
});
}
});
}
catch(e) { outputErrorBuilder(e); }
outputBuilder(DEBUG_MODE ? res : '<succeeded>');
});
app.use(router.routes());
const server = app.listen(HOTCRP_BACKEND_PORT).on('error', err => outputErrorBuilder(err));
outputBuilder(`hotcrp DNS adapter running in ${DEBUG_MODE ? 'debug' : 'production'} mode`);
if(DEBUG_MODE) {
outputBuilder(`URN_PREFIX: ${URN_PREFIX}`);
outputBuilder(`BASE32_URN_LENGTH: ${BASE32_URN_LENGTH}`);
outputBuilder(`APPLICATION_LABEL: ${APPLICATION_LABEL}`);
outputBuilder(`HOTCRP_BACKEND_HOST: ${HOTCRP_BACKEND_HOST}`);
outputBuilder(`HOTCRP_BACKEND_PORT: ${HOTCRP_BACKEND_PORT}\n`);
}
module.exports = server;
This diff is collapsed.
......@@ -43,9 +43,11 @@
],
"globals": {
"_NODE_ENV": "test",
"_HASHING_ALGORITHM": "SHA-256",
"_APPLICATION_LABEL": "_haschk",
"_MAX_REQUEST_HISTORY": "1000"
"_HASHING_ALGORITHM": "",
"_APPLICATION_LABEL": "",
"_MAX_REQUEST_HISTORY": "",
"_BASE32_URN_LENGTH": "",
"URN_PREFIX": ""
}
},
"dependencies": {
......@@ -58,10 +60,12 @@
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-flow": "^7.8.3",
"base32-encode": "^1.1.1",
"array-buffer-to-hex": "^1.0.0",
"axios": "^0.19.2",
"base32-encode": "^1.1.1",
"dotenv": "^8.2.0",
"eventemitter3": "^4.0.0",
"hex-to-array-buffer": "^1.1.0",
"install": "^0.13.0",
"url-parse": "^1.4.7"
},
......@@ -79,6 +83,7 @@
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"del": "^5.1.0",
"do-wrapper": "^4.0.0-alpha.2",
"eslint": "^6.8.0",
"eslint-plugin-flowtype": "^4.6.0",
"fancy-log": "^1.3.3",
......@@ -93,6 +98,8 @@
"jest": "^25.1.0",
"jsdoc": "^3.6.3",
"jsdoc-babel": "^0.5.0",
"koa": "^2.11.0",
"koa-router": "^8.0.8",
"parse-gitignore": "^1.0.1",
"source-map": "^0.7.3",
"source-map-support": "^0.5.16",
......
......@@ -188,7 +188,7 @@ export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
const response = await http.get(GOOGLE_DNS_HTTPS_BACKEND_EXISTS(candidate));
const data = extractAnswerDataFromResponse(response);
if(data == '"OK"') {
if(data === 'ok') {
downloadItem.backendDomain = candidate;
downloadItem.judgement = JUDGEMENT_UNDECIDED;
break loop;
......
......@@ -8,12 +8,14 @@ import { EventFrameEmitter } from 'universe/events'
import {
Debug,
URN_PREFIX,
HASHING_ALGORITHM,
GOOGLE_DNS_HTTPS_BACKEND_QUERY,
extractAnswerDataFromResponse,
JUDGEMENT_UNKNOWN,
JUDGEMENT_UNSAFE,
JUDGEMENT_SAFE,
BASE32_URN_LENGTH,
} from 'universe'
import {
......@@ -85,12 +87,12 @@ export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
});
// ? Construct BASE32 encoded URN and slice it up to yield C1 and C2
const base32Urn = base32Encode((new TextEncoder()).encode(`urn:hash::sha256:${base32FileHash}`), 'Crockford', {
const base32Urn = base32Encode((new TextEncoder()).encode(`${URN_PREFIX}${base32FileHash}`), 'Crockford', {
padding: true
});
Debug.if(() =>
base32Urn.length % 2 !== 0 && console.warn(`URN length is not an even number (${base32Urn.length})!`));
if(base32Urn.length !== BASE32_URN_LENGTH)
throw new Error(`URN length is not ${BASE32_URN_LENGTH}, got ${base32Urn.length} instead`);
const [ C1, C2 ] = [
base32Urn.slice(0, base32Urn.length / 2),
......@@ -104,9 +106,9 @@ export default (oracle: EventFrameEmitter, chrome: Chrome, context: Object) => {
Debug.log(chrome, `C1: ${C1}`);
Debug.log(chrome, `C2: ${C2}`);
Debug.log(chrome, `backend domain: ${downloadItem.backendDomain}`);
Debug.log(chrome, `query response data: ${data}`);
Debug.log(chrome, `query response data: ${data || 'null'}`);
// ? Compare DNS result with expected
oracle.emit(`judgement.${data === '"OK"' ? JUDGEMENT_SAFE : JUDGEMENT_UNSAFE}`, downloadItem);
oracle.emit(`judgement.${data === 'ok' ? JUDGEMENT_SAFE : JUDGEMENT_UNSAFE}`, downloadItem);
});
};
......@@ -14,11 +14,15 @@ declare var _NODE_ENV: string;
declare var _HASHING_ALGORITHM: string;
declare var _APPLICATION_LABEL: string;
declare var _MAX_REQUEST_HISTORY: string;
declare var _BASE32_URN_LENGTH: string;
declare var _URN_PREFIX: string;
export const NODE_ENV = _NODE_ENV;
export const URN_PREFIX = _URN_PREFIX;
export const HASHING_ALGORITHM = _HASHING_ALGORITHM;
export const APPLICATION_LABEL = _APPLICATION_LABEL;
export const MAX_REQUEST_HISTORY = parseInt(_MAX_REQUEST_HISTORY);
export const BASE32_URN_LENGTH = parseInt(_BASE32_URN_LENGTH);
// ? Returns a string HTTPS endpoint URI used to query the backend for URNs. See
// ? the paper for details on what C1, C2, and BD are.
......@@ -49,10 +53,12 @@ export const extractBDCandidatesFromURI = (uri: string) => {
/**
* Extracts the actual DNS response from the Google DoH API response object.
*
* @param {*} response The DoH response JSON object
* @param {*} response The DoH answer string or NULL if there was no answer
*/
export const extractAnswerDataFromResponse = (response: any) => {
return !response.data.Answer ? '<no answer>' : response.data.Answer.slice(-1)[0].data;
return !response.data.Answer
? null
: response.data.Answer.slice(-1)[0].data.replace(/[^0-9a-z]/gi, '').toLowerCase();
};
/**
......@@ -78,31 +84,3 @@ export const Debug = {
NODE_ENV === 'development' && fn();
}
};
/**
* Accepts an ArrayBuffer instance from the `SubtleCrypto` interface and returns
* a hex string.
*
* @param {ArrayBuffer} buffer
*/
export const bufferToHex = (buffer: ArrayBuffer) => {
let hexCodes = [];
let view = new DataView(buffer);
if(buffer.byteLength % 4 !== 0)
throw new Error('Buffer byte length must be a multiple of 4');
for(let i = 0; i < view.byteLength; i += 4) {
// ? Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
let value = view.getUint32(i)
// ? toString(16) will give the hex representation of the number without padding
let stringValue = value.toString(16)
// ? We use concatenation and slice for padding
let padding = '00000000'
let paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
// ? Join all the hex strings into one
return hexCodes.join('');
}
/* @flow */
import {
bufferToHex,
extractBDCandidatesFromURI,
} from 'universe'
......@@ -21,24 +20,3 @@ test('extractBDCandidatesFromURI returns 3LD and 2LD URI with a deep path', () =
expect(extractBDCandidatesFromURI('https://return.base.domain.xunn.io/paper/4/paper/4.faker?something=5&other&nother=fake#hash-mash.cash'))
.toEqual(['domain.xunn.io', 'xunn.io']);
});
test('bufferToHex translate buffer to hex as expected', () => {
const data = new ArrayBuffer(4);
const dataview = new DataView(data);
dataview.setUint8(0, 15);
dataview.setUint8(1, 34);
dataview.setUint8(2, 56);
dataview.setUint8(3, 79);
expect(bufferToHex(data)).toBe('0f22384f');
});
test('bufferToHex translate buffer to hex if all zeros', () => {
const data = new ArrayBuffer(8);
expect(bufferToHex(data)).toBe('0000000000000000');
});
test('bufferToHex throws if ArrayBuffer is of non-conformant byte length', () => {
const data = new ArrayBuffer(7);
expect(() => bufferToHex(data)).toThrow();
});
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment