UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

920 lines (917 loc) 45.4 kB
import { from, lastValueFrom, of, Subject, throwError } from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { CancellationToken } from '../async/cancellation'; import { ErrorMonitor } from '../diagnostics/error-monitor'; import { EnvironmentModule } from '../manifest/environment-modules'; import { Cookie } from './cookie'; import { ApiVersion } from './gateway-url-builder'; import { headerConstants, HttpResponseTypes } from './http-constants'; import { NativeQ } from './native-q'; import { UriBuilder } from './uri-builder'; /** Represents a object that transfers files */ export class FileTransfer { nodeConnection; gatewayConnection; authorizationManager; moduleName = null; /** * Downloads a blob of data * * @param blob the blob of data to download * @param fileName the name of the file for the user to download. */ static downloadBlob(blob, fileName) { let useAnchorTagForDownload = true; const windowNagivator = window.navigator; if (windowNagivator.msSaveOrOpenBlob) { // This is for IE and Microsoft Edge < 16 // for those cases the download anchor tag doesn't generate the right name so we use the MS download system instead // "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393" const ua = navigator.userAgent; const edgeIndex = ua.indexOf('Edge'); if (edgeIndex > 0) { const dotIndex = ua.indexOf('.', edgeIndex); let versionNumber = 0; if (dotIndex > 0) { const versionString = ua.substring(edgeIndex + 'Edge'.length + 1, dotIndex); versionNumber = Number(versionString); } useAnchorTagForDownload = versionNumber > 15; } else { useAnchorTagForDownload = false; } } if (useAnchorTagForDownload) { const downloadLink = document.createElement('a'); downloadLink.style.display = 'none'; const url = URL.createObjectURL(blob); downloadLink.setAttribute('href', url); downloadLink.setAttribute('download', fileName); downloadLink.click(); downloadLink.remove(); } else { windowNagivator.msSaveOrOpenBlob(blob, fileName); } } /** * Navigates to a file. * * @param filePath the file path we are navigating to. */ static navigateToFile(filePath) { const downloadLink = document.createElement('a'); downloadLink.style.display = 'none'; downloadLink.setAttribute('href', filePath); downloadLink.click(); downloadLink.remove(); } /** * Initializes a new instance of the FileTransfer class. * * @param nodeConnection the NodeConnection class instance. * @param gatewayConnection the GatewayConnection class instance. * @param authorizationManager the AuthorizationManager class instance. */ constructor(nodeConnection, gatewayConnection, authorizationManager) { this.nodeConnection = nodeConnection; this.gatewayConnection = gatewayConnection; this.authorizationManager = authorizationManager; } static generateRanges(ranges, segmentSize, totalSize) { const offset = segmentSize - 1; const max = totalSize - 1; for (let first = 0, last = offset; first < max; first += offset + 1, last = Math.min(++last + offset, max)) { ranges.push(new ContentRange(first, last, totalSize)); } } static extractNode(input) { const url = new URL(input); const segments = url.pathname.split('/'); return segments[3]; } /** * The GET call to file transfer endpoint and return a Blob of the requested file * * @param nodeName the node to transfer the file from. * @param sourcePath the path of the remote file to transfer. * @param fileOptions the file options for the action. * @return Observable<Blob> the observable Blob object. */ transferBlob(nodeName, sourcePath, fileOptions) { const relativeUrl = this.gatewayConnection.url().node(nodeName).makeRelative().fileTransfer().file(sourcePath).build(); const headers = { Accept: 'application/octet-stream' }; const token = Cookie.getCrossSiteRequestForgeryToken(); if (token) { headers[headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN] = token; } const request = { headers: headers, responseType: 'blob' }; if (fileOptions) { request.logAudit = fileOptions.logAudit; request.logTelemetry = fileOptions.logTelemetry; } return this.nodeConnection.get(nodeName, relativeUrl, request); } /** * The GET call to file transfer endpoint and manual download of stream * * @param nodeName the node to transfer the file from. * @param sourcePath the path of the remote file to transfer. * @param targetName the desired name for the downloaded file. * @param fileOptions the file options for the action. * @return Observable<Blob> the observable Blob object. */ transferFile(nodeName, sourcePath, targetName, fileOptions) { return this.transferBlob(nodeName, sourcePath, fileOptions) .pipe(catchError((error) => { if (error.response && error.response.type === HttpResponseTypes.Json) { return from(error.response.text()).pipe(mergeMap((errorJson) => throwError(JSON.parse(errorJson)?.error))); } return throwError(error); }), map((responseBlob) => { FileTransfer.downloadBlob(responseBlob, targetName); return responseBlob; })); } /** * Upload a file from fileObject. * * @deprecated Use upload instead. * @param nodeName the node to upload the file to. * @param path the file path to store on the target node. * @param fileObject the file object created on the UI. * @param fileOptions the file options for the action. * @return Observable<any> the observable object. */ uploadFile(nodeName, path, fileObject, fileOptions) { const deferred = NativeQ.defer(); const formData = new FormData(); formData.append('file-0', fileObject); const request = new XMLHttpRequest(); const url = this.gatewayConnection.url().node(nodeName).fileTransfer().file(path).build(); const handler = () => { if (request.readyState === 4 /* complete */) { if (request.status === 200 /* HttpStatusCode.Ok */ || request.status === 201 /* HttpStatusCode.Created */) { deferred.resolve(request.responseText); } else { ErrorMonitor.current.reportErrorFromAjax({ status: request.status, xhr: request, request: { url, method: 'POST' } }); const uploadError = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error; const message = request.status === 0 /* HttpStatusCode.CorsRequestFailed */ ? uploadError.FileNotFound : (request.status === 400 /* HttpStatusCode.BadRequest */ ? uploadError.OperationBlocked : uploadError.Unknown.format(request.status)); deferred.reject({ xhr: request, message: message }); } } }; let tokenValue; const ajaxRequest = { headers: {} }; this.authorizationManager.addAuthorizationRequestHeader(ajaxRequest, nodeName); request.open('PUT', url); request.withCredentials = true; tokenValue = ajaxRequest.headers[headerConstants.SME_AUTHORIZATION]; if (tokenValue) { request.setRequestHeader(headerConstants.SME_AUTHORIZATION, tokenValue); } tokenValue = ajaxRequest.headers[headerConstants.USE_LAPS]; if (tokenValue) { request.setRequestHeader(headerConstants.USE_LAPS, tokenValue); // If ajaxRequest.headers[LAPS_LOCALADMINNAME] will always have default of 'administrator', // so no need to check if it exists and not null request.setRequestHeader(headerConstants.LAPS_LOCALADMINNAME, ajaxRequest.headers[headerConstants.LAPS_LOCALADMINNAME]); } if (fileOptions) { if (fileOptions.logAudit === true || fileOptions.logAudit === false) { request.setRequestHeader(headerConstants.LOG_AUDIT, fileOptions.logAudit ? 'true' : 'false'); } if (fileOptions.logTelemetry === true || fileOptions.logTelemetry === false) { request.setRequestHeader(headerConstants.LOG_TELEMETRY, fileOptions.logTelemetry ? 'true' : 'false'); } } const token = Cookie.getCrossSiteRequestForgeryToken(); if (token) { request.setRequestHeader(headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN, token); } request.setRequestHeader(headerConstants.MODULE_NAME, this.nameOfModule); request.onreadystatechange = handler; request.send(formData); return from(deferred.promise); } /** * Uploads the specified file. * @param node The target computer to upload the file to. * @param path The path on the target computer to upload the file to. * @param file The file to upload. * @param options The file upload options. * @param cancellationToken The token that can be used to cancel the operation. * @returns An observable object that contains the result of the file transfer. */ upload(node, path, file, options = new FileUploadOptions(), cancellationToken = CancellationToken.NONE) { return from(this.uploadAsync(node, path, file, options, cancellationToken)); } uploadAsync(node, path, file, options, cancellationToken) { // if the file size <= transfer size, then just transfer the file all at once, // which will be much faster than the multi - request flow for a single transfer if (file.size <= options.transferSize) { return this.uploadAllAtOnce(node, path, file, options, cancellationToken); } return this.uploadInParts(node, path, file, options, cancellationToken); } async uploadAllAtOnce(node, path, file, options, cancellationToken) { const url = this.gatewayConnection.url().node(node).fileTransfer().file(path).build(); const headers = this.newRequestHeaders(node, options); const transfer = new FileTransferInfo(file.size, 1); const result = new FileTransferResult(new FileTransferContinuation(null, null, file, transfer)); do { const response = await fetch(url, { method: 'PUT', credentials: 'include', headers: headers, body: file }); switch (response.status) { case 200 /* HttpStatusCode.Ok */: case 201 /* HttpStatusCode.Created */: case 204 /* HttpStatusCode.NoContent */: transfer.incrementCompleted(); return result; case 403 /* HttpStatusCode.Forbidden */: case 409 /* HttpStatusCode.Conflict */: const blockedMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.OperationBlocked; throw new Error(blockedMessage); default: if (cancellationToken.isCancellationRequested) { break; } const unknownMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown; throw new Error(unknownMessage.format(response.status, response.statusText)); } } while (!cancellationToken.isCancellationRequested); cancellationToken.throwIfCancellationRequested(); } async uploadInParts(node, path, file, options, cancellationToken = CancellationToken.NONE) { const continuation = await this.newUpload(node, path, file, options, cancellationToken); let canceled = cancellationToken.isCancellationRequested; let error = null; if (!canceled) { error = await lastValueFrom(this.uploadRanges(continuation, options, cancellationToken)); canceled = error !== null || cancellationToken.isCancellationRequested; } if (canceled && options.cancellationBehavior === FileTransferCancellationBehavior.Cancel) { try { // cancel the file transfer if an error occurred or the caller requested cancellation await this.cancelUpload(continuation); } catch (e) { // unable to cancel (e.g. clean up) the file transfer // if cancellation occurred from an error, rethrow it now if (error !== null) { throw error; } throw e; } return new FileTransferResult(); } return new FileTransferResult(continuation); } /** * Resumes a previously started file upload. * @param continuation The information required to continue the file transfer. * @param progress The object used to report the progress of the upload. * @param cancellationToken The token that can be used to cancel the operation. */ resumeUpload(continuation, progress = new NoProgress(), cancellationToken = CancellationToken.NONE) { return from(this.resumeUploadAsync(continuation, progress, cancellationToken)); } async resumeUploadAsync(continuation, progress = new NoProgress(), cancellationToken = CancellationToken.NONE) { const transferSize = continuation.transfer.size; const options = new FileUploadOptions(transferSize, FileTransferCancellationBehavior.Pause, progress); let error = null; error = await lastValueFrom(this.uploadRanges(continuation, options, cancellationToken)); if (error !== null) { throw error; } return null; } /** * Cancels a previously started file upload. * @param continuation The information required to cancel the file transfer. */ cancelUpload(continuation) { return from(this.cancelUploadAsync(continuation)); } async cancelUploadAsync(continuation) { const url = continuation.location; const node = FileTransfer.extractNode(url); const headers = this.newRequestHeaders(node, new FileUploadOptions()); let attempts = 0; headers.append(headerConstants.IF_MATCH, continuation.token); do { const response = await fetch(url, { method: 'DELETE', credentials: 'include', headers: headers }); const statusCode = response.status; // canceled or a new transfer was started if (statusCode === 204 /* HttpStatusCode.NoContent */ || statusCode === 412 /* HttpStatusCode.PreconditionFailed */) { break; } switch (statusCode) { case 409 /* HttpStatusCode.Conflict */: break; case 502 /* HttpStatusCode.BadGateway */: case 503 /* HttpStatusCode.ServiceUnavailable */: if (++attempts > 3) { const giveUpMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown; throw new Error(giveUpMessage.format(statusCode, response.statusText)); } break; default: const unknownMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown; throw new Error(unknownMessage.format(statusCode, response.statusText)); } } while (true); return null; } newRequestHeaders(node, options) { const request = this.gatewayConnection.defaultHttpSecureOptions; this.authorizationManager.addAuthorizationRequestHeader(request, node); const headers = new Headers(); const authAadToken = request.headers[headerConstants.SME_AAD_AUTHORIZATION]; const authToken = request.headers[headerConstants.SME_AUTHORIZATION]; const useLaps = request.headers[headerConstants.USE_LAPS]; const xsfrToken = Cookie.getCrossSiteRequestForgeryToken(); const { logAudit, logTelemetry } = options.logging; headers.append(headerConstants.MODULE_NAME, this.nameOfModule); if (authAadToken) { headers.append(headerConstants.SME_AAD_AUTHORIZATION, authAadToken); } if (authToken) { headers.append(headerConstants.SME_AUTHORIZATION, authToken); } if (useLaps) { headers.append(headerConstants.USE_LAPS, useLaps); headers.append(headerConstants.LAPS_LOCALADMINNAME, request.headers[headerConstants.LAPS_LOCALADMINNAME]); } if (xsfrToken) { headers.append(headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN, xsfrToken); } if (logAudit !== undefined) { headers.append(headerConstants.LOG_AUDIT, logAudit.toString()); } if (logTelemetry !== undefined) { headers.append(headerConstants.LOG_TELEMETRY, logTelemetry.toString()); } return headers; } async newUpload(node, path, file, options, cancellationToken = CancellationToken.NONE) { const url = this.gatewayConnection.url().node(node).fileTransfer().file(path).build(); const lastModified = new Date(file.lastModified).toISOString(); const headers = this.newRequestHeaders(node, options); let response; let retry = false; headers.append(headerConstants.CONTENT_DISPOSITION, `create; size=${file.size}; modification-date="${lastModified}"`); do { response = await fetch(url, { method: 'POST', credentials: 'include', headers: headers }); retry = response.status === 409 /* HttpStatusCode.Conflict */; } while (retry && !cancellationToken.isCancellationRequested); switch (response.status) { case 201 /* HttpStatusCode.Created */: const builder = new UriBuilder(response.headers.get(headerConstants.LOCATION)); // it's the client's job to tell the server which api version they want to use; // the location header will not contain the query parameter builder.setQueryParameter('api-version', ApiVersion.Latest); const location = builder.toString(); const etag = response.headers.get(headerConstants.ETAG); const ranges = new Array(); FileTransfer.generateRanges(ranges, options.transferSize, file.size); const transfer = new FileTransferInfo(options.transferSize, ranges.length); const continuation = new FileTransferContinuation(location, etag, file, transfer, ranges); return continuation; case 403 /* HttpStatusCode.Forbidden */: case 409 /* HttpStatusCode.Conflict */: const blockedMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.OperationBlocked; throw new Error(blockedMessage); default: const unknownMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown; throw new Error(unknownMessage.format(response.status, response.statusText)); } } uploadRanges(continuation, options, cancellationToken) { const node = FileTransfer.extractNode(continuation.location); const progress = options.progress; const ranges = [...continuation.ranges]; const transfer = continuation.transfer; const cancellationObservable = new Subject(); if (cancellationToken.isCancellationRequested) { // Emit to trigger the cancellation cancellationObservable.next(); cancellationObservable.complete(); } // Creating an observable that emits the index and the range const uploadStream = from(ranges).pipe( // Limit concurrency to 3 with mergeMap mergeMap((range, index) => { return this.uploadPartialFileHelper(index, node, continuation, range, options).pipe(tap(result => { switch (result.statusCode) { // No Content (success) case 204: transfer.incrementCompleted(); progress.report(transfer.percentComplete); break; case 503 /* HttpStatusCode.ServiceUnavailable */: if (!cancellationToken.isCancellationRequested) { // retry upload return this.uploadPartialFileHelper(index, node, continuation, range, options); } break; case 401 /* HttpStatusCode.Unauthorized */: case 409 /* HttpStatusCode.Conflict */: case 502 /* HttpStatusCode.BadGateway */: case 410 /* HttpStatusCode.Gone */: case 412 /* HttpStatusCode.PreconditionFailed */: default: const unknownMessage = MsftSme.getStrings().MsftSmeShell.Core.DirectoryList.Upload.Error.UnknownNoMessage; throw new Error(unknownMessage.format(result.statusCode)); } })); // 3 parallel uploads }, 3)); return uploadStream.pipe(takeUntil(cancellationObservable), catchError(err => { // If an error occurs during the upload process, emit it return of(err); })); } uploadPartialFileHelper(index, node, continuation, range, options) { return from(this.uploadPartialFile(index, node, continuation, range, options)); } async uploadPartialFile(index, node, continuation, range, options) { const blob = continuation.file.slice(range.from, range.to + 1); const headers = this.newRequestHeaders(node, options); headers.append(headerConstants.CONTENT_TYPE, 'application/octet-stream'); headers.append(headerConstants.CONTENT_RANGE, range.toString()); headers.append(headerConstants.IF_MATCH, continuation.token); const response = await fetch(continuation.location, { method: 'PATCH', credentials: 'include', headers: headers, body: blob }); return new HttpTransferResult(index, response.status, range); } /** * Gets the name of current shell or module. */ get nameOfModule() { if (!this.moduleName) { this.moduleName = EnvironmentModule.getModuleName(); } return this.moduleName; } } /** Represents the possible cancellation behaviors */ export var FileTransferCancellationBehavior; (function (FileTransferCancellationBehavior) { /** Indicates the file transfer is cancelled or aborted on cancellation */ FileTransferCancellationBehavior[FileTransferCancellationBehavior["Cancel"] = 0] = "Cancel"; /** Indicates the file transfer is paused on cancellation */ FileTransferCancellationBehavior[FileTransferCancellationBehavior["Pause"] = 1] = "Pause"; })(FileTransferCancellationBehavior || (FileTransferCancellationBehavior = {})); /** Represents file upload options. */ export class FileUploadOptions { transferSize; cancellationBehavior; progress; logging; constructor( /** Gets the size of transfer content. The default value is 10MB. */ transferSize = 10485760, /** Indicates the behavior to perform if the operation is canceled. */ cancellationBehavior = FileTransferCancellationBehavior.Cancel, /** A token that can be used to cancel the operation. */ progress = new NoProgress(), /** Gets the associated logging options. */ logging = { logAudit: false, logTelemetry: false }) { this.transferSize = transferSize; this.cancellationBehavior = cancellationBehavior; this.progress = progress; this.logging = logging; } } /** Represents the size and progress of a file transfer. */ export class FileTransferInfo { size; count; completed = 0; constructor( /** Gets the size used in the file transfer. */ size, /** Gets the total number of items in the file transfer. */ count) { this.size = size; this.count = count; } /** Gets the percentage of the file transfer completed. */ get percentComplete() { return (this.completed / this.count) * 100; } /** Increments the number of completed items in the file transfer. */ incrementCompleted() { this.completed = Math.min(this.completed + 1, this.count); } } /** Represents a continuation for a file transfer */ export class FileTransferContinuation { location; token; file; transfer; ranges; constructor( /** Gets the URl representing the location of the file transfer */ location, /** Gets the continuation token for the file transfer */ token, /** Gets the file being transferred */ file, /** Gets the file transfer information. */ transfer, /** Gets the remaining ranges in the file transfer */ ranges = new Array()) { this.location = location; this.token = token; this.file = file; this.transfer = transfer; this.ranges = ranges; } } /** Represents the result of a file transfer */ export class FileTransferResult { continuation; constructor( /** Gets the continuation token associated with the file transfer */ continuation = new FileTransferContinuation(null, null, null, new FileTransferInfo(0, 0))) { this.continuation = continuation; } /** Gets a value indicating whether the file transfer is complete */ get completed() { return this.continuation.ranges.length === 0; } /** Gets a value indicating whether the file transfer is resumable */ get resumable() { return !this.completed && this.continuation.token !== null && this.continuation.location !== null && this.continuation.file !== null; } } /** Represent a range of content in a file transfer */ export class ContentRange { from; to; totalSize; constructor( /** Gets the zero-based start of the content, in bytes */ from, /** Gets the zero-based end of the content, in bytes */ to, /** Gets the total size of the content in bytes */ totalSize) { this.from = from; this.to = to; this.totalSize = totalSize; } /** Returns the string representation of the object */ toString() { return `bytes ${this.from}-${this.to}/${this.totalSize}`; } } class NoProgress { report() { } } class HttpTransferResult { index; statusCode; range; constructor(index, statusCode, range) { this.index = index; this.statusCode = statusCode; this.range = range; } } //# sourceMappingURL=file-transfer.js.map // SIG // Begin signature block // SIG // MIIoNwYJKoZIhvcNAQcCoIIoKDCCKCQCAQExDzANBglg // SIG // hkgBZQMEAgEFADB3BgorBgEEAYI3AgEEoGkwZzAyBgor // SIG // BgEEAYI3AgEeMCQCAQEEEBDgyQbOONQRoqMAEEvTUJAC // SIG // AQACAQACAQACAQACAQAwMTANBglghkgBZQMEAgEFAAQg // SIG // jMAWfshxCOV5mCAHn+8j9u6zF2qBsOpswDYwqG3qG2eg // SIG // gg2FMIIGAzCCA+ugAwIBAgITMwAABAO91ZVdDzsYrQAA // SIG // AAAEAzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJV // SIG // UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH // SIG // UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv // SIG // cmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBT // SIG // aWduaW5nIFBDQSAyMDExMB4XDTI0MDkxMjIwMTExM1oX // SIG // DTI1MDkxMTIwMTExM1owdDELMAkGA1UEBhMCVVMxEzAR // SIG // BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v // SIG // bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv // SIG // bjEeMBwGA1UEAxMVTWljcm9zb2Z0IENvcnBvcmF0aW9u // SIG // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA // SIG // n3RnXcCDp20WFMoNNzt4s9fV12T5roRJlv+bshDfvJoM // SIG // ZfhyRnixgUfGAbrRlS1St/EcXFXD2MhRkF3CnMYIoeMO // SIG // MuMyYtxr2sC2B5bDRMUMM/r9I4GP2nowUthCWKFIS1RP // SIG // lM0YoVfKKMaH7bJii29sW+waBUulAKN2c+Gn5znaiOxR // SIG // qIu4OL8f9DCHYpME5+Teek3SL95sH5GQhZq7CqTdM0fB // SIG // w/FmLLx98SpBu7v8XapoTz6jJpyNozhcP/59mi/Fu4tT // SIG // 2rI2vD50Vx/0GlR9DNZ2py/iyPU7DG/3p1n1zluuRp3u // SIG // XKjDfVKH7xDbXcMBJid22a3CPbuC2QJLowIDAQABo4IB // SIG // gjCCAX4wHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYB // SIG // BQUHAwMwHQYDVR0OBBYEFOpuKgJKc+OuNYitoqxfHlrE // SIG // gXAZMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQLEyRNaWNy // SIG // b3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQx // SIG // FjAUBgNVBAUTDTIzMDAxMis1MDI5MjYwHwYDVR0jBBgw // SIG // FoAUSG5k5VAF04KqFzc3IrVtqMp1ApUwVAYDVR0fBE0w // SIG // SzBJoEegRYZDaHR0cDovL3d3dy5taWNyb3NvZnQuY29t // SIG // L3BraW9wcy9jcmwvTWljQ29kU2lnUENBMjAxMV8yMDEx // SIG // LTA3LTA4LmNybDBhBggrBgEFBQcBAQRVMFMwUQYIKwYB // SIG // BQUHMAKGRWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w // SIG // a2lvcHMvY2VydHMvTWljQ29kU2lnUENBMjAxMV8yMDEx // SIG // LTA3LTA4LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3 // SIG // DQEBCwUAA4ICAQBRaP+hOC1+dSKhbqCr1LIvNEMrRiOQ // SIG // EkPc7D6QWtM+/IbrYiXesNeeCZHCMf3+6xASuDYQ+AyB // SIG // TX0YlXSOxGnBLOzgEukBxezbfnhUTTk7YB2/TxMUcuBC // SIG // P45zMM0CVTaJE8btloB6/3wbFrOhvQHCILx41jTd6kUq // SIG // 4bIBHah3NG0Q1H/FCCwHRGTjAbyiwq5n/pCTxLz5XYCu // SIG // 4RTvy/ZJnFXuuwZynowyju90muegCToTOwpHgE6yRcTv // SIG // Ri16LKCr68Ab8p8QINfFvqWoEwJCXn853rlkpp4k7qzw // SIG // lBNiZ71uw2pbzjQzrRtNbCFQAfmoTtsHFD2tmZvQIg1Q // SIG // VkzM/V1KCjHL54ItqKm7Ay4WyvqWK0VIEaTbdMtbMWbF // SIG // zq2hkRfJTNnFr7RJFeVC/k0DNaab+bpwx5FvCUvkJ3z2 // SIG // wfHWVUckZjEOGmP7cecefrF+rHpif/xW4nJUjMUiPsyD // SIG // btY2Hq3VMLgovj+qe0pkJgpYQzPukPm7RNhbabFNFvq+ // SIG // kXWBX/z/pyuo9qLZfTb697Vi7vll5s/DBjPtfMpyfpWG // SIG // 0phVnAI+0mM4gH09LCMJUERZMgu9bbCGVIQR7cT5YhlL // SIG // t+tpSDtC6XtAzq4PJbKZxFjpB5wk+SRJ1gm87olbfEV9 // SIG // SFdO7iL3jWbjgVi1Qs1iYxBmvh4WhLWr48uouzCCB3ow // SIG // ggVioAMCAQICCmEOkNIAAAAAAAMwDQYJKoZIhvcNAQEL // SIG // BQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo // SIG // aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK // SIG // ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMT // SIG // KU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv // SIG // cml0eSAyMDExMB4XDTExMDcwODIwNTkwOVoXDTI2MDcw // SIG // ODIxMDkwOVowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgT // SIG // Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc // SIG // BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYG // SIG // A1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0Eg // SIG // MjAxMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC // SIG // ggIBAKvw+nIQHC6t2G6qghBNNLrytlghn0IbKmvpWlCq // SIG // uAY4GgRJun/DDB7dN2vGEtgL8DjCmQawyDnVARQxQtOJ // SIG // DXlkh36UYCRsr55JnOloXtLfm1OyCizDr9mpK656Ca/X // SIG // llnKYBoF6WZ26DJSJhIv56sIUM+zRLdd2MQuA3WraPPL // SIG // bfM6XKEW9Ea64DhkrG5kNXimoGMPLdNAk/jj3gcN1Vx5 // SIG // pUkp5w2+oBN3vpQ97/vjK1oQH01WKKJ6cuASOrdJXtjt // SIG // 7UORg9l7snuGG9k+sYxd6IlPhBryoS9Z5JA7La4zWMW3 // SIG // Pv4y07MDPbGyr5I4ftKdgCz1TlaRITUlwzluZH9TupwP // SIG // rRkjhMv0ugOGjfdf8NBSv4yUh7zAIXQlXxgotswnKDgl // SIG // mDlKNs98sZKuHCOnqWbsYR9q4ShJnV+I4iVd0yFLPlLE // SIG // tVc/JAPw0XpbL9Uj43BdD1FGd7P4AOG8rAKCX9vAFbO9 // SIG // G9RVS+c5oQ/pI0m8GLhEfEXkwcNyeuBy5yTfv0aZxe/C // SIG // HFfbg43sTUkwp6uO3+xbn6/83bBm4sGXgXvt1u1L50kp // SIG // pxMopqd9Z4DmimJ4X7IvhNdXnFy/dygo8e1twyiPLI9A // SIG // N0/B4YVEicQJTMXUpUMvdJX3bvh4IFgsE11glZo+TzOE // SIG // 2rCIF96eTvSWsLxGoGyY0uDWiIwLAgMBAAGjggHtMIIB // SIG // 6TAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUSG5k // SIG // 5VAF04KqFzc3IrVtqMp1ApUwGQYJKwYBBAGCNxQCBAwe // SIG // CgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB // SIG // /wQFMAMBAf8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h // SIG // 6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDov // SIG // L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVj // SIG // dHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNy // SIG // bDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0 // SIG // dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv // SIG // TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDCB // SIG // nwYDVR0gBIGXMIGUMIGRBgkrBgEEAYI3LgMwgYMwPwYI // SIG // KwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNv // SIG // bS9wa2lvcHMvZG9jcy9wcmltYXJ5Y3BzLmh0bTBABggr // SIG // BgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBwAG8AbABp // SIG // AGMAeQBfAHMAdABhAHQAZQBtAGUAbgB0AC4gHTANBgkq // SIG // hkiG9w0BAQsFAAOCAgEAZ/KGpZjgVHkaLtPYdGcimwuW // SIG // EeFjkplCln3SeQyQwWVfLiw++MNy0W2D/r4/6ArKO79H // SIG // qaPzadtjvyI1pZddZYSQfYtGUFXYDJJ80hpLHPM8QotS // SIG // 0LD9a+M+By4pm+Y9G6XUtR13lDni6WTJRD14eiPzE32m // SIG // kHSDjfTLJgJGKsKKELukqQUMm+1o+mgulaAqPyprWElj // SIG // HwlpblqYluSD9MCP80Yr3vw70L01724lruWvJ+3Q3fMO // SIG // r5kol5hNDj0L8giJ1h/DMhji8MUtzluetEk5CsYKwsat // SIG // ruWy2dsViFFFWDgycScaf7H0J/jeLDogaZiyWYlobm+n // SIG // t3TDQAUGpgEqKD6CPxNNZgvAs0314Y9/HG8VfUWnduVA // SIG // KmWjw11SYobDHWM2l4bf2vP48hahmifhzaWX0O5dY0Hj // SIG // Wwechz4GdwbRBrF1HxS+YWG18NzGGwS+30HHDiju3mUv // SIG // 7Jf2oVyW2ADWoUa9WfOXpQlLSBCZgB/QACnFsZulP0V3 // SIG // HjXG0qKin3p6IvpIlR+r+0cjgPWe+L9rt0uX4ut1eBrs // SIG // 6jeZeRhL/9azI2h15q/6/IvrC4DqaTuv/DDtBEyO3991 // SIG // bWORPdGdVk5Pv4BXIqF4ETIheu9BCrE/+6jMpF3BoYib // SIG // V3FWTkhFwELJm3ZbCoBIa/15n8G9bW1qyVJzEw16UM0x // SIG // ghoKMIIaBgIBATCBlTB+MQswCQYDVQQGEwJVUzETMBEG // SIG // A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u // SIG // ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u // SIG // MSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5n // SIG // IFBDQSAyMDExAhMzAAAEA73VlV0POxitAAAAAAQDMA0G // SIG // CWCGSAFlAwQCAQUAoIGuMBkGCSqGSIb3DQEJAzEMBgor // SIG // BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEE // SIG // AYI3AgEVMC8GCSqGSIb3DQEJBDEiBCAB7PaY2ixVLxd+ // SIG // Iuun9cPCqf0be+XXSgf1p8pOrRUdGDBCBgorBgEEAYI3 // SIG // AgEMMTQwMqAUgBIATQBpAGMAcgBvAHMAbwBmAHShGoAY // SIG // aHR0cDovL3d3dy5taWNyb3NvZnQuY29tMA0GCSqGSIb3 // SIG // DQEBAQUABIIBAG9KjOat0u0XNdKrsF+vDAzX4HGcW31w // SIG // giB+SIhaNIOo9AsPNjHXmnp0p48mbsHHWalcW9BCI1BM // SIG // aqTZevXQkA2QGqgycQn8BZnyHxJqPFjoZP6mJhqOzk3q // SIG // sZaQIjObghRu4zC01WPS8omj4pHV0rBPGAzHIcp8vp+6 // SIG // uknY9Nu/CY2EnenHeGlwxTuHDmg9GN5rdS7aFR9+X3iK // SIG // ctIX3uwsBhGx6sa878M/zniYu9r8l5Ge55JzME+Oh5DB // SIG // QpAfvX8D/zD0K5+9FZXvVaXHItOozmtuTjnJNCLSvcHs // SIG // 9g/KAHNnYGMpl/F1nEgPotiLldGrcKLqL924HX9jhcpn // SIG // TLShgheUMIIXkAYKKwYBBAGCNwMDATGCF4Awghd8Bgkq // SIG // hkiG9w0BBwKgghdtMIIXaQIBAzEPMA0GCWCGSAFlAwQC // SIG // AQUAMIIBUgYLKoZIhvcNAQkQAQSgggFBBIIBPTCCATkC // SIG // AQEGCisGAQQBhFkKAwEwMTANBglghkgBZQMEAgEFAAQg // SIG // a7XLj/NEPEG3HBxatmGOXhwWQXv2dJoPzD9XLa59sAQC // SIG // Bmet45UxxRgTMjAyNTAyMjAxNTI4NDAuNjUyWjAEgAIB // SIG // 9KCB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgT // SIG // Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc // SIG // BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMG // SIG // A1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9u // SIG // czEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjkyMDAt // SIG // MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGlt // SIG // ZS1TdGFtcCBTZXJ2aWNloIIR6jCCByAwggUIoAMCAQIC // SIG // EzMAAAHnLo8vkwtPG+kAAQAAAecwDQYJKoZIhvcNAQEL // SIG // BQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp // SIG // bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT // SIG // FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd // SIG // TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN // SIG // MjMxMjA2MTg0NTE5WhcNMjUwMzA1MTg0NTE5WjCByzEL // SIG // MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x // SIG // EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv // SIG // c29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9z // SIG // b2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUGA1UECxMe // SIG // blNoaWVsZCBUU1MgRVNOOjkyMDAtMDVFMC1EOTQ3MSUw // SIG // IwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2 // SIG // aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC // SIG // AgEAwlefL+CLkOufVzzNQ7WljL/fx0VAuZHYhBfPWAT+ // SIG // v0Z+5I6jJGeREnpn+RJYuAi7UFUnn0aRdY+0uSyyorDF // SIG // jhkWi3GlWxk33JiNbzESdbczMAjSKAqv78vFh/EHVdQf // SIG // wG+bCvkPciL8xsOO031zxPEZa2rsCv3vp1p8DLdOtGpB // SIG // GYiSc9VYdS4UmCmoj/WdtxGZhhEwlooJCm3LgJ4b4d8q // SIG // zGvPbgX2nh0GRBxkKnbJDOPBAXFklnaYkkgYgMcoR1JG // SIG // 5J5fTz87Qf0lMc0WY1M1h4PW39ZqmdHCIgFgtBIyuzjY // SIG // ZUHykkR1SyizT6Zd//lC+F43NGL3anPPIDi1K//OE/f8 // SIG // Sua/Nrpb0adgPP2q/XBuFu+udLimgMUQJoC+ISoCF+f9 // SIG // GiALG8qiTmujiBkhfWvg315dS6UDzSke/drHBe7Yw+Vq // SIG // sCLon0vWFIhzL0S44ypNEkglf5qVwtAaD5JOWrH8a6yW // SIG // wrCXjx0jhG5aSc0Zs2j+jjF8EXK2+01xUDrE5CrqpFr7 // SIG // 2CD71cwuvFDPjLJCz5XdXqnTjjCu0m239rRkmX9/ojsF // SIG // kDHFlwfYMOYCtwCGCtPFpCSbssz6n4rYLm3UQpmK/Qlb // SIG // DTrlvsBw2BoXIiQxdi5K45BVI1HF0iCXfX9rLGIrWfQr // SIG // qxle+AUHH68Y75NS/I77Te5rpSMCAwEAAaOCAUkwggFF // SIG // MB0GA1UdDgQWBBTP/uCYgJ82OHaRH/2Za4dSu96PWDAf // SIG // BgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf // SIG // BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jv // SIG // c29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU // SIG // aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI // SIG // KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8v // SIG // d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p // SIG // Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEw // SIG // KDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM // SIG // MAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDANBgkq // SIG // hkiG9w0BAQsFAAOCAgEAdKHw25PpZVotXAup7H4nuSba // SIG // dPaOm+gEQqb7Qz6tihT/oYvlDTT+yxnIirnJKlwpgUxS // SIG // IXwXhksb5OsnKJHUK9/NeaRDmmFk5x70NPvISsvOq9Re // SIG // K3wbuKBweXE8tPE+KIaxvzmBvwf4DZ89Dper+7v6hI8+ // SIG // PM12emZcShsmcCpimVmgXdg2BMMyqXS5AcbOgOnp1mUd // SIG // I2PquRXW1eOYIRkyoEq+RAgDpyw+J4ycH4yKtJkWVsA2 // SIG // UKF7SUmlR0rtpR0C92BxBYpLp21EyXzXwQyy+xr/rE5k // SIG // Yg2ZMuTgMaCxtoGk37ohW36Zknz3IJeQjlM3zEJ86Sn1 // SIG // +vhZCNEEDb7j6VrA1PLEfrp4tlZg6O65qia6JuIoYFTX // SIG // S2jHzVKrwS+WYkitc5mhCwSfWvmDoxOaZkmq1ubBm5+4 // SIG // lZBdlvSUCDh+rRlixSUuR7N+s2oZKB4fIg/ety3ho2ap // SIG // BbrCmlFu9sjI/8sU3hhAzqCK9+ZMF8a9VLvs5Lq9svhb // SIG // jWNKGY6ac6feQFtZXoT9MWjvqAVdV372grq/weT1QKds // SIG // c66LDBFHAMKSaYqPlWHyLnxo+5nl3BkGFgPFJq/CugLq // SIG // PiZY/CHhUupUryoakKZnQcwDBqjzkCrdTsN2V8XoSu7w // SIG // Iopt2YgC5TNCueOpNLGa8XWT4KZs+zvMPYBy7smQEHsw // SIG // ggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAV // SIG // MA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzET // SIG // MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk // SIG // bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 // SIG // aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0 // SIG // aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAx // SIG // ODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYT // SIG // AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH // SIG // EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y // SIG // cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l // SIG // LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEF // SIG // AAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7V // SIG // gtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H // SIG // ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKD // SIG // RLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gm // SIG // U3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62AW36M // SIG // EBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+y // SIG // OSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoF // SIG // VZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJi // SIG // ss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGa // SIG // RnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+Autuqfjbs // SIG // Nkz2K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afo // SIG // mXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9 // SIG // ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk // SIG // i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y // SIG // 1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV // SIG // 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0C // SIG // AwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEw // SIG // IwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/ // SIG // LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnp // SIG // cjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8G // SIG // CCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j // SIG // b20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYD // SIG // VR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAwe // SIG // CgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB // SIG // /wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9 // SIG // lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov // SIG // L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVj // SIG // dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoG // SIG // CCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov // SIG // L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNS // SIG // b29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN // SIG // AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pc // SIG // FLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHk // SIG // wo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF // SIG // vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4 // SIG // U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2 // SIG // EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn8Atq // SIG // gcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mj // SIG // dAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZM // SIG // cm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQE // SIG // cb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2f // SIG // pCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBM // SIG // drVXVAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L // SIG // +DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJ // SIG // C4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU // SIG // 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/ // SIG // 2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDTTCC // SIG // AjUCAQEwgfmhgdGkgc4wgcsxCzAJBgNVBAYTAlVTMRMw // SIG // EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt // SIG // b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp // SIG // b24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9w // SIG // ZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT // SIG // Tjo5MjAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z // SIG // b2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO // SIG // AwIaAxUAs3IE5xmrEsHv3a7vnD3tTRf78EOggYMwgYCk // SIG // fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu // SIG // Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV // SIG // TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N // SIG // aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkq // SIG // hkiG9w0BAQsFAAIFAOthnAMwIhgPMjAyNTAyMjAxMjE4 // SIG // MTFaGA8yMDI1MDIyMTEyMTgxMVowdDA6BgorBgEEAYRZ // SIG // CgQBMSwwKjAKAgUA62GcAwIBADAHAgEAAgISVTAHAgEA // SIG // AgITLzAKAgUA62LtgwIBADA2BgorBgEEAYRZCgQCMSgw // SIG // JjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIB // SIG // AAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQB/Go/n5DB5 // SIG // 3ySFShKauo/5PAMMhslfoiTMki8OKjSRACXWD4yBJHqu // SIG // Xs9hboyZNcK2yZsQVGBy6PAG+GgLHghfMINGswafHk8c // SIG // Ssyfxz6jqAOmW7Y59uLVJL1wTwJZK9rW8Uk/+pxXCtBH // SIG // /zkCERr8JPYys9AZiMK0+ypgY0a4M7sti62pzUtQihoC // SIG // QaRGSqvZ5/dmqIb9yuIBJKUz3HyxkK9TmkgoJPJC7fIp // SIG // 9/obu1jCzJJiVREDPgpy6ijIj/Tl+pPWZvghugJLjQm1 // SIG // mK21BoM0RdM8AyDJDnwoYj29UhzWX8rMs4S3+eWqMvCC // SIG // 3Jk/PC4m6oOyrixlRhtcRacaMYIEDTCCBAkCAQEwgZMw // SIG // fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 // SIG // b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p // SIG // Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj // SIG // cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHn // SIG // Lo8vkwtPG+kAAQAAAecwDQYJYIZIAWUDBAIBBQCgggFK // SIG // MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkq // SIG // hkiG9w0BCQQxIgQgWZVQVwNzTV4zllz68TL1SZPrUEl7 // SIG // lfYO1RxggIcfuFEwgfoGCyqGSIb3DQEJEAIvMYHqMIHn // SIG // MIHkMIG9BCDlNl0NdmqG/Q3gxVzPVBR3hF++Bb9AAb0D // SIG // Bu6gudZzrTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw // SIG // EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt // SIG // b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp // SIG // b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w // SIG // IFBDQSAyMDEwAhMzAAAB5y6PL5MLTxvpAAEAAAHnMCIE // SIG // IF2FP3R/vAZxCsgAJaEN0whyGAjsHSLuIPTxdaiAHJKe // SIG // MA0GCSqGSIb3DQEBCwUABIICAALw9qLbSfCQeVkoZpuM // SIG // 8brpmXSQbzHRheNoVN5pLuaN1PYCywyKyvlcvy1D/85C // SIG // 3XS8nb8TjaF+5hOKik2Vy2VI3cmZpWWrear7CAw5/eNp // SIG // ML3HrPiIMfUBm8hiQYOZ4Z42ktJqneTE8Y6oKGBnxaAk // SIG // jYiEY4+JoQPpZJQ8BDCFLu3nZ+L9m1pjGpc5ABVqNXF/ // SIG // CNmuxX4Psk3NaGa6fg11a06s5BwI+56tEWXapdMJ09Vu // SIG // QGi1hSQ8W77MkW6hMwug3XMhuz61Ev10wK+qpcK+PhNd // SIG // Ku7Kj11HRoUB/9f289aeM391SNVeSHloeUukHAWUPPFb // SIG // V8gMCPBhHxid3Mq2Xs6AY8F5lKEo/Ug4uycY1glMULk/ // SIG // +KmV1X+m3qVRoyERSX4HhUCQOKwIXe0nk8XHUweJV/o7 // SIG // BvRYoCr3vpjQ0jNn4KDLvxi15ln70bTkw8nlSD4uzcpH // SIG // QeFAEVv2fAU2ccWMvtrsVK56OXUqcH1t/I8mlA+jmbjy // SIG // h4BGsIHXCqhD9Zmi71xgp9DqD7+mvcBDkbkJjNAgLJMR // SIG // MjC582nW6Ryd6Qg8RVgb9lOjqfpLcJhgEhWXwCmFJ8EZ // SIG // qs0AysB2ewYDjngKlbjAdz66PjaN+L8Tt+Rp6GEH3ET2 // SIG // iLdWhoi2/aB1MVZZXv4i2rhp96UDZ4kSM6grjDCpwkxM // SIG // sjcQ // SIG // End signature block