зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1540656 - Use dns-packet node module in test_trr.js r=dragana
Differential Revision: https://phabricator.services.mozilla.com/D25671 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
0c4d313f27
Коммит
866b54da1a
|
@ -106,20 +106,20 @@ class DNSListener {
|
|||
add_task(async function test1() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`);
|
||||
|
||||
await new DNSListener("bar.example.com", "127.0.0.1");
|
||||
await new DNSListener("bar.example.com", "2.2.2.2");
|
||||
});
|
||||
|
||||
// verify basic A record - without bootstrapping
|
||||
add_task(async function test1b() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`);
|
||||
Services.prefs.clearUserPref("network.trr.bootstrapAddress");
|
||||
Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
|
||||
|
||||
await new DNSListener("bar.example.com", "127.0.0.1");
|
||||
await new DNSListener("bar.example.com", "3.3.3.3");
|
||||
});
|
||||
|
||||
// verify that the name was put in cache - it works with bad DNS URI
|
||||
|
@ -128,24 +128,24 @@ add_task(async function test2() {
|
|||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/404`);
|
||||
|
||||
await new DNSListener("bar.example.com", "127.0.0.1");
|
||||
await new DNSListener("bar.example.com", "3.3.3.3");
|
||||
});
|
||||
|
||||
// verify working credentials in DOH request
|
||||
add_task(async function test3() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-auth`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true`);
|
||||
Services.prefs.setCharPref("network.trr.credentials", "user:password");
|
||||
|
||||
await new DNSListener("bar.example.com", "127.0.0.1");
|
||||
await new DNSListener("bar.example.com", "4.4.4.4");
|
||||
});
|
||||
|
||||
// verify failing credentials in DOH request
|
||||
add_task(async function test4() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-auth`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true`);
|
||||
Services.prefs.setCharPref("network.trr.credentials", "evil:person");
|
||||
|
||||
let [, , inStatus] = await new DNSListener("wrong.example.com", undefined, false);
|
||||
|
@ -156,9 +156,9 @@ add_task(async function test4() {
|
|||
add_task(async function test5() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-push`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=5.5.5.5&push=true`);
|
||||
|
||||
await new DNSListener("first.example.com", "127.0.0.1");
|
||||
await new DNSListener("first.example.com", "5.5.5.5");
|
||||
});
|
||||
|
||||
add_task(async function test5b() {
|
||||
|
@ -175,7 +175,7 @@ add_task(async function test5b() {
|
|||
add_task(async function test6() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-aaaa`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020`);
|
||||
await new DNSListener("aaaa.example.com", "2020:2020::2020");
|
||||
});
|
||||
|
||||
|
@ -183,7 +183,7 @@ add_task(async function test6() {
|
|||
add_task(async function test7() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-rfc1918`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1`);
|
||||
let [, , inStatus] = await new DNSListener("rfc1918.example.com", undefined, false);
|
||||
Assert.ok(!Components.isSuccessCode(inStatus), `${inStatus} should be an error code`);
|
||||
});
|
||||
|
@ -192,7 +192,7 @@ add_task(async function test7() {
|
|||
add_task(async function test8() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-rfc1918`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1`);
|
||||
Services.prefs.setBoolPref("network.trr.allow-rfc1918", true);
|
||||
await new DNSListener("rfc1918.example.com", "192.168.0.1");
|
||||
});
|
||||
|
@ -202,7 +202,7 @@ add_task(async function test8() {
|
|||
add_task(async function test8b() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ecs{?dns}`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh{?dns}`);
|
||||
Services.prefs.clearUserPref("network.trr.allow-rfc1918");
|
||||
Services.prefs.setBoolPref("network.trr.useGET", true);
|
||||
Services.prefs.setBoolPref("network.trr.disable-ECS", true);
|
||||
|
@ -213,11 +213,11 @@ add_task(async function test8b() {
|
|||
add_task(async function test9() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-get`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh`);
|
||||
Services.prefs.clearUserPref("network.trr.allow-rfc1918");
|
||||
Services.prefs.setBoolPref("network.trr.useGET", true);
|
||||
Services.prefs.setBoolPref("network.trr.disable-ECS", false);
|
||||
await new DNSListener("get.example.com", "1.2.3.4");
|
||||
await new DNSListener("get.example.com", "5.5.5.5");
|
||||
});
|
||||
|
||||
// confirmationNS set without confirmed NS yet
|
||||
|
@ -362,8 +362,8 @@ add_task(async function test21() {
|
|||
await new DNSListener("test21.example.com", "127.0.0.1");
|
||||
});
|
||||
|
||||
// verify that basic A record name mismatch gets rejected. Gets the same DOH
|
||||
// response back as test1
|
||||
// verify that basic A record name mismatch gets rejected.
|
||||
// Gets a response for bar.example.com instead of what it requested
|
||||
add_task(async function test22() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only to avoid native fallback
|
||||
|
@ -385,7 +385,7 @@ add_task(async function test24() {
|
|||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first
|
||||
Services.prefs.setCharPref("network.trr.excluded-domains", "");
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ip`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`);
|
||||
await new DNSListener("bar.example.com", "192.192.192.192");
|
||||
});
|
||||
|
||||
|
@ -403,15 +403,15 @@ add_task(async function test24c() {
|
|||
await new DNSListener("bar.example.com", "127.0.0.1");
|
||||
});
|
||||
|
||||
// TRR-only check that localhost doesn't work if not in the excluded-domains list
|
||||
// TRR-only that resolving localhost with TRR-only mode will use the remote
|
||||
// resolver if it's not in the excluded domains
|
||||
add_task(async function test25() {
|
||||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.excluded-domains", "");
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ip`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`);
|
||||
|
||||
let [, , inStatus] = await new DNSListener("localhost", "127.0.0.1", false);
|
||||
Assert.ok(!Components.isSuccessCode(inStatus), `${inStatus} should be an error code`);
|
||||
await new DNSListener("localhost", "192.192.192.192", true);
|
||||
});
|
||||
|
||||
// TRR-only check that localhost goes directly to native lookup when in the excluded-domains
|
||||
|
@ -419,7 +419,7 @@ add_task(async function test25b() {
|
|||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.excluded-domains", "localhost");
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ip`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`);
|
||||
|
||||
await new DNSListener("localhost", "127.0.0.1");
|
||||
});
|
||||
|
@ -429,7 +429,7 @@ add_task(async function test25c() {
|
|||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.excluded-domains", "localhost,local");
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ip`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`);
|
||||
|
||||
await new DNSListener("test.local", "127.0.0.1");
|
||||
});
|
||||
|
@ -439,7 +439,7 @@ add_task(async function test25d() {
|
|||
dns.clearCache(true);
|
||||
Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only
|
||||
Services.prefs.setCharPref("network.trr.excluded-domains", "localhost,local,other");
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/dns-ip`);
|
||||
Services.prefs.setCharPref("network.trr.uri", `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192`);
|
||||
|
||||
await new DNSListener("domain.other", "127.0.0.1");
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ var http2 = require(node_http2_root);
|
|||
var fs = require('fs');
|
||||
var url = require('url');
|
||||
var crypto = require('crypto');
|
||||
const dnsPacket = require('../dns-packet');
|
||||
const ip = require('../node-ip');
|
||||
|
||||
// Hook into the decompression code to log the decompressed name-value pairs
|
||||
var compression_module = node_http2_root + "/lib/protocol/compressor";
|
||||
|
@ -550,6 +552,97 @@ function handleRequest(req, res) {
|
|||
return;
|
||||
|
||||
}
|
||||
else if (u.pathname == "/doh") {
|
||||
ns_confirm = 0; // back to first reply for dns-confirm
|
||||
cname_confirm = 0; // back to first reply for dns-cname
|
||||
|
||||
let params = new url.URL(`http://localhost${req.url}`).searchParams;
|
||||
let responseIP = params.get("responseIP");
|
||||
if (!responseIP) {
|
||||
responseIP = "5.5.5.5";
|
||||
}
|
||||
|
||||
if (params.get("auth")) {
|
||||
// There's a Set-Cookie: header in the response for "/dns" , which this
|
||||
// request subsequently would include if the http channel wasn't
|
||||
// anonymous. Thus, if there's a cookie in this request, we know Firefox
|
||||
// mishaved. If there's not, we're fine.
|
||||
if (req.headers['cookie']) {
|
||||
res.writeHead(403);
|
||||
res.end("cookie for me, not for you");
|
||||
return;
|
||||
}
|
||||
if (req.headers['authorization'] != "user:password") {
|
||||
res.writeHead(401);
|
||||
res.end("bad boy!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.get("push")) {
|
||||
// push.example.com has AAAA entry 2018::2018
|
||||
var pcontent= new Buffer("0000010000010001000000000470757368076578616D706C6503636F6D00001C0001C00C001C000100000037001020180000000000000000000000002018", "hex");
|
||||
push = res.push({
|
||||
hostname: 'foo.example.com:' + serverPort,
|
||||
port: serverPort,
|
||||
path: '/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNjb20AABwAAQ',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept' : 'application/dns-message'
|
||||
}
|
||||
});
|
||||
push.writeHead(200, {
|
||||
'content-type': 'application/dns-message',
|
||||
'pushed' : 'yes',
|
||||
'content-length' : pcontent.length,
|
||||
'X-Connection-Http2': 'yes'
|
||||
});
|
||||
push.end(pcontent);
|
||||
}
|
||||
|
||||
let payload = new Buffer("");
|
||||
|
||||
function emitResponse(response, requestPayload) {
|
||||
let packet = dnsPacket.decode(requestPayload);
|
||||
|
||||
let buf = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: packet.id,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: packet.questions,
|
||||
answers: [{
|
||||
name: packet.questions[0].name,
|
||||
ttl: 55,
|
||||
type: ip.isV4Format(responseIP) ? "A" : "AAAA",
|
||||
flush: false,
|
||||
data: responseIP,
|
||||
}],
|
||||
});
|
||||
|
||||
response.setHeader('Content-Length', buf.length);
|
||||
response.setHeader('Set-Cookie', 'trackyou=yes; path=/; max-age=100000;');
|
||||
response.setHeader('Content-Type', 'application/dns-message');
|
||||
response.writeHead(200);
|
||||
response.write(buf);
|
||||
response.end("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.get("dns")) {
|
||||
payload = Buffer.from(params.get("dns"), 'base64');
|
||||
emitResponse(res, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
req.on('data', function receiveData(chunk) {
|
||||
payload = Buffer.concat([payload, chunk]);
|
||||
});
|
||||
req.on('end', function finishedData() {
|
||||
emitResponse(res, payload);
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (u.pathname === "/dns-cname-a") {
|
||||
// test23 asks for cname-a.example.com
|
||||
// this responds with a CNAME to here.example.com *and* an A record
|
||||
|
@ -603,57 +696,6 @@ function handleRequest(req, res) {
|
|||
res.end("");
|
||||
return;
|
||||
|
||||
}
|
||||
// for use with test_trr.js, test8b
|
||||
else if (u.path === "/dns-ecs?dns=AAABAAABAAAAAAABA2VjcwdleGFtcGxlA2NvbQAAAQABAAApEAAAAAAAAAgACAAEAAEAAA") {
|
||||
// the query string asks for an A entry for ecs.example.com
|
||||
// ecs.example.com has A entry 5.5.5.5
|
||||
var content= new Buffer("00000100000100010000000003656373076578616D706C6503636F6D0000010001C00C0001000100000037000405050505", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
// for use with test_trr.js
|
||||
else if (u.path === "/dns-get?dns=AAABAAABAAAAAAAAA2dldAdleGFtcGxlA2NvbQAAAQAB") {
|
||||
// the query string asks for an A entry for get.example.com
|
||||
// get.example.com has A entry 1.2.3.4
|
||||
var content= new Buffer("00000100000100010000000003676574076578616D706C6503636F6D0000010001C00C0001000100000037000401020304", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
ns_confirm = 0; // back to first reply for dns-confirm
|
||||
cname_confirm = 0; // back to first reply for dns-cname
|
||||
return;
|
||||
}
|
||||
// for use with test_trr.js
|
||||
else if (u.pathname === "/dns") {
|
||||
// bar.example.com has A entry 127.0.0.1
|
||||
var content= new Buffer("00000100000100010000000003626172076578616D706C6503636F6D0000010001C00C000100010000003700047F000001", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
// pass back a cookie here, check it in /dns-auth
|
||||
res.setHeader('Set-Cookie', 'trackyou=yes; path=/; max-age=100000;');
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
else if (u.pathname === "/dns-ip") {
|
||||
// bar.example.com has A entry 192.192.192.192
|
||||
var content= new Buffer("00000100000100010000000003626172076578616D706C6503636F6D0000010001C00C00010001000000370004C0C0C0C0", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
// pass back a cookie here, check it in /dns-auth
|
||||
res.setHeader('Set-Cookie', 'trackyou=yes; path=/; max-age=100000;');
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
else if (u.pathname === "/dns-ns") {
|
||||
// confirm.example.com has NS entry ns.example.com
|
||||
|
@ -694,82 +736,6 @@ function handleRequest(req, res) {
|
|||
res.end("");
|
||||
return;
|
||||
}
|
||||
// for use with test_trr.js
|
||||
else if (u.pathname === "/dns-aaaa") {
|
||||
// aaaa.example.com has AAAA entry 2020:2020::2020
|
||||
var content= new Buffer("0000010000010001000000000461616161076578616D706C6503636F6D00001C0001C00C001C000100000037001020202020000000000000000000002020", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
else if (u.pathname === "/dns-rfc1918") {
|
||||
// rfc1918.example.com has A entry 192.168.0.1
|
||||
var content= new Buffer("0000010000010001000000000772666331393138076578616D706C6503636F6D0000010001C00C00010001000000370004C0A80001", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
// for use with test_trr.js
|
||||
else if (u.pathname === "/dns-push") {
|
||||
// first.example.com has A entry 127.0.0.1
|
||||
var content= new Buffer("000001000001000100000000056669727374076578616D706C6503636F6D0000010001C00C000100010000003700047F000001", "hex");
|
||||
// push.example.com has AAAA entry 2018::2018
|
||||
var pcontent= new Buffer("0000010000010001000000000470757368076578616D706C6503636F6D00001C0001C00C001C000100000037001020180000000000000000000000002018", "hex");
|
||||
push = res.push({
|
||||
hostname: 'foo.example.com:' + serverPort,
|
||||
port: serverPort,
|
||||
path: '/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNjb20AABwAAQ',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept' : 'application/dns-message'
|
||||
}
|
||||
});
|
||||
push.writeHead(200, {
|
||||
'content-type': 'application/dns-message',
|
||||
'pushed' : 'yes',
|
||||
'content-length' : pcontent.length,
|
||||
'X-Connection-Http2': 'yes'
|
||||
});
|
||||
push.end(pcontent);
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
// for use with test_trr.js
|
||||
else if (u.pathname === "/dns-auth") {
|
||||
// There's a Set-Cookie: header in the response for "/dns" , which this
|
||||
// request subsequently would include if the http channel wasn't
|
||||
// anonymous. Thus, if there's a cookie in this request, we know Firefox
|
||||
// mishaved. If there's not, we're fine.
|
||||
if (req.headers['cookie']) {
|
||||
res.writeHead(403);
|
||||
res.end("cookie for me, not for you");
|
||||
return;
|
||||
}
|
||||
if (req.headers['authorization'] != "user:password") {
|
||||
res.writeHead(401);
|
||||
res.end("bad boy!");
|
||||
return;
|
||||
}
|
||||
// bar.example.com has A entry 127.0.0.1
|
||||
var content= new Buffer("00000100000100010000000003626172076578616D706C6503636F6D0000010001C00C000100010000003700047F000001", "hex");
|
||||
res.setHeader('Content-Type', 'application/dns-message');
|
||||
res.setHeader('Content-Length', content.length);
|
||||
res.writeHead(200);
|
||||
res.write(content);
|
||||
res.end("");
|
||||
return;
|
||||
}
|
||||
|
||||
// for use with test_esni_dns_fetch.js
|
||||
else if (u.pathname === "/esni-dns") {
|
||||
content = new Buffer("0000" +
|
||||
|
|
Загрузка…
Ссылка в новой задаче