From 2470b10a2ce96e4711c91c49e99fe5c2b20de9a9 Mon Sep 17 00:00:00 2001 From: Trevor McMaster Date: Wed, 6 Nov 2024 08:36:50 -0600 Subject: [PATCH] Fix Cookie Login (#259) * Fixed a bug in the new 1.0.0 cookie that doesn't allow legacy imports * Cleaned up error messages and configs * Additional files found * Found 2 more * Fixed the yaml files that were being reverted * Fixed race condition on unit tests * Made a cleaner fix for the race condition --- agent/createtest/createtest.yaml | 3 +- agent/createtest/shorter/createtest.yaml | 3 +- agent/src/tests.ts | 3 +- common/test/basic.yaml | 3 +- common/test/basicheadersall.yaml | 3 +- common/test/basicnopeakload.yaml | 3 +- common/test/basicwithenv.yaml | 3 +- common/test/basicwithfiles.yaml | 3 +- common/test/basicwithvars.yaml | 3 +- controller/components/YamlViewer/story.tsx | 6 ++-- .../components/YamlWriterForm/writeyaml.ts | 2 +- controller/pages/api/util/authserver.ts | 27 +++++++++++------- controller/pages/api/util/secrets.ts | 2 +- controller/pages/api/util/testmanager.ts | 6 ++-- controller/test/basic.yaml | 2 +- controller/test/basicheadersall.yaml | 3 +- controller/test/basicnopeakload.yaml | 3 +- controller/test/basicwithenv.yaml | 2 +- controller/test/basicwithfiles.yaml | 3 +- controller/test/ppaasencryptenvfile.spec.ts | 2 +- controller/test/ppaasencrypts3file.spec.ts | 2 +- controller/test/settings.yaml | 3 +- controller/test/testyaml.zip | Bin 431 -> 500 bytes controller/test/testyamlenv.zip | Bin 508 -> 576 bytes controller/test/testyamls.zip | Bin 917 -> 1054 bytes 25 files changed, 40 insertions(+), 50 deletions(-) diff --git a/agent/createtest/createtest.yaml b/agent/createtest/createtest.yaml index f463648c..7a6d1dd3 100644 --- a/agent/createtest/createtest.yaml +++ b/agent/createtest/createtest.yaml @@ -18,8 +18,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/agent/createtest/shorter/createtest.yaml b/agent/createtest/shorter/createtest.yaml index d4de3fa5..354a83cf 100644 --- a/agent/createtest/shorter/createtest.yaml +++ b/agent/createtest/shorter/createtest.yaml @@ -14,8 +14,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/agent/src/tests.ts b/agent/src/tests.ts index cb1795c5..f14aafec 100644 --- a/agent/src/tests.ts +++ b/agent/src/tests.ts @@ -42,8 +42,7 @@ config: headers: TestTime: '\${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/common/test/basic.yaml b/common/test/basic.yaml index 0e3f0996..a16cc49a 100644 --- a/common/test/basic.yaml +++ b/common/test/basic.yaml @@ -14,8 +14,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/common/test/basicheadersall.yaml b/common/test/basicheadersall.yaml index 6b2edade..397f42c7 100755 --- a/common/test/basicheadersall.yaml +++ b/common/test/basicheadersall.yaml @@ -12,8 +12,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/common/test/basicnopeakload.yaml b/common/test/basicnopeakload.yaml index c35d0ad7..2cef87cc 100644 --- a/common/test/basicnopeakload.yaml +++ b/common/test/basicnopeakload.yaml @@ -12,8 +12,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/common/test/basicwithenv.yaml b/common/test/basicwithenv.yaml index b6d2f4da..438003bf 100644 --- a/common/test/basicwithenv.yaml +++ b/common/test/basicwithenv.yaml @@ -19,8 +19,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test test: ${test} general: bucket_size: 1m diff --git a/common/test/basicwithfiles.yaml b/common/test/basicwithfiles.yaml index 96a293df..92543a91 100644 --- a/common/test/basicwithfiles.yaml +++ b/common/test/basicwithfiles.yaml @@ -18,8 +18,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/common/test/basicwithvars.yaml b/common/test/basicwithvars.yaml index 1c2c3dce..0b324bdf 100644 --- a/common/test/basicwithvars.yaml +++ b/common/test/basicwithvars.yaml @@ -18,8 +18,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/components/YamlViewer/story.tsx b/controller/components/YamlViewer/story.tsx index e4d94d4d..d568295c 100644 --- a/controller/components/YamlViewer/story.tsx +++ b/controller/components/YamlViewer/story.tsx @@ -24,8 +24,7 @@ client: headers: TestTime: '\${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m @@ -54,8 +53,7 @@ client: headers: TestTime: '\${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/components/YamlWriterForm/writeyaml.ts b/controller/components/YamlWriterForm/writeyaml.ts index a2abc61f..318a4355 100644 --- a/controller/components/YamlWriterForm/writeyaml.ts +++ b/controller/components/YamlWriterForm/writeyaml.ts @@ -43,7 +43,7 @@ export const createYamlJson = ({ urls, patterns, vars, providers, loggers }: Omi // Default config myYaml.config = {}; myYaml.config.client = {}; - myYaml.config.client.headers = {"User-Agent": "FS-QA-SystemTest"}; + myYaml.config.client.headers = {"User-Agent": "PewPew Performance Load Test"}; // eslint-disable-next-line camelcase myYaml.config.general = {bucket_size: "1m", log_provider_stats: "1m"}; diff --git a/controller/pages/api/util/authserver.ts b/controller/pages/api/util/authserver.ts index 2d779b06..9082c96d 100644 --- a/controller/pages/api/util/authserver.ts +++ b/controller/pages/api/util/authserver.ts @@ -34,9 +34,9 @@ import { } from "openid-client"; import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"; import { LogLevel, log, logger } from "@fs/ppaas-common"; +import { parse as cookieParse, serialize as cookieSerialize } from "cookie"; import { formatPageHref, getHostUrl } from "./clientutil"; import { IncomingMessage } from "http"; -import cookie from "cookie"; import { createErrorResponse } from "./util"; import { getClientSecretOpenId } from "./secrets"; import nextCookie from "next-cookies"; @@ -265,7 +265,7 @@ function getTokenFromQueryOrHeader (req: NextApiRequest, headerName: string = AU log("header token: " + JSON.stringify(token), LogLevel.DEBUG); } if (!token && req.headers.cookie && !Array.isArray(req.headers.cookie)) { - const cookies = cookie.parse(req.headers.cookie); + const cookies = cookieParse(req.headers.cookie); token = cookies[headerName]; log("cookie token: " + JSON.stringify(token), LogLevel.DEBUG); } @@ -374,7 +374,7 @@ export async function authApi (req: NextApiRequest, res: NextApiResponse, requir // If we don't have permissions or the permissions are not greater than requiredPermissions if (authPermissions.authPermission < requiredPermissions) { log("User was not authorized for api", LogLevel.WARN, { token, method: req.method, url: req.url }); - res.status(403).json({ message: "User is not authorized for this api. If you think this is an error, please contact the PerformanceQA team." }); + res.status(403).json({ message: "User is not authorized for this api. If you think this is an error, please contact the Performance team." }); return undefined; } @@ -428,15 +428,20 @@ export function setCookies ( log(`Set cookie to ${token} on ${domain}`, LogLevel.DEBUG); // server side const oneDay: number = 60 * 60 * 24; - const cookies: string[] = [cookie.serialize(AUTH_COOKIE_NAME, token, { domain, path, maxAge: oneDay * COOKIE_DURATION_DAYS })]; - if (refreshToken) { - cookies.push(cookie.serialize(REFRESH_COOKIE_NAME, refreshToken, { domain, path, maxAge: oneDay * REFRESH_COOKIE_DURATION_DAYS })); - } - if (hintToken) { - cookies.push(cookie.serialize(HINT_COOKIE_NAME, hintToken, { domain, path, maxAge: oneDay * COOKIE_DURATION_DAYS })); + try { + const cookies: string[] = [cookieSerialize(AUTH_COOKIE_NAME, token, { domain, path, maxAge: oneDay * COOKIE_DURATION_DAYS })]; + if (refreshToken) { + cookies.push(cookieSerialize(REFRESH_COOKIE_NAME, refreshToken, { domain, path, maxAge: oneDay * REFRESH_COOKIE_DURATION_DAYS })); + } + if (hintToken) { + cookies.push(cookieSerialize(HINT_COOKIE_NAME, hintToken, { domain, path, maxAge: oneDay * COOKIE_DURATION_DAYS })); + } + // Set the cookie and then redirect + ctx.res.setHeader("Set-Cookie", cookies); + } catch (error: unknown) { + log("Error setting cookies in header", LogLevel.WARN, error, { token: token !== undefined, refreshToken: refreshToken !== undefined, hintToken: hintToken !== undefined }); + throw error; } - // Set the cookie and then redirect - ctx.res.setHeader("Set-Cookie", cookies); } function getTokenFromCookieOrHeader (ctx: GetServerSidePropsContext, cookieName: string = AUTH_COOKIE_NAME, headerName?: string): string | undefined { diff --git a/controller/pages/api/util/secrets.ts b/controller/pages/api/util/secrets.ts index 64f8cb3f..50225601 100644 --- a/controller/pages/api/util/secrets.ts +++ b/controller/pages/api/util/secrets.ts @@ -80,7 +80,7 @@ export async function createSecret (secretKeyName: string, value: string | Buffe try { const input: CreateSecretCommandInput = { Name: secretKeyName, - Description: "Testing Secrets Manager for PerformanceQA/Pewpew", + Description: "Testing Secrets Manager for Performance/Pewpew", SecretString: typeof value === "string" ? value : undefined, SecretBinary: typeof value !== "string" ? new Uint8Array(value.buffer) : undefined }; diff --git a/controller/pages/api/util/testmanager.ts b/controller/pages/api/util/testmanager.ts index 9914334c..d2c479ed 100644 --- a/controller/pages/api/util/testmanager.ts +++ b/controller/pages/api/util/testmanager.ts @@ -446,7 +446,7 @@ export async function validateYamlfile ( if (bypassParser) { if (authPermissions.authPermission < AuthPermission.Admin) { log("Unauthorized User attempted to bypass the config parser.", LogLevel.WARN, { yamlFile, userId: authPermissions.userId }); - return { json: { message: "User is not authorized to bypass the config parser. If you think this is an error, please contact the PerformanceQA team." }, status: 403 }; + return { json: { message: "User is not authorized to bypass the config parser. If you think this is an error, please contact the Performance team." }, status: 403 }; } } else { // bypassPaser is false or undefined, run the parser @@ -1167,7 +1167,7 @@ export abstract class TestManager { } else if (PEWPEW_BINARY_EXECUTABLE_NAMES.includes(file.originalFilename)) { if (authPermission < AuthPermission.Admin) { log("Unauthorized User attempted to use custom pewpew binary.", LogLevel.WARN, { yamlFile }); - return { json: { message: "User is not authorized to use custom pewpew binaries. If you think this is an error, please contact the PerformanceQA team." }, status: 403 }; + return { json: { message: "User is not authorized to use custom pewpew binaries. If you think this is an error, please contact the Performance team." }, status: 403 }; } log("Authorized user uploaded custom binary.", LogLevel.INFO, { yamlFile }); additionalFileNames.push(file.originalFilename); @@ -1511,7 +1511,7 @@ export abstract class TestManager { if (bypassParser) { if (authPermission < AuthPermission.Admin) { log("Unauthorized User attempted to bypass the config parser.", LogLevel.WARN, { yamlFile }); - return { json: { message: "User is not authorized to bypass the config parser. If you think this is an error, please contact the PerformanceQA team." }, status: 403 }; + return { json: { message: "User is not authorized to bypass the config parser. If you think this is an error, please contact the Performance team." }, status: 403 }; } } else { // Read in the actual variables so we can inject them and make sure it's valid. diff --git a/controller/test/basic.yaml b/controller/test/basic.yaml index 0e3f0996..2738a4f5 100644 --- a/controller/test/basic.yaml +++ b/controller/test/basic.yaml @@ -15,7 +15,7 @@ config: TestTime: '${epoch("ms")}' Accept: application/json FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/basicheadersall.yaml b/controller/test/basicheadersall.yaml index 967e309c..0c1f0c8f 100755 --- a/controller/test/basicheadersall.yaml +++ b/controller/test/basicheadersall.yaml @@ -12,8 +12,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/basicnopeakload.yaml b/controller/test/basicnopeakload.yaml index c35d0ad7..2cef87cc 100644 --- a/controller/test/basicnopeakload.yaml +++ b/controller/test/basicnopeakload.yaml @@ -12,8 +12,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/basicwithenv.yaml b/controller/test/basicwithenv.yaml index 7c14ad6b..8c7e6b5e 100644 --- a/controller/test/basicwithenv.yaml +++ b/controller/test/basicwithenv.yaml @@ -21,7 +21,7 @@ config: TestTime: '${epoch("ms")}' Accept: application/json FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/basicwithfiles.yaml b/controller/test/basicwithfiles.yaml index ae611b2f..34c91d16 100644 --- a/controller/test/basicwithfiles.yaml +++ b/controller/test/basicwithfiles.yaml @@ -18,8 +18,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/ppaasencryptenvfile.spec.ts b/controller/test/ppaasencryptenvfile.spec.ts index 55849fc6..cfa69388 100644 --- a/controller/test/ppaasencryptenvfile.spec.ts +++ b/controller/test/ppaasencryptenvfile.spec.ts @@ -517,7 +517,7 @@ describe("PpaasEncryptEnvironmentFile", () => { it("Upload a test file force should upload unchanged files", (done: Mocha.Done) => { const lastModified: number = Date.now(); - testPpaasEncryptEnvironmentFileUpload.setLastModifiedLocal(lastModified); + testPpaasEncryptEnvironmentFileUpload.setLastModifiedLocal(lastModified - 1); testPpaasEncryptEnvironmentFileUpload.upload(true).then(() => { log("testPpaasEncryptEnvironmentFileDownload.upload(true) succeeded", LogLevel.DEBUG); // If it's newer, but forced we should upload it and set the time to last modified diff --git a/controller/test/ppaasencrypts3file.spec.ts b/controller/test/ppaasencrypts3file.spec.ts index afc8b8ec..b844717f 100644 --- a/controller/test/ppaasencrypts3file.spec.ts +++ b/controller/test/ppaasencrypts3file.spec.ts @@ -258,7 +258,7 @@ describe("PpaasEncryptS3File", () => { it("Upload a test file force should upload unchanged files", (done: Mocha.Done) => { mockUploadObject(); const lastModified: number = Date.now(); - testPpaasEncryptS3FileUpload.setLastModifiedLocal(lastModified); + testPpaasEncryptS3FileUpload.setLastModifiedLocal(lastModified - 1); testPpaasEncryptS3FileUpload.upload(true).then(() => { s3FileKey = testPpaasEncryptS3FileUpload.key; log("testPpaasEncryptS3FileDownload.upload(true) succeeded", LogLevel.DEBUG); diff --git a/controller/test/settings.yaml b/controller/test/settings.yaml index 0e3f0996..a16cc49a 100644 --- a/controller/test/settings.yaml +++ b/controller/test/settings.yaml @@ -14,8 +14,7 @@ config: headers: TestTime: '${epoch("ms")}' Accept: application/json - FS-User-Agent-Chain: PPAAS-Agent-Performance Test - User-Agent: FS-QA-SystemTest PPAAS Agent Performance Test + User-Agent: PPAAS Agent Performance Test general: bucket_size: 1m log_provider_stats: 1m diff --git a/controller/test/testyaml.zip b/controller/test/testyaml.zip index 43a8ff6511985e6c6c3085b15a4e274f9d24da24..b331a727b7a3b478b88ba55c279b425a47072eb8 100644 GIT binary patch literal 500 zcmWIWW@Zs#-~d7wtF%Z4C{SZ!VBlg*;Ow7#*4dG>AUs$D`4#TAt+zgB? zAoXCPbds zvQ13lx=8d>Q@-}zU7ItGBo;i7w>fU8RsQD3>dVnjtZv53u=7612yXYVo~<9I9yX^< zP~PI`vDp9s literal 431 zcmWIWW@Zs#U|`^2&@8hJe_lvtdZtXG+sn^QW;*Z;5qPuu(7 zqSqLBRlI^N44#H5KCs$SxX$U7ZsJd+LUqPl_3Ce}-X5CdowL6`+1fB}i?4u-gzS6< zk%TSIg1?LRU%MF1clBzJ%6H)J}>nIP|k7WrO|lbrDidT&~@QvZtd`FKT65oYAW{tXR2jxtVYNx6Oe8 z-kpk{l~@)pRd^fwbLJlD!}_Nm*(U0|^3~f~5#(ge*X@hx}lIY%CDnrQ8~ z=lsVo@zd{JGOABDwkvY||8TQEaE3Ft{Mt(!oS9snS(1^OSEg5)n41$C!pp$Eu}V7~ zhD$5B85mhW+Q3984W_dLHJSH7kU}%^oc_uvQ^xY`-++1#cp?|w` z8_!5wUp}w;{LcHbVOEg}OA3^^uQ@b5I(8`P!7K4A3#{B8EO_;W<&H|m)#o?={Y{-b zwf%4Q%)HIplQ!5e-#T(GI;B$Z{Ist*k^J1^JEq)_2)Ofk=X_5HD4$G->Uv2i>a0KX`E+>yx&-rUWRY`%iB;zw~xc(>x`u*+=;UyxBRHyQ}WbVPs$cMOc70Ba;X- jA~KQXK#>Up+ZsVElA|`jo0Scukr4>vf%IvhwG0dZE_vbh literal 508 zcmWIWW@Zs#U|`^2h%U1YKe+YvrwT>}hP_M-41z$>q{QOPV6;gZyFXkIdk7QD5_2lp0@7(Jh9A;k@n&zjcQLy_d6F@ za9^}J`|0L`nf&uAjkylBCUzu*o{*jxrIBFv=kV<)-P%j9@439~wUG0vWsyG8mNok{ zSNbK^-i1fgj%>Zv75m_GRLi%hC!!laRDO6W{vz<#gWYyN9Sg*-yz5Nz`kn7H%lk#- zn_B^_Yg+P7J$x#6rT1G}5!2$zQtN4XHP`QaX=o`nn0LNsC2P9ImRWTcYkz%67R&u( zx2R0`;-Vy{z)exAp4WDVC2YUtwajA6$*OxZU7C+&3#G>MIkI^iSI?dD=kWKs4Qt*- zRoH*9O?Y_Zdx6};xU$xbzcgx=w>r12<89RH72}HCp2ZpQa?+&KS=-k%uhm>Bz&qDZ zeYw5+HJ`r^ves0v*4#hDyP;O_n%ha|7)i@ti~-(^Od<@pBMKN-48VAU0*q)2@MdKL O$uR<t(!oS9snS(1^OSEg5)n41$C!pp$Eu}V7~ zhD$5B85mhW+Q3984W_dLHJSH7kU}%^oc_uvQ^xY`-++1#cp?|w` z8_!5wUp}w;{LcHbVOEg}OA3^^uQ@b5I(8`P!7K4A3#{B8EO_;W<&H|m)#o?={Y{-b zwf%4Q%)HIplQ!5e-#T(GI;B$Z{Ist*k^J1^JEq)_2)Ofk=X_5HD4$G->Uv2i>a0KX`E+>yx&-rUWRY`%iB;zw~xc(>x`u*+=;UyxBRHyQ}WbVPs$cMOXklDqWDG zQVl4>1&c~V94-XLAq=C(;Ur)G!v;KU@BeURH?RpUm6VxLv|6BG?v}!JPOo$mGi8Lz z*_Zt-zqIbMp3cr6k1J)5cPd}K;%J!sr?jbm#McRztrdy@Cw#j`NV9_ooF>}Lwk$WS68_M+Wt313U;`hcaaKW z3vD)>dn+n6W2z+Q<=M7um!9QaT6Xepq7~EBYZ9B{S|6+l%Q-pye&?SVb@5{JIg!$S<`CU+46_zo8_ZrBbtJEnBGgT$aN!DUs*q#mTweGqw~j4tyqQ zX?APDJ-!Twt!sbJS9};b<3ikDMo=v5K2+eO2aE+!SO<7BGKnxFvJ`R*fwB|~Y-_lvtdZtXG+sn^QW;*Z;5qPuu(7 zqSqLBRlI^N44#H5KCs$SxX$U7ZsJd+LUqPl_3Ce}-X5CdowL6`+1fB}i?4u-gzS6< zk%TSIg1?LRU%MF1clBzJ%6H)J}>nIP|k7WrO|lbrDidT&~@QvZtd`FKT65oYAW{tXR2jxtVYNx6Oe8 z-kpk{l~@)pRd^fwbLJlD!}_Nm*(U0|^3~f~5#(ge*X@hx}lIY%CDnrQ8~ z=lsVo@zd{JGOABDwkvY||8TQEaE3Ft{MXwqk#c@aO>+&6~F-A3k+aESOAx2mSm*nl|jNe=%n9a1A)EYwbul(c)0kU zKD&Jdm#C1_QqP@k$7Y%>``Bc=mL>F`taW#nz}J(;pYP57dScCDWhb4L3hy9y|MNZD#Hx7!blY^(NyFX7Xb7Q2v_(`MMlhXap1s2>FZO(qW`Cumhyh>xP zL#>G&385#XCq`)`nEg3?J4(0q((8LJZ+k7|d}>*wkF;gYKFyVWiM4m((X=C5Z*|2! z_#D;pE$WHr#t)Sro{GN+{PkeB-A~5?@hk5-le~WC`^@rw5&7m;0PC8Tyi*UK%3bOG zmR7{HxU$rGT3*fdJ6{@FiVfzS?^(&3uCZlSoyFQ;ACkp#|JW@m6TY}8$tiGCl&a^o z-C+saZ+R`V*mAP!-b|O~W7$Hf@qCVK9>>*lr~Enmy>7#rcTpAgA8ZpI9{FA%_b{%k zb>lCMn&qv|ZR>a&wR*+4Vz*~;M!cLfDRtKNHO*@^R|@dX^;2JN?|#ka?}My06|6P) z5AklO6};wl(m6)b@)u)(HzSh>1MZvvj4K9Uyg>mXUm)wl7QrA55PgY23ak$~x&ypf Q*+5E|fUq1$b1;K=09!tMRR910