adopt latest ESRP changes (#5)
This commit is contained in:
Родитель
4b42060da6
Коммит
2366d8ec30
|
@ -41,29 +41,12 @@ steps:
|
|||
go build -v -o "$(Build.BinariesDirectory)\\vscode-winsta11er-insiders-x64.exe" -ldflags -H=windowsgui ./insiders
|
||||
displayName: Build x64 (Insiders)
|
||||
|
||||
- task: AzureKeyVault@1
|
||||
inputs:
|
||||
azureSubscription: "vscode-builds-subscription"
|
||||
KeyVaultName: vscode
|
||||
SecretsFilter: "ESRP-PKI,esrp-aad-username,esrp-aad-password"
|
||||
displayName: Azure Key Vault
|
||||
- template: ./setup-codesign.yml
|
||||
|
||||
- task: EsrpClientTool@1
|
||||
displayName: Download ESRPClient
|
||||
|
||||
- powershell: |
|
||||
$EsrpClientTool = (gci -directory -filter EsrpClientTool_* $(Agent.RootDirectory)\_tasks | Select-Object -last 1).FullName
|
||||
$EsrpCliZip = (gci -recurse -filter esrpcli.*.zip $EsrpClientTool | Select-Object -last 1).FullName
|
||||
mkdir -p $(Agent.TempDirectory)\esrpcli
|
||||
Expand-Archive -Path $EsrpCliZip -DestinationPath $(Agent.TempDirectory)\esrpcli
|
||||
$EsrpCliDllPath = (gci -recurse -filter esrpcli.dll $(Agent.TempDirectory)\esrpcli | Select-Object -last 1).FullName
|
||||
echo "##vso[task.setvariable variable=EsrpCliDllPath]$EsrpCliDllPath"
|
||||
displayName: Find ESRP CLI
|
||||
|
||||
- powershell: |
|
||||
yarn --cwd build
|
||||
node .\build\sign $env:EsrpCliDllPath windows $(ESRP-PKI) $(esrp-aad-username) $(esrp-aad-password) $(Build.BinariesDirectory) 'vscode-winsta11er-*.exe'
|
||||
- powershell: node $(Agent.TempDirectory)/sign.js $(EsrpCliDllPath) windows $(Build.BinariesDirectory) 'vscode-winsta11er-*.exe'
|
||||
displayName: Codesign
|
||||
env:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
|
||||
- task: GitHubRelease@1
|
||||
inputs:
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"name": "vscode-winsta11er-build",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tmp": "^0.2.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
steps:
|
||||
- task: UseDotNet@2
|
||||
inputs:
|
||||
version: 6.x
|
||||
|
||||
- task: EsrpCodeSigning@5
|
||||
inputs:
|
||||
UseMSIAuthentication: true
|
||||
ConnectedServiceName: vscode-esrp
|
||||
AppRegistrationClientId: c24324f7-e65f-4c45-8702-ed2d4c35df99
|
||||
AppRegistrationTenantId: 975f013f-7f24-47e8-a7d3-abc4752bf346
|
||||
AuthAKVName: vscode-esrp
|
||||
AuthSignCertName: esrp-sign
|
||||
FolderPath: .
|
||||
Pattern: noop
|
||||
displayName: 'Install ESRP Tooling'
|
||||
|
||||
- powershell: |
|
||||
$EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)\_tasks | Select-Object -last 1).FullName
|
||||
$Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName
|
||||
echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll"
|
||||
displayName: Find ESRP CLI
|
||||
|
||||
- pwsh: |
|
||||
$content = @"
|
||||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
function getParams(type) {
|
||||
switch (type) {
|
||||
case 'sign-windows':
|
||||
return [
|
||||
{
|
||||
keyCode: 'CP-230012',
|
||||
operationSetCode: 'SigntoolSign',
|
||||
parameters: [
|
||||
{ parameterName: 'OpusName', parameterValue: 'VS Code' },
|
||||
{ parameterName: 'OpusInfo', parameterValue: 'https://code.visualstudio.com/' },
|
||||
{ parameterName: 'Append', parameterValue: '/as' },
|
||||
{ parameterName: 'FileDigest', parameterValue: '/fd "SHA256"' },
|
||||
{ parameterName: 'PageHash', parameterValue: '/NPH' },
|
||||
{ parameterName: 'TimeStamp', parameterValue: '/tr "http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer" /td sha256' }
|
||||
],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
},
|
||||
{
|
||||
keyCode: 'CP-230012',
|
||||
operationSetCode: 'SigntoolVerify',
|
||||
parameters: [
|
||||
{ parameterName: 'VerifyAll', parameterValue: '/all' }
|
||||
],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}
|
||||
];
|
||||
case 'sign-windows-appx':
|
||||
return [
|
||||
{
|
||||
keyCode: 'CP-229979',
|
||||
operationSetCode: 'SigntoolSign',
|
||||
parameters: [
|
||||
{ parameterName: 'OpusName', parameterValue: 'VS Code' },
|
||||
{ parameterName: 'OpusInfo', parameterValue: 'https://code.visualstudio.com/' },
|
||||
{ parameterName: 'FileDigest', parameterValue: '/fd "SHA256"' },
|
||||
{ parameterName: 'PageHash', parameterValue: '/NPH' },
|
||||
{ parameterName: 'TimeStamp', parameterValue: '/tr "http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer" /td sha256' }
|
||||
],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
},
|
||||
{
|
||||
keyCode: 'CP-229979',
|
||||
operationSetCode: 'SigntoolVerify',
|
||||
parameters: [],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}
|
||||
];
|
||||
case 'sign-pgp':
|
||||
return [{
|
||||
keyCode: 'CP-450779-Pgp',
|
||||
operationSetCode: 'LinuxSign',
|
||||
parameters: [],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}];
|
||||
case 'sign-darwin':
|
||||
return [{
|
||||
keyCode: 'CP-401337-Apple',
|
||||
operationSetCode: 'MacAppDeveloperSign',
|
||||
parameters: [{ parameterName: 'Hardening', parameterValue: '--options=runtime' }],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}];
|
||||
case 'notarize-darwin':
|
||||
return [{
|
||||
keyCode: 'CP-401337-Apple',
|
||||
operationSetCode: 'MacAppNotarize',
|
||||
parameters: [],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}];
|
||||
case 'sign-vscode-extension':
|
||||
return [{
|
||||
keyCode: 'CP-401405',
|
||||
operationSetCode: 'VSCodePublisherSign',
|
||||
parameters: [],
|
||||
toolName: 'sign',
|
||||
toolVersion: '1.0'
|
||||
}];
|
||||
default:
|
||||
throw new Error('Sign type ' + type + ' not found');
|
||||
}
|
||||
}
|
||||
function main([esrpCliPath, type, folderPath, pattern]) {
|
||||
const root = path.join(os.tmpdir(), 'vsce-sign-' + Date.now());
|
||||
fs.mkdirSync(root);
|
||||
|
||||
const patternPath = path.join(root, 'pattern');
|
||||
fs.writeFileSync(patternPath, pattern);
|
||||
|
||||
const paramsPath = path.join(root, 'params');
|
||||
fs.writeFileSync(paramsPath, JSON.stringify(getParams(type)));
|
||||
|
||||
const dotnetVersion = cp.execSync('dotnet --version', { encoding: 'utf8' }).trim();
|
||||
const adoTaskVersion = path.basename(path.dirname(path.dirname(esrpCliPath)));
|
||||
const federatedTokenData = {
|
||||
jobId: process.env['SYSTEM_JOBID'],
|
||||
planId: process.env['SYSTEM_PLANID'],
|
||||
projectId: process.env['SYSTEM_TEAMPROJECTID'],
|
||||
hub: process.env['SYSTEM_HOSTTYPE'],
|
||||
uri: process.env['SYSTEM_COLLECTIONURI'],
|
||||
managedIdentityId: '4ac7ed59-b5e9-4f66-9c30-8d1afa72d32d', // VSCODE_ESRP_CLIENT_ID
|
||||
managedIdentityTenantId: '975f013f-7f24-47e8-a7d3-abc4752bf346', // VSCODE_ESRP_TENANT_ID
|
||||
serviceConnectionId: 'fe07e6ce-6ffb-4df9-8d27-d129523a3f3e', // VSCODE_ESRP_SERVICE_CONNECTION_ID
|
||||
tempDirectory: os.tmpdir(),
|
||||
systemAccessToken: process.env['SYSTEM_ACCESSTOKEN']
|
||||
};
|
||||
const args = [
|
||||
esrpCliPath,
|
||||
'vsts.sign',
|
||||
'-a', 'c24324f7-e65f-4c45-8702-ed2d4c35df99', // ESRP_CLIENT_ID
|
||||
'-d', '975f013f-7f24-47e8-a7d3-abc4752bf346', // ESRP_TENANT_ID
|
||||
'-k', JSON.stringify({ akv: 'vscode-esrp' }),
|
||||
'-z', JSON.stringify({ akv: 'vscode-esrp', cert: 'esrp-sign' }),
|
||||
'-f', folderPath,
|
||||
'-p', patternPath,
|
||||
'-u', 'false',
|
||||
'-x', 'regularSigning',
|
||||
'-b', 'input.json',
|
||||
'-l', 'AzSecPack_PublisherPolicyProd.xml',
|
||||
'-y', 'inlineSignParams',
|
||||
'-j', paramsPath,
|
||||
'-c', '9997',
|
||||
'-t', '120',
|
||||
'-g', '10',
|
||||
'-v', 'Tls12',
|
||||
'-s', 'https://api.esrp.microsoft.com/api/v1',
|
||||
'-m', '0',
|
||||
'-o', 'Microsoft',
|
||||
'-i', 'https://www.microsoft.com',
|
||||
'-n', '5',
|
||||
'-r', 'true',
|
||||
'-w', dotnetVersion,
|
||||
'-skipAdoReportAttachment', 'false',
|
||||
'-pendingAnalysisWaitTimeoutMinutes', '5',
|
||||
'-adoTaskVersion', adoTaskVersion,
|
||||
'-resourceUri', 'https://msazurecloud.onmicrosoft.com/api.esrp.microsoft.com',
|
||||
'-esrpClientId', 'c24324f7-e65f-4c45-8702-ed2d4c35df99', // ESRP_CLIENT_ID
|
||||
'-useMSIAuthentication', 'true',
|
||||
'-federatedTokenData', JSON.stringify(federatedTokenData)
|
||||
];
|
||||
|
||||
try {
|
||||
cp.execFileSync('dotnet', args, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error('ESRP failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
fs.rmdirSync(root, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
main(process.argv.slice(2));
|
||||
"@
|
||||
$content | Out-File -FilePath $(Agent.TempDirectory)/sign.js -Encoding utf8
|
||||
workingDirectory: $(Build.BinariesDirectory)
|
||||
displayName: Setup Signing Script
|
|
@ -1,83 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* This file is generated as the compiled output of https://github.com/microsoft/vscode/blob/main/build/azure-pipelines/common/sign.ts
|
||||
*/
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.main = void 0;
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const tmp = require("tmp");
|
||||
const crypto = require("crypto");
|
||||
|
||||
function getParams(type) {
|
||||
switch (type) {
|
||||
case 'windows':
|
||||
return '[{"keyCode":"CP-230012","operationSetCode":"SigntoolSign","parameters":[{"parameterName":"OpusName","parameterValue":"VS Code"},{"parameterName":"OpusInfo","parameterValue":"https://code.visualstudio.com/"},{"parameterName":"Append","parameterValue":"/as"},{"parameterName":"FileDigest","parameterValue":"/fd \\"SHA256\\""},{"parameterName":"PageHash","parameterValue":"/NPH"},{"parameterName":"TimeStamp","parameterValue":"/tr \\"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\\" /td sha256"}],"toolName":"sign","toolVersion":"1.0"},{"keyCode":"CP-230012","operationSetCode":"SigntoolVerify","parameters":[{"parameterName":"VerifyAll","parameterValue":"/all"}],"toolName":"sign","toolVersion":"1.0"}]';
|
||||
case 'rpm':
|
||||
return '[{ "keyCode": "CP-450779-Pgp", "operationSetCode": "LinuxSign", "parameters": [], "toolName": "sign", "toolVersion": "1.0" }]';
|
||||
case 'darwin-sign':
|
||||
return '[{"keyCode":"CP-401337-Apple","operationSetCode":"MacAppDeveloperSign","parameters":[{"parameterName":"Hardening","parameterValue":"--options=runtime"}],"toolName":"sign","toolVersion":"1.0"}]';
|
||||
case 'darwin-notarize':
|
||||
return '[{"keyCode":"CP-401337-Apple","operationSetCode":"MacAppNotarize","parameters":[{"parameterName":"BundleId","parameterValue":"$(BundleIdentifier)"}],"toolName":"sign","toolVersion":"1.0"}]';
|
||||
default:
|
||||
throw new Error(`Sign type ${type} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
function main([esrpCliPath, type, cert, username, password, folderPath, pattern]) {
|
||||
tmp.setGracefulCleanup();
|
||||
const patternPath = tmp.tmpNameSync();
|
||||
fs.writeFileSync(patternPath, pattern);
|
||||
const paramsPath = tmp.tmpNameSync();
|
||||
fs.writeFileSync(paramsPath, getParams(type));
|
||||
const keyFile = tmp.tmpNameSync();
|
||||
const key = crypto.randomBytes(32);
|
||||
const iv = crypto.randomBytes(16);
|
||||
fs.writeFileSync(keyFile, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') }));
|
||||
const clientkeyPath = tmp.tmpNameSync();
|
||||
const clientkeyCypher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
let clientkey = clientkeyCypher.update(password, 'utf8', 'hex');
|
||||
clientkey += clientkeyCypher.final('hex');
|
||||
fs.writeFileSync(clientkeyPath, clientkey);
|
||||
const clientcertPath = tmp.tmpNameSync();
|
||||
const clientcertCypher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
let clientcert = clientcertCypher.update(cert, 'utf8', 'hex');
|
||||
clientcert += clientcertCypher.final('hex');
|
||||
fs.writeFileSync(clientcertPath, clientcert);
|
||||
const args = [
|
||||
esrpCliPath,
|
||||
'vsts.sign',
|
||||
'-a', username,
|
||||
'-k', clientkeyPath,
|
||||
'-z', clientcertPath,
|
||||
'-f', folderPath,
|
||||
'-p', patternPath,
|
||||
'-u', 'false',
|
||||
'-x', 'regularSigning',
|
||||
'-b', 'input.json',
|
||||
'-l', 'AzSecPack_PublisherPolicyProd.xml',
|
||||
'-y', 'inlineSignParams',
|
||||
'-j', paramsPath,
|
||||
'-c', '9997',
|
||||
'-t', '120',
|
||||
'-g', '10',
|
||||
'-v', 'Tls12',
|
||||
'-s', 'https://api.esrp.microsoft.com/api/v1',
|
||||
'-m', '0',
|
||||
'-o', 'Microsoft',
|
||||
'-i', 'https://www.microsoft.com',
|
||||
'-n', '5',
|
||||
'-r', 'true',
|
||||
'-e', keyFile,
|
||||
];
|
||||
cp.spawnSync('dotnet', args, { stdio: 'inherit' });
|
||||
}
|
||||
exports.main = main;
|
||||
if (require.main === module) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.7"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
|
||||
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
tmp@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
Загрузка…
Ссылка в новой задаче