From 6a2295e202804175e83f23ae364148999610d446 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 9 Mar 2020 00:43:05 +1100 Subject: [PATCH] client IP can be recorded - assuming nginx reverse proxy - choose x-forwarded-for --- config.json | 2 +- config.test.json | 2 +- db.go | 164 +++++++++++------------------------------------ db_crud.go | 150 +++++++++++++++++++++++++++++++++++++++++++ ip2long.go | 38 +++++++++++ main.go | 18 +++--- mysql-model.mwb | Bin 0 -> 7358 bytes purchase.go | 2 +- 8 files changed, 236 insertions(+), 140 deletions(-) create mode 100644 db_crud.go create mode 100644 ip2long.go create mode 100644 mysql-model.mwb diff --git a/config.json b/config.json index e1d85f2..bd3e721 100644 --- a/config.json +++ b/config.json @@ -12,6 +12,6 @@ "Driver": "mysql", "User" : "sp", "Pass" : "sp", - "Schema": "goblog" + "Schema": "leanwork_pay" } } \ No newline at end of file diff --git a/config.test.json b/config.test.json index 25cd387..d19079e 100644 --- a/config.test.json +++ b/config.test.json @@ -12,6 +12,6 @@ "Driver": "mysql", "User" : "sp", "Pass" : "sp", - "Schema": "goblog" + "Schema": "leanwork_pay" } } \ No newline at end of file diff --git a/db.go b/db.go index 9f0e9a8..543f3f2 100644 --- a/db.go +++ b/db.go @@ -2,149 +2,57 @@ package main import ( "database/sql" + "fmt" "log" "net/http" - "text/template" - - _ "github.com/go-sql-driver/mysql" ) -type Employee struct { - Id int - Name string - City string +type TransactionDB struct { + h *sql.DB } -func dbConn() (db *sql.DB) { - dbDriver := Config.DB.Driver - dbUser := Config.DB.User - dbPass := Config.DB.Pass - dbName := Config.DB.Schema - db, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName) - if err != nil { - panic(err.Error()) - } - return db -} - -var tmpl = template.Must(template.ParseGlob("form/*")) +var db TransactionDB -func Index(w http.ResponseWriter, r *http.Request) { - db := dbConn() - selDB, err := db.Query("SELECT * FROM Employee ORDER BY id DESC") +func (m *TransactionDB) conn(c AppConfig) error { + dbDriver := c.DB.Driver + dbUser := c.DB.User + dbPass := c.DB.Pass + dbName := c.DB.Schema + h, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName) if err != nil { panic(err.Error()) } - emp := Employee{} - res := []Employee{} - for selDB.Next() { - var id int - var name, city string - err = selDB.Scan(&id, &name, &city) - if err != nil { - panic(err.Error()) - } - emp.Id = id - emp.Name = name - emp.City = city - res = append(res, emp) - } - tmpl.ExecuteTemplate(w, "Index", res) - defer db.Close() -} - -func Show(w http.ResponseWriter, r *http.Request) { - db := dbConn() - nId := r.URL.Query().Get("id") - selDB, err := db.Query("SELECT * FROM Employee WHERE id=?", nId) - if err != nil { - panic(err.Error()) - } - emp := Employee{} - for selDB.Next() { - var id int - var name, city string - err = selDB.Scan(&id, &name, &city) - if err != nil { - panic(err.Error()) - } - emp.Id = id - emp.Name = name - emp.City = city - } - tmpl.ExecuteTemplate(w, "Show", emp) - defer db.Close() + m.h = h + fmt.Printf("%x", h) + return err } -func New(w http.ResponseWriter, r *http.Request) { - tmpl.ExecuteTemplate(w, "New", nil) +func (m *TransactionDB) close() { + defer m.h.Close() } -func Edit(w http.ResponseWriter, r *http.Request) { - db := dbConn() - nId := r.URL.Query().Get("id") - selDB, err := db.Query("SELECT * FROM Employee WHERE id=?", nId) - if err != nil { - panic(err.Error()) - } - emp := Employee{} - for selDB.Next() { - var id int - var name, city string - err = selDB.Scan(&id, &name, &city) - if err != nil { - panic(err.Error()) - } - emp.Id = id - emp.Name = name - emp.City = city - } - tmpl.ExecuteTemplate(w, "Edit", emp) - defer db.Close() -} - -func Insert(w http.ResponseWriter, r *http.Request) { - db := dbConn() - if r.Method == "POST" { - name := r.FormValue("name") - city := r.FormValue("city") - insForm, err := db.Prepare("INSERT INTO Employee(name, city) VALUES(?,?)") - if err != nil { - panic(err.Error()) - } - insForm.Exec(name, city) - log.Println("INSERT: Name: " + name + " | City: " + city) +func (m *TransactionDB) addRequest(r *http.Request) error { + if err := m.conn(Config); err != nil { + return err } - defer db.Close() - http.Redirect(w, r, "/", 301) -} - -func Update(w http.ResponseWriter, r *http.Request) { - db := dbConn() - if r.Method == "POST" { - name := r.FormValue("name") - city := r.FormValue("city") - id := r.FormValue("uid") - insForm, err := db.Prepare("UPDATE Employee SET name=?, city=? WHERE id=?") - if err != nil { - panic(err.Error()) - } - insForm.Exec(name, city, id) - log.Println("UPDATE: Name: " + name + " | City: " + city) - } - defer db.Close() - http.Redirect(w, r, "/", 301) -} + r.ParseForm() + //assuming form has been parsed + pickupUrl := r.Form["pickupUrl"][0] + receiveUrl := r.Form["receiveUrl"][0] + signType := r.Form["signType"][0] + orderNo := r.Form["orderNo"][0] + orderAmount := r.Form["orderAmount"][0] + orderCurrency := r.Form["orderCurrency"][0] + customerId := r.Form["customerId"][0] + sign := r.Form["sign"][0] + valid := isLeanworkFormValid(r.Form) + ip4 := getClientIPLong(r) -func Delete(w http.ResponseWriter, r *http.Request) { - db := dbConn() - emp := r.URL.Query().Get("id") - delForm, err := db.Prepare("DELETE FROM Employee WHERE id=?") - if err != nil { - panic(err.Error()) + insForm, err := m.h.Prepare("INSERT INTO request(pickupUrl, receiveUrl, signType, orderNo, orderAmount, orderCurrency, customerId, sign, valid, ip4) VALUES(?,?,?,?,?,?,?,?,?,?)") + if err == nil { + insForm.Exec(pickupUrl, receiveUrl, signType, orderNo, orderAmount, orderCurrency, customerId, sign, valid, ip4) + log.Println("INSERT: customerId: " + customerId + " | orderAmount: " + orderCurrency + " " + orderAmount) } - delForm.Exec(emp) - log.Println("DELETE") - defer db.Close() - http.Redirect(w, r, "/", 301) + m.close() + return err } diff --git a/db_crud.go b/db_crud.go new file mode 100644 index 0000000..3472148 --- /dev/null +++ b/db_crud.go @@ -0,0 +1,150 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "text/template" + + _ "github.com/go-sql-driver/mysql" +) + +type Employee struct { + Id int + Name string + City string +} + +var tmpl = template.Must(template.ParseGlob("form/*")) + +func dbConn() (db *sql.DB) { + dbDriver := Config.DB.Driver + dbUser := Config.DB.User + dbPass := Config.DB.Pass + dbName := Config.DB.Schema + db, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName) + if err != nil { + panic(err.Error()) + } + return db +} + +func Index(w http.ResponseWriter, r *http.Request) { + db := dbConn() + selDB, err := db.Query("SELECT * FROM Employee ORDER BY id DESC") + if err != nil { + panic(err.Error()) + } + emp := Employee{} + res := []Employee{} + for selDB.Next() { + var id int + var name, city string + err = selDB.Scan(&id, &name, &city) + if err != nil { + panic(err.Error()) + } + emp.Id = id + emp.Name = name + emp.City = city + res = append(res, emp) + } + tmpl.ExecuteTemplate(w, "Index", res) + defer db.Close() +} + +func Show(w http.ResponseWriter, r *http.Request) { + db := dbConn() + nId := r.URL.Query().Get("id") + selDB, err := db.Query("SELECT * FROM Employee WHERE id=?", nId) + if err != nil { + panic(err.Error()) + } + emp := Employee{} + for selDB.Next() { + var id int + var name, city string + err = selDB.Scan(&id, &name, &city) + if err != nil { + panic(err.Error()) + } + emp.Id = id + emp.Name = name + emp.City = city + } + tmpl.ExecuteTemplate(w, "Show", emp) + defer db.Close() +} + +func New(w http.ResponseWriter, r *http.Request) { + tmpl.ExecuteTemplate(w, "New", nil) +} + +func Edit(w http.ResponseWriter, r *http.Request) { + db := dbConn() + nId := r.URL.Query().Get("id") + selDB, err := db.Query("SELECT * FROM Employee WHERE id=?", nId) + if err != nil { + panic(err.Error()) + } + emp := Employee{} + for selDB.Next() { + var id int + var name, city string + err = selDB.Scan(&id, &name, &city) + if err != nil { + panic(err.Error()) + } + emp.Id = id + emp.Name = name + emp.City = city + } + tmpl.ExecuteTemplate(w, "Edit", emp) + defer db.Close() +} + +func Insert(w http.ResponseWriter, r *http.Request) { + db := dbConn() + if r.Method == "POST" { + name := r.FormValue("name") + city := r.FormValue("city") + insForm, err := db.Prepare("INSERT INTO Employee(name, city) VALUES(?,?)") + if err != nil { + panic(err.Error()) + } + insForm.Exec(name, city) + log.Println("INSERT: Name: " + name + " | City: " + city) + } + defer db.Close() + http.Redirect(w, r, "/", 301) +} + +func Update(w http.ResponseWriter, r *http.Request) { + db := dbConn() + if r.Method == "POST" { + name := r.FormValue("name") + city := r.FormValue("city") + id := r.FormValue("uid") + insForm, err := db.Prepare("UPDATE Employee SET name=?, city=? WHERE id=?") + if err != nil { + panic(err.Error()) + } + insForm.Exec(name, city, id) + log.Println("UPDATE: Name: " + name + " | City: " + city) + } + defer db.Close() + http.Redirect(w, r, "/", 301) +} + +func Delete(w http.ResponseWriter, r *http.Request) { + db := dbConn() + emp := r.URL.Query().Get("id") + delForm, err := db.Prepare("DELETE FROM Employee WHERE id=?") + if err != nil { + panic(err.Error()) + } + delForm.Exec(emp) + log.Println("DELETE") + defer db.Close() + http.Redirect(w, r, "/", 301) +} diff --git a/ip2long.go b/ip2long.go new file mode 100644 index 0000000..caa04ee --- /dev/null +++ b/ip2long.go @@ -0,0 +1,38 @@ +package main + +import ( + "bytes" + "encoding/binary" + "net" + "net/http" + "strconv" + "strings" +) + +func ip2Long(ip string) uint32 { + var long uint32 + binary.Read(bytes.NewBuffer(net.ParseIP(ip).To4()), binary.BigEndian, &long) + return long +} + +func backtoIP4(ipInt int64) string { + + // need to do two bit shifting and “0xff” masking + b0 := strconv.FormatInt((ipInt>>24)&0xff, 10) + b1 := strconv.FormatInt((ipInt>>16)&0xff, 10) + b2 := strconv.FormatInt((ipInt>>8)&0xff, 10) + b3 := strconv.FormatInt((ipInt & 0xff), 10) + return b0 + "." + b1 + "." + b2 + "." + b3 +} + +func getClientIP(r *http.Request) string { + //a := r.RemoteAddr // always be 127.0.0.1:300456 port number may vary + a := r.Header.Get("X-Forwarded-For") + s := strings.Split(a, ":") + return s[0] +} + +func getClientIPLong(r *http.Request) uint32 { + s := getClientIP(r) + return ip2Long(s) +} diff --git a/main.go b/main.go index 3937313..5575809 100644 --- a/main.go +++ b/main.go @@ -7,15 +7,15 @@ import ( func main() { readConfig() - log.Println("Server started on: http://localhost:8080") - //http.HandleFunc("/", StartPay) + http.HandleFunc("/", StartPay) - http.HandleFunc("/", Index) - http.HandleFunc("/show", Show) - http.HandleFunc("/new", New) - http.HandleFunc("/edit", Edit) - http.HandleFunc("/insert", Insert) - http.HandleFunc("/update", Update) - http.HandleFunc("/delete", Delete) + // http.HandleFunc("/", Index) + // http.HandleFunc("/show", Show) + // http.HandleFunc("/new", New) + // http.HandleFunc("/edit", Edit) + // http.HandleFunc("/insert", Insert) + // http.HandleFunc("/update", Update) + // http.HandleFunc("/delete", Delete) http.ListenAndServe(":8080", nil) + log.Println("Server started on: http://localhost:8080") } diff --git a/mysql-model.mwb b/mysql-model.mwb new file mode 100644 index 0000000000000000000000000000000000000000..4908adea0c733badea946aa627124c99336ff06b GIT binary patch literal 7358 zcmZ{pWl$VYv!>DD5`ydC?(Xh3xI@t3?hMWehJsd0@-B}#G%vih~>@SSu-S*m(d;ioqTeCR%jxby* z%BGpL+iMBS`VmbmPK-l`6i)I;S9Yvuzdu3CF{d&+0_A`x#Nxzkl}? zlx;#Nktunv`cQ|8*dG4OsTMe3hK9=HZ}E%s!Cz+tg97>LVKbGI$hL-m6FhOWXC}P= zdfEO1@UKtA_=Uj&L*x@G#BN!R8vj**@8Jcp8;-mPt5`_}XYk2!*Dx8YTYEX?zKGEZ44>miHfJyF4D(AFz_r}!jReZCW7M-Lt z4s}lRgzo)ML{3y#(UD^QpXVbtHqu@yMr=qBcSuxTK+J~%hE=GWXtZ@|RwblQgVY&k z>+c!uT?VruHZ9!P$;%_PuWzNfOl07hV#tz9Rpt`w$>CwkinnnU`r9~e91g9AcSFsN zJL&5Mn(9?GWZpFtp@JPF9L`>EuK56Dh-u`e$)}kNXwFQma?C|YajK2zjS%NBAZlOy z_Vb0|KzafHien@>D+C@-?Frrh{PIi`H=F#^Hz@T`fdVPLLQLKlBGW);Psf2dteiMh z+sDpidN~(kN`Ex^d31(`F|OTNmk#ks5aRiHT&DS94TO;s#x-&>PbI|zQTgFIK6e^n z@KTRtB4-9!{)zGO_(M3d(J8K-jlkIjg~^FNG1K31AefSEdz&}b3*`giElyuxOw(sd z1S(^HR}2X#bL@CE|Ewnr`Ez_$|H!8Wm>Jk16TOtq9Kg9M)GzeT15J~OF_;<)E?*_1 z->ITfB0A%vMz8#+;GsI%do3)Cc+kjh5IGF{K+0i@sP?LzM%CGX_V1nG$k=+(is1=G z`jXA_v|m#;Qi=TE<~tm4xNCMYvA7#!dgfhasea`x?eNLWuae|z-wieMZ)WuZn^A?H zU31oZt=sO%OYMS~2Agwv-O=1-C5i!&l@=EuyZU_unBTBQp*VQquSK#(!2`M?-C&Z5_552R! zY2qg#VP>fGBhQBX%A!k{v8 z^Od_UHZ&$|oJ>lx<^|u9yTE;rU6D1$pPp?6&+YzJ$BW3UxS%qfe;qMFDN(xK=tX*Hy1WU2W;aH7i#r1MQ^5sBkc9E z_}0<>cTFjOi?$aV5S#DACH@Ce)ld+qRLQxXwBgOPXu6?ZRoB{%7)3BDOzs#cG*;w1 zT6jf;B)LDxPsvuTSR3eU?aX)nkr-xv+j&Kr4^C`%;u|rB%@wAqmgv9|?8IFxMB9|o z^>t7(WzRI>YL?^U=pf53dA_E9Md}_o8mUZX9+fQy=IuVIrJqckh}aA-@U&?4I}p!pOLlt}rx0CP5&7`iJuNN%<>GzgUi{T>>cnYCVo@a=NX+cScB7j?$q z1K&#I&r4{tjRii|@PR*SohSm+q-1JI^h$@av4yOy#d_<>xcUqjlY!E|wN@Q$)-p<_Qby zv(e3~gbnN@!*`W(0!589f`#HwEx_f+#AxZr#kE<=^Ss)W!k&MamfiMzh}u5RNMe7> z8LHaBSe&ir+>)75bXOH+4ZCb!D%Xjk3r+}TMLb$y9Lto58CmpU%aLOdWh8*8j~hyH zbak}1lGENcVIez-Fq)umXa7P`P+rTFACCo{uPO=flNJyc#2-_#$@FIac8=)Ln1Q+4 zi8qyrIJ7YEkuyjvkp3}=*ZMsy5_+38@-Ut2{7{jQ15*n=I`mKyu3J7xxbm6aV=_Ld zU!hg`V^>jX8xvX4NaGTfGA9J%ht;qT9<@K>hiZ=q{^k)RPReFrc+p|EL+ zJK8{`+-rb%QqO2^yizNnTY#IVi~nz05>ZEYl1`1;>65WTIYtqztye+u1xDw)5Dw`b{Uf=>57NPwmL=}bcjEx z{k$@iPC6A2oik#)EHqhGR}W5(6PD1k`kgnVyWK910=%Xsi9s&Q_GLy*R-F^RXKwQ{ z>PeX)M>*T{#SU=;NCLkbjAGY;4?8+pM_4emTg9* zEWzMU$vaW5<~n9-4oAM@Iy37#FPk{ZHDG|eR36?LmPKUQN?Ki|Ic0gH1Ed+F4i-;|S6*B;~KLf@CyX14q zhRik(Pg-VknoIDnOI0@N(V~(>$#E{?r~O6wUKLbn^PZV~2+Wx(sS}t1#r1TnxSc8E zm9~Dl0adWLkM(B6w76-Pdtf)AXK-qqQ-cmZC=FVdZFGm*5Hj|sW`rJb>Csp z*$n4uKA+_rtoX*t?;G?xx-NWC8P4ZRJ=l8i41F0tAl-1Tix5`h$!NT$QwyX~T-wX6 za~>LO@5f4!qI@gZcwds9IC?v|lW7MfHI4_5$qjiqHn$x~j^~fQe|I8J#al3Wo%tdy z+XA!kOncMka)ZQBas){Rwjl}WOG>kl@H?A340Ip*DiKJLsV*^k7ji^+g(o7qPwx%M z#FD~;WlBQ}dg&WEd7arJEftr~^vxwj(POR8gwN7aua;*wD*mE-HO+!3@&01f6DU;d zaCJg3CR8lRTWZWDkq{xFY#$6y$y6;{9q2t~r+0^h!K}9Jy`m-5EHz6al*&(hiBFtQ zMLyod@dYl@WBd*S%O|+XSe#;)+{}x7?gSV)rYLlYMPsF7spdVhtuGy*|0*eId|s() zjYy7xDHs!u3DfH`HkWvhICY69w5bf&%nx&q2B z_wWT5@7ylwgba=XJyBsM+u?(MGB99Sv?9o3PinWFXz8b?@oLMum5=~5uaIg?DR&@?dj9iMZ>e|-DXT%*? zn)|Hk8uPUJKaOwz>sOYe$&XlgZ@cX6Bb_Jlhr4Z=l&>_XyF|`{ts(&;)g>TMGqbd$ zS%Murscy^$X>gzstoBiP>Z*k#4WZI<#U=HA>llu$qYuJlr^NZ7vE2l#9aRKAMJ+2H zOH3b0#Qi3sUyCuoPw5sKLf4wbZvbXuA1vsznvcwbWa%SwniY@4F7Ws)CL}p4ZF3=w z)2ACnXW2BZsGrqycgt$j%lll%y4KXO4SdSgPFhsnE?Tt8`g1B}UBeko5O=F<)(Hgg zvZ63cmRD5*&?<4=wO?D&v|q#j)PC72$$Qs)zcV{qHi@|Wc2?{sRl)kZ>CBaF#k z+40fFI7^eK2n}wLluKwhjllBujF;M@uMd5nroqcec;66PIeWfYSk{wsg3aEva0VS z=*DtOV4~L}!`CKw#@lb3llvj5|I4XuS*fI9V#1JEVX=y7Vap$UM6QP&eBIg0b626x zB5N-{xBJ)UW3XKz%Kb<8U>HAyPDkIm2+2zpF&2Nnv6cl4Bd3;T$C|b^Ka7s_!*jBI zT}a8^^?M)nceWXMte^5bGynsT;m^Q^rRb*+u;Ef)=V}-rr zSht+6uabpvKRwlISg0wl3YRh%p8FxBJ$Zih@K>x-D5+^2?X);A3}!Ga3cz@Fzx(zO zsm1KTaj%h6pF!dJZqxN%GD|_?$aGmFAm3G{VksE>t5DVaoImeV*bjZW)aEv1>ROuc8kZ z4~J)~mM+SjuRso0CVL&N>SO!)u8Nh5ga}!=_D{rhAQV;93Vix}M$s zW$es4dc)kj{QLyAs8>xE(X8UxF#lhfubbBg2{Ch1tK(l!aY#jKHO@Vd=T}q%^wVBf zg=-i(&2G>cq<`8!QEHUjU5{U|)bVl&I7jP~@lBC%#VP!l#Y5WTRX@vE7&A7(%h(@T z9`=9=3OO&O5n+i;pKQLn(_wTGe&)#1w;)CzK9$ez;zxNy9RXz5Z|_F40~{x!`_e?Do?&ud^`lt{k#4f- z%cG>f-%dgv5GuHNOO?p;^+UBMTS*-mUT%Lq)2G95)nsZq`bPDc1##BML}5M#ZdcZ- zVFoOFwIX(97NAIZDZZgj?j!F$)mnFEs13A*xJKpokVgK%ex@9*=|PgiktQhrc7uf* zkBeQ~UZ0SC+&h{M(OTjN{4??=GVwy{n=ft@XedN@>_Ps|AZghp_qBU?;Zbrvy?8A* z8P_QTPkPx3t1?^fgRKzEj!*A8Tqd29X9#-CBy@6`xeTaVZOT2%115{_B{0B<0jR%A`Vw+>~wo~JcYAv zfoA;C{vfwkmBySk__|Xr-vB?hwv0JTW!0Wb4_70GJ7#Ur!wqS3iY zUWt`{?m_xS!A69qSTYC|HDRI%zZY#IGrk(H@rO|#`i?=A7$sNfzW8?acSDH)=})j9 zkgi59MQy+xhlRulO^_s!)X68qBYn{hPyYl2pK^ABO7!3#9NE@MziMl^m)p6wr0vk* zvkZRgEE(2v#R^(al@=9y%w$hUN*^qB>*Y_{%SSqtG+5$BdgCI38LHpuD~lOBtC7Qt z=(8!J!z)fJtt%VplGIX(WT?v>G;eG}dcFr5QcunuAAV-PN6W;M+Am*MulvxXV>gMG zdKL1r^>r$rup4i864Lquzg5~m69BB%O4i~|F+7jwiqK6R{x7sHvjSOWyUNlJ#jz@J9@qkZ!V0iP&#a^ptGl%aJ zR*h>br*X3sf`;^%wbF@`a?!(e2P)fwHnMYe`HE?6<%_wMi=yIe)b}NYri-K^iz7AV z$LZ8WC!N{dj6`j#+U<@CjY~ao25QU1unhXnNbAPT)JLsMAmRvI5DN0Ij zt(LxRz?CNaY_HkNakiW0b^K$ocYjgc!GnFvqVJ}5u;gFIFY;p6h4 z?_;Hv@2sB4u4Ra38c#0Fj&?k*tgx%pX4%3B-rZPlR!);#qLTde24-fwINk+2gep!E zP*t;X)j>UI%+*}(#!NBypZeh^m=o`7JO%JY==2`E+1!{lj9Lz18 z%IYYpMTLm!Heb})?9pe>&$C|K3yv^{9v_(WuSJkcd63Dw}6nz-@9V+AzTX!=n7RUTclOU5icF_=CI?_e4&eF z*X*%>P~rTtu30gAYsV&Tjy>AWIu{OJpVAOn1uiQanZ(QF-)Mh4Oy~=8-hDC&<(tk` zm0sn8qsOl-sVKIP1Jgz6)=&|QwlzxhB7v(vCFnB|)#%g-m{jAM6FvrJ7au1VjVIG( z)4a*g!oSEZp(z{m$y?uobC|>e6LdC`>HeS@H)km!J)6D#2rYm&C5GX3th~xP-C1?* zn%3qu2$m$kQ?fZXV z&_8J8<9K#u1pSu+p+Z5S{4HV@W~>&b?xrjjW*4b5ssn7;qw8qYdX1dqhMON_F~jlG zQruP{k&icrVF5_Rf^|9+{-h7N-Rl9whi3Wp2v1Zm(jRU*CK4*h3&YWlO_`>wC z5kP~`41m}sMw5>!cb}IHq0;D&=gynYKO3kZzblOh^@lPsSrvyHf z1nCv)G%HVf%OHPD%~E58=MnS7uzoHuNCS@sW7eFj`v#wOQ2Hs+VCv9?XML=!U4A3j zs;j7lumVxP%gy`8T)bv!B}1ai(VQPG6|^$RI`QhWou!Hy7T?##w=b6b#O|h3>AOF& zQicQ6iuvEgvt-1@B;TfN1I}L5+eVyC<9Eyli42S}7Bx$?GdvZ2;VD_&Pm(vD#)J7R zAm1jvZU)_(yD4eC)PVC+=_WuknBZ+y;P5a9N3x0-ei{2Wkv!LxP(5P!#6VO=`SF`~ z9BLXdSz!;z}n(O*DCXc%nR{~6x>9mW5S*5H5TAIkfW