From f26529b1a36574815f061064860ab5101615c556 Mon Sep 17 00:00:00 2001 From: ju09279 Date: Fri, 6 Sep 2024 22:56:08 +0200 Subject: [PATCH] auth finished --- backend/HttpClient.cs | 85 ++++++++++++++++++++++++++++++ backend/Program.cs | 87 ++++++++++++++++++++++++++++--- backend/cookies.json | 4 ++ frontend/src/script/types.ts | 13 +++-- frontend/src/stores/userStore.ts | 53 +++++++++++++++++-- frontend/src/views/MeView.vue | 35 +++++++++++-- proxy/auth.go | 66 +++++++++++++++++++---- proxy/database.db | Bin 20480 -> 20480 bytes proxy/db.go | 7 +++ proxy/middleware.go | 29 ++++++++++- proxy/routes.go | 53 +++++++++++++++++-- 11 files changed, 397 insertions(+), 35 deletions(-) create mode 100644 backend/HttpClient.cs create mode 100644 backend/cookies.json diff --git a/backend/HttpClient.cs b/backend/HttpClient.cs new file mode 100644 index 0000000..6d2f11b --- /dev/null +++ b/backend/HttpClient.cs @@ -0,0 +1,85 @@ +using OsuParsers.Enums.Replays; +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace shitweb +{ + public class ApiClient + { + private const string CookiesFilePath = "cookies.json"; + private HttpClient _client; + private HttpClientHandler _handler; + + public ApiClient() + { + _handler = new HttpClientHandler(); + _client = new HttpClient(_handler); + } + + public async Task InitializeAsync() + { + LoadCookies(); + + } + + public void SaveCookies(String cookie) + { + var cookies = _handler.CookieContainer.GetCookies(new Uri("https://proxy.illegalesachen.download")); + var Cookie = new CookieInfo(); + + Cookie.Name = "session_cookie"; + Cookie.Value = cookie; + var json = JsonSerializer.Serialize(Cookie, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(CookiesFilePath, json); + LoadCookies(); + } + + public Boolean LoadCookies() + { + if (File.Exists(CookiesFilePath)) + { + var json = File.ReadAllText(CookiesFilePath); + var cookieInfo = JsonSerializer.Deserialize(json); + + var cookie = new Cookie(cookieInfo.Name, cookieInfo.Value); + _handler.CookieContainer.Add(new Uri("https://proxy.illegalesachen.download"), cookie); + return true; + } + + return false; + } + + + public async Task UpdateSettingsAsync(string endpoint) + { + var requestContent = new JsonContent(new { endpoint = endpoint }); + var response = await _client.PostAsync("https://proxy.illegalesachen.download/settings", requestContent); + try + { + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) { + System.Console.WriteLine(ex.Message); + } + } + } + + public class CookieInfo + { + public string Name { get; set; } + public string Value { get; set; } + } + + public class JsonContent : StringContent + { + public JsonContent(object obj) + : base(JsonSerializer.Serialize(obj), System.Text.Encoding.UTF8, "application/json") + { + } + } +} \ No newline at end of file diff --git a/backend/Program.cs b/backend/Program.cs index 230e6a5..fdaa089 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -1,11 +1,17 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; using shitweb; +using System; +using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; +using System.Drawing.Text; +using System.Net.Http; +using System.Text.RegularExpressions; var builder = WebApplication.CreateBuilder(args); @@ -25,18 +31,24 @@ builder.Services.AddCors(options => var app = builder.Build(); app.UseCors("AllowAll"); +var apiClient = new ApiClient(); + + app.MapGet("/ping", () => "pong"); -// Define the API routes -app.MapGet("/api/v1/songs/{hash}", (string hash) => - { +app.MapGet("/login", () => { + return Results.Redirect("https://proxy.illegalesachen.download/login"); +}); - return Results.Ok(new { hash }); - }); +app.MapGet("/api/v1/songs/{hash}", (string hash) => +{ + + return Results.Ok(new { hash }); +}); -app.MapGet("/api/v1/songs/recent", (int? limit, int? offset) => - { +app.MapGet("/api/v1/songs/recent", (HttpContext httpContext, int? limit, int? offset) => +{ var limitValue = limit ?? 100; // default to 10 if not provided var offsetValue = offset ?? 0; // default to 0 if not provided @@ -225,4 +237,63 @@ static ImageFormat GetImageFormat(string extension) } Osudb.Instance.ToString(); -app.Run(); \ No newline at end of file +startCloudflared(); + +Task.Run(() => +{ + Thread.Sleep(500); + if (!apiClient.LoadCookies()) + { + Console.WriteLine("Please visit this link and paste the Value back into here: "); + + var cookie = Console.ReadLine(); + + apiClient.SaveCookies(cookie); + } + + Console.WriteLine("Ur Osu songs should now be available, please delete the cookies.json if it doesnt show up and try again."); +}); + +await apiClient.InitializeAsync(); +app.Run(); + +async Task startCloudflared() { + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "cloudflared", + Arguments = "tunnel --url http://localhost:5153", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.ErrorDataReceived += (sender, e) => { + if (!string.IsNullOrEmpty(e.Data)) + { + ParseForUrls(e.Data); + } + }; + + process.Start(); + + process.BeginErrorReadLine(); + + await Task.Run(() => process.WaitForExit()); +} + +void ParseForUrls(string data) +{ + var urlRegex = new Regex(@"https?://[^\s]*\.trycloudflare\.com"); + var matches = urlRegex.Matches(data); + + foreach (Match match in matches) + { + Console.WriteLine($"Login here if not done already: {match.Value}/login"); + apiClient.UpdateSettingsAsync(match.Value); + } +} diff --git a/backend/cookies.json b/backend/cookies.json new file mode 100644 index 0000000..3648a73 --- /dev/null +++ b/backend/cookies.json @@ -0,0 +1,4 @@ +{ + "Name": "session_cookie", + "Value": "session_cookie_c_PhxGlrrYcWY1h9cQ6mu_LxsDIVu0u_Nv0qAK1WmvcZiZXQ_FcwXZZXDk5Tm0qrqZTHHs800Pv5bTMGIrfa8Q==" +} \ No newline at end of file diff --git a/frontend/src/script/types.ts b/frontend/src/script/types.ts index 629dae5..baa4a9d 100644 --- a/frontend/src/script/types.ts +++ b/frontend/src/script/types.ts @@ -6,12 +6,19 @@ export type Song = { url: string; previewimage: string; mapper: string; - }; - +}; + export type CollectionPreview = { index: number; name: string; length: number; previewimage: string; }; - \ No newline at end of file + +export type Me = { + id: number; + name: string; + avatar_url: string; + endpoint: string; + share: boolean; +}; diff --git a/frontend/src/stores/userStore.ts b/frontend/src/stores/userStore.ts index ad2e3e1..7182ebf 100644 --- a/frontend/src/stores/userStore.ts +++ b/frontend/src/stores/userStore.ts @@ -1,10 +1,27 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -import type { Song, CollectionPreview } from '@/script/types'; +import type { Song, CollectionPreview, Me } from '@/script/types'; export const useUserStore = defineStore('userStore', () => { const userId = ref(null) const baseUrl = ref('https://service.illegalesachen.download/') + const proxyUrl = ref('https://proxy.illegalesachen.download/') + + const User = ref(null) + + function saveUser(user: Me) { + localStorage.setItem('activeUser', JSON.stringify(user)); + } + + function loadUser(): Me | null { + const user = localStorage.getItem('activeUser'); + return user ? JSON.parse(user) : null; + } + + function setUser(user: Me) { + User.value = user; + saveUser(user) + } async function fetchSong(hash: string): Promise { try { @@ -30,7 +47,7 @@ export const useUserStore = defineStore('userStore', () => { } } - async function fetchWithCache(cacheKey: string, url: string, cacheDuration: number = 24 * 60 * 60 * 1000): Promise { + async function fetchWithCache(cacheKey: string, url: string, cacheDuration: number = 24 * 60 * 60 * 1): Promise { const cacheTimestampKey = `${cacheKey}_timestamp`; const cachedData = localStorage.getItem(cacheKey); @@ -96,5 +113,35 @@ export const useUserStore = defineStore('userStore', () => { return fetchWithCache(cacheKey, url); } - return { fetchSong, fetchActiveSearch, fetchSearchArtist, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, userId, baseUrl } + async function fetchMe(): Promise { + const url = `${proxyUrl.value}me`; + + try { + const response = await fetch(url, { + method: 'GET', + credentials: 'include' + }); + console.log(response); + + if (response.redirected) { + window.open(response.url, '_blank'); + return { "redirected": true }; + } + + if (!response.ok) { + console.error(`Fetch failed with status: ${response.status} ${response.statusText}`); + return { id: -1 } as Me; + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Fetch error:', error); + return {} as Me; + } + } + + setUser(loadUser()); + + return { fetchSong, fetchActiveSearch, fetchSearchArtist, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, fetchMe, userId, baseUrl, proxyUrl, User, setUser } }) diff --git a/frontend/src/views/MeView.vue b/frontend/src/views/MeView.vue index 2afe33e..de890ab 100644 --- a/frontend/src/views/MeView.vue +++ b/frontend/src/views/MeView.vue @@ -12,6 +12,8 @@ const actionColor = ref(''); const infoColor = ref(''); const borderColor = ref(''); +const loginStatus = ref('Login'); + function update() { var input = document.getElementById("url-input") as HTMLAudioElement; console.log(input.value) @@ -34,6 +36,24 @@ function save(bg: string | null, main: string | null, info: string | null, borde console.log("bg", bgColor.value, "action:", actionColor.value, "info", infoColor.value, "border", borderColor.value) } +async function getMe() { + + const data = await userStore.fetchMe() as Me; + if (data.redirected == true) { + loginStatus.value = "waiting for login, click to refresh!" + console.log("redirect detected"); + } + + console.log(data) + if (data.id === null || data.id === undefined || Object.keys(data).length === 0) { + return + } + + console.log("active user: ", data.name) + userStore.setUser(data); + +} + onMounted(() => { reset(); }) @@ -69,12 +89,17 @@ function reset() {

Meeeeee


-
- + +
+
-

User: {{ 'JuLi0n_' }}

-

Api: Not Connected

-

Sharing:

+

{{ userStore.User.name }}

+

{{ userStore.User.endpoint == "" ? 'Not Connected' : 'Connected' }}

+

Sharing:

+ +
diff --git a/proxy/auth.go b/proxy/auth.go index 3690c92..cc4c227 100644 --- a/proxy/auth.go +++ b/proxy/auth.go @@ -86,8 +86,7 @@ func (c *OsuApiClient) sendRequest(req *http.Request, v interface{}) error { return nil } -func LoginRedirect(w http.ResponseWriter, r *http.Request) { - +func LoginMiddlePage(w http.ResponseWriter, r *http.Request) { cookie, ok := r.Context().Value("cookie").(string) if !ok || cookie == "" { @@ -98,13 +97,32 @@ func LoginRedirect(w http.ResponseWriter, r *http.Request) { var clientid = os.Getenv("CLIENT_ID") var redirect_uri = os.Getenv("REDIRECT_URI") + "/oauth/code" - http.Redirect(w, r, - fmt.Sprintf("https://osu.ppy.sh/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s", - clientid, - redirect_uri, - strings.Join(scopes, " "), - cookie), - http.StatusTemporaryRedirect) + + html := ` + + + + + + Login Required + + +

Redirecting...

+ Click here if ur not being Redirected! + + + + ` + + loginURL := fmt.Sprintf("https://osu.ppy.sh/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s", + clientid, + redirect_uri, + strings.Join(scopes, " "), + cookie) + fmt.Fprintf(w, html, loginURL, loginURL) + return } func Oauth(w http.ResponseWriter, r *http.Request) { @@ -204,13 +222,41 @@ func Oauth(w http.ResponseWriter, r *http.Request) { user.UserID = apiuser.ID user.Name = apiuser.Username user.AvatarUrl = apiuser.AvatarURL + user.Share = false SaveCookie(user.UserID, cookie) if err = SaveUser(user); err != nil { fmt.Println(err) } - JSONResponse(w, http.StatusCreated, user) + var html = fmt.Sprintf(` + + + + + Login Success + + + + + + + + + `, cookie) + + fmt.Fprint(w, html) + return + } type AuthToken struct { diff --git a/proxy/database.db b/proxy/database.db index 104ea1678bf6b6d6749b86660be27f2f9fd20f0e..dc6a2a0cf50a64c927d82cd211c6eb209081a1df 100644 GIT binary patch literal 20480 zcmeI4OQ`HxTF3Xf_q@+N=iGjt?!Lm^pwRcyx6aM`aiP&vDwWE!s`98*q7bt3P9;@I z>XE7f2Ym;E(ue~GI&mV1IC1VokVbGMIB_D<2u_@cLq)_@$L`biWtSsMZK<$p?_?z_ zzwi5hE7@!P*=%#F=9~C7S+!NLxy5hZzqxns=9g}7Z*Fb`_;~<7Ki=MfJNJKl0Ds>5 zPy6R~g`3yAUw{7clbid`|LW!?@!}U={4?CSZdV1a3S1SqDsWZcs=!r&s{&UAt_oZg zxGM19pupt*!&l$=&OLV%fHGb;C0yS5>$us*%kYbb4|qf1EaBGT`l@jIg=;@r237nC z?D_HcjCUTr`tZYhpReECGhD3}d0YhT+nslR>OOCqZ$Ena(bN~PN#=sLy$!M zbi0p!)cfOGLl_E%p!0%xdum&Mlt&-mj+eIsK^0)8JZJLUKzRD(WAxpJuY?cp-Q>$C z-v21|V7pnpIriV?;G=JMPix=DrviOXy7%bSt5^3vZ{FzfhV$mzJ3pnvo2LItgCFN~ zO6DZ?PiJ~+jh9gkBig(4z_06eRp6??Re`GlR|T#LTow3VRp2+lul>zme){z0gP-?* z>%sS*nDSQ5k)^M>w}L*Hz^S8^Z8r7#vrj(_Uf1>E^*Z|$B~S#xS@suEipCIx#VO`> zQO9X~kdb)K<>OpdX6Q6mc{u}Ocse%iqlD_Y%)W*fWsx(5jnYvc*ra}t)>2g?fhkc^ z(Jwqv+q*?djICj&TMNt+?c>;)9<^h7)Eqr`k6xu?Q}|swc-5>NixR0<8Uc@wV+-ET z@p7-QG3^+3@VdP==%21XXrhtB>G574xTrQ*U{c=Yfipy;Vzt*#!4>v?Y`KJ5Xozlw zhcVyfppvUI=Z~7%<=#B&-kv{dgm!$>AI9n)Gp7ujoMqjIoCywybc^KpU;ukpb}Onsm-QBG+b!5nXpT8!mb=!amD@A*x^MO z>f)T|Em8sEN<#))C!cCjVpM7CZP&T7wUyY&l6uUCp3X`N3n*t>5*T|}?73>{98h1_ zZgC)cXkA7|;u5*)(oK)(*RDY(%81#_52jwCEE0geB2s>Wv$>zdGZ0d;uMJzjU&hqP z+K0xr@gsXL6xc3{Wh!*~VVDeCCbpvGE=#Miau^!mFjIEx4N{Qpbk%3ksbd_I%IgM< zURZ|Q^h?o}XTB-O^*F&(;LAp1so3I(IqNc8@eF05G9|kf*NislZ7|dY8g3(QV_FQlR5I(YNg4-#R z?%>CyH|a=LRQROjsDJ~w+DDCq+R{6lQ+pN%_R9s<*YJvqRDVG6r0Q$wtZY&PYTqu9 zNwy+c+)`wr*&6Qhu-Mj;gmOHUh#rzr37;<~OrUrP0LW@?*4EBiS&dFQ3j*)A(>!NV ziPiSYb(HTm>PT67qp+zklCz4`qH%CE#I&rjLOLu~lX9=RwK+TPcAGIHDz)ekY(!EA zkFywWWzs8hPiKaBM2AyEhpQw4yCfU*?807;Q-1n@8+l7tQk5)WBGMl6ey03k7kCMgt47>XogoQe>Fp_2fmSp=b3 zl*I`mqKJeF1KN6>)AScV1>OJHP7!hwi^ zBqGR=K`=TBP}stv00pp9Lc)G9I0}uK00A(@gbD~i1qmPk?7+lS2yhBe5soAn8p4+! zP5>68=olv%7$ZeTIJ_*t$OOSbL{kaFUio^k6*_gy47$X@1Kqq(&`$01HWiMPf9@Vea5GLJ*i@9AgQFqd0>HBm;sthLs9f7#K>h zED3)}5J>l+_F$IM{LmHq7Lg5$;3yFgmz(GZ`alphl zEIxc|kpT@mvQz|6oC-oFpy>#O)j(l{Q4#?fg~7-03=YR3fhJK1d(Z*O1Qdf~2@cB? z(;>`_I1h^bem>Pk!*kc>Kf1%SZqE=rz2xBqwz4a9u4{E_SFhyEnni)Guoq2r<% z8&0We>#O_qK$>llt*`HphEq_d+J$gZJD>W)u$z0{e6S#`3TfKLvqqyegCLU~2`o*N z+9Y76-~Dd=t#icvz9&n0II9~Pi^8HeinoEi`ita(0iOYDyv>HDnBw-iq?VLpO zY1(JUiIeyf!#g%MYm> zlsAMEi{*CQG-VtS;XHSNzD=76s?JdND2N+<6{^uDk)3`iq1r6x=UO*t*vTo!Z=55# zT8yf~h;l|X$Am^Wq_+z?Tu5ZM_G4SDbg}GZVTV}$cp`9~E{!5%t50N^nK3GjcDUJ& z6Q$|69X2SY&ANz_k$fV)I!A<4GMy#IUE%kQsxy`2F6mX8CdxXA6nkv#Y&|& z^{h=8QR10_W19_!XI;MW42AESEu4c38{V^lpk=KlCZlml-9d%biH|X)$R%E+L&E0ax;B{A+{ZhtQbm1?OV$zm% z9j?gIp;~NYm?S~!^~gi3sWzHn9VCAB9MMP1wC3j-?IA@QXYbKc4Enizw494VAJ9Url&TC#pEmJfvN?`I;r~QbHo{^4SI*T*0w9r1czf<7@g*f^=?NaDyv55aW_3O+;Yu15u65d zcD$p!8Agm2aZoR&sdV)DEv~m&S=+h7%Ef$93{J!^oh7v3j1P|upfYnqz-z)HK)&q;B={h4`=JzZ2=c()QxAj%X&XcvSDQ++!(ssCEN` znBYt^ljSX?VWVWlbL&V}PC33jM=axNjEq`VO#S{S8u=u$n(j8cJ>J8nGu6$HTFB($ zUR+p~yXe9V9~m~Qa#aHsh&TugLEbodRTj;Fh`Yux=C;K+r#HZhbHoFOZ3G`E%L7%- zr!M34hqby{MTnP+S(D{1XS(yC95Z{&pn%=?0_~{p1l8|4x?xo;n*ddBL~Ii9)|58j z#B-p%AwEAxWc=l*nI43pDlNufvl-3DDLa=$ZC9e2tWJ<^(-a+QbF6E}rDDfY5*uL| zx4k(hy7dgsr!i(>tVfQ-rs6@ASvMS?o{sm<62*mVB!+~UhXcL#22@6RO~=<&FW#D_ zFD9A6Hc2_ET@Lr<{dBM-GIl46;V9Gw8MPSh`Hab&N#t>}SQ(LH2@vvW{&2ZzS{8SAp(89v-KUjFV3L`Oaa+OZ@ZLgp z`J-PBnXbfTeNLWoe0q*Z7USbYu4Z-<9G5%N(C1+97jES?(IBr^HgHG%B=%PI!gh?k zrfRus&6bsKI~*ZwkTzSfQPv@zk8CKUQb*=6I?|^cpPVBOXHBINWT$~N7TRgOqzLg? zh9y^Je3tGu2g`{C<3`TI#kE?=L=zASBrLmgZ^o( zK0ZgRthF36;e1{3T)!JRJJfSm>1<{yBI4^oZ&FP;K7_wt2Fl2EBR-k;d4of`W~6UT z3n7*VMIoBrPKd=^(Om>p7yjn-JU%){Jk~6k)N3^9uapt!`iNuJuHO&yBBAZd;p6>` zH=zWQAyTf(5Zpk{BYiRt-KE6#QyNYadaNa@6ty1A4?+-;iYcB{eRz&IDxI>+`jZ)E zxnT?%j9Z9S)k}_8+->M^phTj_>id)i0pAjHH434HDEa-#vT62(ix4X=;F)dK@24`} zS(15F18^cfI7f{4JDqLj9`toKXwfloTNL($t4(<|lsC1X&t+eh;j%ghL}!o8#KH>3 zUNEgvuBTgDvW_z~Y6qm6se-&d&IkD-%}&Jo=ZLB}*ffJ&TnQ%04~!8@3FC$1MRkde zz-nmw_Hfi|n^|#$mZnJ+JhJRV-I(YV)VCQBh0C!RGYdFfT~kzwWa^~X3l=RGcV-oya=dqrSWc8|E&43!R)4zYZe}caE$@BFK?cwjg{Ptt#@edyS#WUm4uipRs`41mb z4}Rt4Z$J6H_kQ=$`;Y$N{y#o__Wa?CKlzzl_*{d&DsWZc|BM19G$HQ&4>=GY+y%Ro z`S7i~V3+b8zIhkyQkKIv?t)#)ZFqGT>{3R<*YAQ|%47K2U9d~p3tznpb}47!D|f*z zWh#96PS}O~gfHC%yOfpi{#~$3xd<=sf?di$cySl(Qr^MyJ7E{H4c@y8b}7f;*sDcOe^#6dy~5Gp0A znlOtLiFw3~pWp`=@D23^oMgg?$&vP1XYaMn+UxVL9zXx;@rzF$Uw!$D;^V7t{_ywQ ztFIqkfBnrL|MvQ)Z^wWB_rE^<;ln2%&%XHnUq9;CPt7IR%-;R;XTQI${5nOwb(|P9y~XJiO<`?$S*mg6Gf+#m*YF6ol0q0V zC(Wdc+ojgE-M=h#T2KEEo<+sWGM+3h|HD_8R_VKb)EabAtN-r9&!6tq;%+=&gK~5| zmIkq~!(MsZZkn#R`A%UD_PTqB(pwKK4|PwnYAFv7S!O%&Y%k&X#FIiihyHwM-0HsD z)~%#ilI!XmyMASrou{H*j#@gZ_nzCwe7jaC_M2(lb{B`kRZlS**}L24>*wMq9cFBT z`nO9d*~f6#E$6pSJd-cgvptaMY!50}-&ioZO2)UFJ! z1HH(qvD($9NpCh=oaUpFJ?{JCo+q>Rct1049w&$0^Qb?nFYC=YxmQV+=k?Z#+RMvA zlv%Yf&T3bU*X_Xqp;W_oJ-NcN>Vm^CCHgdPf_}Yt!@bGrxsO)`Q}8e8_*rJx@nE{l zmm3@K%o?N44(c(u+-hyKX*3tbbFy45XusT0YB*wry(^#AopS};i&EC*hKN~OYF6u! zlwy6|H7C8xbG~BJzHgaQ6*YEj__AC*lgr?kb&E&rj?AfKP;72-+CXGZU@z;_#jIn| zQ~g%RuA5_WiK+nvl3+!p*Le{X6C({aCb-O59;ID>Z7 zyp${RQFN+wvdL5j}Ltl#pzbBtn(Bg03;3vhlWeYsq@@WhNa?+3W*$omOzIY zMpk*}C}7q=M>y05d20Y+#6m12b$9JF*P1Gf737*budUG*8b~luSa5C;VV)8yv{TwM z#$>oMz!@T>Mi?_=vBlm}MhRe!5QTyutGO`%1~$hvqnbjFwUbg=jV;lPTgfz*{0@+V zT53g*Gz^j;0B9}06-%uaR01WHvfr^Fg&d|(I7F?n9t3qj59 z@NM9zvRni1jTJPA&@5=?7;`NhKtVh*TwsE=hg2ELwc(tyKuDBkS_$T$6kIC95F&TT zaL@@+5Kl4ojw2xf)*J$;xEGozOuZ*iSp_|jPzOr_JF`FuNDO0I2&ckCgpd#bNZW8vL83JD?1cud1usrA-UX$%d($_C2; z=7d@0D3cCJV!~Iw4CzBsdmYw_EW;)+SZ%SgR0A7qP~YZKDk+Q>UUEeokkBfOJPK$S z5klvz6e1*u1L}Cls?ib~qcJwhLywU69?=jg?2HkdF;01iKO%V`^8oz{@;8vYA>ucP zaxPKG!><8)1Hi`*4-b#Iw-33u|Ma_>X^m)yJezumw3@R0iu as{iD^?7aT*j~>7G%a6=