kopia lustrzana https://gitlab.com/rysiekpl/libresilient
1954 wiersze
74 KiB
JavaScript
1954 wiersze
74 KiB
JavaScript
import {
|
|
describe,
|
|
it,
|
|
beforeEach,
|
|
beforeAll
|
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
|
|
|
import {
|
|
assert,
|
|
assertThrows,
|
|
assertRejects,
|
|
assertEquals,
|
|
assertNotEquals
|
|
} from "https://deno.land/std@0.183.0/testing/asserts.ts";
|
|
|
|
import {
|
|
assertSpyCall,
|
|
assertSpyCalls,
|
|
spy,
|
|
} from "https://deno.land/std@0.183.0/testing/mock.ts";
|
|
|
|
|
|
// elements of a query and of a response
|
|
const dohPktFrgs = {
|
|
// === header section ===
|
|
// 16 bits of flags:
|
|
//
|
|
// ,-------------- 1-bit QR header field, 0 means "question", 1 - "response"
|
|
// | ,------------ 4-bit OPCODE header field, we want 0 ("standard query")
|
|
// | | ,--------- 1-bit AA header field, only relevant in response (1 means the response is authoritative)
|
|
// | | |,-------- 1-bit TC header field, we want 0 signifying the message was not truncated
|
|
// | | ||,------- 1-bit RD header field, we want 1 signifying we want recursive resolution
|
|
// | | |||
|
|
// | | ||| ,------- 1-bit RA header field, only relevant in response (1 means recursion is available)
|
|
// | | ||| | ,----- 3-bit Z header field, reserved for future use, always 0
|
|
// | | ||| | | ,- 4-bit RCODE header field, to be set to 0, and inspected in response for error codes
|
|
// | |\ ||| | | /|
|
|
// |/ \||| |/ \/ \
|
|
qflags: [0b00000001, 0b00000000],
|
|
rflags: [0b10000001, 0b10000000],
|
|
// === question section ===
|
|
// _dnslink.resilient.is
|
|
name: [8,95,100,110,115,108,105,110,107,9,114,101,115,105,108,105,101,110,116,2,105,115,0],
|
|
type: [0,16],
|
|
class: [0,1],
|
|
// === answer section ===
|
|
// name pointer always starts with 0b11, and in our case it only makes sense
|
|
// that offset is equal to 12 (i.e. first byte of the qustion section, right after the header)
|
|
nameptr: [0b11000000, 12],
|
|
ttl: [0,0,13,110], // some positive number
|
|
// 1-byte length, followed by string response
|
|
dnslink: [100,110,115,108,105,110,107,61], // the "dnslink=" string
|
|
ipfs: [47,105,112,102,115,47], // the "/ipfs/" string
|
|
https: [47,104,116,116,112,115,47], // the "/https/" string
|
|
// example.com/test
|
|
https_addr1: [101,120,97,109,112,108,101,46,99,111,109,47,116,101,115,116],
|
|
// example.org
|
|
https_addr2: [101,120,97,109,112,108,101,46,111,114,103],
|
|
// QmiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFS
|
|
ipfs_addr: [81,109,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83,105,80,70,83],
|
|
// === additional section ===
|
|
// this should be ignored when interpreting the response
|
|
additional: [0,0,41,4,208,0,0,0,0,0,0]
|
|
}
|
|
|
|
/**
|
|
* simple yet effective
|
|
*
|
|
* examples:
|
|
*
|
|
* - valid single question packet:
|
|
* dohPacket([
|
|
* [0, 1], dohPktFrgs.qflags, [0, 1], [0, 0], [0, 0], [0, 0],
|
|
* dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class])
|
|
*
|
|
* - valid single answer packet, with name repeated in full:
|
|
* dohPacket([
|
|
* [0, 1], dohPktFrgs.rflags, [0, 1], [0, 1], [0, 0], [0, 0],
|
|
* dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class,
|
|
* dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
* [0, (dohPktFrgs.dnslink.length + dohPktFrgs.ipfs.length + dohPktFrgs.ipfs_addr.length + 1)],
|
|
* [(dohPktFrgs.dnslink.length + dohPktFrgs.ipfs.length + dohPktFrgs.ipfs_addr.length)],
|
|
* dohPktFrgs.dnslink, dohPktFrgs.ipfs, dohPktFrgs.ipfs_addr])
|
|
*
|
|
* - valid single answer packet, with name pointed to:
|
|
* dohPacket([
|
|
* [0, 1], dohPktFrgs.rflags, [0, 1], [0, 1], [0, 0], [0, 0],
|
|
* dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class,
|
|
* dohPktFrgs.nameptr, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
* [0, (dohPktFrgs.dnslink.length + dohPktFrgs.ipfs.length + dohPktFrgs.ipfs_addr.length + 1)],
|
|
* [(dohPktFrgs.dnslink.length + dohPktFrgs.ipfs.length + dohPktFrgs.ipfs_addr.length)],
|
|
* dohPktFrgs.dnslink, dohPktFrgs.ipfs, dohPktFrgs.ipfs_addr])
|
|
*/
|
|
let dohPacket = (elements) => {
|
|
// can't just elements.flat(), because that does not
|
|
// flatten Uint8Arrays, and that's what we really care about
|
|
return Uint8Array
|
|
.from(
|
|
elements
|
|
.map((el)=>{
|
|
if (el.constructor === Uint8Array) {
|
|
return Array.from(el)
|
|
} else {
|
|
return el
|
|
}
|
|
})
|
|
.flat())
|
|
}
|
|
|
|
let decodeUrlDoHRequest = (request) => {
|
|
return Uint8Array.from(
|
|
Array.from(
|
|
atob(
|
|
request
|
|
)
|
|
).map(a => a.charCodeAt(0))
|
|
)
|
|
}
|
|
|
|
beforeAll(async ()=>{
|
|
window.fetchResponse = []
|
|
window.resolvingFetch = (url, init) => {
|
|
let response_data = null
|
|
if (typeof window.fetchResponse[0] === 'object' && window.fetchResponse[0] != null) {
|
|
response_data = JSON.stringify(window.fetchResponse[0]);
|
|
} else if (typeof window.fetchResponse[0] === "function") {
|
|
response_data = window.fetchResponse[0](url, init)
|
|
} else {
|
|
response_data = window.fetchResponse[0]
|
|
}
|
|
const response = new Response(
|
|
new Blob(
|
|
[response_data],
|
|
{type: window.fetchResponse[1]}
|
|
),
|
|
{
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: {
|
|
'Last-Modified': 'TestingLastModifiedHeader'
|
|
},
|
|
url: url
|
|
});
|
|
return Promise.resolve(response);
|
|
}
|
|
|
|
/*
|
|
* prototype of the plugin init object
|
|
*/
|
|
window.initPrototype = {
|
|
name: 'dnslink-fetch'
|
|
}
|
|
})
|
|
|
|
/**
|
|
* we need to do all of this before each test in order to reset the fetch() use counter
|
|
* and make sure window.init is clean and not modified by previous tests
|
|
*/
|
|
beforeEach(()=>{
|
|
window.fetch = spy(window.resolvingFetch)
|
|
window.fetchResponse = [
|
|
{test: "success"},
|
|
"application/json"
|
|
]
|
|
window.init = {
|
|
...window.initPrototype
|
|
}
|
|
window.LR = {
|
|
log: spy((component, ...items)=>{
|
|
console.debug(component + ' :: ', ...items)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('browser: dnslink-fetch plugin', async () => {
|
|
window.LibResilientPluginConstructors = new Map()
|
|
|
|
window.fetchResponse = []
|
|
window.resolvingFetch = null
|
|
window.fetch = null
|
|
|
|
await import("../../../lib/doh.js");
|
|
await import("../../../plugins/dnslink-fetch/index.js");
|
|
|
|
it("should register in LibResilientPluginConstructors", () => {
|
|
assertEquals(
|
|
LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).name,
|
|
'dnslink-fetch'
|
|
);
|
|
});
|
|
|
|
it("should fail with bad config", () => {
|
|
init = {
|
|
name: 'dnslink-fetch',
|
|
dohProvider: false
|
|
}
|
|
assertThrows(
|
|
()=>{
|
|
return LibResilientPluginConstructors.get('dnslink-fetch')(LR, init)
|
|
},
|
|
Error,
|
|
'dohProvider not confgured'
|
|
)
|
|
init = {
|
|
name: 'dnslink-fetch',
|
|
dohMediaType: false
|
|
}
|
|
assertThrows(
|
|
()=>{
|
|
return LibResilientPluginConstructors.get('dnslink-fetch')(LR, init)
|
|
},
|
|
Error,
|
|
'dohMediaType not confgured'
|
|
)
|
|
init = {
|
|
name: 'dnslink-fetch',
|
|
dohMethod: false
|
|
}
|
|
assertThrows(
|
|
()=>{
|
|
return LibResilientPluginConstructors.get('dnslink-fetch')(LR, init)
|
|
},
|
|
Error,
|
|
'dohMethod not confgured'
|
|
)
|
|
});
|
|
|
|
it("should warn if dohMethod and dohMediaType are set such that dohMethod will be ignored", async () => {
|
|
init = {
|
|
name: 'dnslink-fetch',
|
|
dohMethod: "POST",
|
|
dohMediaType: "application/dns-json"
|
|
}
|
|
// this will fail. that's okay.
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch (e) {}
|
|
|
|
assertEquals(
|
|
LR.log.calls[0].args[1],
|
|
'Warning: "POST" dohMethod is going to be ignored as dohMediaType is not "application/dns-message"'
|
|
)
|
|
});
|
|
|
|
it("should fail if doh.js is not correctly loaded", async () => {
|
|
|
|
let f = window.resolveEndpointsJSON
|
|
window.resolveEndpointsJSON = undefined
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'resolveEndpointsJSON() is not defined, is doh.js loaded?'
|
|
)
|
|
|
|
window.resolveEndpointsJSON = f
|
|
f = window.resolveEndpointsBinary
|
|
window.resolveEndpointsBinary = undefined
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'resolveEndpointsBinary() is not defined, is doh.js loaded?'
|
|
)
|
|
|
|
window.resolveEndpointsBinary = f
|
|
})
|
|
|
|
it("should attempt to load doh.js if importScripts() is available", async () => {
|
|
window.importScripts = spy()
|
|
// this will fail. that's okay.
|
|
let plugin = LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
assertEquals(
|
|
LR.log.calls[0].args[1],
|
|
'importing doh.js'
|
|
)
|
|
assertEquals(
|
|
window.importScripts.calls[0].args[0],
|
|
"./lib/doh.js"
|
|
)
|
|
});
|
|
|
|
it("should perform a fetch against the default dohProvider endpoint, with default settings", async () => {
|
|
|
|
// this will fail after the fetch() is done
|
|
// but we only care about the fetch() being done in this test
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch (e) {}
|
|
|
|
assertSpyCall(
|
|
fetch,
|
|
0,
|
|
{
|
|
args: [
|
|
"https://dns.hostux.net/dns-query?name=_dnslink.resilient.is&type=TXT",
|
|
{"headers": {"accept": "application/json"}}
|
|
]
|
|
})
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response is not a valid JSON", async () => {
|
|
|
|
window.fetchResponse = ["not-json", "text/plain"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Unexpected token \'o\', "not-json" is not valid JSON'
|
|
)
|
|
|
|
// technically, a quoted string parses as JSON
|
|
window.fetchResponse = ["\"not-json\"", "text/plain"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response is not a valid JSON'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response is does not have a Status field", async () => {
|
|
|
|
window.fetchResponse = [{test: "success"}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'DNS request failure, status code: undefined'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response has Status other than 0", async () => {
|
|
|
|
window.fetchResponse = [{Status: 999}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'DNS request failure, status code: 999'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response does not have an Answer field", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'DNS response did not contain a valid Answer section'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response's Answer field is not an object", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0, Answer: 'invalid'}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'DNS response did not contain a valid Answer section'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response's Answer field is not an Array", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0, Answer: {}}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'DNS response did not contain a valid Answer section'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response's Answer field does not contain TXT records", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0, Answer: ['aaa', 'bbb']}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Answer section of the DNS response did not contain any TXT records'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response's Answer elements do not contain valid endpoint data", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0, Answer: [{type: 16}, {type: 16}]}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'No TXT record contained http or https endpoint definition'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if the DoH JSON API response's Answer elements do not contain valid endpoints", async () => {
|
|
|
|
window.fetchResponse = [{Status: 0, Answer: [{type: 16, data: 'aaa'}, {type: 16, data: 'bbb'}]}, "application/json"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'No TXT record contained http or https endpoint definition'
|
|
)
|
|
})
|
|
|
|
it("should successfully resolve if the DoH JSON API response contains endpoint data", async () => {
|
|
|
|
window.fetchResponse = [
|
|
{Status: 0, Answer: [
|
|
{type: 16, data: 'dnslink=/https/example.org'},
|
|
{type: 16, data: 'dnslink=/http/example.net/some/path'}
|
|
]},
|
|
"application/json"
|
|
]
|
|
|
|
// this might fail after the fetch() is done
|
|
// but we only care about the fetch() being done in this test
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// 1 fetch to resolve DNSLink,
|
|
// then 2 fetch requests to the two DNSLink-resolved endpoints
|
|
assertSpyCalls(fetch, 3)
|
|
assertSpyCall(
|
|
fetch,
|
|
1,
|
|
{
|
|
args: [
|
|
"https://example.org/test.json",
|
|
{cache: 'reload'}
|
|
]
|
|
})
|
|
assertSpyCall(
|
|
fetch,
|
|
2,
|
|
{
|
|
args: [
|
|
"http://example.net/some/path/test.json",
|
|
{cache: 'reload'}
|
|
]
|
|
})
|
|
})
|
|
|
|
it("should use the configured method when using DoH wire format", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// we should be doing a GET
|
|
assertEquals(fetch.calls[0].args[1].method, "GET")
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// we should be doing a POST
|
|
assertEquals(fetch.calls[1].args[1].method, "POST")
|
|
})
|
|
|
|
it("should error out when DoH wire format request method is other than GET or POST", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "OPTIONS"
|
|
}
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'dohMethod can only be "GET" or "POST", but is set to: OPTIONS'
|
|
)
|
|
})
|
|
|
|
it("should set Accept, Content-Length, and Content-Type headers correctly when the DoH wire format", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// we should have Accept set when doing a GET
|
|
assertEquals(
|
|
fetch.calls[0].args[1].headers['accept'],
|
|
'application/dns-message'
|
|
)
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// we should have Accept, Content-Length, and Content-Type set when doing a POST
|
|
assertEquals(
|
|
fetch.calls[1].args[1].headers['accept'],
|
|
'application/dns-message'
|
|
)
|
|
assertEquals(
|
|
fetch.calls[1].args[1].headers['content-type'],
|
|
'application/dns-message'
|
|
)
|
|
assertEquals(
|
|
fetch.calls[1].args[1].headers['content-length'],
|
|
'39'
|
|
)
|
|
})
|
|
|
|
it("should set the request data correctly when using the DoH wire format", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
// DNS wire format query packet without the two random ID bytes at the start
|
|
let qpacket = dohPacket([
|
|
dohPktFrgs.qflags, [0, 1], [0, 0], [0, 0], [0, 0],
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class
|
|
])
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
assertEquals(
|
|
// strip the actual query off of the URL, base64-decode it,
|
|
// put it in an Uint8Array, and drop the first two bytes (which are randomly generated)
|
|
decodeUrlDoHRequest(
|
|
fetch.calls[0].args[0].split('?dns=')[1]
|
|
).slice(2),
|
|
// comparing to a DoH wire format packet without the ID field
|
|
qpacket
|
|
)
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
assertEquals(
|
|
// request body without the first two bytes of randomly generated ID field
|
|
fetch.calls[1].args[1].body.slice(2),
|
|
// comparing to a DoH wire format packet without the ID field
|
|
qpacket
|
|
)
|
|
})
|
|
|
|
it("should set the request ID field randomly when using the DoH wire format", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
// this will also error out, and that's fine
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
// this is not *entirely* repeatable, as there is a *slight* chance
|
|
// that the random values will turn out the same.
|
|
//
|
|
// that probability is pretty small though.
|
|
// it will hit us at somepoint, but it won't hit us very often
|
|
assertNotEquals(
|
|
// strip the actual query off of the URL, base64-decode it,
|
|
// put it in an Uint8Array, and only use the first two butes (randomly generated request ID
|
|
decodeUrlDoHRequest(
|
|
fetch.calls[0].args[0].split('?dns=')[1]
|
|
).slice(0, 2),
|
|
decodeUrlDoHRequest(
|
|
fetch.calls[1].args[0].split('?dns=')[1]
|
|
).slice(0, 2),
|
|
"ID values are randomly generated and should differ, although there is a tiny chance that they matched randomly."
|
|
)
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
// this will error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
// this will also error out. that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
assertNotEquals(
|
|
// request body without the first two bytes of randomly generated ID field
|
|
fetch.calls[2].args[1].body.slice(0, 2),
|
|
// comparing to a DoH wire format packet without the ID field
|
|
fetch.calls[3].args[1].body.slice(0, 2),
|
|
"ID values are randomly generated and should differ, although there is a tiny chance that they matched randomly."
|
|
)
|
|
})
|
|
|
|
it("should generate a valid DoH wire format question packet", async () => {
|
|
|
|
// valid question packet, without the first two ID bytes (which are randomly generated)
|
|
let qpacket = dohPacket([
|
|
dohPktFrgs.qflags, [0, 1], [0, 0], [0, 0], [0, 0],
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class])
|
|
|
|
// we want DoH wire format, GET method
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
// this will fail, that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
assertEquals(
|
|
decodeUrlDoHRequest(
|
|
fetch.calls[0].args[0].split('?dns=')[1]
|
|
).slice(2),
|
|
qpacket
|
|
)
|
|
|
|
// we want DoH wire format, GET method
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
// this will fail, that's okay
|
|
try {
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
} catch(e) {}
|
|
|
|
assertEquals(
|
|
fetch.calls[1].args[1].body.slice(2),
|
|
qpacket
|
|
)
|
|
|
|
})
|
|
|
|
it("should throw an error if the DoH wire format response is not an application/dns-message media type", async () => {
|
|
|
|
window.fetchResponse = ["not-dns-message", "text/plain"]
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response Content-Type should be: application/dns-message; is: text/plain.'
|
|
)
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response Content-Type should be: application/dns-message; is: text/plain.'
|
|
)
|
|
})
|
|
|
|
it("should throw an error if a DoH wire format response's header is not correctly formatted ", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
"invalid",
|
|
"application/dns-message"]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: response cannot be shorter than request!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// modify the ID
|
|
response[0] += 1
|
|
response[1] += 1
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response ID does not match Request ID!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// modify the QR bit, needs to be 0b1xxxxxxx to indicate a response
|
|
response[2] = 0b00000000
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: QR bit does not indicate a response!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// modify the OPCODE flag, needs to be 0bx0000xxx in a valid response
|
|
response[2] = 0b11010000
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: OPCODE contains an unexpected value (should be zero)!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// modify the TC bit, needs to be 0bxxxxxx0x in a valid response we can handle
|
|
response[2] = 0b10000010
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: Got a truncated response. There is no reason for it, and we cannot handle it.'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// modify the RD bit, needs to be 0bxxxxxxx1 in a valid response
|
|
response[2] = 0b10000000
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: Recursive resolution was requested but this is not reflected in response.'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the Z field, needs to be 0bx000xxxx in a valid response
|
|
response[3] = 0b11110000
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: Response\'s Z field is not zeroed out!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, 1 means format error
|
|
response[3] = 0b00000001
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates a format error!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, 2 means server failure
|
|
response[3] = 0b00000010
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates a server failure!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, 3 means the name does not exist
|
|
response[3] = 0b00000011
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates a the name does not exist!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, 4 means not implemented error
|
|
response[3] = 0b00000100
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates a not implemented error!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, 5 means the request was refused
|
|
response[3] = 0b00000101
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates the request was refused!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// modify the RCODE field, any code > 5 is an error as well
|
|
response[3] = 0b00001101
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s RCODE field indicates an error (code: 13)!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// QDCOUNT needs to be 1 (we only asked one question)
|
|
response[4] = 0
|
|
response[5] = 0
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s QDCOUNT is different than request\'s QDCOUNT (should be 1)!'
|
|
)
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
response[6] = 0
|
|
response[7] = 0
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Response\'s ANCOUNT indicates no resource records received in response!'
|
|
)
|
|
|
|
})
|
|
|
|
it("should throw an error if a DoH wire format response's question section is not correctly formatted", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
(url)=>{
|
|
// take the request
|
|
let response = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
response[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
response[6] = 0
|
|
response[7] = 1
|
|
// question section needs to be otherwise repeated one-to-one
|
|
// so let's modify it in random ways
|
|
response[14] += 5
|
|
response[17] -= 3
|
|
response[response.length - 3] += 2
|
|
response[response.length - 1] += 9
|
|
// now it is our response
|
|
return response
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
return await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: QNAME, QTYPE, or QCLASS do not match between request and response.'
|
|
)
|
|
|
|
})
|
|
|
|
it("should process a valid DoH wire format response to a GET request and use the alternative endpoint it contains", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
(url, reqinit)=>{
|
|
// after the DoH wire format request we need to handle also the request to the alternative endpoint
|
|
if (url == "https://example.com/test/test.json") {
|
|
return JSON.stringify({test: "success"})
|
|
// this is handling the DoH wire format request
|
|
} else {
|
|
// take the request
|
|
let request = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
request[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
request[6] = 0
|
|
request[7] = 1
|
|
// responses start after the question section
|
|
// so we can just bolt it directly to the request and call it a day, kind of
|
|
let response = dohPacket([
|
|
request,
|
|
...response_data
|
|
])
|
|
// now it is our response
|
|
return response
|
|
}
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
let response_data = [
|
|
// name, type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
// this should succeed
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
|
|
assertSpyCall(fetch, 1, {args: [
|
|
"https://example.com/test/test.json",
|
|
{
|
|
cache: "reload",
|
|
}]
|
|
})
|
|
|
|
// now same, but with a name pointer instead of a full name in the response
|
|
response_data = [
|
|
// pointer to the name in question part, type, class, TTL
|
|
dohPktFrgs.nameptr, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
// this should succeed
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
|
|
assertSpyCall(fetch, 3, {args: [
|
|
"https://example.com/test/test.json",
|
|
{
|
|
cache: "reload",
|
|
}]
|
|
})
|
|
})
|
|
|
|
it("should process a valid DoH wire format response to a POST request and use the alternative endpoint it contains", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
(url, reqinit)=>{
|
|
// after the DoH wire format request we need to handle also the request to the alternative endpoint
|
|
if (url == "https://example.com/test/test.json") {
|
|
return JSON.stringify({test: "success"})
|
|
// this is handling the DoH wire format request
|
|
} else {
|
|
// take the request body
|
|
let request = reqinit.body
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
request[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
request[6] = 0
|
|
request[7] = 1
|
|
// responses start after the question section
|
|
// so we can just bolt it directly to the request and call it a day, kind of
|
|
let response = dohPacket([
|
|
request,
|
|
...response_data
|
|
])
|
|
// now it is our response
|
|
return response
|
|
}
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
let response_data = [
|
|
// name, type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
// this should succeed
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
|
|
assertSpyCall(fetch, 1, {args: [
|
|
"https://example.com/test/test.json",
|
|
{
|
|
cache: "reload",
|
|
}]
|
|
})
|
|
|
|
// now same, but with a name pointer instead of a full name in the response
|
|
response_data = [
|
|
// pointer to the name in question part, type, class, TTL
|
|
dohPktFrgs.nameptr, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
// this should succeed
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
|
|
assertSpyCall(fetch, 3, {args: [
|
|
"https://example.com/test/test.json",
|
|
{
|
|
cache: "reload",
|
|
}]
|
|
})
|
|
})
|
|
|
|
it("should error out on invalid DoH wire format responses to a GET request", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "GET"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
(url, reqinit)=>{
|
|
// after the DoH wire format request we need to handle also the request to the alternative endpoint
|
|
if (url == "https://example.com/test/test.json") {
|
|
return JSON.stringify({test: "success"})
|
|
// this is handling the DoH wire format request
|
|
} else {
|
|
// take the request
|
|
let request = decodeUrlDoHRequest(
|
|
url.split('?dns=')[1]
|
|
)
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
request[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
request[6] = 0
|
|
request[7] = 1
|
|
// responses start after the question section
|
|
// so we can just bolt it directly to the request and call it a day, kind of
|
|
let response = dohPacket([
|
|
request,
|
|
...response_data
|
|
])
|
|
// now it is our response
|
|
return response
|
|
}
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
// invalid response: invalid pointer offset
|
|
let response_data = [
|
|
// invalid nameptr, type, class, TTL
|
|
dohPktFrgs.nameptr[0], dohPktFrgs.nameptr[1] + 5, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: unexpected name pointer offset 17 (expected: 12)'
|
|
)
|
|
|
|
// invalid response: wrong name
|
|
let badname = [...dohPktFrgs.name]
|
|
badname[4] += 1
|
|
badname[5] += 1
|
|
response_data = [
|
|
// wrong name, type, class, TTL
|
|
badname, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: NAME in an answer does not match the QNAME from the request.'
|
|
)
|
|
|
|
// invalid response: the first byte of the response has to start
|
|
// with 0b11 (a name pointer) or 0b00 (a name)
|
|
badname = [...dohPktFrgs.name]
|
|
badname[0] = badname[0] | 0b01000000;
|
|
response_data = [
|
|
// bad byte name, type, class, TTL
|
|
badname, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: answer's NAME starts with something else than 0b11 or 0b00."
|
|
)
|
|
|
|
// invalid response: type not indicating a TXT record
|
|
response_data = [
|
|
// name, invalid type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type[0], dohPktFrgs.type[1] + 4, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"No TXT record contained http or https endpoint definition"
|
|
)
|
|
|
|
// invalid response: incorrect class
|
|
response_data = [
|
|
// name, type, incorrect class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class[0], dohPktFrgs.class[1] + 10, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: unexpected CLASS: 11"
|
|
)
|
|
|
|
// invalid response: RDLENGTH must not be zero
|
|
response_data = [
|
|
// name, type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// incorrect RDLENGTH
|
|
[0, 0],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: RDLENGTH is zero"
|
|
)
|
|
|
|
// invalid response: RDLENGTH must be 1 higher than TXT RR length field
|
|
response_data = [
|
|
// name, type, incorrect class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// incorrect TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: RDLENGTH does not match TXT record length"
|
|
)
|
|
})
|
|
|
|
it("should error out on invalid DoH wire format responses to a POST request", async () => {
|
|
|
|
init = {
|
|
name: "dnslink-fetch",
|
|
dohMediaType: "application/dns-message",
|
|
dohMethod: "POST"
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
(url, reqinit)=>{
|
|
// after the DoH wire format request we need to handle also the request to the alternative endpoint
|
|
if (url == "https://example.com/test/test.json") {
|
|
return JSON.stringify({test: "success"})
|
|
// this is handling the DoH wire format request
|
|
} else {
|
|
// take the request
|
|
let request = reqinit.body
|
|
// response[2] needs to be valid for a response
|
|
// we're taking this from a request, so we need to massage it
|
|
request[2] = 0b10000001
|
|
// ANCOUNT needs to be 1 or higher (we do want a response)
|
|
request[6] = 0
|
|
request[7] = 1
|
|
// responses start after the question section
|
|
// so we can just bolt it directly to the request and call it a day, kind of
|
|
let response = dohPacket([
|
|
request,
|
|
...response_data
|
|
])
|
|
// now it is our response
|
|
return response
|
|
}
|
|
},
|
|
"application/dns-message"
|
|
]
|
|
|
|
// invalid response: invalid pointer offset
|
|
let response_data = [
|
|
// invalid nameptr, type, class, TTL
|
|
dohPktFrgs.nameptr[0], dohPktFrgs.nameptr[1] + 5, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: unexpected name pointer offset 17 (expected: 12)'
|
|
)
|
|
|
|
// invalid response: wrong name
|
|
let badname = [...dohPktFrgs.name]
|
|
badname[4] += 1
|
|
badname[5] += 1
|
|
response_data = [
|
|
// wrong name, type, class, TTL
|
|
badname, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
'Invalid response: NAME in an answer does not match the QNAME from the request.'
|
|
)
|
|
|
|
// invalid response: the first byte of the response has to start
|
|
// with 0b11 (a name pointer) or 0b00 (a name)
|
|
badname = [...dohPktFrgs.name]
|
|
badname[0] = badname[0] | 0b01000000;
|
|
response_data = [
|
|
// bad byte name, type, class, TTL
|
|
badname, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: answer's NAME starts with something else than 0b11 or 0b00."
|
|
)
|
|
|
|
// invalid response: type not indicating a TXT record
|
|
response_data = [
|
|
// name, invalid type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type[0], dohPktFrgs.type[1] + 4, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"No TXT record contained http or https endpoint definition"
|
|
)
|
|
|
|
// invalid response: incorrect class
|
|
response_data = [
|
|
// name, type, incorrect class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class[0], dohPktFrgs.class[1] + 10, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: unexpected CLASS: 11"
|
|
)
|
|
|
|
// invalid response: RDLENGTH must not be zero
|
|
response_data = [
|
|
// name, type, class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// incorrect RDLENGTH
|
|
[0, 0],
|
|
// TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: RDLENGTH is zero"
|
|
)
|
|
|
|
// invalid response: RDLENGTH must be 1 higher than TXT RR length field
|
|
response_data = [
|
|
// name, type, incorrect class, TTL
|
|
dohPktFrgs.name, dohPktFrgs.type, dohPktFrgs.class, dohPktFrgs.ttl,
|
|
// RDLENGTH
|
|
[0, (dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// incorrect TXT RR length
|
|
[(dohPktFrgs.dnslink.length + dohPktFrgs.https.length + dohPktFrgs.https_addr1.length + 1)],
|
|
// TXT RR: dnslink=/https/...
|
|
dohPktFrgs.dnslink, dohPktFrgs.https, dohPktFrgs.https_addr1
|
|
]
|
|
|
|
assertRejects(
|
|
async ()=>{
|
|
await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
},
|
|
Error,
|
|
"Invalid response: RDLENGTH does not match TXT record length"
|
|
)
|
|
})
|
|
|
|
it("should fetch the content, trying all DNSLink-resolved endpoints (if fewer or equal to concurrency setting)", async () => {
|
|
|
|
window.fetchResponse = [
|
|
{Status: 0, Answer: [
|
|
{type: 16, data: 'dnslink=/https/example.org'},
|
|
{type: 16, data: 'dnslink=/http/example.net/some/path'}
|
|
]},
|
|
"application/json"
|
|
]
|
|
const response = await LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
|
|
// 1 fetch to resolve DNSLink,
|
|
// then 2 fetch requests to the two DNSLink-resolved endpoints
|
|
assertSpyCalls(fetch, 3)
|
|
assertEquals(await response.json(), window.fetchResponse[0])
|
|
assertSpyCall(
|
|
fetch,
|
|
1,
|
|
{
|
|
args: [
|
|
"https://example.org/test.json",
|
|
{cache: 'reload'}
|
|
]
|
|
})
|
|
assertSpyCall(
|
|
fetch,
|
|
2,
|
|
{
|
|
args: [
|
|
"http://example.net/some/path/test.json",
|
|
{cache: 'reload'}
|
|
]
|
|
})
|
|
})
|
|
|
|
it("should fetch the content, trying <concurrency> random endpoints out of all DNSLink-resolved endpoints (if more than concurrency setting)", async () => {
|
|
|
|
init.concurrency = 3
|
|
|
|
window.fetchResponse = [
|
|
{Status: 0, Answer: [
|
|
{type: 16, data: 'dnslink=/https/example.org'},
|
|
{type: 16, data: 'dnslink=/http/example.net/some/path'},
|
|
{type: 16, data: 'dnslink=/https/example.net/some/path'},
|
|
{type: 16, data: 'dnslink=/https/example.net/some/other/path'}
|
|
]},
|
|
"application/json"
|
|
]
|
|
const response = await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json');
|
|
|
|
// 1 fetch to resolve DNSLink,
|
|
// then 3 fetch requests to random three of the five DNSLink-resolved endpoints
|
|
assertSpyCalls(fetch, 4)
|
|
assertEquals(await response.json(), window.fetchResponse[0])
|
|
})
|
|
|
|
it("should pass the Request() init data to fetch() for all used endpoints", async () => {
|
|
|
|
var initTest = {
|
|
method: "GET",
|
|
headers: new Headers({"x-stub": "STUB"}),
|
|
mode: "mode-stub",
|
|
credentials: "credentials-stub",
|
|
cache: "cache-stub",
|
|
referrer: "referrer-stub",
|
|
// these are not implemented by service-worker-mock
|
|
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
|
|
redirect: undefined,
|
|
integrity: undefined,
|
|
cache: undefined
|
|
}
|
|
|
|
window.fetchResponse = [
|
|
{Status: 0, Answer: [
|
|
{type: 16, data: 'dnslink=/https/example.org'},
|
|
{type: 16, data: 'dnslink=/http/example.net/some/path'},
|
|
{type: 16, data: 'dnslink=/https/example.net/some/path'}
|
|
]},
|
|
"application/json"
|
|
]
|
|
|
|
const response = await LibResilientPluginConstructors
|
|
.get('dnslink-fetch')(LR, init)
|
|
.fetch('https://resilient.is/test.json', initTest);
|
|
|
|
// 1 fetch to resolve DNSLink,
|
|
// then 3 fetch requests to the three DNSLink-resolved endpoints
|
|
assertSpyCalls(fetch, 4)
|
|
assertEquals(await response.json(), window.fetchResponse[0])
|
|
|
|
assertSpyCall(
|
|
fetch,
|
|
1,
|
|
{
|
|
args: [
|
|
"https://example.org/test.json",
|
|
initTest
|
|
]
|
|
})
|
|
assertSpyCall(
|
|
fetch,
|
|
2,
|
|
{
|
|
args: [
|
|
"http://example.net/some/path/test.json",
|
|
initTest
|
|
]
|
|
})
|
|
assertSpyCall(
|
|
fetch,
|
|
3,
|
|
{
|
|
args: [
|
|
"https://example.net/some/path/test.json",
|
|
initTest
|
|
]
|
|
})
|
|
})
|
|
|
|
|
|
it("should set the LibResilient headers, setting X-LibResilient-ETag based on Last-Modified (if ETag is unavailable in the original response)", async () => {
|
|
|
|
window.fetchResponse = [
|
|
{Status: 0, Answer: [
|
|
{type: 16, data: 'dnslink=/https/example.org'},
|
|
{type: 16, data: 'dnslink=/http/example.net/some/path'}
|
|
]},
|
|
"application/json"]
|
|
|
|
const response = await LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
|
|
// 1 fetch to resolve DNSLink,
|
|
// then 3 fetch requests to the three DNSLink-resolved endpoints
|
|
assertSpyCalls(fetch, 3)
|
|
assertEquals(await response.json(), window.fetchResponse[0])
|
|
assert(response.headers.has('X-LibResilient-Method'))
|
|
assert(response.headers.has('X-LibResilient-Etag'))
|
|
assertEquals(response.headers.get('X-LibResilient-Method'), 'dnslink-fetch')
|
|
assertEquals(response.headers.get('X-LibResilient-Etag'), 'TestingLastModifiedHeader')
|
|
});
|
|
})
|