From f0681b77bdc2629f0e80b350bd35ef81b45b245c Mon Sep 17 00:00:00 2001 From: wenyifan Date: Fri, 2 Jun 2023 15:41:51 +0800 Subject: [PATCH] init commit --- .gitignore | 4 + 9d18eed7.0 | 32 + Evan_Assurance_Root_CA.crt | 32 + Evan_Timestamp.pem | 81 ++ admin.go | 145 +++ build.bat | 3 + cert.crt | 82 ++ global.go | 48 + go.mod | 16 + go.sum | 70 ++ parsec.go | 74 ++ parsec_manager.go | 429 +++++++ parsec_test.go | 15 + private.key | 27 + socket_model.go | 110 ++ socket_service.go | 262 ++++ utils.go | 65 + web/data/changelog/content.json | 5 + web/data/errors/codes.json | 445 +++++++ web/data/notifications/downtime.json | 1 + web_api.go | 351 ++++++ webapp/data/changelog/content.json | 5 + webapp/data/errors/codes.json | 445 +++++++ webapp/data/notifications/downtime.json | 1 + webapp/favicon.ico | Bin 0 -> 110847 bytes webapp/index.html | 47 + webapp/index.html.bak | 46 + webapp/parsecd | Bin 0 -> 1437039 bytes webapp/static/lib/matoya.js | 1512 +++++++++++++++++++++++ webapp/static/lib/parsec.js | 46 + webapp/static/lib/weblib.js | 252 ++++ 31 files changed, 4651 insertions(+) create mode 100644 .gitignore create mode 100644 9d18eed7.0 create mode 100644 Evan_Assurance_Root_CA.crt create mode 100644 Evan_Timestamp.pem create mode 100644 admin.go create mode 100644 build.bat create mode 100644 cert.crt create mode 100644 global.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 parsec.go create mode 100644 parsec_manager.go create mode 100644 parsec_test.go create mode 100644 private.key create mode 100644 socket_model.go create mode 100644 socket_service.go create mode 100644 utils.go create mode 100644 web/data/changelog/content.json create mode 100644 web/data/errors/codes.json create mode 100644 web/data/notifications/downtime.json create mode 100644 web_api.go create mode 100644 webapp/data/changelog/content.json create mode 100644 webapp/data/errors/codes.json create mode 100644 webapp/data/notifications/downtime.json create mode 100644 webapp/favicon.ico create mode 100644 webapp/index.html create mode 100644 webapp/index.html.bak create mode 100644 webapp/parsecd create mode 100644 webapp/static/lib/matoya.js create mode 100644 webapp/static/lib/parsec.js create mode 100644 webapp/static/lib/weblib.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9a3f8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +parsec +parsec.exe +config.conf \ No newline at end of file diff --git a/9d18eed7.0 b/9d18eed7.0 new file mode 100644 index 0000000..3c9af9b --- /dev/null +++ b/9d18eed7.0 @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbjCCA1agAwIBAgIII1tVz2zmPdEwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwIBcNMDgwMTAxMDAwMDAwWhgPMjA1MDEyMzEy +MzU5NTlaMEwxCzAJBgNVBAYTAkNOMQ0wCwYDVQQKEwRFdmFuMQ0wCwYDVQQLEwRF +dmFuMR8wHQYDVQQDExZFdmFuIEFzc3VyYW5jZSBSb290IENBMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAq4vXbtUk8y6n3AgH7ApVQ7KOSwymwQnRg0HQ +iZJfamIWYsa1uN7n2/W3tJy82ij0JyIqqiUfTZu8dr6yd12Um7RTN1j32x+3hzSF ++2Hag08vd4ksrHAm61MjZVqdQdi5+EMwR5YlaEYFrgBYsKfo+B4vjFYz2PW7IFlI +SSeHBrtJdKf+pIr0+e2Sp7O91NwAp3JiDChqqyMQbKLaXag0G0o422Mj555L2k6r +E1lNm2JzMKaHqP2Ql/GtCaZXmsD4oRtBfMOr5OkNeGA0lA9OXkZxDXYTGrgGfaY+ +kq7ig12eBwa2f09x++aPAYxci8I5fZGCpCPY5siAUpw7r9Tui8d2KOCVDGF4FZ3+ +vDnLDPXrrN/2FYHt0p/kNqn9WINLOtfQyk8Ko+zicxjTA9DH5tNyKvxs5NncVXeH +Gjgmto3OAJcRNfu/NOo3IEHRmyOHCtMK/U1oR3ByYsRxc9raKrPI/CBcJQ48XIut +OxdQ+5cK+7IumEXqiAxnvKK1SMBVMnSav7jrfAfq66RNOOTvxng1p2W52dTJIIZB +KcEpzrnMGmLLDba9Zp7v/6PsXYLtwzg0oOJZcnnqOCMz2jMt9o06HFlqYke8Z8Nb +35njmwN4PWyHpEZhbC9VttXqVVD7UaAMStsP2yS2L6Wdt3Af7Mr/kurhLBwK6XOp +H403rdsCAwEAAaNSMFAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9GuH/q/ +TaECv2bMT75Rrso+Hu4wCwYDVR0PBAQDAgGGMBEGCWCGSAGG+EIBAQQEAwIABzAN +BgkqhkiG9w0BAQsFAAOCAgEAjsWIGSK2ZJOiCBoH4CqMQNJa2nWOAAY99op8BXRi +Sx8tpbid+mZ6IOIBOY4GLT82lkbDbGmAOXhjDD9pMWYMbxko35MNX3j1/9BAIKU/ +W4U5NEIEnWogJJirJjtW+3BGSrbtZyTODCGf2nuJQsXz+YnBAVUNjKILmAVR1bCx +KSXo9YJdrfroHTxk+TB6wewiO8cs5/YlMfKQEyUxTdMOEzRdGvl0dkw6t4346BcS +FqY0tpJ6tvbatVjc+ka//ZxBdKTHWJqkcR0f5g91L0AMllRnAUKAyXIzxUlMB8zN +zfKXhSce2Wk+39kBaDrw4YS6SiJTXVX5ID2Myz/NPDY3upbjPVtDABloF35PcKlr +lekQmYUtN+QoHCb+LEkmHn6/AUdUke7J0Vr1gtqEjqC4f5zPVAx81ZLoBU3FQvb/ +DN8QLfdI2/qelfubCRv/XDH+ybhd9aQizKacbcEvpCCLCDkkDqhQyToVoJyeWlGY +s+v2oTcP2x+gPs/2uFGsCQE2U8re9B6BOwYaYaAhoPmHSz4hr7oYc45xop1B+70G +GTGHMHbrbKMPld66dKJQEJ9+mtkJbBrsX/ZHbRHCUjU2yMasSV/S9YHYWWqhC8E+ +BSCwrlnMnu4TTxF7uHx0nzWOJFRVfmowsfN4ffHDVoAXfuN7NLc56JsJszVyY0ij +YY8= +-----END CERTIFICATE----- diff --git a/Evan_Assurance_Root_CA.crt b/Evan_Assurance_Root_CA.crt new file mode 100644 index 0000000..3c9af9b --- /dev/null +++ b/Evan_Assurance_Root_CA.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbjCCA1agAwIBAgIII1tVz2zmPdEwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwIBcNMDgwMTAxMDAwMDAwWhgPMjA1MDEyMzEy +MzU5NTlaMEwxCzAJBgNVBAYTAkNOMQ0wCwYDVQQKEwRFdmFuMQ0wCwYDVQQLEwRF +dmFuMR8wHQYDVQQDExZFdmFuIEFzc3VyYW5jZSBSb290IENBMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAq4vXbtUk8y6n3AgH7ApVQ7KOSwymwQnRg0HQ +iZJfamIWYsa1uN7n2/W3tJy82ij0JyIqqiUfTZu8dr6yd12Um7RTN1j32x+3hzSF ++2Hag08vd4ksrHAm61MjZVqdQdi5+EMwR5YlaEYFrgBYsKfo+B4vjFYz2PW7IFlI +SSeHBrtJdKf+pIr0+e2Sp7O91NwAp3JiDChqqyMQbKLaXag0G0o422Mj555L2k6r +E1lNm2JzMKaHqP2Ql/GtCaZXmsD4oRtBfMOr5OkNeGA0lA9OXkZxDXYTGrgGfaY+ +kq7ig12eBwa2f09x++aPAYxci8I5fZGCpCPY5siAUpw7r9Tui8d2KOCVDGF4FZ3+ +vDnLDPXrrN/2FYHt0p/kNqn9WINLOtfQyk8Ko+zicxjTA9DH5tNyKvxs5NncVXeH +Gjgmto3OAJcRNfu/NOo3IEHRmyOHCtMK/U1oR3ByYsRxc9raKrPI/CBcJQ48XIut +OxdQ+5cK+7IumEXqiAxnvKK1SMBVMnSav7jrfAfq66RNOOTvxng1p2W52dTJIIZB +KcEpzrnMGmLLDba9Zp7v/6PsXYLtwzg0oOJZcnnqOCMz2jMt9o06HFlqYke8Z8Nb +35njmwN4PWyHpEZhbC9VttXqVVD7UaAMStsP2yS2L6Wdt3Af7Mr/kurhLBwK6XOp +H403rdsCAwEAAaNSMFAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9GuH/q/ +TaECv2bMT75Rrso+Hu4wCwYDVR0PBAQDAgGGMBEGCWCGSAGG+EIBAQQEAwIABzAN +BgkqhkiG9w0BAQsFAAOCAgEAjsWIGSK2ZJOiCBoH4CqMQNJa2nWOAAY99op8BXRi +Sx8tpbid+mZ6IOIBOY4GLT82lkbDbGmAOXhjDD9pMWYMbxko35MNX3j1/9BAIKU/ +W4U5NEIEnWogJJirJjtW+3BGSrbtZyTODCGf2nuJQsXz+YnBAVUNjKILmAVR1bCx +KSXo9YJdrfroHTxk+TB6wewiO8cs5/YlMfKQEyUxTdMOEzRdGvl0dkw6t4346BcS +FqY0tpJ6tvbatVjc+ka//ZxBdKTHWJqkcR0f5g91L0AMllRnAUKAyXIzxUlMB8zN +zfKXhSce2Wk+39kBaDrw4YS6SiJTXVX5ID2Myz/NPDY3upbjPVtDABloF35PcKlr +lekQmYUtN+QoHCb+LEkmHn6/AUdUke7J0Vr1gtqEjqC4f5zPVAx81ZLoBU3FQvb/ +DN8QLfdI2/qelfubCRv/XDH+ybhd9aQizKacbcEvpCCLCDkkDqhQyToVoJyeWlGY +s+v2oTcP2x+gPs/2uFGsCQE2U8re9B6BOwYaYaAhoPmHSz4hr7oYc45xop1B+70G +GTGHMHbrbKMPld66dKJQEJ9+mtkJbBrsX/ZHbRHCUjU2yMasSV/S9YHYWWqhC8E+ +BSCwrlnMnu4TTxF7uHx0nzWOJFRVfmowsfN4ffHDVoAXfuN7NLc56JsJszVyY0ij +YY8= +-----END CERTIFICATE----- diff --git a/Evan_Timestamp.pem b/Evan_Timestamp.pem new file mode 100644 index 0000000..1a0481e --- /dev/null +++ b/Evan_Timestamp.pem @@ -0,0 +1,81 @@ +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIIRgtihNJXdBgwDQYJKoZIhvcNAQELBQAwSjELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHTAbBgNVBAMTFEV2 +YW4gVGltZVN0YW1waW5nIENBMB4XDTA5MDEwMTAwMDAwMFoXDTMwMTIzMTIzNTk1 +OVowRDELMAkGA1UEBhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4x +FzAVBgNVBAMTDkV2YW4gVGltZXN0YW1wMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEApfi74R3iPCAOs5mas6J/GYzmkJsgd9iqCZla55l9sDt3L+umpzuz +PbsVUMJ8P4+Px/qA4YAaSC7Srnrr8I1CpoMXv78pQcZ7HuKvy6aTuOcRpQP0CNBp +TRBjuWfsNZp++6dw0Vrh72/Ns4TwMQk5KoWt/+Z7uiCqHJfIss49QpLh3jBvoXeE +puLFSgRArI+5f2C6Rnu1jsVLDMUQIEeOFLixUFaSflyRRK3alrrkcjFmx48vGkjq +RK3o5RAk91jGh9LXxSOP9Z6b4ux+F19p1I0fNzPUBEcJpI2bxXlqQuNRzeqQVcV7 +HhcqLHTAVdQU7TSJGGS07j1r8qqIcSiBLwIDAQABo4GDMIGAMAkGA1UdEwQCMAAw +HQYDVR0OBBYEFGSZBnFg3fovfS6FKbgM7RPcxfR6MB8GA1UdIwQYMBaAFGvYa8eJ +vx15Fl5lNKZH1gDJYhh1MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD +CDARBglghkgBhvhCAQEEBAMCBBAwDQYJKoZIhvcNAQELBQADggEBAJ30DQkU/ITD +BBdz6NWPUHL/s5J6y16KoU2jf0kquXo0srfv69QueEqHYrrwhBnbmUMP6hlva6iy +Jmi/7MxZmBZFLERzy7xoX6ilaZ/Cz+pfrZFG3yF7rysWCyzSC1FwGElShSxs0g7A +mvJkfdMI3ZRZQF666VGKugbv2Q2Xey8PieTpHPpWCChpu8+MHdEvcy6HzR88XHsZ +GXkEnbktKHIN7MWaBO51FeyrLO7zCYEHZbMeqZ+jgeKeGzzGfMEvt2BKsWGTyO4I +Cdoa2+1ZLAjY9O8MWJt5JkCVJYsvdI8dk38VgKdiP6Wf2MiOhBWo/T6lKnVY1EhI +dI0VxXLD4X4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEojCCAoqgAwIBAgIIHJxNBAcVXhowDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwHhcNMDkwMTAxMDAwMDAwWhcNMzAxMjMxMjM1 +OTU5WjBKMQswCQYDVQQGEwJDTjENMAsGA1UEChMERXZhbjENMAsGA1UECxMERXZh +bjEdMBsGA1UEAxMURXZhbiBUaW1lU3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDlatIMGNtfKEToe7u19hC4lxHUS5HyWS5Utp7gQ/La +zE3sXYAhsuNdVsWmXWL6O4xU7P/kX2i7OJyumw63xPsqrSKq0l1vN4y5GHFK0XBj +wynyPbF9GaUMfKGio/Z3wt64UtIEc+LYi/IAzLOsr16fszwVUYNxDFkD6fPd27UT +Fh7Pk0rQufatQX9stBAD0A0zsAGhM83ID69q3eFahgCsBL45YRaHrVZ8TdavtM0M +DCiKC6qEGtiF/Px+OMyKMnxAKnzXMPDYHfzXttcW2PhsWVU7O7cn5Xwk2R17DhL/ +p/s36rWWZRsaYL97iE35DherZIwYK2EUAFxTVfX1ibHZAgMBAAGjgYkwgYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUa9hrx4m/HXkWXmU0pkfWAMliGHUwHwYD +VR0jBBgwFoAUK9GuH/q/TaECv2bMT75Rrso+Hu4wCwYDVR0PBAQDAgGGMBMGA1Ud +JQQMMAoGCCsGAQUFBwMIMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQsF +AAOCAgEAUcvv6I96lTEjWd4Q50Z6Sm82UDQ48ZCGvT6OuKpqJ3/Em6+dfAT6laT0 +r0DSFEdqLNJVVHwqPN0FU48JVFAFu1UpZvc0YirsN1iBc/UTmtb6Z7F4NrjSz61/ +PG2nKzonZO5jfYRFt80tyYCULbN1pTZ49P/sfwf0cDrSwWAtp/7B1H7wuzpIcuM2 +XAJldRY3deGFIYWehMrvrgJZEJcRhDCpKk4mxQDnHWBXND8OPL+h0p4BgepT6jJv +u/mjy5C1cIFdvL1h/hSAv46TpdKp5M4FbB83LB9b4elChUzDW9a40Y5nxhjFPWJF +BbRxlKXTbcRseUe/FVf3EALU+hgw7B+cQ8LGmdfggb10DNe3hWiPATPmr1bYSNrv +WNj9Fz64UrENlfg4g+EL+4POfQTYbXfPwb4zFwxz79RkI/3T5WEsMDWzw2ReCz67 +lSrHObeu2H1qt2jhQyS7kJD0IEsVQ1KhsPreXPYMDhxY7xAtJCsfxHlkrPRGj2ov +jnqbdbFBDBA0hBbi/RyMAezIxqD5q26JdrnwLLQ41L9+1wXk99UpGu8obh/STEL8 +MvhHapDGh5u76DZMey47PrvTdzpoKxUxfetlRPD9rCdhmm8+k4Flb8knv09T8i01 +BMVa7lCgGtrSJIRKmjl9FPrja9Jn6OYfSQHIioueDQDTtOmf734= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbjCCA1agAwIBAgIII1tVz2zmPdEwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwIBcNMDgwMTAxMDAwMDAwWhgPMjA1MDEyMzEy +MzU5NTlaMEwxCzAJBgNVBAYTAkNOMQ0wCwYDVQQKEwRFdmFuMQ0wCwYDVQQLEwRF +dmFuMR8wHQYDVQQDExZFdmFuIEFzc3VyYW5jZSBSb290IENBMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAq4vXbtUk8y6n3AgH7ApVQ7KOSwymwQnRg0HQ +iZJfamIWYsa1uN7n2/W3tJy82ij0JyIqqiUfTZu8dr6yd12Um7RTN1j32x+3hzSF ++2Hag08vd4ksrHAm61MjZVqdQdi5+EMwR5YlaEYFrgBYsKfo+B4vjFYz2PW7IFlI +SSeHBrtJdKf+pIr0+e2Sp7O91NwAp3JiDChqqyMQbKLaXag0G0o422Mj555L2k6r +E1lNm2JzMKaHqP2Ql/GtCaZXmsD4oRtBfMOr5OkNeGA0lA9OXkZxDXYTGrgGfaY+ +kq7ig12eBwa2f09x++aPAYxci8I5fZGCpCPY5siAUpw7r9Tui8d2KOCVDGF4FZ3+ +vDnLDPXrrN/2FYHt0p/kNqn9WINLOtfQyk8Ko+zicxjTA9DH5tNyKvxs5NncVXeH +Gjgmto3OAJcRNfu/NOo3IEHRmyOHCtMK/U1oR3ByYsRxc9raKrPI/CBcJQ48XIut +OxdQ+5cK+7IumEXqiAxnvKK1SMBVMnSav7jrfAfq66RNOOTvxng1p2W52dTJIIZB +KcEpzrnMGmLLDba9Zp7v/6PsXYLtwzg0oOJZcnnqOCMz2jMt9o06HFlqYke8Z8Nb +35njmwN4PWyHpEZhbC9VttXqVVD7UaAMStsP2yS2L6Wdt3Af7Mr/kurhLBwK6XOp +H403rdsCAwEAAaNSMFAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9GuH/q/ +TaECv2bMT75Rrso+Hu4wCwYDVR0PBAQDAgGGMBEGCWCGSAGG+EIBAQQEAwIABzAN +BgkqhkiG9w0BAQsFAAOCAgEAjsWIGSK2ZJOiCBoH4CqMQNJa2nWOAAY99op8BXRi +Sx8tpbid+mZ6IOIBOY4GLT82lkbDbGmAOXhjDD9pMWYMbxko35MNX3j1/9BAIKU/ +W4U5NEIEnWogJJirJjtW+3BGSrbtZyTODCGf2nuJQsXz+YnBAVUNjKILmAVR1bCx +KSXo9YJdrfroHTxk+TB6wewiO8cs5/YlMfKQEyUxTdMOEzRdGvl0dkw6t4346BcS +FqY0tpJ6tvbatVjc+ka//ZxBdKTHWJqkcR0f5g91L0AMllRnAUKAyXIzxUlMB8zN +zfKXhSce2Wk+39kBaDrw4YS6SiJTXVX5ID2Myz/NPDY3upbjPVtDABloF35PcKlr +lekQmYUtN+QoHCb+LEkmHn6/AUdUke7J0Vr1gtqEjqC4f5zPVAx81ZLoBU3FQvb/ +DN8QLfdI2/qelfubCRv/XDH+ybhd9aQizKacbcEvpCCLCDkkDqhQyToVoJyeWlGY +s+v2oTcP2x+gPs/2uFGsCQE2U8re9B6BOwYaYaAhoPmHSz4hr7oYc45xop1B+70G +GTGHMHbrbKMPld66dKJQEJ9+mtkJbBrsX/ZHbRHCUjU2yMasSV/S9YHYWWqhC8E+ +BSCwrlnMnu4TTxF7uHx0nzWOJFRVfmowsfN4ffHDVoAXfuN7NLc56JsJszVyY0ij +YY8= +-----END CERTIFICATE----- diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..958a981 --- /dev/null +++ b/admin.go @@ -0,0 +1,145 @@ +package main + +import "net/http" + +func configHandler(w http.ResponseWriter, r *http.Request) { + w.Write(formatJson(parsecService)) +} + +func externalHandler(w http.ResponseWriter, r *http.Request) { + peerId := r.FormValue("peer") + addr := r.FormValue("addr") + if peerId == "" { + w.Write([]byte("Peer is empty")) + return + } + peer := parsecService.GetPeer(peerId) + if peer == nil { + w.Write([]byte("Peer not found")) + return + } + peer.External = addr + w.Write([]byte("Success")) +} + +func addUserHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("user") + password := r.FormValue("pass") + + if username == "" { + w.Write([]byte("user can not be empty")) + return + } + + if password == "" { + w.Write([]byte("pass can not be empty")) + return + } + user := parsecService.GetUserByEmail(username) + if user != nil { + w.Write([]byte("user exist")) + return + } + + parsecService.AddUser(username, password) + w.Write([]byte("Success")) +} + +func editUserHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("user") + password := r.FormValue("pass") + + if username == "" { + w.Write([]byte("user can not be empty")) + return + } + + if password == "" { + w.Write([]byte("pass can not be empty")) + return + } + user := parsecService.GetUserByEmail(username) + if user == nil { + w.Write([]byte("user not exist")) + return + } + + user.Password = password + w.Write([]byte("Success")) +} + +func removeUserHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("user") + + if username == "" { + w.Write([]byte("user can not be empty")) + return + } + + user := parsecService.GetUserByEmail(username) + if user == nil { + w.Write([]byte("user not exist")) + return + } + + parsecService.RemoveUser(username) + parsecService.RemovePeerByUser(username) + parsecService.RemoveSessionByUser(username) + parsecService.RemoveWsSessionByUser(username) + w.Write([]byte("Success")) +} + +func publicHandler(w http.ResponseWriter, r *http.Request) { + peerId := r.FormValue("peer") + public := r.FormValue("public") + + if peerId == "" { + w.Write([]byte("peer can not be empty")) + return + } + + if public == "" { + w.Write([]byte("public can not be empty")) + return + } + + peer := parsecService.GetPeer(peerId) + if peer == nil { + w.Write([]byte("peer not exist")) + return + } + peer.Public = public == "1" + w.Write([]byte("Success")) +} + +func assignHandler(w http.ResponseWriter, r *http.Request) { + peer := r.FormValue("peer") + user := r.FormValue("user") + action := r.FormValue("action") + + if peer == "" || action == "" { + w.Write([]byte("peer and action can not be empty")) + return + } + if action != "clear" && user == "" { + w.Write([]byte("user can not be empty")) + return + } + switch action { + case "add": + parsecService.AssignAdd(peer, user) + break + case "remove": + parsecService.AssignRemove(peer, user) + break + case "clear": + parsecService.AssignClear(peer) + break + } + w.Write([]byte("Success")) +} + +func trimHandler(w http.ResponseWriter, r *http.Request) { + parsecService.RemoveOfflinePeers() + w.Write([]byte("Success")) +} diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..bb0b154 --- /dev/null +++ b/build.bat @@ -0,0 +1,3 @@ +packr build -ldflags "-s -w" -trimpath +set GOOS=linux +packr build -ldflags "-s -w" -trimpath \ No newline at end of file diff --git a/cert.crt b/cert.crt new file mode 100644 index 0000000..c5fa5c5 --- /dev/null +++ b/cert.crt @@ -0,0 +1,82 @@ +-----BEGIN CERTIFICATE----- +MIIDwjCCAqqgAwIBAgIIeR+AB+cH7DwwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xGDAWBgNVBAMTD0V2 +YW4gVExTIFJTQSBDQTAeFw0yMjAxMDEwMDAwMDBaFw0yNDEyMzEyMzU5NTlaMEUx +CzAJBgNVBAYTAkNOMQ0wCwYDVQQKEwRFdmFuMQ0wCwYDVQQLEwRFdmFuMRgwFgYD +VQQDEw9FdmFuIFBhcnNlYyBUTFMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDg4Lt8vIfUjfeHJPaQSGpyMbxqtj4BUFMNCBqblIofcn3yVwanu0wCBVH+ +k1bjmBp6PpQ8wR/UboMWPHoVsz0XkcyWKeLju+eY0mnTA7wpImVzar225xLDb1CZ +rMnhPwX+yALGv8bbJNaQZgHp5cO4EiGqdqg60Z9/Q8MtxVkoarxIxlMQoXe/MLwf +1o9h2hU3nsuXRp5TKrcyYWF6V+S0muIUMEp3ZcNcYKdCslO4087Xae1eGWo7lt4p +Vhynh5dHqmadfh7u+ME2D0dQBnTx/Zo8YRMf+OAQtCdfP7aLw3uYy4ZXiR75c1Xm +uJPAx1ihYH+QHQqixcgA6/5G4N0VAgMBAAGjgbUwgbIwDAYDVR0TAQH/BAIwADAd +BgNVHQ4EFgQUGrW3LHAdxjM6y7+vDUyRuB2k6cUwHwYDVR0jBBgwFoAU5MxP/8gg +1Xg99Ioo6XKieL4HPeYwCwYDVR0PBAQDAgPoMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAjBgNVHREEHDAaggpwYXJzZWMuYXBwggwqLnBhcnNlYy5hcHAw +EQYJYIZIAYb4QgEBBAQDAgbAMA0GCSqGSIb3DQEBCwUAA4IBAQCUpZpoUqTJ8TUO +Y0xzJpnm7CfcSqYNPQHO3dUJBqsOZqTyIFM0TQsjXUQFxA2ERqkxwovlk9d07ToV +7T8E6vw++SDkIuIpDALYaw0F00nUOTwMe86/Sa+vyZHTqn9ghrE4Sx7cOhkqyM4V +sHIjjDsypLpvuDRTNUY6g+DaCMhnKG27toAHYQHTcmsj/pM5yyrWsl6E+3TW89gk +T7tXrnKTex/p+ihpm7RTzcL5WKmfobmXwi9+qdmA08C53RPSG1CMwthY7tqTK0Q7 +ugUMH9Ug15XPK96pCxcjOew4bEmfTIMXEGu63q+ZfvJFIko1ZrTLCB3RxvoNZ+P5 +xh4HBIi1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEhDCCAmygAwIBAgIIUw9sncwT5HcwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwHhcNMjIwMTAxMDAwMDAwWhcNMzIxMjMxMjM1 +OTU5WjBFMQswCQYDVQQGEwJDTjENMAsGA1UEChMERXZhbjENMAsGA1UECxMERXZh +bjEYMBYGA1UEAxMPRXZhbiBUTFMgUlNBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAox2P/00Y8RimWNxKi/iA3ERywf21imyNy+v8d47Bg2iH8PaZ +RAKYya19OkLQOsWFCS6L8/Bx/c/HPZfMOS62ro9IQ6D5UWP0rK0nSd7gvUU63f6K +6ZJ5LR1owAVx0xZ9b+sL89J+VlNuVR8+SjYx2OlOUonk3IPHkvm9WCtFMA2bkHjd +uG2KSJTn4roXr5nVhIi53RnSaiyBlRJ0OeJ2IPgmA/U5v/0Rr2I3YN7NJ/n+3bCO +kNeX/o3qa+C4PoedXfnlIGJyqcc3l6SCSsZ5IFyvQDgE8kuO9UYXk/jFQnYyHsNb +QA/ZiiR2mzQ12eeDxaqZAYYeC0ys/OL7ZvZt5QIDAQABo3EwbzAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTkzE//yCDVeD30iijpcqJ4vgc95jALBgNVHQ8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGCWCGSAGG+EIBAQQE +AwIAxzANBgkqhkiG9w0BAQsFAAOCAgEAPcEmb/L749blVCP5+RDFN+lS78MYQyMT +ZikN3AJP6pV5pTM3uc+zD+sproRslJum7DkeuPn2WltRN3bcB4cemvFMZyx0luFc +XVJ3Gy+wx0L0JexBri5B9iFovZaHxKM0uaBmHDyHfGV6NhsFJWlPL9XQTRQD6EQ4 +meWRNpEs+W5RCXMMQ7VqyRLovC4OdbnPkyZv+UGKajGzGPzm6hZfG3bA5TxSFney +ZerpxSnLASkzAe1w5G91RU3dPKPiULJq01l5uv2fy7p5KYDWSqxOrB0KvpZtAVRx +6VbmFEogmG66M2OGEm9ptz4FOh08KsTfcuS3KWiyVjBi/IcqzbOMWkkIrw0SM7Bu +tbgIrS4bbYUFfCJqYsRw0Nfmn1Ndll2sLD3iTqL5kqSaAsI9zGyEn5QPnZCGvx3R +ryAHAI+ImvfFMZjvbZ95++IBJ/7CDYlp25oVd/JBzxq2fpNqO+Z5r4Cg+UFve1uU +c6M1KGeKF15sQi69gkbcUPQpYaxT5+zTag88V0NIwhuGgmaMvvo1vN5O+tziL4B4 +HxJNaUJbv2rolJ7stspw8C6L6daP+7qfyIaZONJCGEXt46V4mDH9vV5/JWyhhuSE +rL39I9OzjaUhsooodJwcKUuU1BAzg3oaXxANzEPMILDHo7dPDWkY+JoVa/VW5Huc +2kzz4/hrJdU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbjCCA1agAwIBAgIII1tVz2zmPdEwDQYJKoZIhvcNAQELBQAwTDELMAkGA1UE +BhMCQ04xDTALBgNVBAoTBEV2YW4xDTALBgNVBAsTBEV2YW4xHzAdBgNVBAMTFkV2 +YW4gQXNzdXJhbmNlIFJvb3QgQ0EwIBcNMDgwMTAxMDAwMDAwWhgPMjA1MDEyMzEy +MzU5NTlaMEwxCzAJBgNVBAYTAkNOMQ0wCwYDVQQKEwRFdmFuMQ0wCwYDVQQLEwRF +dmFuMR8wHQYDVQQDExZFdmFuIEFzc3VyYW5jZSBSb290IENBMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAq4vXbtUk8y6n3AgH7ApVQ7KOSwymwQnRg0HQ +iZJfamIWYsa1uN7n2/W3tJy82ij0JyIqqiUfTZu8dr6yd12Um7RTN1j32x+3hzSF ++2Hag08vd4ksrHAm61MjZVqdQdi5+EMwR5YlaEYFrgBYsKfo+B4vjFYz2PW7IFlI +SSeHBrtJdKf+pIr0+e2Sp7O91NwAp3JiDChqqyMQbKLaXag0G0o422Mj555L2k6r +E1lNm2JzMKaHqP2Ql/GtCaZXmsD4oRtBfMOr5OkNeGA0lA9OXkZxDXYTGrgGfaY+ +kq7ig12eBwa2f09x++aPAYxci8I5fZGCpCPY5siAUpw7r9Tui8d2KOCVDGF4FZ3+ +vDnLDPXrrN/2FYHt0p/kNqn9WINLOtfQyk8Ko+zicxjTA9DH5tNyKvxs5NncVXeH +Gjgmto3OAJcRNfu/NOo3IEHRmyOHCtMK/U1oR3ByYsRxc9raKrPI/CBcJQ48XIut +OxdQ+5cK+7IumEXqiAxnvKK1SMBVMnSav7jrfAfq66RNOOTvxng1p2W52dTJIIZB +KcEpzrnMGmLLDba9Zp7v/6PsXYLtwzg0oOJZcnnqOCMz2jMt9o06HFlqYke8Z8Nb +35njmwN4PWyHpEZhbC9VttXqVVD7UaAMStsP2yS2L6Wdt3Af7Mr/kurhLBwK6XOp +H403rdsCAwEAAaNSMFAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9GuH/q/ +TaECv2bMT75Rrso+Hu4wCwYDVR0PBAQDAgGGMBEGCWCGSAGG+EIBAQQEAwIABzAN +BgkqhkiG9w0BAQsFAAOCAgEAjsWIGSK2ZJOiCBoH4CqMQNJa2nWOAAY99op8BXRi +Sx8tpbid+mZ6IOIBOY4GLT82lkbDbGmAOXhjDD9pMWYMbxko35MNX3j1/9BAIKU/ +W4U5NEIEnWogJJirJjtW+3BGSrbtZyTODCGf2nuJQsXz+YnBAVUNjKILmAVR1bCx +KSXo9YJdrfroHTxk+TB6wewiO8cs5/YlMfKQEyUxTdMOEzRdGvl0dkw6t4346BcS +FqY0tpJ6tvbatVjc+ka//ZxBdKTHWJqkcR0f5g91L0AMllRnAUKAyXIzxUlMB8zN +zfKXhSce2Wk+39kBaDrw4YS6SiJTXVX5ID2Myz/NPDY3upbjPVtDABloF35PcKlr +lekQmYUtN+QoHCb+LEkmHn6/AUdUke7J0Vr1gtqEjqC4f5zPVAx81ZLoBU3FQvb/ +DN8QLfdI2/qelfubCRv/XDH+ybhd9aQizKacbcEvpCCLCDkkDqhQyToVoJyeWlGY +s+v2oTcP2x+gPs/2uFGsCQE2U8re9B6BOwYaYaAhoPmHSz4hr7oYc45xop1B+70G +GTGHMHbrbKMPld66dKJQEJ9+mtkJbBrsX/ZHbRHCUjU2yMasSV/S9YHYWWqhC8E+ +BSCwrlnMnu4TTxF7uHx0nzWOJFRVfmowsfN4ffHDVoAXfuN7NLc56JsJszVyY0ij +YY8= +-----END CERTIFICATE----- diff --git a/global.go b/global.go new file mode 100644 index 0000000..8e80343 --- /dev/null +++ b/global.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "time" +) + +const ROLE_CLIENT = "client" +const ROLE_HOST = "host" + +var ( + Logger *log.Logger + configPath string + LastUserId = 1000 + parsecService *ParsecService + TimeLocation *time.Location = time.FixedZone("CST", 8*3600) +) + +type ConfigData struct { + LastUserId int `json:"lastUserId"` + Users []*UserInfo `json:"users"` + Peers []*PeerInfo `json:"peers"` + Sessions []*SessionInfo `json:"sessions"` +} + +func SaveConfig() { + config := &ConfigData{ + Users: parsecService.Users, + Peers: parsecService.Peers, + Sessions: parsecService.Sessions, + LastUserId: LastUserId, + } + marshal, _ := json.Marshal(config) + ioutil.WriteFile(configPath, marshal, 0777) +} + +func ReadConfig() { + b, _ := exists(configPath) + if b { + configData, _ := ioutil.ReadFile(configPath) + var config *ConfigData + json.Unmarshal(configData, &config) + parsecService.LoadConfig(config) + LastUserId = config.LastUserId + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c86a58 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module parsec + +go 1.18 + +require ( + github.com/gobuffalo/packr v1.30.1 + github.com/gorilla/websocket v1.5.0 + github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0 +) + +require ( + github.com/gobuffalo/envy v1.7.0 // indirect + github.com/gobuffalo/packd v0.3.0 // indirect + github.com/joho/godotenv v1.3.0 // indirect + github.com/rogpeppe/go-internal v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5ec2acc --- /dev/null +++ b/go.sum @@ -0,0 +1,70 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0 h1:oLd/YLOTOgA4D4aAUhIE8vhl/LAP1ZJrj0mDQpl7GB8= +github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0/go.mod h1:XzXWuOd1wJ63MtICHh5+PnvCuxsB/d58T8TswEhI/9I= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/parsec.go b/parsec.go new file mode 100644 index 0000000..5bc2657 --- /dev/null +++ b/parsec.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "github.com/gobuffalo/packr" + "io" + "log" + "net/http" + "os" + "os/signal" + "time" +) + +func main() { + Logger = log.New(io.Writer(os.Stdout), "", log.Lshortfile|log.LstdFlags) + var port string + var address string + flag.StringVar(&port, "port", "443", "serve port") + flag.StringVar(&address, "listen", "", "listen address") + flag.StringVar(&configPath, "config", "config.conf", "config path") + flag.Parse() + + parsecService = new(ParsecService) + + ReadConfig() + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, os.Kill) + go func() { + <-c //阻塞直至有信号传入 + Logger.Println("Saving config") + SaveConfig() + Logger.Println("Save config done,exit now.") + os.Exit(0) + }() + + go func() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + <-ticker.C + SaveConfig() + Logger.Println("Saving config") + ticker.Reset(time.Minute) + } + }() + + http.HandleFunc("/v1/auth", authHandler) + http.HandleFunc("/v2/auth", authHandler) + http.HandleFunc("/me", meHandler) + http.HandleFunc("/friend-requests", friendHandler) + http.HandleFunc("/v2/hosts", hostsHandler) + http.HandleFunc("/exit-codes", exitHandler) + http.HandleFunc("/metrics", metricsHandler) + http.HandleFunc("/events", eventsHandler) + http.HandleFunc("/", serviceHandler) + + http.HandleFunc("/admin/config", configHandler) + http.HandleFunc("/admin/external", externalHandler) + http.HandleFunc("/admin/addUser", addUserHandler) + http.HandleFunc("/admin/editUser", editUserHandler) + http.HandleFunc("/admin/removeUser", removeUserHandler) + http.HandleFunc("/admin/public", publicHandler) + http.HandleFunc("/admin/assign", assignHandler) + http.HandleFunc("/admin/trim", trimHandler) + + box := packr.NewBox("web") + http.Handle("/data/", http.FileServer(box)) + + err := http.ListenAndServeTLS(address+":"+port, "cert.crt", "private.key", nil) + //err := http.ListenAndServe(":"+port, nil) + if err != nil { + Logger.Println(err) + } +} diff --git a/parsec_manager.go b/parsec_manager.go new file mode 100644 index 0000000..6507836 --- /dev/null +++ b/parsec_manager.go @@ -0,0 +1,429 @@ +package main + +import ( + "github.com/gorilla/websocket" + "math/rand" + "strconv" + "sync" + "time" +) + +type UserInfo struct { + UserId int + Email string + Password string +} + +type PeerInfo struct { + PeerId string + Owner string + Assign []string + Online bool `json:"-"` + Build string + Name string + Players int + Public bool + Secret string + External string +} + +type SessionInfo struct { + SessionId string + Email string + UserId int + HostPeerId string + Role string + AttemptId string + Guid string + WsConn *websocket.Conn `json:"-"` +} + +type ParsecService struct { + sessionLock sync.Mutex + wsSessionLock sync.Mutex + peerLock sync.Mutex + userLock sync.Mutex + Sessions []*SessionInfo `json:"sessions"` + WsSessions []*SessionInfo `json:"wsSessions"` + Users []*UserInfo `json:"users"` + Peers []*PeerInfo `json:"peers"` +} + +func (p *ParsecService) LoadConfig(config *ConfigData) { + p.sessionLock.Lock() + p.userLock.Lock() + p.peerLock.Lock() + defer p.sessionLock.Unlock() + defer p.userLock.Unlock() + defer p.peerLock.Unlock() + + p.Peers = config.Peers + p.Users = config.Users + p.Sessions = config.Sessions +} + +func (p *ParsecService) Login(email string, password string) *UserInfo { + p.userLock.Lock() + defer p.userLock.Unlock() + for _, user := range p.Users { + if user.Email == email && user.Password == password { + return user + } + } + return nil +} + +func (p *ParsecService) AddUser(email string, password string) *UserInfo { + p.userLock.Lock() + defer p.userLock.Unlock() + for _, user := range p.Users { + if user.Email == email { + return user + } + } + newUserId := 0 + for i := LastUserId + 1; i < 10000; i++ { + exist := false + for _, user := range p.Users { + if user.UserId == i { + exist = true + break + } + } + if !exist { + newUserId = i + LastUserId = i + break + } + } + tmp := &UserInfo{ + UserId: newUserId, + Email: email, + Password: password, + } + p.Users = append(p.Users, tmp) + + return tmp +} + +func (p *ParsecService) GetUserByEmail(email string) *UserInfo { + p.userLock.Lock() + defer p.userLock.Unlock() + for _, user := range p.Users { + if user.Email == email { + return user + } + } + return nil +} + +func (p *ParsecService) CheckUserExist(email string) bool { + p.userLock.Lock() + defer p.userLock.Unlock() + for _, user := range p.Users { + if user.Email == email { + return true + } + } + return false +} + +func (p *ParsecService) AddPeer(email string) *PeerInfo { + p.peerLock.Lock() + defer p.peerLock.Unlock() + + newPeerId := GetRandomString(27) + + for { + exist := false + for _, peer := range p.Peers { + if peer.PeerId == newPeerId { + exist = true + break + } + } + + if exist { + newPeerId = GetRandomString(27) + } else { + break + } + } + + peer := &PeerInfo{ + PeerId: newPeerId, + Owner: email, + Assign: []string{}, + } + p.Peers = append(p.Peers, peer) + return peer +} + +func (p *ParsecService) GetPeer(peerId string) *PeerInfo { + p.peerLock.Lock() + defer p.peerLock.Unlock() + for _, peer := range p.Peers { + if peer.PeerId == peerId { + return peer + } + } + return nil +} + +func (p *ParsecService) NewSession(email string, userId int, PeerId string) *SessionInfo { + p.sessionLock.Lock() + defer p.sessionLock.Unlock() + r := rand.New(rand.NewSource(time.Now().UnixNano())) + sessionId := GetSHA256HashCode([]byte(strconv.Itoa(r.Intn(999999)))) + + for { + exist := false + for _, session := range p.Sessions { + if session.SessionId == sessionId { + exist = true + break + } + } + + if exist { + sessionId = GetSHA256HashCode([]byte(strconv.Itoa(r.Intn(999999)))) + } else { + break + } + } + + session := &SessionInfo{ + SessionId: sessionId, + Email: email, + UserId: userId, + HostPeerId: PeerId, + } + p.Sessions = append(p.Sessions, session) + return session +} + +func (p *ParsecService) NewWsSession(sessionId string, role string, guid string, wsConn *websocket.Conn) *SessionInfo { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + session := p.GetSession(sessionId) + if session != nil { + for _, wsSession := range p.WsSessions { + if wsSession.SessionId == sessionId && wsSession.Role == role { + wsSession.WsConn = wsConn + wsSession.Guid = guid + return wsSession + } + } + + wsSession := &SessionInfo{ + SessionId: session.SessionId, + Email: session.Email, + UserId: session.UserId, + HostPeerId: session.HostPeerId, + Role: role, + Guid: guid, + WsConn: wsConn, + } + p.WsSessions = append(p.WsSessions, wsSession) + return wsSession + } + return nil +} + +func (p *ParsecService) RemoveWsSession(sessionId string, role string, sessionGuid string) { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + var tmp []*SessionInfo + for _, session := range p.WsSessions { + if !(session.SessionId == sessionId && session.Role == role && session.Guid == sessionGuid) { + tmp = append(tmp, session) + } + } + p.WsSessions = tmp +} + +func (p *ParsecService) SetupOnlineStatus(hostPeer *PeerInfo) { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + for _, session := range p.WsSessions { + if session.HostPeerId == hostPeer.PeerId { + return + } + } + hostPeer.Online = false +} + +func (p *ParsecService) GetWsSessionByPeerId(role string, peerId string) *SessionInfo { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + for _, session := range p.WsSessions { + if session.Role == role && session.HostPeerId == peerId { + return session + } + } + return nil +} + +func (p *ParsecService) GetWsSessionByAttemptId(peerId string, attemptId string) *SessionInfo { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + for _, session := range p.WsSessions { + if session.AttemptId == attemptId && session.HostPeerId == peerId { + return session + } + } + return nil +} + +func (p *ParsecService) GetSession(sessionId string) *SessionInfo { + p.sessionLock.Lock() + defer p.sessionLock.Unlock() + for _, session := range p.Sessions { + if session.SessionId == sessionId { + return session + } + } + return nil +} + +func (p *ParsecService) GetPeersByEmail(email string) []*PeerInfo { + p.peerLock.Lock() + defer p.peerLock.Unlock() + var peers []*PeerInfo + for _, peer := range p.Peers { + if peer.Owner == email || StringListContain(peer.Assign, email) { + peers = append(peers, peer) + } + } + return peers +} + +func (p *ParsecService) RemoveUser(email string) { + p.userLock.Lock() + defer p.userLock.Unlock() + var tmp []*UserInfo + for _, user := range p.Users { + if user.Email != email { + tmp = append(tmp, user) + } + } + p.Users = tmp +} + +func (p *ParsecService) RemovePeerByUser(email string) { + p.peerLock.Lock() + defer p.peerLock.Unlock() + var tmp []*PeerInfo + for _, peer := range p.Peers { + if peer.Owner != email { + tmp = append(tmp, peer) + } + } + p.Peers = tmp +} + +func (p *ParsecService) RemoveSessionByUser(email string) { + p.sessionLock.Lock() + defer p.sessionLock.Unlock() + var tmp []*SessionInfo + for _, session := range p.Sessions { + if session.Email != email { + tmp = append(tmp, session) + } else { + if session.WsConn != nil { + session.WsConn.Close() + } + } + } + p.Sessions = tmp +} + +func (p *ParsecService) RemoveWsSessionByUser(email string) { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + var tmp []*SessionInfo + for _, session := range p.WsSessions { + if session.Email != email { + tmp = append(tmp, session) + } else { + if session.WsConn != nil { + session.WsConn.Close() + } + } + } + p.WsSessions = tmp +} + +func (p *ParsecService) RemoveSessionByPeerId(peerId string) { + p.sessionLock.Lock() + defer p.sessionLock.Unlock() + var tmp []*SessionInfo + for _, session := range p.Sessions { + if session.HostPeerId != peerId { + tmp = append(tmp, session) + } else { + if session.WsConn != nil { + session.WsConn.Close() + } + } + } + p.Sessions = tmp +} + +func (p *ParsecService) RemoveWsSessionByPeerId(peerId string) { + p.wsSessionLock.Lock() + defer p.wsSessionLock.Unlock() + var tmp []*SessionInfo + for _, session := range p.WsSessions { + if session.HostPeerId != peerId { + tmp = append(tmp, session) + } else { + if session.WsConn != nil { + session.WsConn.Close() + } + } + } + p.WsSessions = tmp +} + +func (p *ParsecService) AssignAdd(peerId string, email string) { + peer := p.GetPeer(peerId) + if peer != nil { + if !StringListContain(peer.Assign, email) { + peer.Assign = append(peer.Assign, email) + } + } +} + +func (p *ParsecService) AssignClear(peerId string) { + peer := p.GetPeer(peerId) + if peer != nil { + peer.Assign = []string{} + } +} +func (p *ParsecService) AssignRemove(peerId string, email string) { + peer := p.GetPeer(peerId) + if peer != nil { + var tmp []string + for _, s := range peer.Assign { + if s != email { + tmp = append(tmp, s) + } + } + peer.Assign = tmp + } +} +func (p *ParsecService) RemoveOfflinePeers() { + p.peerLock.Lock() + defer p.peerLock.Unlock() + var tmp []*PeerInfo + for _, peer := range p.Peers { + if peer.Online { + tmp = append(tmp, peer) + } else { + p.RemoveSessionByPeerId(peer.PeerId) + p.RemoveWsSessionByPeerId(peer.PeerId) + } + } + p.Peers = tmp +} diff --git a/parsec_test.go b/parsec_test.go new file mode 100644 index 0000000..50556a1 --- /dev/null +++ b/parsec_test.go @@ -0,0 +1,15 @@ +package main + +import ( + "encoding/json" + "fmt" + "testing" +) + +func TestName(t *testing.T) { + var req *SocketRequest + str := "{\"version\":1,\"action\":\"conn_update\",\"payload\":{\"mode\":\"desktop\",\"name\":\"DESKTOP-4TKVI8T\",\"desc\":\"\",\"game_id\":\"\",\"secret\":\"\",\"max_players\":20,\"players\":0,\"public\":false,\"guests\":[]}}" + json.Unmarshal([]byte(str), &req) + fmt.Println(req.Version) + +} diff --git a/private.key b/private.key new file mode 100644 index 0000000..86339e8 --- /dev/null +++ b/private.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4OC7fLyH1I33hyT2kEhqcjG8arY+AVBTDQgam5SKH3J98lcG +p7tMAgVR/pNW45gaej6UPMEf1G6DFjx6FbM9F5HMlini47vnmNJp0wO8KSJlc2q9 +tucSw29QmazJ4T8F/sgCxr/G2yTWkGYB6eXDuBIhqnaoOtGff0PDLcVZKGq8SMZT +EKF3vzC8H9aPYdoVN57Ll0aeUyq3MmFhelfktJriFDBKd2XDXGCnQrJTuNPO12nt +XhlqO5beKVYcp4eXR6pmnX4e7vjBNg9HUAZ08f2aPGETH/jgELQnXz+2i8N7mMuG +V4ke+XNV5riTwMdYoWB/kB0KosXIAOv+RuDdFQIDAQABAoIBAEq03hRWXZmTgEP5 +V6AfLp25QCsDWB3/nVea9ZvyAODpnEXB+4gFhP623cKBGECL61/pIj38uqJMBGiC +ttw2q3kFCr5oM+QMLKhsXpOnjf7sWl+5ekUlBuq+NDyZVofp9AfsUl/Mnjd3SYC3 +IrOdjSO9gkmrGcBQm3gf/ttZ0IDINRhVpcK3mwfdlsmYaEnICoVrwe30xQK0GRfT +uSLiwn/RCKoeOS0aVL/LT9Qs9hKlMPBoV78PrwPzQ2JLbyyjpA+AUtXME8mS46Yb +UVFZnCzGpCPTVXp3pTAQcHAGUypKMlMp3i4deiz6UN4ESOcBMMqVlcwKwt4d+frC +P36ybgECgYEA/PV08Egl3sCK/nq2GPnpZ8kWQym4nrlFWw6nm/NR9wNpA3e/mWSP +/P2uGIafFeX8mii1eJlWcCW6tJC61wwIBSrrhoN4YH7Rj1Bplls4RD+AJlit2gg+ +HT9j9dus7U0JvW+H7Uk7xZBshV1+pT+eirkXxvpUH4OlRc+ZHNZ/8nUCgYEA45TZ +eXLd7D/H2wDNqZleKp26QRsfD10FuI9f1VjKgnlTQdxG40P9fvLxxjFFllgX7YzH +dKJvL9kpKcQULRo4BoddWUfMWFt5ZN52njq1euMWxOXqtNywpypr489nqp6QBHWY +nhatkztaFpBopeTkO5c37QH0M5bEyxXCgpwMrCECgYEAuTU+mW85yw5OtmRCT6cr +LcIdeq9hbVVZYoIoVhahPKpSiSd0MWtfwWw7u9lVQUNS38xOki4zC4mUWgBdzHYS +qTXznFlGGeDArp3BsUS4vb+ApJLpN2oxkFiJZ8mfo190ci7m5uVnzg8gZcU+pN8f +xZIfxqAiV7CbobGN+X9TzsECgYAoRllOQuO/QXJO8Y9z6i5eAFfL2c7fWyj+BnGB +Qhtkh7ASQbdR1OBxrPDYkDOubZyeb4GExJJEt3uvZoHjkXZEwYPlnu0s3dNX5H69 +dcpUGwgWhFHK/BtPGhTJ1hSUf0chYuZFY+IH4kMJJzk90ooJebNuACCFWLMu9YTc +tF0RwQKBgHvoxHCZ9QIXtBPj6GiKEzYaI6Jz2G52HPTsSdLJWNuCvuND94DZ78Eo +lP754ntgpatyPYm8uxZMKkY9YAkyth8KZngrhOcRitSY3PWkQgh+cGO3gAZ5o7Mc +N1lJjMHGDFvQHi+d/M17ZJb9/BaK49JH4LlPPptV5T+2RgUGZCKy +-----END RSA PRIVATE KEY----- diff --git a/socket_model.go b/socket_model.go new file mode 100644 index 0000000..ff2df43 --- /dev/null +++ b/socket_model.go @@ -0,0 +1,110 @@ +package main + +type SocketRequest struct { + Version int `json:"version"` + Action string `json:"action"` +} + +type ConnUpdateRequest struct { + SocketRequest + Payload ConnUpdatePayload `json:"payload"` +} + +type ConnUpdatePayload struct { + Mode string `json:"mode"` + Name string `json:"name"` + Desc string `json:"desc"` + GameId string `json:"game_id"` + Secret string `json:"secret"` + MaxPlayers int `json:"max_players"` + Players int `json:"players"` + Public bool `json:"public"` + Guests []Guest `json:"guests"` +} + +type Guest struct { + GuestId int `json:"guest_id"` + UserId int `json:"user_id"` + Gamepad bool `json:"gamepad"` + Keyboard bool `json:"keyboard"` + Mouse bool `json:"mouse"` +} + +type OfferModel struct { + SocketRequest + Payload OfferPayload `json:"payload"` +} + +type OfferPayload struct { + To string `json:"to"` + Data any `json:"data"` + AttemptId string `json:"attempt_id"` + Secret string `json:"secret"` + AccessLinkId string `json:"access_link_id"` + From string `json:"from"` + IsOwner bool `json:"is_owner"` + SkipApproval bool `json:"skip_approval"` + Permissions Permission `json:"permissions"` + User OfferUser `json:"user"` + HostUser OfferUser `json:"host_user"` +} + +type Permission struct { + Gamepad bool `json:"gamepad"` + Keyboard bool `json:"keyboard"` + Mouse bool `json:"mouse"` +} +type OfferUser struct { + Id int `json:"id"` + TeamId string `json:"team_id"` + Name string `json:"name"` + ExternalId string `json:"external_id"` + ExternalProvider string `json:"external_provider"` +} + +type AnswerModel struct { + SocketRequest + Payload AnswerPayload `json:"payload"` +} + +type AnswerPayload struct { + To string `json:"to"` + Data any `json:"data"` + AttemptId string `json:"attempt_id"` + Approved bool `json:"approved"` + From string `json:"from"` + UserId int `json:"user_id"` +} + +type CandexModel struct { + SocketRequest + Payload CandexPayload `json:"payload"` +} + +type CandexPayload struct { + To string `json:"to"` + Data CandexData `json:"data"` + AttemptId string `json:"attempt_id"` + From string `json:"from"` +} + +type CandexData struct { + FromStun bool `json:"from_stun"` + Ip string `json:"ip"` + Lan bool `json:"lan"` + Port int `json:"port"` + Sync bool `json:"sync"` + VerData int `json:"ver_data"` +} + +type OfferCancelModel struct { + SocketRequest + Payload OfferCancelPayload `json:"payload"` +} + +type OfferCancelPayload struct { + To string `json:"to"` + AttemptId string `json:"attempt_id"` + From string `json:"from"` + UserId int `json:"user_id"` +} diff --git a/socket_service.go b/socket_service.go new file mode 100644 index 0000000..4e5536c --- /dev/null +++ b/socket_service.go @@ -0,0 +1,262 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/beevik/guid" + "github.com/gorilla/websocket" + "net" + "net/http" + "strconv" + "sync" + "time" +) + +var ( + upgrader = websocket.Upgrader{ + //允许跨域访问 + CheckOrigin: func(r *http.Request) bool { + return true + }, + EnableCompression: false, + } +) + +func serviceHandler(w http.ResponseWriter, r *http.Request) { + var mutex sync.Mutex + sessionId := r.FormValue("session_id") + build := r.FormValue("build") + role := r.FormValue("role") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if len(sessionId) < 10 { + sessionId = r.Header.Get("Sec-WebSocket-Protocol") + w.Header().Add("sec-websocket-protocol", sessionId) + } + + session := parsecService.GetSession(sessionId) + if session == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + + wsConn, upgradeErr := upgrader.Upgrade(w, r, w.Header()) + + if upgradeErr != nil { + return + } + + defer func() { + if reco := recover(); reco != any(nil) { + fmt.Printf("Service Runtime error caught: %v", reco) + } + }() + + hostPeer := parsecService.GetPeer(session.HostPeerId) + hostPeer.Build = build + + sessionGuid := guid.New().String() + + wsSession := parsecService.NewWsSession(sessionId, role, sessionGuid, wsConn) + + go func() { + for { + mutex.Lock() + err2 := wsConn.WriteMessage(websocket.PingMessage, []byte{0x70, 0x69, 0x6E, 0x67}) + mutex.Unlock() + if err2 != nil { + parsecService.RemoveWsSession(sessionId, role, sessionGuid) + if role == ROLE_HOST { + parsecService.SetupOnlineStatus(hostPeer) + } + wsConn.Close() + return + } + time.Sleep(30 * time.Second) + } + }() + + for { + //wsConn.SetReadDeadline(time.Now().In(TimeLocation).Add(time.Second * 60)) + messageType, p, readError := wsConn.ReadMessage() + if readError != nil { + Logger.Println("Service WebSocket断开,PeerId=" + hostPeer.PeerId + " Role=" + role + ",信息:" + readError.Error()) + parsecService.RemoveWsSession(sessionId, role, sessionGuid) + if role == ROLE_HOST { + parsecService.SetupOnlineStatus(hostPeer) + } + wsConn.Close() + return + } + if messageType == websocket.CloseMessage { + Logger.Println("Service WebSocket断开,PeerId=" + hostPeer.PeerId + " Role=" + role) + parsecService.RemoveWsSession(sessionId, role, sessionGuid) + if role == ROLE_HOST { + parsecService.SetupOnlineStatus(hostPeer) + } + wsConn.Close() + return + } + strData := string(p) + Logger.Println("DATA:" + strData + "\n") + + var baseRequest *SocketRequest + json.Unmarshal(p, &baseRequest) + mutex.Lock() + switch baseRequest.Action { + case "conn_update": + var connUpdateRequest *ConnUpdateRequest + json.Unmarshal(p, &connUpdateRequest) + peer := parsecService.GetPeer(session.HostPeerId) + peer.Name = connUpdateRequest.Payload.Name + peer.Players = connUpdateRequest.Payload.Players + peer.Public = connUpdateRequest.Payload.Public + peer.Secret = connUpdateRequest.Payload.Secret + peer.Online = true + case "offer": + var offerRequest *OfferModel + json.Unmarshal(p, &offerRequest) + + targetPeer := parsecService.GetPeer(offerRequest.Payload.To) + if targetPeer != nil { + wsSession.AttemptId = offerRequest.Payload.AttemptId + targetWsSession := parsecService.GetWsSessionByPeerId(ROLE_HOST, targetPeer.PeerId) + if targetWsSession != nil && targetWsSession.WsConn != nil { + targetUser := parsecService.GetUserByEmail(targetPeer.Owner) + offerRelay := &OfferModel{ + SocketRequest: SocketRequest{ + Version: 1, + Action: "offer_relay", + }, + Payload: OfferPayload{ + To: offerRequest.Payload.To, + Data: offerRequest.Payload.Data, + AttemptId: offerRequest.Payload.AttemptId, + Secret: offerRequest.Payload.Secret, + AccessLinkId: offerRequest.Payload.AccessLinkId, + From: wsSession.HostPeerId, + IsOwner: targetPeer.Owner == wsSession.Email, + SkipApproval: targetPeer.Owner == wsSession.Email || targetPeer.Public || StringListContain(targetPeer.Assign, wsSession.Email), + Permissions: Permission{ + Gamepad: targetPeer.Owner == wsSession.Email || targetPeer.Public || offerRequest.Payload.Secret == targetPeer.Secret || StringListContain(targetPeer.Assign, wsSession.Email), + Keyboard: targetPeer.Owner == wsSession.Email || targetPeer.Public || offerRequest.Payload.Secret == targetPeer.Secret || StringListContain(targetPeer.Assign, wsSession.Email), + Mouse: targetPeer.Owner == wsSession.Email || targetPeer.Public || offerRequest.Payload.Secret == targetPeer.Secret || StringListContain(targetPeer.Assign, wsSession.Email), + }, + User: OfferUser{ + Id: wsSession.UserId, + TeamId: "", + Name: wsSession.Email, + ExternalId: "", + ExternalProvider: "", + }, + HostUser: OfferUser{ + Id: targetUser.UserId, + TeamId: "", + Name: "", + ExternalId: "", + ExternalProvider: "", + }, + }, + } + + targetWsSession.WsConn.WriteMessage(websocket.TextMessage, formatJson(offerRelay)) + } + } + case "answer": + var answerRequest *AnswerModel + json.Unmarshal(p, &answerRequest) + targetSession := parsecService.GetWsSessionByAttemptId(answerRequest.Payload.To, answerRequest.Payload.AttemptId) + if targetSession != nil && targetSession.WsConn != nil { + + answerRelay := &AnswerModel{ + SocketRequest: SocketRequest{ + Version: 1, + Action: "answer_relay", + }, + Payload: AnswerPayload{ + To: answerRequest.Payload.To, + Data: answerRequest.Payload.Data, + AttemptId: answerRequest.Payload.AttemptId, + Approved: answerRequest.Payload.Approved, + From: wsSession.HostPeerId, + UserId: wsSession.UserId, + }, + } + targetSession.WsConn.WriteMessage(websocket.TextMessage, formatJson(answerRelay)) + } + case "candex": + var candexRequest *CandexModel + json.Unmarshal(p, &candexRequest) + targetSession := parsecService.GetWsSessionByAttemptId(candexRequest.Payload.To, candexRequest.Payload.AttemptId) + if targetSession == nil { + targetSession = parsecService.GetWsSessionByPeerId(ROLE_HOST, candexRequest.Payload.To) + } + if targetSession != nil && targetSession.WsConn != nil { + candexRelay := &CandexModel{ + SocketRequest: SocketRequest{ + Version: 1, + Action: "candex_relay", + }, + Payload: CandexPayload{ + To: candexRequest.Payload.To, + Data: candexRequest.Payload.Data, + AttemptId: candexRequest.Payload.AttemptId, + From: wsSession.HostPeerId, + }, + } + targetSession.WsConn.WriteMessage(websocket.TextMessage, formatJson(candexRelay)) + + if hostPeer.External != "" { + addr, _ := net.ResolveIPAddr("ip", hostPeer.External) + candexRelayExternal := &CandexModel{ + SocketRequest: SocketRequest{ + Version: 1, + Action: "candex_relay", + }, + Payload: CandexPayload{ + To: candexRequest.Payload.To, + AttemptId: candexRequest.Payload.AttemptId, + From: wsSession.HostPeerId, + Data: CandexData{ + FromStun: true, + Ip: "::ffff:" + addr.IP.String(), + Lan: false, + Port: candexRequest.Payload.Data.Port, + Sync: false, + VerData: candexRequest.Payload.Data.VerData, + }, + }, + } + Logger.Println("UseCustom External: IP=" + addr.IP.String() + " PORT=" + strconv.Itoa(candexRequest.Payload.Data.Port)) + targetSession.WsConn.WriteMessage(websocket.TextMessage, formatJson(candexRelayExternal)) + } + } + + case "offer_cancel": + var offerCancelRequest *OfferCancelModel + json.Unmarshal(p, &offerCancelRequest) + targetSession := parsecService.GetWsSessionByAttemptId(offerCancelRequest.Payload.To, offerCancelRequest.Payload.AttemptId) + if targetSession != nil && targetSession.WsConn != nil { + offerCancelRelay := &OfferCancelModel{ + SocketRequest: SocketRequest{ + Version: 1, + Action: "offer_cancel_relay", + }, + Payload: OfferCancelPayload{ + To: offerCancelRequest.Payload.To, + AttemptId: offerCancelRequest.Payload.AttemptId, + From: wsSession.HostPeerId, + UserId: wsSession.UserId, + }, + } + targetSession.WsConn.WriteMessage(websocket.TextMessage, formatJson(offerCancelRelay)) + } + + } + mutex.Unlock() + + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..3ba5fe2 --- /dev/null +++ b/utils.go @@ -0,0 +1,65 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "math/rand" + "os" + "time" +) + +func exists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +func GetRandomString(l int) string { + str := "0123456789abcdefghijklmnopqrstuvwxyz" + bytes := []byte(str) + result := []byte{} + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < l; i++ { + result = append(result, bytes[r.Intn(len(bytes))]) + } + return string(result) +} + +func GetSHA256HashCode(message []byte) string { + hash := sha256.New() + //输入数据 + hash.Write(message) + //计算哈希值 + bytes := hash.Sum(nil) + //将字符串编码为16进制格式,返回字符串 + hashCode := hex.EncodeToString(bytes) + //返回哈希值 + return hashCode +} + +func formatJson(v interface{}) []byte { + marshal, _ := json.Marshal(v) + return marshal + //var b bytes.Buffer + //json.Indent(&b, marshal, "", " ") + // + //return b.Bytes() +} + +func StringListContain(list []string, target string) bool { + if list == nil { + return false + } + for _, item := range list { + if item == target { + return true + } + } + return false +} diff --git a/web/data/changelog/content.json b/web/data/changelog/content.json new file mode 100644 index 0000000..f6b7fb8 --- /dev/null +++ b/web/data/changelog/content.json @@ -0,0 +1,5 @@ +{ + "ver": 8, + "blurb": "- Client no longer disconnects when copying large amounts of text.\n- A new prompt to say Arcade goes away April 15, 2023.\n- [Windows] Windows 7 is now fully unsupported.\n- [Windows] Switch the WebSocket system to WinHTTP.\n- [Windows Paid] Virtual Tablets now works with up to 3 Screens.\n- [MacOS] Prompt to download the Apple Silicon build of Parsec.\n- [Ubuntu] We no longer block non text clipboard for other apps.", + "date": "150-87b" +} \ No newline at end of file diff --git a/web/data/errors/codes.json b/web/data/errors/codes.json new file mode 100644 index 0000000..cb3e1f2 --- /dev/null +++ b/web/data/errors/codes.json @@ -0,0 +1,445 @@ +{ + "3": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host OS closed the Parsec application due to a login/logout event, please reconnect." + }, + "4": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host shut down." + }, + "5": { + "url": "https://support.parsec.app/hc/en-us/articles/115002625091", + "type": "warning", + "title": "", + "desc": "You have been kicked by the host." + }, + "6": { + "url": "https://support.parsec.app/hc/en-us/articles/115002624531", + "type": "warning", + "title": "Your connection attempt wasn't approved in time", + "desc": "The host needs to approve your connection for you to join." + }, + "8": { + "url": "", + "type": "warning", + "title": "Your connection attempt was rejected", + "desc": "The host declined your request or blocked you." + }, + "9": { + "url": "", + "type": "warning", + "title": "", + "desc": "You canceled the connection attempt." + }, + "11": { + "url": "", + "type": "warning", + "title": "The game or computer you tried to join is full", + "desc": "Wait for a free slot or ask the host to increase the number of players." + }, + "12": { + "url": "https://support.parsec.app/hc/en-us/articles/6371362020365", + "type": "warning", + "title": "", + "desc": "You were disconnected from the host due to inactivity." + }, + "30": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "warning", + "title": "Your browser can not make connections in the web app", + "desc": "The Parsec web app only works in recent versions of Chrome and Edge, please use one of these browsers or install the Parsec app on your client to continue." + }, + "99": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601752", + "type": "warning", + "title": "The computer you're connecting to is no longer available", + "desc": "Check if the machine is online, get a new link if applicable, or try to restart Parsec on both ends." + }, + "101": { + "url": "", + "type": "warning", + "title": "Multi-factor authentication is required", + "desc": "Your team requires that you enable multi-factor authentication before making connections." + }, + "112": { + "url": "https://support.parsec.app/hc/en-us/articles/360049831391", + "type": "warning", + "title": "You don't have the permission to connect to this computer", + "desc": "You're not in the same Parsec Teams group as the computer you're attempting to connect to." + }, + "-14": { + "url": "https://support.parsec.app/hc/en-us/articles/115002625712", + "type": "error", + "title": "Your device failed to decode the video stream", + "desc": "Lower the host computer's resolution or check our article for more information." + }, + "-18": { + "url": "https://support.parsec.app/hc/en-us/articles/360001690972", + "type": "error", + "title": "Your device had issues decoding the video stream", + "desc": "This device may be incompatible with Parsec, please check our article for more information." + }, + "-19": { + "url": "https://support.parsec.app/hc/en-us/articles/360002529252", + "type": "error", + "title": "Your device isn't supported by Parsec", + "desc": "This Android device does not work with Parsec currently." + }, + "-1002": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626391", + "type": "error", + "title": "You don't have access to a host computer", + "desc": "Running Parsec in CLI mode requires that you enable hosting on a Windows machine." + }, + "-1003": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626611", + "type": "error", + "title": "Parsec does not have sufficient permissions on this device", + "desc": "Parsec was unable to write required data to your device." + }, + "-1400": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1401": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1402": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1403": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1404": { + "url": "", + "type": "error", + "title": "This game cannot be captured because it is running as administrator", + "desc": "Ensure your game and game store isn't running as administrator before trying to host." + }, + "-1405": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1406": { + "url": "https://support.parsec.app/hc/en-us/articles/6367984442765", + "type": "error", + "title": "Arcade's attempt to capture the game timed out", + "desc": "Ensure the game is the active window, or check our article for more information." + }, + "-1407": { + "url": "https://support.parsec.app/hc/en-us/articles/360059600491", + "type": "error", + "title": "Arcade is missing some important files to capture this game", + "desc": "Please check our article for more information." + }, + "-1408": { + "url": "", + "type": "error", + "title": "Arcade already interacted with this game previously", + "desc": "Restart your game before trying to host it again." + }, + "-1409": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1410": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1500": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1501": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1502": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1503": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1504": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1505": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1506": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1507": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1508": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1509": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1510": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1511": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1512": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1513": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1514": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1515": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-2001": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626051", + "type": "error", + "title": "You do not have permission to make this connection", + "desc": "You can't connect with this peer_id." + }, + "-6023": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601011", + "type": "error", + "title": "The peer-to-peer network connection between you and the other computer failed", + "desc": "Something is preventing Parsec from making the connection, check our article for more information." + }, + "-6024": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601011", + "type": "error", + "title": "Failed to connect to Parsec STUN servers", + "desc": "Something may be blocking your connection to the Parsec STUN server, check our article for more information." + }, + "-6101": { + "url": "https://support.parsec.app/hc/en-us/articles/360000059931", + "type": "error", + "title": "Parsec couldn't communicate with its servers", + "desc": "Something is preventing Parsec from making a websocket connection to its backend." + }, + "-6112": { + "url": "https://support.parsec.app/hc/en-us/articles/360023178732", + "type": "error", + "title": "IPv6 is disabled or not supported", + "desc": "Your device must support IPv6 even if you're using IPv4." + }, + "-6200": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The network connection between you and the other computer failed with webRTC", + "desc": "Please install Parsec on your client which is more likely to work." + }, + "-7000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626232", + "type": "error", + "title": "OpenGL was unable to be launched", + "desc": "Make sure your device meets our minimum requirements and is setup correctly." + }, + "-7007": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626272", + "type": "error", + "title": "This device has an incompatible version of OpenGL", + "desc": "Make sure your device meets our minimum requirements and is setup correctly." + }, + "-6107": { + "url": "https://support.parsec.app/hc/en-us/articles/4410906958861", + "type": "error", + "title": "", + "desc": "You must re-authenticate." + }, + "-12007": { + "url": "https://support.parsec.app/hc/en-us/articles/115003074512", + "type": "error", + "title": "The network connection was lost", + "desc": "The connection is unreliable, or the other computer has crashed." + }, + "-12010": { + "url": "https://support.parsec.app/hc/en-us/articles/115003074532", + "type": "error", + "title": "The computer you were connected to disappeared", + "desc": "The computer may have crashed or lost the internet connection." + }, + "-13000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002623532", + "type": "error", + "title": "The computer you're joining is trying to use an unsupported resolution", + "desc": "Change the resolution to something else on the host computer's Parsec settings." + }, + "-13008": { + "url": "https://support.parsec.app/hc/en-us/articles/115002623751", + "type": "error", + "title": "The computer you're joining is trying to use an unsupported resolution", + "desc": "Change the resolution to something else on the host computer's Parsec settings." + }, + "-13009": { + "url": "https://support.parsec.app/hc/en-us/articles/360000159992", + "type": "error", + "title": "The host resolution is above our maximum supported resolution of 3840x2160", + "desc": "Try to lower the resolution of the display on the host computer." + }, + "-13012": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host is not allowing any more guests." + }, + "-13015": { + "url": "https://support.parsec.app/hc/en-us/articles/360047224232", + "type": "error", + "title": "Something went wrong with the client device while initializing the stream", + "desc": "Your government or ISP may be blocking Parsec's encryption, you can try to use our web app or a VPN to get around this." + }, + "-14003": { + "url": "https://support.parsec.app/hc/en-us/articles/360002165172", + "type": "error", + "title": "We were unable to capture the screen of the computer you were attempting to connect to", + "desc": "Check our article for more information." + }, + "-15000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002624051", + "type": "error", + "title": "The computer you are attempting connect to does not support hardware video encoding", + "desc": "The host computer may have unsupported hardware or its drivers need an update, check our article for more information." + }, + "-15002": { + "url": "https://support.parsec.app/hc/en-us/articles/360000513331", + "type": "error", + "title": "The host encoder failed", + "desc": "This could be because on the host either HDR is active, the display resolution is too high, or the graphics driver needs an update." + }, + "-15106": { + "url": "https://support.parsec.app/hc/en-us/articles/360033132792", + "type": "error", + "title": "The host encoder failed", + "desc": "This could be because on the host either HDR is active, or the display resolution is too high." + }, + "-15107": { + "url": "https://support.parsec.app/hc/en-us/articles/360001383432", + "type": "error", + "title": "The resolution is too high on the host", + "desc": "Reduce the resolution on the host computer to connect." + }, + "-17001": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626412", + "type": "error", + "title": "Your Raspberry Pi is configured incorrectly", + "desc": "Disable the experimental OpenGL driver or check our article for more information about how to set up Raspberry Pi correctly." + }, + "-18000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626352", + "type": "error", + "title": "Parsec couldn't communicate with its servers", + "desc": "Check if your internet is working, and if it is, check our article for more information." + }, + "-22008": { + "url": "https://support.parsec.app/hc/en-us/articles/360002165172", + "type": "error", + "title": "", + "desc": "The host could not capture the screen." + }, + "-32001": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-32002": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-32003": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-800097": { + "url": "https://support.parsec.app/hc/en-us/articles/360004310251", + "type": "error", + "title": "IPv6 is disabled", + "desc": "Your device must have IPv6 enabled even if you're using IPv4." + }, + "-800098": { + "url": "https://support.parsec.app/hc/en-us/articles/360004310251", + "type": "error", + "title": "IPv6 is disabled", + "desc": "Your device must have IPv6 enabled even if you're using IPv4." + } +} \ No newline at end of file diff --git a/web/data/notifications/downtime.json b/web/data/notifications/downtime.json new file mode 100644 index 0000000..027809c --- /dev/null +++ b/web/data/notifications/downtime.json @@ -0,0 +1 @@ +{"display":false,"title":"","message":"","type":"","link_title":"","link_url":""} \ No newline at end of file diff --git a/web_api.go b/web_api.go new file mode 100644 index 0000000..78d3b27 --- /dev/null +++ b/web_api.go @@ -0,0 +1,351 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "net/http" +) + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` + AuthType string `json:"auth_type"` + HostPeerId string `json:"host_peer_id"` +} + +type LoginResponse struct { + UserId int `json:"user_id"` + SessionId string `json:"session_id"` + HostPeerId string `json:"host_peer_id"` + InstanceId string `json:"instance_id"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} + +type ParsecCommonResponse struct { + Data interface{} `json:"data"` +} + +type MeData struct { + Id int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Warp bool `json:"warp"` + Staff bool `json:"staff"` + TeamId string `json:"team_id"` + IsConfirmed bool `json:"is_confirmed"` + TeamIsActive bool `json:"team_is_active"` + IsSaml bool `json:"is_saml"` + IsGatewayEnabled bool `json:"is_gateway_enabled"` + IsRelayEnabled bool `json:"is_relay_enabled"` + HasTfa bool `json:"has_tfa"` + TeamEnforceTfa bool `json:"team_enforce_tfa"` + CanUpgradeSession bool `json:"can_upgrade_session"` + CanDowngradeSession bool `json:"can_downgrade_session"` + AppConfig AppConfig `json:"app_config"` + CohortChannel string `json:"cohort_channel"` + MarketingOptIn bool `json:"marketing_opt_in"` +} + +type AppConfig struct { +} + +type FriendResponse struct { + Data []FriendData `json:"data"` + HasMore bool `json:"has_more"` +} + +type FriendData struct { +} + +type HostData struct { + User UserData `json:"user"` + PeerId string `json:"peer_id"` + GameId string `json:"game_id"` + Build string `json:"build"` + Description string `json:"description"` + MaxPlayers int `json:"max_players"` + Mode string `json:"mode"` + Name string `json:"name"` + EventName string `json:"event_name"` + Players int `json:"players"` + Public bool `json:"public"` + GuestAccess bool `json:"guest_access"` + Online bool `json:"online"` + Self bool `json:"self"` +} +type UserData struct { + Id int `json:"id"` + Name string `json:"name"` + Warp bool `json:"warp"` + ExternalId string `json:"external_id"` + ExternalProvider string `json:"external_provider"` + TeamId string `json:"team_id"` +} +type HostResponse struct { + Data []HostData `json:"data"` + HasMore bool `json:"has_more"` +} + +func authHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + data, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + var login *LoginRequest + json.Unmarshal(data, &login) + Logger.Println("Auth:" + string(data) + "\n") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if login == nil { + Logger.Println("Error:Auth Failed:" + string(data) + "\n") + w.WriteHeader(403) + w.Write([]byte("{\n\t\"error\": \"Your email/password combination is incorrect.\"\n}")) + return + } + loginUser := parsecService.Login(login.Email, login.Password) + + if loginUser == nil { + Logger.Println("Error:Auth Password Error:" + string(data) + "\n") + w.WriteHeader(403) + w.Write([]byte("{\n\t\"error\": \"Your email/password combination is incorrect.\"\n}")) + return + } + var newPeer *PeerInfo + if login.HostPeerId != "" { + peer := parsecService.GetPeer(login.HostPeerId) + if peer != nil && peer.Owner == loginUser.Email { + newPeer = peer + } + } + + if newPeer == nil { + newPeer = parsecService.AddPeer(loginUser.Email) + } + + session := parsecService.NewSession(loginUser.Email, loginUser.UserId, newPeer.PeerId) + + response := &LoginResponse{ + UserId: loginUser.UserId, + SessionId: session.SessionId, + HostPeerId: newPeer.PeerId, + InstanceId: "", + } + w.WriteHeader(201) + w.Write(formatJson(response)) +} + +func GetAuthorization(w http.ResponseWriter, r *http.Request) string { + auth := r.Header.Get("Authorization") + if len(auth) > 10 { + return auth[7:] + } + return "" +} + +func meHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + + authorization := GetAuthorization(w, r) + + var user *UserInfo + session := parsecService.GetSession(authorization) + if session == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + user = parsecService.GetUserByEmail(session.Email) + if user == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + + data := &MeData{ + Id: user.UserId, + Name: user.Email, + Email: user.Email, + Warp: true, + Staff: true, + TeamId: "", + TeamIsActive: false, + IsSaml: false, + IsGatewayEnabled: false, + IsRelayEnabled: false, + HasTfa: false, + TeamEnforceTfa: false, + CanUpgradeSession: false, + CanDowngradeSession: false, + IsConfirmed: true, + CohortChannel: "release19", + MarketingOptIn: false, + AppConfig: AppConfig{}, + } + response := ParsecCommonResponse{ + Data: data, + } + Logger.Println("Me:" + string(formatJson(response)) + "\n") + w.Write(formatJson(response)) + +} + +func friendHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + authorization := GetAuthorization(w, r) + + session := parsecService.GetSession(authorization) + if session == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + + response := FriendResponse{ + Data: make([]FriendData, 0), + HasMore: false, + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(formatJson(response)) + +} + +func exitHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) +} + +func metricsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(204) +} + +func eventsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(204) +} + +func hostsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("access-control-allow-origin", "*") + if r.Method == "OPTIONS" { + w.Header().Add("access-control-allow-methods", "*") + w.Header().Add("access-control-allow-headers", "*") + w.Header().Add("access-control-max-age", "300") + w.WriteHeader(200) + return + } + authorization := GetAuthorization(w, r) + + var user *UserInfo + session := parsecService.GetSession(authorization) + if session == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + user = parsecService.GetUserByEmail(session.Email) + if user == nil { + err := &ErrorResponse{ + Error: "invalid session ID", + } + w.WriteHeader(401) + w.Write(formatJson(err)) + return + } + + var hosts []HostData + if user != nil { + peers := parsecService.GetPeersByEmail(user.Email) + + for _, peer := range peers { + if !peer.Online { + continue + } + host := &HostData{ + User: UserData{ + Id: user.UserId, + Name: user.Email, + Warp: true, + ExternalId: "", + ExternalProvider: "", + TeamId: "", + }, + PeerId: peer.PeerId, + GameId: "", + Build: peer.Build, + Description: "", + MaxPlayers: 20, + Mode: "desktop", + Name: peer.Name, + Players: peer.Players, + Public: peer.Public, + Self: session.HostPeerId == peer.PeerId, + } + hosts = append(hosts, *host) + } + + response := &HostResponse{ + Data: hosts, + HasMore: false, + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(formatJson(response)) + } +} diff --git a/webapp/data/changelog/content.json b/webapp/data/changelog/content.json new file mode 100644 index 0000000..f6b7fb8 --- /dev/null +++ b/webapp/data/changelog/content.json @@ -0,0 +1,5 @@ +{ + "ver": 8, + "blurb": "- Client no longer disconnects when copying large amounts of text.\n- A new prompt to say Arcade goes away April 15, 2023.\n- [Windows] Windows 7 is now fully unsupported.\n- [Windows] Switch the WebSocket system to WinHTTP.\n- [Windows Paid] Virtual Tablets now works with up to 3 Screens.\n- [MacOS] Prompt to download the Apple Silicon build of Parsec.\n- [Ubuntu] We no longer block non text clipboard for other apps.", + "date": "150-87b" +} \ No newline at end of file diff --git a/webapp/data/errors/codes.json b/webapp/data/errors/codes.json new file mode 100644 index 0000000..cb3e1f2 --- /dev/null +++ b/webapp/data/errors/codes.json @@ -0,0 +1,445 @@ +{ + "3": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host OS closed the Parsec application due to a login/logout event, please reconnect." + }, + "4": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host shut down." + }, + "5": { + "url": "https://support.parsec.app/hc/en-us/articles/115002625091", + "type": "warning", + "title": "", + "desc": "You have been kicked by the host." + }, + "6": { + "url": "https://support.parsec.app/hc/en-us/articles/115002624531", + "type": "warning", + "title": "Your connection attempt wasn't approved in time", + "desc": "The host needs to approve your connection for you to join." + }, + "8": { + "url": "", + "type": "warning", + "title": "Your connection attempt was rejected", + "desc": "The host declined your request or blocked you." + }, + "9": { + "url": "", + "type": "warning", + "title": "", + "desc": "You canceled the connection attempt." + }, + "11": { + "url": "", + "type": "warning", + "title": "The game or computer you tried to join is full", + "desc": "Wait for a free slot or ask the host to increase the number of players." + }, + "12": { + "url": "https://support.parsec.app/hc/en-us/articles/6371362020365", + "type": "warning", + "title": "", + "desc": "You were disconnected from the host due to inactivity." + }, + "30": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "warning", + "title": "Your browser can not make connections in the web app", + "desc": "The Parsec web app only works in recent versions of Chrome and Edge, please use one of these browsers or install the Parsec app on your client to continue." + }, + "99": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601752", + "type": "warning", + "title": "The computer you're connecting to is no longer available", + "desc": "Check if the machine is online, get a new link if applicable, or try to restart Parsec on both ends." + }, + "101": { + "url": "", + "type": "warning", + "title": "Multi-factor authentication is required", + "desc": "Your team requires that you enable multi-factor authentication before making connections." + }, + "112": { + "url": "https://support.parsec.app/hc/en-us/articles/360049831391", + "type": "warning", + "title": "You don't have the permission to connect to this computer", + "desc": "You're not in the same Parsec Teams group as the computer you're attempting to connect to." + }, + "-14": { + "url": "https://support.parsec.app/hc/en-us/articles/115002625712", + "type": "error", + "title": "Your device failed to decode the video stream", + "desc": "Lower the host computer's resolution or check our article for more information." + }, + "-18": { + "url": "https://support.parsec.app/hc/en-us/articles/360001690972", + "type": "error", + "title": "Your device had issues decoding the video stream", + "desc": "This device may be incompatible with Parsec, please check our article for more information." + }, + "-19": { + "url": "https://support.parsec.app/hc/en-us/articles/360002529252", + "type": "error", + "title": "Your device isn't supported by Parsec", + "desc": "This Android device does not work with Parsec currently." + }, + "-1002": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626391", + "type": "error", + "title": "You don't have access to a host computer", + "desc": "Running Parsec in CLI mode requires that you enable hosting on a Windows machine." + }, + "-1003": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626611", + "type": "error", + "title": "Parsec does not have sufficient permissions on this device", + "desc": "Parsec was unable to write required data to your device." + }, + "-1400": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1401": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1402": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1403": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1404": { + "url": "", + "type": "error", + "title": "This game cannot be captured because it is running as administrator", + "desc": "Ensure your game and game store isn't running as administrator before trying to host." + }, + "-1405": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1406": { + "url": "https://support.parsec.app/hc/en-us/articles/6367984442765", + "type": "error", + "title": "Arcade's attempt to capture the game timed out", + "desc": "Ensure the game is the active window, or check our article for more information." + }, + "-1407": { + "url": "https://support.parsec.app/hc/en-us/articles/360059600491", + "type": "error", + "title": "Arcade is missing some important files to capture this game", + "desc": "Please check our article for more information." + }, + "-1408": { + "url": "", + "type": "error", + "title": "Arcade already interacted with this game previously", + "desc": "Restart your game before trying to host it again." + }, + "-1409": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1410": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1500": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1501": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1502": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1503": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1504": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1505": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1506": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1507": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1508": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1509": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1510": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1511": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1512": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1513": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1514": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-1515": { + "url": "https://support.parsec.app/hc/en-us/articles/360037758052", + "type": "error", + "title": "Arcade had trouble capturing this game", + "desc": "Please check our article for more information." + }, + "-2001": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626051", + "type": "error", + "title": "You do not have permission to make this connection", + "desc": "You can't connect with this peer_id." + }, + "-6023": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601011", + "type": "error", + "title": "The peer-to-peer network connection between you and the other computer failed", + "desc": "Something is preventing Parsec from making the connection, check our article for more information." + }, + "-6024": { + "url": "https://support.parsec.app/hc/en-us/articles/115002601011", + "type": "error", + "title": "Failed to connect to Parsec STUN servers", + "desc": "Something may be blocking your connection to the Parsec STUN server, check our article for more information." + }, + "-6101": { + "url": "https://support.parsec.app/hc/en-us/articles/360000059931", + "type": "error", + "title": "Parsec couldn't communicate with its servers", + "desc": "Something is preventing Parsec from making a websocket connection to its backend." + }, + "-6112": { + "url": "https://support.parsec.app/hc/en-us/articles/360023178732", + "type": "error", + "title": "IPv6 is disabled or not supported", + "desc": "Your device must support IPv6 even if you're using IPv4." + }, + "-6200": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The network connection between you and the other computer failed with webRTC", + "desc": "Please install Parsec on your client which is more likely to work." + }, + "-7000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626232", + "type": "error", + "title": "OpenGL was unable to be launched", + "desc": "Make sure your device meets our minimum requirements and is setup correctly." + }, + "-7007": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626272", + "type": "error", + "title": "This device has an incompatible version of OpenGL", + "desc": "Make sure your device meets our minimum requirements and is setup correctly." + }, + "-6107": { + "url": "https://support.parsec.app/hc/en-us/articles/4410906958861", + "type": "error", + "title": "", + "desc": "You must re-authenticate." + }, + "-12007": { + "url": "https://support.parsec.app/hc/en-us/articles/115003074512", + "type": "error", + "title": "The network connection was lost", + "desc": "The connection is unreliable, or the other computer has crashed." + }, + "-12010": { + "url": "https://support.parsec.app/hc/en-us/articles/115003074532", + "type": "error", + "title": "The computer you were connected to disappeared", + "desc": "The computer may have crashed or lost the internet connection." + }, + "-13000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002623532", + "type": "error", + "title": "The computer you're joining is trying to use an unsupported resolution", + "desc": "Change the resolution to something else on the host computer's Parsec settings." + }, + "-13008": { + "url": "https://support.parsec.app/hc/en-us/articles/115002623751", + "type": "error", + "title": "The computer you're joining is trying to use an unsupported resolution", + "desc": "Change the resolution to something else on the host computer's Parsec settings." + }, + "-13009": { + "url": "https://support.parsec.app/hc/en-us/articles/360000159992", + "type": "error", + "title": "The host resolution is above our maximum supported resolution of 3840x2160", + "desc": "Try to lower the resolution of the display on the host computer." + }, + "-13012": { + "url": "", + "type": "warning", + "title": "", + "desc": "The host is not allowing any more guests." + }, + "-13015": { + "url": "https://support.parsec.app/hc/en-us/articles/360047224232", + "type": "error", + "title": "Something went wrong with the client device while initializing the stream", + "desc": "Your government or ISP may be blocking Parsec's encryption, you can try to use our web app or a VPN to get around this." + }, + "-14003": { + "url": "https://support.parsec.app/hc/en-us/articles/360002165172", + "type": "error", + "title": "We were unable to capture the screen of the computer you were attempting to connect to", + "desc": "Check our article for more information." + }, + "-15000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002624051", + "type": "error", + "title": "The computer you are attempting connect to does not support hardware video encoding", + "desc": "The host computer may have unsupported hardware or its drivers need an update, check our article for more information." + }, + "-15002": { + "url": "https://support.parsec.app/hc/en-us/articles/360000513331", + "type": "error", + "title": "The host encoder failed", + "desc": "This could be because on the host either HDR is active, the display resolution is too high, or the graphics driver needs an update." + }, + "-15106": { + "url": "https://support.parsec.app/hc/en-us/articles/360033132792", + "type": "error", + "title": "The host encoder failed", + "desc": "This could be because on the host either HDR is active, or the display resolution is too high." + }, + "-15107": { + "url": "https://support.parsec.app/hc/en-us/articles/360001383432", + "type": "error", + "title": "The resolution is too high on the host", + "desc": "Reduce the resolution on the host computer to connect." + }, + "-17001": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626412", + "type": "error", + "title": "Your Raspberry Pi is configured incorrectly", + "desc": "Disable the experimental OpenGL driver or check our article for more information about how to set up Raspberry Pi correctly." + }, + "-18000": { + "url": "https://support.parsec.app/hc/en-us/articles/115002626352", + "type": "error", + "title": "Parsec couldn't communicate with its servers", + "desc": "Check if your internet is working, and if it is, check our article for more information." + }, + "-22008": { + "url": "https://support.parsec.app/hc/en-us/articles/360002165172", + "type": "error", + "title": "", + "desc": "The host could not capture the screen." + }, + "-32001": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-32002": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-32003": { + "url": "https://parsec.app/downloads", + "label": "Download", + "type": "error", + "title": "The web app failed to connect to your host", + "desc": "Please install the Parsec app on your client and retry the connection." + }, + "-800097": { + "url": "https://support.parsec.app/hc/en-us/articles/360004310251", + "type": "error", + "title": "IPv6 is disabled", + "desc": "Your device must have IPv6 enabled even if you're using IPv4." + }, + "-800098": { + "url": "https://support.parsec.app/hc/en-us/articles/360004310251", + "type": "error", + "title": "IPv6 is disabled", + "desc": "Your device must have IPv6 enabled even if you're using IPv4." + } +} \ No newline at end of file diff --git a/webapp/data/notifications/downtime.json b/webapp/data/notifications/downtime.json new file mode 100644 index 0000000..027809c --- /dev/null +++ b/webapp/data/notifications/downtime.json @@ -0,0 +1 @@ +{"display":false,"title":"","message":"","type":"","link_title":"","link_url":""} \ No newline at end of file diff --git a/webapp/favicon.ico b/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11ad166e84527181edfe22a77b35a8b67351bda GIT binary patch literal 110847 zcmeHQ30#cN8-KS$m(oGx>QG4sA>>Gh(m^UjsN_t}5QS(@;W>XZ^ ztQp6?RDzjy)ROjhCS)eSNN7gQ8@VDayozW7jgHs3YbS#bk2r;tVQ%F?a|+ z^uI`zq8hAZP+DLIuizif|6ouQgj3a@HT9X|qA{3EE2<#dwegdLkJ~yMl2eU@NsMNN zUU9z@0kaR_1mLSBRY+#Ey-@NwvSS*FQ`0E#d+v7_t}0+9AR9ms-0w+2q<$XR*QN{a z_wb&URSKTn0Cxc;medzwBlL;%6Z%N{a>B3ddwKX~2cQt*3gLYrqnf6XT|-uo@h$EW z3+gM%U)lFv;M+WF_|`%E9a(7>MV_s+D#_p0Ic<#6gAY~s-VnaeM>$uS?;zizwLg*l zsv+mRJoMrJYgLGU>GxX)t|la|%{6kkxoj_r$ngEW!x{ow0Q{+!#~)jM3}?OdW^&wP_WJ>%IjQh1o5y~_AIY8( zO9+Y4$O7)Q%i*8>J-8=BrC$til}kQj$=7J@caYzM;G5Qvrc~K) zdA@H8emrtfNH2*Vwv6O#HY|egj*wV&W3pH4x8==@`YsIL_qaCC?cnzq&G*3nTnI}J z!z(XLy6_TViYf*a0x|%s-$4xAfIeaw0|>>|GoZ>SP*-gI!}b?+T1|f3^u#ok0E`Aa z0c1N$WsN4+DJgQeMWu17ENt)x{bc~!1GWP`0@(Uxj^63FQ0|f-j~M?h-O7dme$e?q z0iL}88R#z!%un+>9w*;+>p&jXj06Z(#tX0i;0O9G3a|v60(=9M`i;+J{NE3Li~P=i z_>QuoKB3=j0BZm_fS>%O?fm!;-_U6x4bNi%_i2A<6ZI?VW!hd<|G_=&w>CWQ2NcqN zpvm?ZbjAFRCrG%>CI};+9R{ENWB%C8>o2WKMuRR%7`=cL?C3zwdQB*e1E2mQmj{0M zUqA*)z9p+IB1vXwU(mhk1a$wiSl2+TJB!g>P5ifEd?GQVj$dwD>eqgDcb&jW~q9z&i6*P8z`CZYe|z;Am1od^C&|KEihq5NWi`G67t zm){e#eyFhgf?NFdr~JcCe@vhKs^5xZG#v~nq6==K00jVU{x2ed-4BrOd)lG?;oJYS zz%R7@zAf&t{}ANGr?kIlV%e|}{=}|h*?BicGYY~eSHFS#a^%k^Yx-^@-a4$(@esS6tmH11i6Oa4{X_=8fOb?INw@K3E8RW~Z_9*jL@b?(N1W;-FgZ7&zeXuRE z8~sO_9UHzT8~bk{S?f$Vx-C}YG59$dz)oip)0WqZN}sv%BM*H*?o&RxtarEkMu2W> zi+<;UpBn*MfC|&N(g5Hkh5_vhnAcGg;U~k5SEbqHU;0~b-@BGv)4g=f(u=O^WrTR6f_g+$h`7lQF8lZJ;gb)5>xY!=)2>3~-gS}Lr^`(kPkWmaB zei4M-uO{K!Pz7{aH-T`x0hzqvm3=M~KGxF?)eRr~#;_XzYygbgb#kwX^nk#cL4T#%Z)2rS2GHJsVA!&K89^ceZ=s)0S*DM zESDQL)@wtgUy<;Bn~0~zrJw54nuHJGVS4}4N1)}vvfW=KpWUXv<3EfPMd=bhgQM&= z6YZvk;SU6Ve(6(TT4UJbn%^Ub$IT_5B0FNeh;4!o zS!Au%7V>h9IbCm}Zpkg$rMC&j;;y0_`kNZY|EKWLUmU|aJ#8A@XTWf=OtYZ9VI4Ir zNDADPwjWT2+792T(R1=_750VNVAyW~UH}Eww5ga^vHe^3;jgsfq z4u2nn&F;@by#Gf0<`dowMz9b_=R!$rTjZJdO@CrT0);vjXYh^?>vkZgn_$k zKxvxO&!{hK8{`rgtES5<_8t0TdHLNB{3tj4VNm{4mkcB)CVG)^E$)4XFn(%p{Ro>g z-il-9FuqI2|22foZUa^)eD)Z=*n2Wf{W2K<@xnTQ4)2G_?dQp5klip|`*C|GI&4h; zFaTR$R4;r^8kZj~r%%Q12O1`jhf5tuNsJ~PHuhh)0-6DS7c~2QdFlTnjz4paJk$Ne z#K|)`>CMs4IPQb}TW0{bf7`BB!^gHqIsHgX>-PX0(^m%Y-RA#Y{E-8kdw{Y}ZZ2@< z)Q7P!J{P0O+duy8SZhUzQBnAq))@9E#Wb?tjfrC~blJkNvHv^;!0kU*jqCDc;D&Dx zW~olWw6+)jKtfD+lZ+5+$XgI!j$X-!FgF9*0r*bqO2Suz@NWay?OIQb%WPfp701eP zEHp;*JKzGq9#F0RcX?sse;9vD&KP(H3#w=~j8h!{u(1bkTEBoW=L4DoY9-F)$wFW* zg~Y2+}3;S@V0KEa_j-{37&+q;tghdEp8j})AnG^zY0W1KMQZAt=sLrIz-wVZBY-i$j}$W?SYbkNUK;0$+yJHH6zX94i$BZ*T(kx= z81NE+^9ne&VFF;k|BE7)8#FB!nBoBJ!(0TE?JM7gu}CeFLt%Zy0F>+3pZ*i%gfuO{ zaSa@+-U|2#_@$l&J@`Fx@XD*u4mk?4`vY2H9R5~3c*aGr9q0*oQl8xOf2d1$4Grei zCkY_C{NIotWWs!m>oUv%iGWg_56{1(J^qLC?}RlF+@R3N`+UCR&hU|tad;J8pM;0vHiY0m5J zPsA-~37WAxxAT`?d}M)a5^o34xvOc>D?ZK`zRMGe)fg&k=z9 z>d0E&H+3mLgh%HUT;ntq@VGn~D*BIZqvQN~MQ-X!eh3d`$2k}?0DFxcm=(%V_8s;u z?8LI!a|UzTpQ@mbTdw{`{aGXZ1G1p(IELm0cn_$u+?eLpjL&3ho2%r^^eODI;5C-p zei{qr^{+0{AN)u8djqOm57G0&!(}r`9enU@AoeKRbT=&5{mi&k0pUxXjWzU#b zaJ(U~+X3=m@gM^0%Q$^PG-FWy?SDi5IhDzb?=Y`e!Cc8i{9mE`m|rHT z-XJ(v^Bw0yIpZa?+-TB$YNZ_xE3hnu*C+WgFRbpny_~$F$yqxUqiFzf5W3AAPS2gy zKlwqYaPl|iaW>Mx#PtiD@wr${sIv?HCHYr@>_7GU{xSJO*z(7=E$wOxPE1xuM?a|4Q-?PrsgC5A^6?k$;3-2BtT? zo@f_5f38S&{2%M`-NS;))`fp1|J?N-hO*P^k)Bm1H~kL#W=Swt1?$4-dhpNZe`vcE zM(Ku`Q{SvXt}}qLW3=V=e{inNS>Y+}BjU9Gew1IF`{7Gx@;Zj_tENA3&Ie?FzRH3u z=@Iu5*oSf2fInm}2HA(!x%{x681|a6*CdZ^exKbJC@Wt@cDmoN zu-ieBjq7(gb6<4+c@DC(=f8No+ z0qY5@T`jNe9SZVSc|C7=@%o+ov`qZTP0zEQA~zPekm5bCSK;S$$Mio5V6UqrJng9R zWUTDJs>)BtqcRio3dR%n3hsCFC;8h9%IZp#8|USB0@MIhO=QP-)t3C2S8#3p3X|RB zX^0KIo}A9-XyUy7ZGbC43Q$?HR{32`<;OI~F(7)Lxah|`F|X{nE+817LiYaLZ>X}M z6*#Pk{FvrA2DHO*C3#;u&&*!4L(7iq3XTKJ0M%YsP=V;|zpEoZ=5brmJOX<<$^Aux zxnn>-$&K>kx&)kCkpoa)zAe~8!dbIIr#G6ot|0=T z1E}*kQm*{J9lyf7f@?t6Sx1mpYx;2706*(QTo-W>-~g!AbrF@Z#+4t}gqK?{VP3)g zK;F760{c++FJ9+Oy?K$5qm zOL=((bvdrb>;|Z{bs7A`iz~k(n8yKZ4p4S`9M6Y62v36hSES4Dg3O};4FOOO)s8;r zM|O0}l^;!<&s_xh<>+M98`sayoLB5k^K|8EeVwq{OM^^dB+8De-pMb#Or$+@Qgo8 zLOh29t^@d5E7Xtik(`44U1i2ab0O}1F#aSCSLxQjAH);$I<8CW1o-XvHMl8P2O$6D z#2?#jxYqlR_D8Cc%CNqQ)^FJE$G%sU#=ol(|CMAf?0m1m_*zxs!;c$?1I8Wq1NQ^4 z06+EHz+NuLmHl4{|0~I!`VoJSANC`E!MI~xz79}cUCwvh!5{WMiwg4pUynb?N9XlM z@aztFfHK!6>MQ>cM~pkRO^pD!$F&ept8qvF>RbFlCRpRmj(aP3UJSrCYOTkc_Orgk zzyFVO7O*bIF{x_+zUp#1PW+hlA^spQ=5+?3BVZ@s6QIsG=k@z<#-FDy$GPm;fDB&Q zs{4%Nt~f7JmGA#n{6RM49P4sC+v7B##&tQy8~13=>ySj421lwi*ZZgA59@z1-UuZ? zD4@o5IXzxIs_8@a+~X(MgB(44X@zUKA_AX{9w%HF}(#W5T>%J&r%tbDQY+@M9vMxE~wm zb{+=}C~t1(KN)}I9M96la~P80e8URnhUh%-9mX=QE;EWfg=OS->H8jXkzod9>Q$j@_2biC1g16V>>(k(2hg1NtoJHD^a@y9iL{#_2U&;R&&uSw~= z8Xb3>r%C`Ak$oD%LuFoII?wx;nmnd; z?7ts>$v5nMM&+#2z;wWMJu3li$bRk8aXOwc(f@M%&qKU%eD;^Q|= zi9fEbDtBF#ApWKOtN(KO7w|90|AO*g&bm^8{wvjgzW;ptx8n9+x%v;{i)~+A|6mHZ z2>5;ful)IMY+u&;{1?bm<^CVL{a?lSqrcp=0II|}?GDoz`pV?Va@+FO z+J@Hl{5OPK3;7?`)yB1%0l7+- z3-Z-0{;+l)$9drnmpC}*H5vS?(*D8n)1?6Rl;x4*6K9pjJ?4Kub{_-US+(`6cKl&H zAIJ6a49b;ey9k_f0c9M<1o_PGyzX({0PoLWpXL4YUJ8smt^spm$3L$2_O4VX{;6=T zE9TGmQA{$r*+cf2K9*bFxbX9g-{P3!oc5>5%P_NwxPJn1$Mul4MLpXe{^S3aeb*b_ z;nYt!ze@8Nm8Av78~3^48NqMDdsh^9o&0N!&~JsuBhJN|TCj{6j2To#esEnT5b`{{fOI?l9-`*LyYoPF*r zm^{+e{bOy$A34XnU4weZu=_9EJJ9HOmNju-?-qbA;IHlPsEzp3b@>#vYlP)Fg%s}6 ztE?S^`+_e41_A#1eukRHAN3pF$FUXeC4L)j#EJK>ZFib`+*iy5GzZj4ea>%*sc!r+ zuVY`$uk%ro65v$PS$cH5u`Z7TbO+Svd=Gx(_dEC8_+vY8BjC4v7+SyKy-Rp+Gkxz8 z#EXtAuSw5^kAe5nfO^3>Z<_GLf7$Qik9i&Mf?WgW55m3Eoby{M&g95u0HBBo{8nDN28=Fu>J$m@E?$B;cE$4g; zns=z0BRCMoqh|41&|WF+Xa5?J6^!VD`oJsuuV9y zfo;Nx3~ZCdHJKa}oU#80XKWJ>Za{Y|t_cR((HWZP3{7-~W<~`ji~D~$rYwF3|9f&w z2HeSuc44mBsHP^1<6k2t$CP2q$)d!$CJ4p}|Mw=OCtH4Hu4&9Q{WvD_fba202Q)GH z(NyM|ejJlc7yOUTFum9;foaUO`*BPTtGOI!kj@-3@$Mk@kLia7j*>G0a7e_Db0W|p z_|;kpV4Y#c+2@Y?CvXoe?ghm;PdtA<5AYNa3m67_4dnZA=9_9Qe1S)QF9m>8Y&YRK zCRPCC_${Ec{Rq!=e+uWnaZk}6Ko?jKgX_)zp5TIzYm@@W9d}Jr8+Z-^RQYTRaEtd0 zSqk((jVSmlANPlTk`>&?4SHM#ICKGA0aRzcAM1w>01EvS{OW%SAXflf$JPyi`=M)k zk5YJFssQc{7SsdvnSJ1ylA%r@_bmW;F6a{gU9Z=~%$%Wh!HgVjrU89O13-HKj*B-S zJC)(SQJwmnEdLj>0nf<01i%h(3Q(i{!SMZ${KEKL96fAV8jLOBx}Ge+3jnTl2nXQU z!hdQ!>0in)obv$KC;i z1h4IMC2ZTeR*8xbP`Z|efetW z4nFzEHUO?2tbPLgpMHt+G_b}E_hjk-!T?p?hYN1`F4tB4hEM)sUqIU)0{*L>iE&+{ zIhBWH9@n&a0C3)|PWtlTFW>ouZ~kKi{8yEI-~)NbzC4~eupdzFx>op~Z{BL_8v*}S zWwYw;fE(l;fqnT!fNJkUhi~gNZwUCWIV9ltsvl^fBw0Bd0acI4OjzsTb;D858nv*uR8z07u~LxhiANV=nkOX z^yNVoLHVyboqV~3v_TMof!-6`L2Wi<~IWVIs5|$SkL1gO58hp0#GM+YJ$J@Du4XH^N+f| zA7>wIJK$zDpkDUn>GO_iJOBK@@(<}qx9hPlKOS%!P%r!PxJLu;G4#o|FHh_G5pvH+g#B9bat)rv-G+TG_4t-|YkcN2Z{GQT;UBq&x*o^udjPO~_Z3hp zWvDj&!Ln^9md(D~@Rr{Q&b|yd+aJ&5ezV?~2>AceU#sd9@I$Z5FokDae^>8#XV>X) zkEw_9EpmAL9Fo7)fP1zf+-bo+^G?8jRrxOK4*3Tt1Cr|-_q48eYV?AH_uE7=L;IG0 zcdwxQm!-8zcI2L`_i;R@UX9pQGO&Rq8o?ziqAC_)c<9Pt)iXFedS<{Ht>~sO#~Zb{xaOJLP_JHvbRa>GN&{`oAo-RkCyW z$Mtz10e@WoqpnAcQA%as!|`ELkKgNhnsZ)Lz<-taF6-!T@{eVHu+$q8Vj4vrEwkg^ z&A{!?@p4<0X953ZsjZUzZ}5-2fCr#4iPEHOMc8}DaST(y zf7R)y=lklU{g3+IS>Y+!HfR-jz1D=I=PMh-sal?R1DxZ1P1L;@AT>G{M?v%uOzH0nd3Hy#51>jnP>?E7<1O`Ar@ ze$|Tjtm^-GH{AokbU;%Q0cULP`}KSs0snjw&vjlG3i0slOS z!N+4g;UCY-uWksUhH>57K#@))W5E0{8L&9?AH76!6bqdeN>0{8v7{Eg1i= zJbfVl{IX`?NB^(N?SDb}rwimCHU;JX*Zj+yrv>G|^7P?NEBaYb{;S`P`NPG(PR9QQ z{0sQ!jQfACoW{OkIE zFh$tsP(wVV0B(R*w`Sif#J*FiBk;Z%kPY}{KDs9V3HawfJ?pyu2R`A< zq5_<|Y6_SRNCni%In>C1uyHi`8m;}iwd(l(J*->&r|y4+bi;J4)%{;}$v;zv@c(E{tHNdIG=8>^I`&ZQdRnWw42+&eM?nzv`zt^DSGh|4|x^4f(seukvCt2%E$aXjf4$p(bbjH4QU2s@k zKT)QUO&+t)C}f{a8N>HE#HWFK#eb>nV^*nC{^4xjLTKNe_MA$lYhHpge?I~5^{dRY z^C8qho*I|PHQ$ldJ%joS@a|4x)Zn}t&Uu7<7Q?^hye{~Md3pBvUt1m4kPx#dGEnku zt=`F2-7;SGJ1p~#4PTQm%dOeDfO>&|6k=Fd8hA<#6I2E-LQ7E=64QYT^|d8`Q5g{{{`;< zYy7j@cW`gyxaRlCemDOg_eSzRmcv`_@vaG+*TFI`Pomqv`QuucbpMH8UAF)KWZMtR zJl-X?vd=E^WO@Ip-y8XJUyhe|e8%~F?p+g_jLOa*|5r1^zs^7Q=lThMBr~+#(A#Q-(He_q#@muc%e|ETM2Mf1o!SdVpO?r2i9TcC?M~M|L0$QPSh(-X93Up(oxcpp<$nN}b#}G}-IpKLG>z;T zv4niUeRVi)TeC8ccg5ZZOaU|zlzIMmr(JXT?+@ljK&|h|!7@Kk;vEU<9Ya!=IFMr8 zPYe5jfU}x_z8Abbti^!7{J*v5f=>JTU~>641+!MqTfn|NuDhJ3eg*baO(&ms=+{PH z{ww60Gk`$=NkDx_!hc!@F8_F@+G9YC%Rk+hA0hXQY_Sg`ufj}gt1q7ce9s58f_g*H zzN@8N$K@Z-+li|w|ETNn+-Ii2QF7DI8O}^>R~u#iBXAxKfOVIGzI-jwUg`~=t7t`i z!aYE)fX~&*1Dw8m0da4cLJo|XPx7{O;quP+9$MZu0rusu14aX6$sXKSEztL+#MR;h zaE|;d0Pu`GzUlxh^SFm{P2cV0=?WXpzWJK9>oE^x0agGsVJ|(byA-tVYLV_=yy^%1 zTXFJ$9`MhqN*+MoaS!Eeon&%;mU~V2P}1cd`|?KtRsca?{uid|?hoLcK69}hJZ}S( z+aI9EZQT?fk)1vZOv@T_6p>F`)>+ z4ZsUP7I6P)Z1el+I~+s4KMEQE=efX{$TjcF=K$vsfR3Q&g$48SwaaOBe6ywsAs-OT z2V#I0fDQnI9u!f3xYT~&_ZZC&wJ>grefe{MApqD*hkd#Z)e%?l%^ykuvJd9a<5gvW zFI+oE*V)xE@tj0|fHFYPm;Xcj)ifBID=^IfI3B|<_w*jh-tg@|duBpS)A3*ao;}a9 zPYd@)xB#A2F%NtPcesag96+AL;98vj&K}Btna}FT&dvj|ZE;s_gx>s_fmql|m5 zcct&SuW38&9c`z*qwTbJw4L^jw$t8m?Zsv6rQUJhmwLywmwLywmwsNpy;NE*A3uWQ zxUU~}VT=Vk|8V3HD`QtKWA`g#CqLVT%h(&0u`8Fc`<1bi|93k(*Zkmvurno(om~QX z?5v;DpPhU>d~!135BHor{=?3rAK3h}z2o^Feg7$c)Qa7Yz>g>Ym$l<--uGqf9DVuY z`?7Y9e*N*DqriXIIr{jAUAc_^er4=z>koOj>bvxNu6i%EE0=Na_p_bW_OyGtHlXcv zZ9&_)-j#;W^^UgF-qCj2JK9crN861#(v+qz?H%`hsdrp^sdrp^sdrp^sdrp^sdrFu z!taN79QzONI3@jucZ4v({SWU5VS)XJcZ5J*<_DTEjWR7o>mk&wa%`R=< zStsvRIkrP6NGdxh(aFIiKVQLB zgA(!0B0KM9ZXM*-MBgURUB{+SSFgpow56=VOL3=^rx%aibx3HS+Mz)&7pOF~+P~0m zxc?%xUh2t%9j~k}IFjO4JZflqqY1{=dvuqC4Zh*{QZDJ)0+&b9!D0<|?MzS~vZ(!G z4}TB0+2`k9Oi);>Eozyy^U6V!Z*xo5==fWj`#889>Au0MzYM@LfiBon7Tvq>etjgoo3yPqNc}5 zWrhkDH!7K5bVlms8s={2m)$cA+6JU#Yiv>sKjChyGkr>1E!HkpKa(R*Kl`42%~DY7 z_56hIhINw5rwn&{Yn43GdfbZxi!2Ow%|8)&-E;)C!hi5K`P6o~yWW`}*=#U4fh#i zq7>gnahY$|xPD)rkB?X|`<=#=kkRh573GsuQYC`rnyub&;ZDAd(`Zw-f<9(0Mpul& z^p!m$Gh9U;Q_jpUyY`$rFY?T&3i{ za^J4hyJpGGUB-E}iD+!Irw@v; zM=cTdZy`NdXiJl4Psgo|y*^;c`L50NyEOi)vnw!;xl=n^*?)nLl<%y*_9EkKuP#(d zIV_gaKI>Y+=E-VjnoDcxj|}u@HQqM3!{Tw*&HU!t8cBU_GSF;j%Yo{BU)r)LmEEUG z)-CMksJ`e+NY16)$Gv@hit`3{m)q>NqJd4WklVKB@g_|mzdhV^%e9q;m*q0X42i7e zEK}(`?>5IKU-!6nwP{Ik!upYCUh9o$FnyJ$bnY?MOvz0atN>PV=q=YK>9@8j1`VKY zOuKYiPfJ|4XRLIaxMM*Z+#~u}kMDa+QDO}1k;#@e*9J!)lGT6O5hySJRreVpF7)hq*T`9rr^vy+a>N+(LLvpX|a?AimfIU48uzK>|P z(e8Uv?2Ze2r?rTiHd{rZ{f7%X`?Vi4C`HqBgt~}Ug9dGag=hC|=``9~YPD-t98+?} zo}EG&>seAhdM8iKv>zRE_Tt5sS97<_51*IcZt%{i22+OH%UmreA^1vrO<4@e%9{hG)zcX{1Cba3PGa*H&X`y&Cm1Kjk1G@@t z`0rFW(fQPpt8#{YMm4x~E@y7Rg)_T4X{;q%JSET0D;SlYa8YjN=53#4-ZNa~o4qjf z%^aR%GEm~lhV-*9srk%~@8=H+VJ*!#u*+dkl(TQU=O?^gFIN<6kl^jsB{y2U!?7#t zJ~eh4o72k5XM4}HdDJmcpL03G3oK^kuvWJT8E?MQHEH^Vho-))XvuKt7t7A--kmEZ z?~-+6YA-5Vv~_Ocx-?JEcD7eDR&?&sG19Wbx#AOy$y^tlb0^|*(~(!!X|7m!#46fEPTwj}V(JVL>7bX;KUC zbaEQ#=&v}U?I^89*|8dPslm%L4(yrUS##E@UJq#o_o)pkkdW2WPT z0&OpMonb;$l81kg{oPm1un-q`xDQ;nMe&eDvQ37qf3J(9LbM%^ST$?Cz2~6Ijj4Mr zoo()P-Z1CY@!+iKO|FI*22ceuzK0V$9F<}QMaJKqs?@+W>xO-&29)nftHL7xea+?- zHA(WleKR#ptL=p4fs(#L4ZQ-b;|{ddk$&~)kV?Fn_xRNiE63BDKb;+8=jL;>canFq z_26CR0bTX9M581c$}X9+_t@;CD&c0{LslD6do4U4Zn?MgO;az>Rjp$^?rASQDg2Jv zR(6X+YItP)xz^of!fr)KOELG)NfB~dkR_Lt`rXF$gIDXs9UDwHDV}3`=e5?~=s$4O z4*v}CCmZ6a!ro%*^Y0p@_b6(Jbgnrv#bJ6t*XE#fEt<(&?@rwpe^ge~H0uUal+{Q9 zlyT~z?j2n2tX*cDxOv6(O`?!tyXy}QblCge%68FtBjF*VQo z)Irj$w{OCBi)O#vnSLQE{g7LncEY%>dbt)^AtyvluA6&4jF5YI@8HeX=XK&5=3j`q zxh&VmLv+&2PLs!^ELJ?RV+zB5=E!N2(>8W=nYl12;f;_#LwryZn*l?bxqC}BH_r+= zAj&%R4ZLm@vRhmJ(hA$%Y7Lhf>e@Fwye{)=(-UnbGwkP$oHBW?sqdJSg+hr5Z%$kI z+?e$+Y@?(_L5~A-6;8C7#EADZJCfkxCpSLev&`M~w^}($F;C=OUtv4)(ueDrZc`_p z5S}(BW&2#+tK;?EU+8sgP_-*eQph$gg8*nvlf7h-bE??;*BmEX)tI|ug{W}AptFgO| z-8b`op-a7e@Sw$}*2&RvM(qrZ<6CUH(dJab9Hf_no;ZLDpL-{T&o5EFE@d<~@4^QEC#haC>B4=drCo^X0!cEKk+Jpd3frf>PAYVG z(O~YFlvQdCM||zBv$AKypjht=%SG9joT$Q>xvM1p=m!oK zy>>kpoozI1R_+;7XZ6QaFTLbJaaWtJ5>Fj6e#@;^qEgI^qKRWt?l0*GT0CjSRJU$j zzni#GGDTYBx;Bre^k-cHEwR>nK#vvAjOHEO_9VgdeXCUI{^p>3b%!*yeWfD)r0r2k z{1n8ht#@{EVf!aeek_KmZ}&IaNh@qEGY+iva@rW!v)Q6-g%MQX!UsK7bbIGFbG`K8 zd~C{1$;=B;jx1fd*7A@KoDQ7a+skX~eIXU+z1_Ob|9)*|v*{Kc*0r7^!hj$f_LA-; zG&#&FKgnCgYwH~$72my0B_6c$ksZ?^FM_3wPWSWx9q&doR(_=gr&J^vzvXC6(vi@mM=>3viZqkF^_Q_Ja(U*Gm zddlRDeM*WVl%8~1e{;Cl)G;YfSo(7Ljc04*@3^r2+gL?v*>)hV$&!{gE@cnx^nCNj zhfQKv*y{MZn-})Ha^HOZHAAnhSAXpGL(VuFdcXX=8D<8X0g}Oq{`>zO#cmJT-U|VF5 z@eDU1)x~ESkB-hr>9<4aG1=-h$GyFWV-r&+buVU%!N-NIrfhOk%^=UZXbzCGH=KT6 zKHJY%cK+76!AD|-?ud*&y(4FE#%#y95Jxk$C#~YCX!m8^CdK--m$`jl;0*VZS8gRO zD4v)$xOq;R#kHp~hvhs++Gw{u-oB-A_l{;Nncj?VZ(Tze$Mj1^W~|+{yNy=6=F9ib z+q5w+TfyMr!R4Ks>TNq4^m6i)JHZ`fCYxTHUfg5#b$?-VpYx8|M}l)+e=$v))>SCl zQqFmN#B9a0ao<~QRZf)@d+=^mw=SZ!dOeotHT(K!Ur%4lPFJpWpZvV(PDjVI32w^m%=%h%Z?(SZ^!(N5 zCwtu4+-0APS&Ib*p+YTH%~Y?1=qV%{NkV)rWv*oINXsv1=YHV$FopCtKFznbo?*DJ z>y*gzNA&V;Geu3WOQ^IwG^KNdqOW=1IU4;|PUthN@9;j}az{jtOi4ZSzRi9jwy96A zCLF&acckUr-{Y<#>9|Ty`gp9Dv^DR#-Fgiaa zWc<7xn-|OY_nFzr$70l^a7)IK2Dg|ZOOM4T4C}n5u&7J%%eYfXvo9QnW><#n=GLkD z_9D~Qw$HioZ2KtTtoYG$`s|l#x$l-`pHqr%j11xZ*YAy5@U5Wu`?{V!xfeDKTadMC zq*2#N6K|Z~{ybj|h}Yk(s5doBHtVI~p@uVOkDv;*GR>^VYp&^VJnT{Ssg({c^A5D> zxx^}bzn1bj&k0AzH~C`JF?di=Q1ebAJ+!GZ0g)L`Ef|wj}J~4 zD=+k$)n?L&BcL4T%bwl--CgJS$3^7U=&Op47Y`fpVX&jMl=Pc+2PpCIrBXd-%xLz| zX#I$sQ!6hwowZ%PO-ht#m&RwGU9)>NGv!>u*ujGh&UF^CjK0%5R%r@D)+GJW^uU)( zly5BT_T<{rX{XkRUTi5Au%f}l=!>xrj!n+qyjYw$IYZsi@XgEIh!#>zYNm6vmG#TR z2YcqNKV1^h_M7v?sHp2xrew6$G8=E9bULEbmabd(Pg(kzsSxR~Gd4NL?o^I0B@}nU z+ig(F_+Iy3uYbH?=*aO~XWVbS?QTw5z_s(+ub=4Ryvca&-79;)mdvzVzgF(^x6Th1 zi<(&ZxJ*q>p8et2>-#<3hwhAe($LpT#dWhn|DaprvL_GF8$S5yO8KJ$Cp5mGFp44D zDMTu9$3u-)QFe3An9ig6F!dHZXkwoJ{9MnXqF~?1aW6k_2(z`{xVU4G9%Z)o_S9xG z=VVV5dp$K{VuoWCr z(;IIW(@62|yAt1YeJy?G-84OwRyk8mvcREp*qgBqOBYVQs`80Ih2l~^37r{&!8 zM)6qF`RQ^?DWwH|H`3xMD_Nx%3ZwQ4Sp>}NoU4=3D#`m%+b$M{Zr_I7DDiQfU$Vh| z!Fsb{hl-{%wzI;HF7ureEgYOrChHdG5!`X1_1ds2!4Q#A>9@3@c?Rylx$dy!S z(Mqim!^l5ReR>Xa`Pq(>hNR3*>3e8F+*~P2LDwOFS;jHel7M2FV-khop{}vt`p9j} zPQB4_>%RPXEB#FpC@W8or2dMFnD71bR|UKf6868o;KBWji^=;UzrVGUd$+SBNYrHY z($#i~(mPNU^W&{B)Uyt)!Uh3cDzan8p#JGo(Fx#y`$E(L6xy zP+HQm*gNu@8k`$Osc+X3h2&9rG|0-YS9;G}gZPx3w6|;23?3d|*f-I81ch!Aw2F3i z7QHpvbSfiGG+onwo>=6#aR%upb&7_V`UQ$IRc%z|)S3kKbP07e8R@p~>8yKQEUaQ; zRT9UpoN`{)o#94Vxi^1!je5~<$)&R@M*a!vPB|v(ifx7udF8F-KZ@aYO2ekN(z(W7 zTU5GH-Gn|`ho&gibPjRRX7-{8-cRcruI%Z+_Wr z#;ksMAzBaZF3c0#Ld`C8*|f(u0QN{ymrd39|zd2M&6E$p9ZH9~5 zt6W;a(mp+@)3+N!th>X*_AGtia8NjY(>v9%47dLB5S-?+VN|m<=X9xH>(PqT1@(?n z)R@)bqRfug*MwN%>fqHMcBZm|wcfnUGOTSnzBGJ(2z& zplA#dF>K-Eu(mBzAA``N}fT3lA)u959{tXV{O!w zi#i{650eegIY?PKU!?ljUs&45U9au=DA8AmP)J5--?CVnx3?h`a%I5<_-~RTb$Ppz zXN>ls5#3((9qMs+cI1rr>QVt?-5H_1M6#$}i#y314s}uMs%JG-k&>Ob4%>)tzGqo+fMOG)Hir~dXq z+q7FAoZaM0uaC_d_3jipXnBzAtmz9oH>V~@7aNCNmRr(Wa>JA6(* z+xW@&n`#G>dO`ni7;Dzv{`Z=B?>ID}r>TegNnr^Mr}fVAd$uUHJUX%a7o+ZB3syFI z7WnciL;B7HDnmbm;pl31%*^@DK5v!j&>+8dUUf(0=BrOPoDVJeg7s0$V^efGiV1z{ zvYdJy8SJ(8V&fhydjfIGpbn%Ot1j+yb>F6 zKfKfC(ar`jy#@_Zq!^9QstzAwv-08ojt2XcPem(+Kc-$F1QQc{MSW%GLF~gQg9>RXDzxNUr>wU>WImyl@w$p@^fbkkiN)Hx1j4e`C zq@JyTcHa0L=a%Oj2aG%rI(zHK_CAkEg>tGEn|dqNpY-dG4Ocs`e{pl;lN;COf?A{hVz1){Qhz z+uB)CPcn1OVohdp579#TXpuX`?bV?+8@PA0vtf?Q04IlQccnUvdpz#Rum=Yg*-Bko zDfv#^%_#nAj#_}$;yx)6JqxcNl4yAKon>~5{R(_HQdKz=zZl|EiXoiE|v|UBKz+Sa*+%c?)uHe?pC|>LmA8uBZ|K7?AY*)pRvo+ zjeYv&x|)pkmA%r>M7HJTFaB>9JdjrmVkYP5tiO}9?9AgY=k$BTI{7!q5mk?3Eb2r> zHqOW#BH3%%QI!c3FF|Ha+B|jI=`)yFpYO~wczVicLI;(O(&}N1j3({PmnJppB{R6l z#V&mC(Z)y94lFZX(c(-7qmk$3M53`z+taT3#<_}VDG}BT$79-;#;~;Bi%s5oTs%{I zx30^%g)3z;(swKG`0)0gsV`&Mp0?YiDW1K57Zt zFvt}jXn1&7W0iQ@&{M3f%(V+1G#-+^X7QB`%bVur-s+XTAiG_fZ%Ow!k%=GW2U*Ii zQLMzyD)y~4UW#UDC-wI4bo!lSOf#A88i_?kF)7WydKxbnW*E_T-fS2cI-`E~%_qeM ztxOapX9(wtxycqk#i-6MpdH@;%hR$_)muHF0Xk;4Z)Qcyp%x)a5c z^4E1W4gD;q_T}P2H~ITL-Bw%OX<%}=fBrK6nA|NYryGFlfL2vzYV%ITm*NC#Zjn$7Ba*a_qI5g2@0V65o96sazacTu68&d;~bu*qP!Eqppl>7cPLRbZ-FGDetlE$2k=L+E7m2s?8X8;c_b zTD6kTY1PdCtPL;5Gj1_EW#+V! zO+6fV_-4jQZ`NF~;Ay&)Z>Tk6cY?U-P(#lpDyK!#(PQC*RIWpqS>Rop^AW06${EvG z?h77_TfIb-c{3^9e<<}(ZD+%v(;@FIV!c@gloXR$2yMUKAzx&QWeQ~>!k;e*skkM5 zk(toW@cHwT!bGU+SC{qH{jnMdyM0DfAl*r*n_77mt^*AW??gWaPYFzi?kZ0zVM!+ddM{J zfyK70y}8m1U23(HX3B(f@0fAy*1x}Rthd|9YmK*S1)ueUK7-HlKxqA2ygoNiY^w0O zBA21!lbiV5pS5T5<`3TO4v&E@$tAZ=!;%6r-dpW@sF0WL-!>uL|D=+t%(UgKZ|fJ$ z6nDd3$(lDiJ4%M`3Ye*LX%=gQQx41fq+YU{r&TNLhmc^uH^ZM);X=R zq+&lO#xb=TWDk@kc_4$ac0{|e^Kx1h4z#+gkR=f;YV$pA-;u`AHp|}MGFMIAaq!N-QP%rC z{ff>&CnVIqSx&1^`Sq#MJ~=GP?cmeH_li4fX((-&Jms#)!=qVg;=Qzz4{Lo)iHv`7 zCD*TrF|fEjE3jYG(UA-dFPAo#j3h<3c?(DQ8=QXksD^PIR$O^g}$*_U3|U^Uo3Qal>YwmCEc_Q5r-YZ&iZSb zG)T8HT>QoPoMO_Kw%6pIbqO22ZH9+c;}>~}d2khIYlFwbI|*aC(@eqx-m z?95iLqc*30x;>@s9U(*6OJ}~R#!^R?=W4{?v)-;YMB?gK^@5@53C0l~_8MNIRzp7x z%$S;?>vnqG<9Deu{M)m%bL2e6EN4dDFmGoPI^&iH^Z~yyZ+?$^*Ftkw%Gg=)oi2~; zDy088xrOvn;kiMP-i)EthTiKAJ)Sh}{^)O?$;K5_|C70{j$1MV7HD0{F_|jV`@+_A z{~2ypq6woDo+eF8*fHv@i-ghAgcai5m*~vxoZBLU*+BQ1lIc`Yo22!-HcnKeqRxcf zYGOLj`SHhGMTrg#2EHr0IY6jLA#1U2i?4nwZ5jO+Li=sNTGpn_C)&j=;^VfII5%u2 zIzuY{OhUi!Vh_bxg^__%#WS@MQW|GdCm4q(-XV-l{+6P1L{g`oEm4N9v+p3&M%sJK zJtKo2DIH@Rlbzsv)$c$ + + + + + + + + + + + + + + + diff --git a/webapp/index.html.bak b/webapp/index.html.bak new file mode 100644 index 0000000..39ff715 --- /dev/null +++ b/webapp/index.html.bak @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/webapp/parsecd b/webapp/parsecd new file mode 100644 index 0000000000000000000000000000000000000000..7706edd6eb136ed9fe9e8e9dd154b2885e209b75 GIT binary patch literal 1437039 zcmeFa54@GvRpiGCC^|UO78Pfhk&d**k;;63 zYwzbg&pG!3A-(VC{Y*bY0%t$_&)RFRz4qE`uf6u(!J6?$!ypL4y|s<2qb*wk{S$6k z9qONWiz4(d9|=Evh5QYI)m432d?gRWTb|hxKC?O?uTKqJAtD-<$^8?o?(2L7t7|?? z-lzE!tgdXyf0YufuIHJBi82J?(}bu?I6Y%HIDpT%k7&!&B!%P@$_CN>(h$B#<{tr4 z#;27@A6C0V{pqI-s0i`2ZweV`C-K&D5G0_6>(^TFw4hqE6$L)zb;jEePy$sWDbu_IK&&^yq~NKBV(E>UB@2- z>iX>&avSG8WrU_Lo-)?-u~gxro>BrxJWY@DPh_p4dli1_DblK2p5YImD;5xR`y{(z z|CBi3Dbi!;WIV>d360?=?ECSFK&Me*L@FtbHU1l;g%yj)%$d=*Z@=b!*3iSc%`+lel4I^LsXqJ+kVd zHS5+tJ~k3GlLVBbq{Y?vwqdu_3Nm8Ro=R~(r)x=MlAIisSmAtIH*`X-}8Mxxav(nT0ipkv5lKI zuHCrawX4}xYV*d&T=bltiq>pcJK{@RXP`E3ek>dR;SFo=UO%#C%(Z29SHjyNQZT5x z=PLdUBWpK4`q&t4fAgcX-pV@Ent8FeixIznY<kditT-LqjW zOravib%xo!Bb&dMc5Zz1t{>h!;ybV5TVwn^OCYZAf`0ed$ePW*N4o2ML%C0eJea(r ztOq;jzhh)GqyW$& zqmT613LKddiuk~tb4-u zQYr4u8y?zt-$u`)0%08i86O^7^Qarp)Rt}Dyk_mYJzNt$z`bwXhDY*@t^o$uN9C1W zZ;=2PTlcQ}Hm+T>dELeht{>)D%J*+r_t3_%N4t{-l(gY48^oT2h`MXth6i1o>k&m+ zmN!U%*eL5anD>m0Z5(sRHy>XAhLQCnn@7HH0)F zr+gOyjMy8xYa&i~`^XOBeA_U*{SAQ)2eQ3=-IF8h-?4e)*vLAUjY`P&j>q5Cm6Im^ zi*+OKd2Hj@X4mG~u7)?#VI%Kya?iS`kJhXkU-Pc@eq2GW?;n@&v97fqd)dS5@7bUn z|6*ip^T?B~m$I?3HGT};KwYJjw{KjxVKYoxcj6v zcdbX}-?Qm)#{`B!!^ghy@eOM`t0#5+cq zJ~|=-UbiNyF`X2HPG7uZ?Yi-CC}rQ*brAoaHH?fW7eDlbOQ<}wej^jSO=`2<45XR; z(Nzxt(=)HdudyMSPl9u-rFs5GMt<1Qr>2;(H4nLV59;gPYsObSv~g`dTN^BiNG7z$ z-nDVf*n@7|bm}hRojehY_pEtj1cd}VEv~6J!5AA^PuD)-m4rEpHX-ZSrc~m(PSOUn zn#adFbh%LpCKX4uQRkt@*RLNpV5J1%S4wbWBClHgXfsBiV%Dr@C_2XM;4D>R1C4eN z2bAqG`alxZ<+P}W*ROhL{o_n2wHx2FW_;bM@eOMp8-MpkroXX~Cy*ttsjnG(cwAi= zgqO)NWI05HE9YxK_;q6&H(U|e>^#%e;SH@7pV5Vn(!1by(`E3hKg5*1e&hH^5M3t6 zAUPg-P@3u$al5|AW_`1M&BN%Jmw`D%W~o#!256q|!F6M+tjm(i0IdO(S@^+V@Cq5m zM@AkA=3XX)i1D7Wb?6|M12OxtHJjhP>LHYc4(@%I0oG?(H$FD9A$ZwkvY>qfZEWPx zjZZLg*Nvg?ZybYlFO$9Dvl|uam&w$$Opk9^kCtJGUICm|0~X*q8|S^8daAWB z;QvPLAB90wiU23>V^Nb`CUOQ%*k^7ZPE4NYwFeD`bsmo zJ&2Pmqjfd_9^m?>Yc9(skfF4_Psv%ZfH|q9#*0iVQui` zwJ1*NK?u<*^)N{WlQ0UxEV+rlNnbQrZGcmxxG1~}DurPL{m2+5z>njA8YvQ!P@(oH zphUG=e|;G7QzaboF9m}7-C;~}y^lr!3uG-vgPs883hJffwQ#}?w7Oq)SNy6Y~!fGG( zr0m#xKb|ydx5ZVI9E3&`ylfR#5GO=T>Cu$DYO|EoFsQCVTrmlLv)1 zsrnn$pQ%1u{e7gy-&QYH-&@;W`(SOd_W9c0+OOC4*ZxE8-`74_`&8|7wO_BCs(q>U z<=S(#uhdT0{-X9m<*#aIYVWI@t^K#!*QuJ?t|s3T z6B8HTKQVFWQ(KnCb+W#CG`kdLCjtldW@3P`D|9)@JaK6G(r7aOJ`^sEw)>CsfB5qs z{iWA0jkf9==m+@C$iFigj`k}Y+!5?^aa-%d(OzX!?O-FAyM31D^?N8B6^MY~k#JNf zDEMBo6FjOMKjy;6tkiIHT)#k4`U%36j!x?L(xpp(&tIAso$mgQCu$x8-~MQu2mp+q z1mpQVY3IqDJzF0w6U>mhSeqL6Tzynjw8=H5FPGV*EDg@O%wO*G;!OeiJO=v?!x@3^`4t1?#wiCr>4d= zUIiy-K`V?_WWoIC(DNUKm!tX7t1M zKm^SXLa$&{tR%_~v|~Cv`-e;MTh#?AUA=8VJJIj<1&k`1z{J19EsYV90F?7y^(Q;nRS?%~96@ zjXzAnm|4lvpzlPu36vN%jH@i1)v!SkXQ#qVNN~|O$i8_=i4=*zJ_}oLRvJE%MQ<96 z8d=1rszDHbCVVqI#7Eec_}>KVdx;3>+2xx+;pLmM>gF-k6R94?<2f>&x^gYCY7tU~ znAMbIyPg>x8VZBb9|;B3+=>Q3Dt1VPAf<-v3g0*w4#ZvY|9ALSp|6}#30Yu7J=jOg zroucs^zuy*d)B6GVDp%@$=VY8wp<|7xhuCNF^VMx#m<97*lAppJxjYPD3$1>t~u=_ zI~Qm=-;{mV_Y6h@bh^aN~eN)?%T}P`b zzMW&5*UnuE=)-+6#OrjTH90{v1hfL-YtlGw{Hrhux742|Rzo9R5CsVAP(Rh3X|Nzl z+RVNBsqdUra?y{F<`_t;q$(?trGw;vBamH(;f{mSNU7@5sK$&XFcV9}-=WGXW z)NoGwccuerjU0Xan>P5)bELsI;51H>#$q%vq3qcx8iK@d46I4pw~V&q=5LxHR#fc7 zU!!risc*&L4K;W}8@yo}mL!EuLY>*DEGZgqG8m}@-jx9PX{{Sjmt4Qq03kFPX{q`i z#GgUG_^vR>`lVqo$Q$n>Qz3E_H{ub3jNbv**fef_KrIz13@bI5MnQu}fj7-#aL3t} zd*`)tI>1@E@JQpv>w_LZ%;Y&B5Hn&*J^)A~OMoIzP!RkkoGsyF%CP83SW<J|J?NYD*C^MUH4i(cMn-xBAFX68hb>mAl)un~n(I1kfav z0!`Q1I^pJ`f#Be%mxeYFH86KQ3o_Zivw!yuWS>(YpR?Z=A^QsksW=AFph(5p%p>9{ zCNt*SG-!p)rX-mb#uv;7Z@g^~t!H6Cqxgl4%-qE0GdD4sx0&a}7q#q?&D~I`ruiw% zA|`kBN%hTx0g{@gIJgE7p7sC>RloxThft^#0ByqS1_(Hm2r43{X_AR_AjWy6;NUO~ z9?8PT8+*epQgqZ4ii$5YMhNW%Wwkf(?G>NWUSjk1YV??q_L=Z=YdLG*(R=uYjYFW*- zOe{#ywXCj|QN@dE8Tn926V2dtHLjbqS8E)lCB>w}2&=j}r<*)eIwuw}tdLaVa>w^j z2VO%EIg$pCG$th@sKs-RWF;K;Hsq=Z=7vV3p&?4bc#ICwnocL3@o9_}iNhkXN8~gD zmd1{x;N(ckk1o=kpBiGZKyrLC%2SKpBknd5pYXL$bOEzo0zdsz6`Vy0AV)=*p zcgx+AR$xb}IFuZw*7qy)w);igc#jQ5nGq(QGy-IGTcY@S0KD(oK~_Y22eP8>uZ*c5 z3Xlh80A$wO+ynKXL6z3FYe5=LPPRkZ*-Hh*85P136~aBOpg~5_OHYg~oD$Lr`7=nixuR$}+^5`o?;|)xR}xM%8Wv2Rgha!hS1M zjXYIE5DedGY2xyJTS(CCZCgxU5bNSbB93bg{+wj>K#k!@SjSk$HY$4;>W1@xZ*>J8JV z`HQlxAZc@DzHv6e7FTG4J%y|+6QXo$D{t$DHe!;g$7HW(7iJ!eD@bi;nMvAJ%}tot zFsOx_S@a3H9624vjS3o73dfYnS;#O)MGTHt=JZ)8V&!qX{{QMnMq4)qcp}q z(=;_b(#vKetot=2IYnL6P~;#bZ1ISV_TG8Tx$dELR-g@ zwS@6HVNKi;Ic|xZk*o&ydTzn!r*euxt=|I2n2uRP=LD>+)hi343oBUzF$Bz1wTw^f zOLdwuv3$n^Se0NcEXj8S7guJXn;O7u;+<^hr9O#g6nTXyD``6m9h(dI9S*svOfD+@tgW0y&4IC`}!KEEh_t1F&nU-iY) zqPQzF$CYVjf9G>FeGaX*SxsDI2L@v?OUryU?D~feQ%l3Vr8PAhc}Yt}khJzj%(4X{ zEEm)ckLUnjwEGdHa^5i4s=Q9?))r6C*EA7lXW3!O<88u~#@U&1KO4u9ts;sl zI#O_|yrR0xO025!V)ybwaVD{8KMp6GK&UT>>US?%G`@UCyT8N${d-#7i^0kZt$~!x10cfB+*W@B zA;vt6A-#Wx_(K*w#^j)uMnkJ%b<23mj+B^Wb&CZhgOD7yFwmA3<>{!jUpD^WehL5= zrv-A~uiaGkQOC02UJ8<_DyXxp30{)cDcGORGPA$g^LI|}XoE{SuzKFIXs$2BK0tRN z)xqWg*=(CYB^zBOZ=oa=8sY2d(6XpW*9^{&P;ch=PQqbAc?7t$ps(e6K|cw-%U8wO zD7!L|HOfr4G1ySm>jl>WvU}xkT7YX8y%01AT_cqS z7W@kdq!GZid2D^uDFrqq{hvZ*`#?OUr%Zg30G&V&A zn|sQ(cSqkuO9zuKzqvs~SZ zjN(kh_LVxc?`li0DBE}KAo~lE)sOy`_IEA4qU^v7fb3m*McF}vs%cB+ylWvr_mn{Y zm~H^#kx5s2H?23n?D}^aqVK-;=5#Ois+!kq0ir%yPPMzP9c1?s8ds>XZjgIs0HhJU z3(>tZMD$`b$?BjvtwiaaJeGl%YseDNE29iXiPrpj*~1$wCgBkkDApHou7LeT| zuumhf3#1=`-x5e;hAyan1bXAgwE)+?saJ9^L(wf7fidQR#CRcM1XeCP0+Y;-z-y_o zTdkV9e)A*nT0nO9V=e9LS-<%a_$`4nqIa#|{0RJ(iEbmp<}-{7y;RpKytCl`K#X5f zf3C~0M)NadK2Dh7G&~qxEX3xsCTiF3{`}FS-#wWolPxz5wVcA=YS_qZxkX58(0vWL zpII6M`G3?Ojt*5TQ5=L7Sfp=zOJn8kg)iK{e6rQ=LZ6ys5vB@O5I4(0K`_}Ga&e!U zY&TkiQcy~TDN=wL>=bc@1*Rz6-x@#=VHH&e2FoYgb=tE7pQ~BS;J1*%YIjFFNr(8H z%V%FYNN5hBBpo2sw73Q#P4@}QLa#ZQCJ%Ytyc0D~n^CXQQ6AaN`16wg$#2fdzEt)< z`JeTu`g8?HhBN}PT`NB1w@GbhbYl5xc1DSx`0hz?lVsb`u4u0Vv!Ond9qp==G;|=> ztetIHEmr^rFA=pT+9}V4w7SFK)9{_znAoY=rO>t51VAV1msD-j>p#$zq3WdhD9Z%J zVtBNLsR#Y2VW$2;20XF&lr~0Ng9Ot-mYn>>%s(KX(g8MqY~^0sYZ%l*Q}!?OKe2Uw zXRo1aS4*qgAUkE%w+Cfc5`5v+yR)_0l1uGS=IM4e29>8%Tkp=sz;N_KP_!`|t+r}f za>DTl&`j-Xp%;tJtO^|U4I4G(2SE1!1Hu(;G6D?RZq5JZ6ohd_!K9ahaBs9jJI4Qq zkm-wJMGas_OPpkoLlOrCL*`U0SXgO@cPz%ulO~;5X2H3QKj^PY8Izl8^h`$`EA2Ey zB^iO{bz(^>Z(ceIuO#_MCkum*56f*1YjtE;oumPhFyFa$hMOc);#8B| z#0rdX%WP{wvz$Hw!xjTZNInnkaP_lf@}EM|#3q1OxjhmlyVWj)rsP9I>EJ3+kmkHH zzO6lLY0SQZ!pquCJN0DhuI&CKNu?#0E^KKMh)vgG`Ts}i*^by~k+L9q9WyFF%a{j6 zC?-pOZo|5%opLm2))@|UEn{!t!34ipUP7OAFQFs#3F5fu z=9kbX-L6Yf8g%&tCD}{plPMsrPr4ADsXnpcL6_X7elbbKB$aoKHXA0EyRMNWJJi`d z`$iKVqBPK9tpWG*cK5S4M9wW9RRZFM7>uZ#y7K0cAC#EuFN?P4S+g6YH;=xZQEA_4-~;eIvHT)yBdC2u#b|hbY2s@ zy7C-Xo|9M3b6R=MUOCTs<+*s}JX={bXd=84K9kDx{FFS|zGSrdZ?&^_GwG*E_kD7* z6I9G`mA-(3SD2lA&1mztWUOT_3bNhR-deNgeN>rECZ}ZUBGz{7OU`>mOOh~JQ3$@v z9brH>LiH(W|K+s?qdIiHQgog)71^zAD;o*R5lOK}yaE8|I5Pd>_(Klckgf13GMc}@b{6wmHAr5T7==!?3`rMo`ZdOR>Wl*xlMXoON3p=b>hoXNC1Sy z;n2|qbZie06(YqMK&F~Oa$j|VwRh6`SjiRYx0-+)~WV!(d z*Rh4c?3r`q+)gO%gywfbw{=3xIw2&uAHJ?_Z4rBz4gimW--e?%KnpqvHdYkVR6O)A+$%KTBoGwwn+2 z&|XAImq6eMzLmc?zBD@P6E9K9MECCA+?s~v%BQWL?0BcsvkS#EddXsgfDGHJIajFt z=PE>9sC0N{I_Wh<$W^+KWuFag-J)otpIiicLdzgLg!b~Q?|u5-T^SWqxoHI}(vgx+ z?BDlgpEx{G_{44xL0BdI4+BD9bKTDhL6=Vx;!1Y_1Uf92wq+r zp@*orc}&V_8Z@4Z{gE~&!?M6Ugkf!JMj@e1quEpXff1b#e&@{ABU)|Yz&4)h07aU} z-%;l|Ie!qT84(8{7=m8(dJi@+t88F8`jpFG427KDVvaypMhx4D)wFjA;`9a%I--y} zy-|s3YfIy%a@o1ufhv!RSa=iCTc?wRRX_ocjwLzF44D!pS4P3(Y2`H&L@Np*%5X%s z-~>8ud%`Kru@NggZKOIMF*)98q;iV*ihOdID_Kxbn_qA}m7NYon_pDehdkox>)$F8= z<{5!&9CK`j;W=qDbNF#aXhu0^`^>?kUk)T8KEo?SNi*e#NH=tG#G{MC1-NHuPd#+v zhj!GZ5O(Qk4%Vr}4xKQIw#aK*GA5_{xID{hKZ6J~Qb{c@4}NzD)l^XPke2*Kk24q9 zl9FDoQLQ^_9MS*ZN*hft+>rg$2TLl#6)z`wEoWuQz2v5yz_h>=)d3w5>be=n#rhei z@uleh%=M-+*s2e$|yWRXpdb*xgmd^E26s z-P|nrMefr>hji-|b-tTx96Rc3U$q@+6FB0mTetmTs_^%_!Ex7gQ?}5U-XCU87wQ~y zL=p`Ey)Zx!F~?0vAm9(ih~c{KwD3IMeW0>D)4R4{y+-%PJuJ)xf);|2u)xOlP?Dth0?=d0X#f9^u<4bgyJpJE)P4mh^alem-8}< zpgTdG>4{|b{RgdtsPY2V7AH;k*t^(awnxh!jn}O{LhB~2cPIH4vwqV`-w{kK2COUZ zrZ$eqBOG;b<>V4y8%J7-;8_>U;Adqzr%xRHsZQXZTij^;BKH$*NuJK(h8ZSlM!7ao zipFtH<~SRuc0x(`{PwVR{2WaI+(lCo;KRMc{8Gj7B|v6N)PO=7>k};Ha?^3%zwVs< zbM#1Ph z&n?bu(TNgvNAQVZ?vt_5#|dTsZ>g_8dt>&+G9x_D)zyJU5?zfkFz;%!P9j&Vqxn@w zdlscnEkbrSCD5n*TA$kQ#Nx&>RG5O;fW0F)wFpKh_en95^*CbUG$7OmcAMjwMUBQo z#`~qBh8Ns!d}jk-VQ}~kY_5M+YTf_5G5gStl6nP&-^*{$4 z<2ShT72jn2wQCbr6g9$BxZlJJgDv0{XDouWPkRL1&Edy6Oz4N7f;Ec;`1F;^~R2VJOlXSUm^j$>snECUG#B0H-J z9QX?0D{Qt`(%_T06=i3=$28tl-OkC~evH+$|Als++jc)sBg~H}&Q7Z(0Jg?mw8mW^ zs$X?U9`o02?CVsm{IcphAuNV-YhcWJ?IVQ+J=k z&w0u(h~B<44R-9q7k4YjhPKMGIctd?sQ$6Gl=*ZzcK*`VK@H12!8IlJ)=>$1+Yql z0SbObqLo=U#8a-sy2+kacUi7ldm+c%_|>_U$PX}_jvIr^b0ZWqZe$=_of~Pm=f;28 zNUF>*A%DHsG@hIBt(eCrs4Q1`5;3XYBr<7Q$qpii+$E0aLi=rSNF2e!WInM3pY&V4w&BTvvxo9_mU933s0Xs`?Y}a9OQW*aHGlpG%BEb#8WD!5OC%!^9DEMH?6xVP*WuVRHfIe~9+u7=)$XPdM4hpq}%#M=0Aj2eB@$;#}WH4)* zPmUTpj9vm~EsG8ft1sBH;Ws-vJeuNqL-FhgLHY9#JWLQ7ATNSx%GiW_)v^w^FANJc z%`gXs4Vt9Ff#H-LuZT&KOi)#`H0?M(Z0J(wU^ajkR1QI{6BY4r5=G^T1JN=aG~}O- zLzLZ0&}^@?GA>Ki2zfwP$-@ zee2wccf888`~KgW_)N1o<=EuESr5$P-`V%i!v|(oI>dj98B)wn5{7nij&srS$DHBxsmKrlAQx znZ;21TyIt~bUySR#~$EDXDC9f0FQpAQ)XMWTPF>t$bx=~<;_k>R4t&}1%wT+X3S#rqzZx5HyM#OYTd%e(_| zr_C};8RSVgz<+(z+$>kfzC2gUV#tXZCQiVnTmqYY{gV>4?h?Vz9zrqx6yN!sG+(bo ziM#AG>Lxk*vb1<3JPc;cAjyDxAD870+cxvF!x9P*uYyWN^zkLiA>p9dLe6S~%Qc9* zSMui9tlc3=t}7{%_ivx|AW0%$Kk+#^@**PBfE?KHs~pTT0|)ZTp%Rx$hG0GIyI-ng z8X|leMzaRdxO*#_848@N0JDr0K+2R0^wS|K(C-QaunWzlnJ0&FR(ocXTTIxI1DUQ+ zRwst-{MUnbc@EU6_6P4;rAtl7#(2(~DqVK#uuU#;w%r2l#*UpP>Bw2%0bX&o%@T+f zc&>61aq;7RU=R)0RJT=#ZLX11S;v!cwuj#ab5Ys;}6x9j24#;-h>MG+j4MwhF zRXr31ArM8-Db*0kP|6X~2&!$T6C5u0&!b7f!eGFRA=T{=S~PJN8(83nY+^BA6N!Iv zFNm7f%~TjjJ~7zS8s3|_(3lpmi)s0Qv||TKKqr6D$*04o+E3hFV*Jm4clOu%%=vz& zl;mD9lXJThGr5rQ<1=Sm8-u=sjRO^ti@NTw_Y zjX#SpP)YI>ijl3bjCy$Ze8N-<0v~4{r(;M&5sDLx^lH))qs(9s_riHOjcj9rxJDv+?4%od8Kxk@DbgkvXKMIX8zw?1@1I!_(P znn97GPbF&@TcH+e##Aqf%gnPr_V$ZdQ@`0SY>EXo_6tWUY+W(-3(;|5u>G9^?vt%c zvm>t^O&QIo3%KI^_>(3l%&oZNyGnn`p_GzKJ$ zz}h7uzQwasdD5v9%Pq5xikZZ24ENZx@O1LK%%EU* zA<=YPPAxJGXXE}c5d@9fdy4T~iYsQ)2zO4G9}3QQnp8bZw=p4-|oBrD&o+^K!B~2@cR&=OmH8Cn&O=LNyENp*&{W@x;Wn7}MT~TsW z<%$v5X`-4x{KDQ8G2s7Au{0Tu@QL9rRQS-V%#rxVwjx#WMpM)Qi#oNPs9iiRV{!Xj zHc{c|r+wTW7k43cS$6RW3iiiCdd}t8nz$U#y3hCd&u#AW{nWE9hdE(!yFNhNsl0B4 zo;Xpcog+vETZL_&jBQJC+sbhqy2*>2D2jAibIipF)x6P1rbZoJ&~XksSft7kP^j#8 zQO{EC#AiHudtKa52^F7h4?u*SU8WZZQ9YA0q4w@t02uC!ZT>#&D3J0V{sV>Oqb2;0 zVYk)qc1*wey?8shwl3=%EP~G$!QDl0ZxP(TsMD~0i^VIu!-7|^9`t(a;keiF zYsct-m*AE2pfAFIOC^IZ{k@sb=(*v%PBr9=a%^APTT`~xN0r^9FZjwzaRR~eHptEk zs_rbteVh6iiwf?U1`6xk9=}8weJTRGyRqISpbq}w0{)RAsL?M(j!y&rkL5Ub~Y;@cAORy9n+rg8Pf$!6JCL2p(I8<;D4{OKxcLEPZafleRk{ zF>z;yccCr(d{j?xCDxSWhbLe0apkk1cTWFclw%-YmgDPpA zD!BUDT1dYS6n}zEu_u)BKUU}wBh;@I~k-YNEK^u zOTSo_;G88nd9%|+@N5x0Uj#2M$w?~F%3CG;0S@+uE}!XGBB{h}@iu)!rDr9Go<@!99g+fdUv=9fm5vu)y|ZzsK~;TJz)jh5V;2ei+4={9AasOut|xrA-K2LW~)T z=Os3v)>a04SHeXlTwFGq{pUSQ1x})!f9+`YUyDy-58a{S7}g~#z~`{Oss9+iMhd$7 zh{DwUyvEJ$Vhr!?jH>J`qopjSfq04$V-TR{k^ZIe34KGJiPyOrj{r^IhXj=x_v!ac zHyk5%F0l8mbOm?uD|EK&SG~3COJDoo>)5=ubT+B=3vo1MoYt_#qPXpyt}K^l8nrH# z@^Blc!8`5CpR(WK=z#y;uXMr75z#0X$+rs_z_;r6+5e9M>2PPD~whYqG77rqW2@QCI7U_qyT`(F^7nEY@imyF`HX*wIJr$ zO3P4GIMs2*`;x?6qNv-K1XUTHgwB!_$M_XFq`r61K)lUe&pxr3cMc!Yp9tcAlKq4W z>g4iIx}Z#L|Ml;F@yk!@9%h0c>Wm16TtF?ZDWb%Ym8{_pDuZ9c|LJ#Tr}UZIZ9dZ% z^%WPIb)EPYn2Wc2UlX^Nq|S;~#F((s3U8uC5ORLBbrB0#{6Of|b~Wp>^_+}KE;D)9 z);NcgsDF=!Me$0#s8&Z;U-jV(GlT1cIwL1=E?{jL*Y)sRzIsVc6Tz}=@7hS zUaJ8Lje7>|;WvTMZMEi}nfB{yG;XmSZD}Drhj$?;f@43hq5=7c*0jV1XwdvyEe&K( z6B=zc0^d5Qi@EtczEU`)wKo$Xqd@XVHs?`Rd%DvGbPT6^2dl=q>{t)92TFT}15@8D z-~h}ff4rgUH^h2Mbni0OXhCyxDtCc;Udb)jR7q-<@)uhPk3^-_g~8Kz1Wh&n;_Y&= z)v+{|@v_hi@bK>yy+*%<#PS92AMDTGU%ucy`~BZ3IQQu1g6yyLowF{S#bO@!VlkdQ zF%w`caQ(;ksel31u$##X#+o{Oy}Y=>EhLGnsIG^5r~z83#=;|uFPBzSbJ-Jb-S@Y5 zO9|YtG@vV-A}QsxdWMHUYCVK~OWgp1al=wcK!xMV(i(rq&7ieoN4 zz76xlw!_d2qXL&4{~K|9n*7ty72_?yQNqK~tuA*;?j1ficN+WL46LF+)fLDuIHthA z^aa>-bOmsoVByUSph-H)e#NExL{8uMD~Da?hK4AkO1x$xkzwzhxl6~XkzrSQZShY+ zmiw> zpW7i$GL@avfyc!M(DUOS1`WRw4BcdFB^Luj{qSj5&KI)XVY+;NwAd_9O^K~Topa>7 zBljezv|_Ag@VPUa`6H=SF@qgM;u=|h5Gf&R&7VF}k_EA&u>}kOb57=rD0kI8!9S#@ zwVtzvcD&V&q#9`5%UIRv!kRot*S-`7<6K7T z86?m~M(Y^_7(hjl(E!ppD|YcDqoZ6v!yZ>*fS(crq#)tv54#?``5(pYC3MX`h*AS% zI*Sq5*&aDpX4q6v&OFVc!%y^9O-Hln8p4gw+|c!|XXqGF^=O?SIb}996eCkyBDXMz znt$_Spn7u&SErU>350C6Lx@r-{(&rN<@D8y0z7MK(=uI6x2R?*VPcv9ZZ9xU&I2nu z6l=xwT}~Ku3W8EN21c8IY@`uWMcIFnvh5$BvxczYEh#iWXZ;;|3Wm1GKQD)$TeAt660;-qKuV|mRTEWIt=sPdxUyrsIWi{!~<6B{?q z$cLdgI#K}(owRe$HJIvr@%D00xILI@+|}7zzf4cKj@W7@d30%Oy6<_Z`r8(j;ZIdz zuiDcN{<9uFmaU>+1%Gm8;6HE2f#HOMz@y4Nz4h#xnR@p4c3^<(ZpYr4fxka*hpnS& zk1go6R%3xLPIo?}D9f&$yO#9e@!XOb>91W&#AaAZdg1S$8TflW`~$u4_stCa10Me2 zUib%R2L53W|5z{lBQpd4n1_$quba>IFPoA6J-E#E?~yY6?2xxC&PWwUmf4zd*=Xn7 zF6PNj7$acwgq!qWvvYU$H1o`zGlIjpJ6$s`_QF3uGw?5Z`2K11lHAm@=fzy`lI14v zpW8Tm{&gh+Ws|QPmF(=&0bY6)uX9!G>8fJaOjfbSSFyjVioG*g#s1gX_MkJAWa8XI%4On=QI=c@h--A$rJWT<IB$mwe0 ziX&YWykxdG<|?pfQaqM4cJfz$e8L?u@VXO&ms#nI$3`akZh3WfX0SPLuh7Uejj0W@ zgi}@sM1|3}D_Grc?B z&Ig;6<+JWy<(P?P7#nyMX(r%-EF#7>Z(quet)b&J!kIwF;CT&p#^8C4d&c1T6~c_c z^DCMegXdRjGX~GEEN2X!UlGq3JimgUF?h}xGIP`5a|>or&@_Ibf)T6Gv&|U9?qQf6 zWO8&Hc4kK9f?q#d#@Uv)n6U{{mAi%xOSDXU*aF);1TH%;8jW6+Vf=}VKr5NHybZqF zpiN3$_AcMhtnM31gVj(G6rEc(IqY#2@467W1g!KTgHQw_&pSE`o~*r=gRRY2dq&(i zk3LCH&Hiy2_tKlatuPImev6$vynz)~R zO71eqgWP4HU>0SeQnv{&hy?~8lW+_FsM5}cDVOE8&LiWTFKtpX#7Hz$)?<)j2_u(g|KDPl?MW3BUAV5 zxI-I;+{d^$f$^go6g0NCy2p)?1Lnu=E>^H~{E}#;U0PwHd@lv*rsjWk)-oqgidqa; zoZg`suWo28BpHIt9f1VFbDT^7J(li^+fII|ajEvTioOqo(oEiKP=Lm@e%m5^Ex1lG z1Z$oW7rA(CdozO9D=RqXJS72PSkaPd_KXBww3*u!ZMOyNNYuV3XG9Uq_YGFarWeil zFt_I*)Vhzx1$cDF9ia5Dwc(*;~q)LIXE^%#%UQAm!EOLdmT)f@4g-Y|bm@oY0 z+JMNq(yg0U6j?95z6IlQ>xnn=5S32S z^YVLygAMLJ&OS$>e)x))X8Ji%-0GLhP4?d15{mA?=lX(BepPaZcvAjmD3W%4fo=h5A`ZaVy-l1}N*+v=J+1CHjKj44 zf_rTUXS{6FreTm#=ciR&=RoC0-0cKFq5Zi+5)lWDT`-^w}{gH3CdcE z;-CppUAZ`I!VWmXH7{r#Y-eBGY^!k#R-MaZoI>Ph)Dq|@pM>|ZF^N|>hws%l7P}K0!?zKS%T`FlhzjkxBcd|5+{*|7* ztUO)onoYs(oX!^Vdy-$*C=R8$HQ2&nB6Q3cWT$GL^Ie|9HpNlw(4yu^4LB%szz>d4 z;0`m$G@a|omtyqMmpF;TS7&)63~uvE13Ets5=(dRjH+?RDQ822Y(BpcJ$trh&TEJQ zKj?H{K_x%d&80;y9bY}4F0VZ?=1F%B5YGUvSm9)?*Ef$Wk_(Qia!o^eMo3J%!vY_5 z+ii5>FbW&70O9)O;luPi}H+4Oy#roF|{JG@fm~GC#YZ zn6E6@nXiUWKKfgsl{OK;v45Md8v57GS3z1Y%~uRp(^zPzwTgKsyoUK|NX5Biz~(v7 zMNaX!55mFdOwc`l)|szmNodRHClUYBrfV)iYkWg~>TjZU` ztr1-Rqjq;@S}>)ezl_NUj9)W0G%G3Jt}n7fwb56_AV>3g>~OaHn9Et4zX+*NO@`0U7)fJZs-uatoQk76smYV<LtTyAG7-7EuK8tSF{X=cYaG#vkN7>u4oAEE9MRX%lKuHWDNYAkkXtrui2U)}2K$IutYx6SCz7}F#A=8) zx{r8o@-~Yw%*r_qnHNJj=J1pl_vS%4&(SG)c=MyDZujy+xfJJ2d-AyVV#|3>P07Q1 z6O@wjT(pbl%z7Z5of5-)B|UkZ>Ano-!jwE0r^HN{J9fF$)+sUDdSYyp_lqBy7%*D& zXTSGC!FgZl;k+)h^x0doqvgd~_CHEzvX1wb{L>Oc|50z^sb4MX{Nvul-~P2C@kDRp z{=G%wpY$ev?AMFLKkZHYS}E~A^(OB4&7$PV-o&q#6923x(Z+M370KOPBGc{yVNJ=M zveNas|C3bEp+fb-@s(z^3A5Dkl14fbR|h_#>}Mqf_)Ia`H-BELq1!5nv@S`Trxg>e zC`8U>H)tZ|=_tuPTRk!(?Kej)2LAyL3z}H*w{^%yLiF67?RXi9?qz0g;PKL|z`TQr z8sH9Vsv&AX?t@ROdOYxTms)5In(BdXGf{S0qTA~W23P`w2*j!?de-l4a`qX5K5t3T z*2!E(2OAEz>+@AQIjy$*yIrp(SSDPQ?QusNbj?bX3r=^SMW>W>28SW5(GHCfz&d0j z^&>J`X^l7yml}+RF0t+fV<4#rcBMrNs3G?|2aK%ed0B+kCr5h+zN$r zdb!nsGkVw-@^wpYTL2v0a9m(3uZ&*JA!LHtnb%;+mWe9LP7|bJW36MaF)1%C{sPhA zD`R_85Y_56qvj?`!dWHEkB+{|%48F_^Ql`z;9*_+7Z`j0o_*UKL>1APQYLP9>Xc*m zx?STGlB)B5qyYu@o_6G4QL=Mh&r9eu|3tn;^zZ^ZO2% z>55?I#)L4-Z%j}lq_|iMbbW#P(NHo&SeX&%SM5&8JjQfsPerJ&c|^>`d6TQ-d|hAU zXJ&U> zX#~nOMay(nna*-d!8%BnRjd6c`4vN+_NC85Tq)GKM&k+F5gfo~Jh0=$VmRMubUT9G z+l*3I0k6IiD{~cseNWv{%n1!%w5O$YhO3*!95EM!wQQ^KF2{~Izh?Dr@UO||VD4QE%)N^pxla6`8^qki=N1xRec}g2C4U}% z_ni*zXYX)u_uR?Osl~B#s^3#Qr^~-QFCAqQO>ej$hHKs2#t0R0yjMfko~fSrlpR9P z1(Ph(UNGGc;dw=Rht8#&Ad$qdb41Rm0p*N-)oKkBnm>j{rC-s@aZ;k`8SNtSWLQ6S za`Mt$-sYz~vz-yoY0YY(Q<^;GF3^sWX%XBLT8hX*_G;{!&`zXVwF$l9=py%9u~VZl z_fr0|C9mP-^)*;aR!MgBZ084V9u+a6p9V9FM^)*XAw%6oZ4%nzCUeT2c(VXtqJDY+ zG`k0Y(*?T$AZIRz0c7d{;ErWb=S}Qbh+}!r<6ZrN@zC#tZJ77 zaAFDor^9yxs9zqy$teJACDC0T=D8dKN2iqEYCWBwK|M4j-QCgVIN@+P6^{7H4wpDR z;Yd&VfmgCx%}&i9ffp1b2{vxN6aLkk*EU!M}AkVKaF1i~I=g9rk->Ox%A%k5C=XROj_h3%?@GrQG4WNBPFMVeCPxa)1vI05fycn{r# zdRP}4M48=MjOeuP$xfgTUZVKOr-l9>`{nY(inf8TMQzOdfdMb zJbU0n=;bk0A2Yu@212T5bW>-cVVH{?Le=&(`E}c^iPtPF0!_ny(J=V6w>s<>^?<3J z2Ka|fj29Gvho>nzx>g&*6w6#)Aqrvw6AaY%k=N@hk;D=2u z6j3_9q6RjA7Q_~sX*vdecJeJ|6za$=e%yW4dI@Z_-e7H$0U^_*y|se`|CJ}Xe}kY5n( z9;Vvdc$j{Aa))25`&Bs)Qm_nErUl#{W#isFF<8TwKZew=&q?ByY6u&ag?}cNTZo#@ zx|zroekZroTEhMF?B@ZKJ42b9PYqkU07eK8IeQ-sG)1Xqn$~Q1ZHR_zRzaI()Ozgg zu@TB_cU~z~vpmS|P+diz4Q0=|__|d~lo5&4ooW3;?O7$Ck6C-%6+Cn|t|DF<$3QTW z?erz$Qi*tvO-N!8O0d7i6%_cR(E>3yimp!8gSV#p27d=k45}eEgo$=xJlPm;wpr>t z$xx0r3-w}mOV{nf9hF${OgrKq` zT=ts@7o#>lP@eJHvn&E0`25lPKDk5xzWzhjffLI=+~Evnvsq~jAI=gVIuNl)ls)@O z>+z<=G1u`sv7C9tMZf~&;$eiG31(w%&L=P!)k6{-#|+n{*X?LuXVI(twAWPs&fb|` z2U^`-tf|X)vD=H(+YHlX?0z#ycP2^El)!(>yce`;@}@CU42H5v*CszK@;23dF4rcp zpJ(i5(I&Yjxi&%IyiK#y**n^^yV_)2-lSO{YFA5bs`gyZf?CZFJmLju^RM2kxp+u~(-LU+a7Pffmt?S(m;r04tq-pe-JVbi5uIEGIYI1ns1|NUV;PuLI5nWOO z%oVF-c!3h{q+~7@jnBInRu(!jY;ohfdb6IUUPV>cf%{+$0fLp{IZ-UNVu z7r`Vze!D+Up%^gBV4H?P3IlrIRATW#<@2146!*D_Z}dYaR$TJ-(&wge=D0D0gA*o` z!)`@}vrL_pn!H0KZd@YPqo+spSOwxc9~5TM$GD-DLy}=7A+S?M{0xIdi$_E<><+Ct zNKS%#^L%?s`S$sI`<1Ve&Jm;mL9s^eFmD+*FXTF!Xy~Ml^PLrTEO5yGrZ-H2*H@Bn$P1dwjh;wKD?GrJbTqXc^GI50QmXH1O;gyOQLChLa*tcK`dO@@ zC$MbGJ#H)Jaoa$3z!#U-$pCH($%=X0%Hy{bC68M!^D0Nlj2DH8V{Ee)~TQ?~O6YN~mId4pF&`L$_^IGp(b8N(v z&V^XeyCXKzxhzy}%AM~89{s^|)j8JiG0`$f2jq^(q$BrvQ*oImS^VHA%gRWXVqVpK zUWHBHohuD_ZhDi--2}RH;tlBl%BZc^2uj1gp>RK)YqHYUNJEQ>7 z!T^4?Wo*~dmQ}2~{VO$-L*l67%n1`{`vR`TP0m#6`usnHK>@lfUNz5*uZL_$hBjAXwRq-9K0?CmiH+8@;W-R2_tQD|1SM*i~Wb5g4 zno1eet$0|TSc0n^j6EfT>q+U7A&F)jpv`#)>pD_z&-~Uq*iC|cI2_`0l)38;RqNCIXxfetJnQEU&}6_f6~$N1w z)~@ldmzHRs`5vDcZ|JL+WPxKobTxIXK;HH|pufRF$Qz z-Z;SE=7WhR11oXTZG`3(|*K{ z>8Wa2(I4Ux8#J;n8_)0~=)}T;U%*VDPQKu+xfsoH9WdiLcS-r*Q~vj)m`MzL!JV!( zNkhe3tO)|Mb+NbK<*1$Tn?NeXQ>I(QQo88=r0T7Jo1N*P3$zWYHiMj|k{hOs zQ>5w_?8P$1F^jXNbPQcpZ(eamAsKy~!9d1|<&*7Mg=we?)jVKwIrD+f%pF8u_7>B zuih-(i|?8xi`ca^tH+)-RA4*Q-K?IBUF}D~8+xRTDT&H+V;-j!b{I;|)y2}e|eSQAhv#5v+o*2v~GS1X8cSJ!ms#$4l zD^w>D=j1+PT5mD$GTl6p-ms~4lT}~K+(Cx+4R$S6dXsy<_XaB-r8kj1egY^rl%Q;q zDT+FWyv4yV{B-WJM1sRXSP?93!{%m5owlsyI~jGq4U6WYZP;4AlQGNalyAe5x-*^i zp?35AihkSuB1?0RQzD&-((j-Ove#BziJtwe?%5i%iQ9%PTvLY3HDxIGN*T&d#b&=9 z%1&5dr~Ey9A7TWMnTQjZd-RR2=TCgbo;LTC=-O`d zm||noyBIRobi8t|+H_es^QNOqPo)j}xcKC7svVQFPPNM$2pbM7V$*odnriX+C@^R! zjBKe6>4A65s046mB(QS_Mq0-+tL7P~%Q03*IRsLNv1WP3LL2R3tX#Ww`}?`BI=jSJ zv!`P$LGDt6du-BO`XbaFGRDIEXD@oy4JM5v}(azk21Wo^ygr#@9qjKOJFbRy~))`b-}x>7EXk3Plx8|Fvr`k0w1%?3(KO_qU})Uad@6a z_U?8L+;#@XP!_!rh+eo|4XQ~(K@K2d#uk)DXDwq-N~6}XATy7l<&boYy=8{0fqub>)F@+4mm^^nCtd1vxM92>cW9~8M59qWVy1-AzN$wIV zKAc#-qtztM&CpCHiu=?KW+Qf3-So{U8fd!&GS9LhLEQFgd$u^`xIAE7srGe#?--{F z$MW?~1TM)M1GD_aLrFemHJM=;Bh$+evp;o?T;Tv{fVokC{6YI=49h0kP5WlYg;;5- zDdGF)wQpwQGbLSaBjR(pU(QaiV<23&lN5N;vaz{ZC%%&nBy-@*Zf>vBaylr?vUnZS zX1x;MOmFs}?zc)J*(GF(miFn)^VrMGt&=X1*SC$7oPf&529111nRksi(6c7@QNh2C zBWk{Y8(}Rs`P!994p2`#WgXF>OU+ZaeX~w4-`t|vcEh%W^kQ>_ZB)C=+o#0x#aO0XCh~aRKa6BSgW$0+^DWqHYO`gk zRY~IAB}}xJ&~#oCZRsn-YA~?HTN{ue^)6^F`a4hCE_jhnR}{$)fDP)Z9)P?W}S4 zO!jOYqj`3bFq$eZ2Ci36@qL3gS>NBD!=4m$$JIfGc5@F?xBdm0P()oK9XLNtbmFtk zysBsASQU}sCT@uM*B{Rfetxr zPF=e6H^1A$F*c1gnN}xvh;v?zcSAhsJz>BSj5PmK6#r&(bv{pU&cZ;rlCZn3{-CdK zO6R?n^nvVX-S0x*KqhTf*#^xghPAF4qBbg-YhDf3YHM?cn>l=XxyBeBIJ97%H1cy@;>Dea$s|_Xf8frG*@@*woO5@5=tD3LNB7P!VF>N!m*1;h-9OjA z8X@|x(~es;Elm#iy(fhTcrf`g>WmEUYAuItFV zN`AAUmv>G=*fQbRBDoe-Cz%R3*|JE}po`eS_iZ*SD%nW}qBPywoofl$I+k_5^b&0B z9lH$f_oyEKGNdj#&n=fYRBp`%sGlC0p}Q1DZf>NMoX@Jk5>Rs6Ddwy&dx zxo9O49P|c)WhbKZLsR_Fx)%fx{Dt<*Y(k>-v|#{|1Srl&wVRX3u~|TA0;R`XFwG1* zds&2opA`WH5OOcH#JtcVDRxr5guKaER9i$v1gfPdckg3YE_d%^S1x&>p|*4i8fl;e zs$}=Fv>Q*%)mq&7+|8@xmfo-|!Zi%a$c2$3P(pM{scyxDhU<)#^pm105WA|1!pSR5 zLly5|?3_GaEIX!*CN1~n4ufV2)0#EjL>L-ovMFoIYk<{(ojtQr^Fy+RxmAC;n0%h3 zl{%lU&YGICI>MGO3Ejc4e0aLC=!LUZm3b@79+3|{WD=C=N^*0XE0G^~p&2iJ80Kw` zy4%e3+iCNH=m(~4uP=@Y3(gqGa;$&2)6Q2p1Tx4bQF>7DE=ip z4K6Mv3wFVCOLIEvj3^oANBcNhs}uW|+>xytC9G>3NBV>n~kG={rwVhRsqp<$)rXM)=5!OBo}|=qo+#9sGA9{~+-f!JGx3W&NrtdYF79W@U~mvRWoJkVsmT;`B{6&4T`@&{W(u9|%?y@brTTg< z{Slw-)>RhpJCBz-_S9pjgpT^hE3ex0Ti4H2)BWp6<)$B5g4bME(~mi~pqXg;(Iu|w zyr=1!PHi1y1kS`L-t3I9w&ZqHh>kqPW=m>y6Sr=%X>2Yl z6z^uO))?Ev1$8)^>CT@bce8-qQeH`KYDMV{UXM2dFf7c?dceC`%ZoJS6?mzgrk82u zK_R9?IcTvWkodW(o3#qxGsxwlH>tS%t0{K_=rT4qHpqA$n8huT`##DT$bQ@Y{c2zK zy9#t1Pk*d*<=2P5v-{4Nu0upqj+y-YF9@u2X#K5MWWTD<4%nBTxa-pe*xek<={`R5 z^-mV5pFI?0+q(|ey!3NTe|4yU`-fA!^LCd~zc?lJn{Sk7^qkfw!c&J)0~XSi^UXtdzw3ilq7vfOF|@-R*#r>F(@oZM@*O%iyd*MHd$nb(l9o_}A`DbO#OD1DWHON48>^lO3`=EW<$9 zEuFJTB{9i_&A-+pBjt+fc!^@=aMVl#)b^~I{l+hn#7A?q`E$y3vr7oF-~Gvb&E|ix zD3zndLH0$B%rt0zge7*6owfm+f90|a+Q|x>9eWiw$?X4M?7e@GT~~GId(OE(`gY%L z=^Fj8tX8aZk40iD7T9Y2LUynY4mJ?pz#mgnlmkZf>ebYYFt3pC^CJmZZ4g8OO^~}q zkcmQ?Ac&GEj7B&?A;L6R3`(Avm_+qZz)8f!n4KEr+Bxd^KG>#WjkM5bAeDYD* z4jJ$XUXLg-ONd1cg8 zieLOs<;%YqNXGYMGaey5b+WD3{S5IzX?+r~C?ws(RC0d3JyA;GsPl#QQDIk_`P}@{ zbRC;ig^O0XHuDfWe?ppqhGG+t-HC5rC6gVGu)5e87SQM}@?W5* zVZMzmkui0W>R4%pzo8PXqJ*5OUV`@q7s)~05granRk7p4W_2Q#nTm(0XUL!;f`*=f zjgI)-xE98*!#wD#4bz&uPNQ(p=#m|;#HeT>*O7?D0NT0_r~P^A-n@2X8-RNpL-S)qAa&&ufO1|2nTMKdNky zVOjjqA4d69HAie;GS2@p_!f{!)hU;SpPgU`A4|Ydw^_>)n5*bs6&lk->zG85f7@!r zBoc;VEXP4I;OuE%V!G?q-|dSPiv_h@CcLXIDEkykhNBOz|vu#n7b| zIo8X%SzFZ2lbgzT+a^ zZ6s|IcQt^B{br8XEMI2N6Y;>425NWyPV5}A#F1EbpifcbbT)!OlEMQ(S|R1@r?WOs zY?<_oz?wdrNr++f6!v)Du)G*z+R-Gpnwezk>;T|^Y$FDntY*}8ww zgm0zVR*Tk3CNswgW(R*=;(AX+C8%mLpTsFGVrv$+sM<_u6Xa3sF=Cr5VLp^#mRq=K#*YU<+I~|_H?2LO z0*c9mLk1xGJ%}%!)ovLYn4y380*gL~3ST{i-up%F3laLUr0;B8=lp_&4ib!8^2TKQxbl95Zo4T-a$v!+3kFw2GE znj~zm?9wFR=KPZI&|FEFnHALR`<^7s{=-5RL#qiVWw#{#YF>^`_;FG^NQafs3KV3; z-Uo8lEZ|6-$1I;|{nZ(16nU*azn!QZvMOc`tsP1rg6RWIC<_831^A<@dhcW}^If2T zkl`u@y%9ejfPd&Pq5T}(i&|pIzaivOLk(Jm3Y`SdN`e6B5eOs|dRL*7COu`eedft{ z#W06Fz+mpL^V{GM2$yg5>;D(br@`T591?MVnD5!RC-yq=xw5~$)}(%!@xKD|brA>w z*f0RD*WdH&ML!@b%$2q+Y7rSSh{!lH2z6SH4ECx(2GFHI1`)GF_s*u)GTtXi4}Wqm*WK_|+N5w(z=#0xeAE zLIDAbQb;xh8pv9n4ub|P1r$D^!VrSHvAgc5|BML<*F_i_JpC}*}5qsmT?MDkSC7Tl5T_c!m zW~cW|PM-K86i+9YB}5}YO-n@MfV4ArWtQ-8M1x$!B%ui8xHr(hJ3b`BqxhDhPuwMs zxpe(|MY7XHTDqdQnqK59c(|}p^~s@4LKmat`4i!Ybuc1-8y=LOV&^kVS#9S_@4*fA zY96%mckFmc3>%l8k^ii&5jQ&AHTfNthL8@uC13hh{Bs;*r1Oeq;{50Q97nr7?6-$w ze|yAlkLZ>Iq#*w9qT+X|V*Fzw^4#8Ux1g?BNsog>L&t?aBXatuBR3FeU`g{wa@_lA zxJ((TA@5jE@{7{3f#ieg?(u94iOGtNR*H@eq;J#Mj%TCFX*wbqSs(F?2kzBRuwZH{ zs`5>2jiwn;{_grM@qXEhm>IaNWRQ`ZgL3xU6Ffh?ClBJ5j16H&Zsc`%b7S9g=vEptv8fOfuOx3V8;OL z>%-4Lvv5i6{;opKtV2U{Xc_5-; zqlSUXKqzooq)?cBiS~*F@Qy;yII5=eH%8AKuZ4#Wrhj@o{9!^sH4bCwX%h8bpHVe3Rtoo9-Bgej}oeT5c3FRomz1*GsjKngQ26 ztP>mgJ%j<*(A7RjC5IDi^s4Sv%#S9SBOm#D8dvFSEu~NLDt)z2JSt>AY0DLwAy$(o zN76Oba!A)zA{x-L*Q8f*mIy~I#9A&|3&yV@>n&UfGsuwE8^49D+bv{WZ;xo5i1?ve zWiPT%Ne*|w+ayiecI#S?_t-4esexl>H=?`~1hfYf`z?1aVn(eoDJ>%KhY zc(*JT@FNWt68l;Xwz90Qjo_ybXMBU__Ko@=<{2`ejC&ReYCYeuEEN8N(+O?sDAJQ< zIpO_D+z>(ZAIfL#7#dNDlA2kNO5JEcb^R)T98v3F8(xTdB3R;s6okRvQ~YXR3Vfh9DeyAq-Oiyfa(`5~KM&*yz(1t2W(oS#m4?o3+#1kA0x5xbU zaJW70x5sn~U;;hJEO!|Y=n;bEYsWBu7puQ%tDhx_ILcSjGCj0J6KlFgVoRrK6T4Uw z%jCTkg8->zDNZb<{%2wn`NR{dla;6J|6D+aiS5VKKDk1&EQ*4MxL5^hh(Qd*oA88q z6BQ`ivmAT4nb9oFyWCqDix-?&lnLTZ(1AG-Dv(TZ$D$VuT2vAVDx`34qOvmEcCMCY zyD6biVY9vW9o=`zd(^EuI#+nl8f5wrW>{BYWP;1-japtKF6%A=pwLLAf0FTx0a)OG zYgXYX7lRb8aVpD9PQQn!U6 zEBLgWBn%BkC08p4Ru;mKd~Y2wCDBQ}{P`NevWkGd?1+(lu}N&NOcliQc(&|0$?}9H z+$lDSNwzyGa(FyjF8zh0JFr;P)8*;tG~gm|)5D90Qr?wO$V0h8i|eG}8Jo^H5};Qs zf6SiuJyOUq2^g!T6mnco%ThgtW|0uC{aI$yFnYZBnqK1#hvV#OQxCgK)22lm75a3* z5X*>KDBPxw-_4G#Lha66L%&;Ji!q<(VObronq-j13p9g6|F*k;TgVI|3MP&-m zk;Hk+DPTI$16PSi*f*#_$U%k1Q*^56L#6TuRS=cUnaF@1c6Hgt)gn1p%fvmW4Rk2$ zAkl=ThY6j}26VnrrA+{lUGYXOF-V6&+fPM<;~j}@Pf;U3>CnE~z>{80Wg1Rc1!$_O z_}m5=_s~{V6;!}m4Cg-cXlNY*lPV~LrT^1);6hl^?O|1A4d=QwVaYowwS{v?c#E(Y zelLdK3ol*3+x`#B4)W`XcEZ zFE%uiBAw%8NnEU$n~&I%5JHsgJuKb)lCl%gEQIqJaw48PRMLX?ugxF)l`1VLN~KN+ z2wNpdXR-&nCA;Cz^0Ti#xYZSxJF9hT^lk~Dh47`ADYR#waN|sdh)f}~VPc2&(KxTV zSB8L+KdjWWtTg_xGF{2KGQN9xL8Fk7GOM7@Hd7Oy=8@D?plokk?TuS|inJ*v4G5nY^$##FpAR&NItTQra=kg4V}-tkJ5BT zV7xy0N%qW!xEcVEHHl>5MSlRoTu@y$0HF%$OUcA6rAv!+X|uahv>ejpTCmidbmgYx zC;5?7%6K&b_TmVzL#4fhOS6~gDIuQ>+BKv#Mx^|k2YgXVZBj90DWqGOl`ipxHVQf^ zR5Y8h2Z%ivP<1Y*WriXxg)yfwBeri0W~Tuf)<-7=XsZr3;?= z`eOFdQ61!7KYr4rcdflOHUxu^hN_XVNPKLp2~FHV*2Cg-DZw^S-d&-elstsQV9SYK z1yZ-0P`k9wsT7CI=45o_rk#%ZR~2*{j68&gAQ--Ab*T*sc}1Ce`B^30Tx<_lAkj)p z5|y_r;3itUM!POy#wO)ksFB#5$<0mM5f0mBFVwOF0*Z!3AcU=`(RAG76_^2XX4`?A zO*>_iYubTGKekqB>iZqHV!Y%sB{ph{ty5;3ti613z2bPUuE)LlvTVAsjg|7__^` z1Y8HpDV~h-%;Q;*Dn`>W&gjsi$&HR*{-_XeT6tH>J{DlTMQ8$2tGdfws96=7beOfW zagQuyPjyynz!_sdi62Tb-^>m%VwMk{3h8)7u@1n#E?o)x3PhU#3V#qf?ChN7#51 zM$VJF$EQL-&9w+#tJ39O1ct%zfmdIV)oJGy>0UabTJ~0o(2KNayrJ#LuyLa!VnFvZ zb?c)0CECJ!p)hn39uE-5-6)>|!2T@cm`W>I$kQPg0eZs8y9^nq8PgdD6H@KU&UZw$ zZH3#?$!rGH8n7HT8N}9+Oho^VHG~w8aLWR{vpY^tOdA^Rf}^S$9fV@{fI<}O(ySB5 zk|(ghm+w?-cZF4A}RPfH5QFfzO?>p zwOm7tIsaqeSSkaDlClX!N+?{T&R=5!-ZkkpOi>|F()CMOuBd>1obj$%1!@S%{jsD^ zpybJ!BvXO1y-P}E*Yvg5Z@D7!9>WGZR3s~$;a5$GuoS(}pmCVMdKC-kJ&{4;F*EO^ zoZxfkF3k!cy@Bnx_$mXfS&OkCTV}h=wD;2F3H=sl=VD4A?U6)Gv8m;>gV_IlYsb_d zEXi(OFoLlxtkJdD84gS@tnjnCM%fz!Y8=m2p}t!MTB$~A_&s4#71b=SD6PxqD6KL) zjCkkd$jnx|0I@1vMhDcNV)wC|2b$oq#xiBaOyzIF;Fi-&^|?$0O)LqOqA~>xd@WNz zwUm)?*V|kPb00E{<$RS`0e&x8ES6v~3WRRH!r19nymFUQv`ZCJJRduAGgD21G?p(l zTTU$DsE7HH1bII+H~n7 z-IVeteBZ`0E@%Ccb1~B8+sB!0;v9-csOo-+Fp#d|?Q+9_rsf?UVKl{$hyx#)hLTpT z)~qh=uA=Ks0!>v}EgOwU)#^6$*&~5jnn9;li!g*NcHQE8(cn}wpCXV59jCG^U2emg z0vovAKAx@;Vo*v+f@ETnnJYzx(PL{en##bn@RdZHWqh?-xW}+9wNP)VjSjAeeyu>3 ztYRT}LqK`(lRj0%tY(EsX;uWSbVLKJhHLSMYSjJ&SjlW)swZSvFD06&0O1b06*Oej z%uv&AW%E9=F@}+yT?q6e#ugy4sS!H~*OM!DzOoC2HJo~t|7^1fyW1~~uHF<(6)5t$#IN`^$7;kqPh1B;R z5vcbNRMQ<@*(PuYyP&Y{!OQ_;lvf~x3(aBV4VwQvk zyDv_cXgJ-`{I`D}X`}PETE;fa(J(-GJmxkVqC{GHExEr!u?9mC?Cbh*5M~?jS*WB8 zn`8UBKT6Tb5Rm%fahBMN#l(Kh332y&-ywVhYkxM{(OoN9@1E|DyU2GV17q*RI(|Ak zx-Vf|`#0?9zLZ~5O>}RPPmKTZ6LxR*YS#PDL~UETPBe8@`V##p%vcMAnKP2QbczLl zW;H2ukx7Obc}$k`WN+`u>U4}}SaeS)vPNrY^?_^y%VvvWY--3!qlipMN_@4a_ti$e zS;domy(iaNJqVWguxc!?G`;pfwrN^C<6(&etg=9tXeG2ptH(_BU4`c1x(c6$DyZsV zQ3JWp6vg`5F~pA^GGv?4QYX?8d?CrbDujoRkQNCByWgHAZ&zR!Z_3Rs6l~qc8-;x( z>4BlJjB7c_Eg%>%m$HvI5nN9VbkEx z5qdr|8+Cs5JZZu-lDv@<6NI>l$zXP?kt@T}L?tA=bhofH zsX%s*CF`PZS$La1wU7b`$BpTa2YDQYW?$UBUih~+?oO0&HAg4sIBbSOZH=DMKIqM= zA_+&5@au;HC$cQ;VQhN9RM;K`yFWc2$rOSaoSe1F_-ho;@nL)77$Vv>*`+(?GRQi?UQTe&=ATpCMGPxZnBWhOF? z2f5jm>~sk3ehqQcve1Vx^pC8~|F~+f&(Bs~{_^0IClOwpHZ8>ef`0a z`~xSrEgXjVfB0zGp`%w+?deLj-yhCDbE;bH@k+ISbVb!x-J2IC@=sUYoB4um{`X3o zzk1^UY!~{{i06wToQNYJw=>}_&V<(mCM;$wCj2rL5+e>Q7h1GM^af$sNSK)fmrhCA zE&rwUMfG|VC%AIqYr@@G^cmQye45C&_GsG_`Wg}mHb9++BOW{!N|{7~R1B@6JRH=k zprF+#8F-*ZdsM9vn)4U?%c*|&$MWAX3F#^JRz|FZN^`cj__o)aJ(!hkl49s<#@P$r zZS2{X0{nTc_%jk$kH7`~tgIv12n%fE7%4ne=@DnrXj@e#?N9re^hlLSr#%+!Q*0DpBp&|s)Wq#0U%T!n?v`yIMsNf}UjpjX zb0wk*99uXx@Te%un{^WEQP997_P%DxBi&$McBMu3^75ZBONfR0@nvFf68|ha?6#?s z#6E)_RnF;nbh4Z1;xd8dUUOL8ins#2-*U^Fu?g>JVx@ZjZv2P1~UWF6(nrY!!us>gq?WHAdIX_gC^9=L2ym&Yh${kiY zES5H2mLsc9mJ|zi(4DPjZvN4};DN{Ut#pmt_u30fQzLp#fV@&I>7UCLUv|N5>ht3UM8iXbV4!p^V{{2Zu;FV#8y_^EmZ01rWqck zpc*jK8!F244Mz--`=$~bNn}u2Un#hLFyKEJQ=VVf@NUWufluL>vP9t+TeUeM&#W0u zM$(&QnP(YvS#9H5L8KF`S|Z%t!FOSUJ{REx4X!Sj!aOCHpMyMIGc9Hx_U@x{Gb?9m za7%vQhFMLM)J=tB7-0QJD^&?1sX&`K!3sez||V$09o&eSQ_u* zH!#@AhU44SG4$XAm7s999VGuVRP;ZvB(NuvGTH%zliiX-u;$-^3aC9Mt)0vZVQsw^ zKNCui$sA*!Tb$XLI0OAw0;js3Sg6Q5bo^#jfO_9Z{#8kTg%pX!XRjFt`A{LQq06>U zQI-U;f%Mv`bYvV|ORSOV6+8hodWX5z)EgpfmgMdGvSA+benhSb44upyYIwjPaVFOGm1vMy3AHU7Pnirb8P0Cz1vM}~OCkT; z&iAlkd@(z)3>~#oJ3`$dZ-D zP-tnkS^}V9&$T7mn03sx`d*Cc{91@<=dg>O7NQN%!Lh^~nD>WX++;h}Wv!D%T^5>a zx!S^&{K=bZHvxjv1hGE{3ywDJ}@RVAc zkATsb6P3>Rl#cf?X$E5@3G8isrE5}y`L##~-oQDkx>k_}kyeK7%M>;8CkZ%cp=-1_HAvoOUu?@ZPG=h`G-$&d8kC=>5sevg zyCL1c5*z35s13?++7{{t%?`G}iAr4)19e$xBLXx|Z4($8Hl`DA9?vGGvW=AwHx7IV zpu`*aL?0}p0~_6#{bbGR%&hULl;ph&SCc<&6Ej~`k=0~UCB55(?W58mB0c*VJXt?V zsK}rws9<&}p|S$fw%SlO81H_>9{k&X`=htviL?bMt1KlZbGat1`2j=4K$`$yOL7#w z+p1*}HU!#;$tRswxTuRaZw9;G0J4xB)k_Ad%A=ChPJvZea{4}!Elk!o2wX@A5j@kI zNqEv9IlEn`d!gQW{N0c|zm~i7@tdztw%{D@{HdNpv67zB<5HxAu6MGo*rm8nw$R14 zb+VQ&*@QupdwheafC!rx>W~pO@!n9|8*i?qetC?`>+y)zU=#dNAW!H2R!f3j@z1}` z2>;lYkyHrPi*Td*y|`2SkS%#`vnIY1*L#zHH{uKS zEr5No>q@gt=r5wR+_E(~;!cQRsXxPIE(0X+x)N=xtoA>8qHU3VVj(=T1)v^MJXkP+ z38V+fDQcY&HrUvR!@R>cLCEW6nf*0(&Gl_Pa5Hon&R&|oBu&Q6R@Qtzwz7Uh5e8yQ zV_Arc%|dh&;yqW}lOi>OFhmFe&fQ%K5-+QK9=5VMRhnGeS8K49;n^W``LId1S7tJk zpDA=3r?p`}C@5aIl0guBfA20^ZTaHPUkW9AHcp#EpWd4CpR&9>BZo6qqwMZ*!g;6Y zad6#Opv8d027;?0SW&P92M+;ki1iy>2$*00H$t3(qkt^I>8kC$n>XUo>D_EkmwTS0 zIVe-lkKQctE2TS@@qH4X7ZNbm#UB7x<=ocu!&d(lnJR}%xNDsV14#o`pi4cUx$-Le zky=v+tr#fB=s*tmn}j2=CKYJrs7T(rM6ND_q`lWf|=II{O!BI>Zb zR8c-EV4AZwAI%|LO1(2~LaGRpkz~<8UzZ4Z<}{a(qVPZ02+=PJqSZz46f?MqASptn z{ANqGbu;Y2QIww5px?C>{P4I_;>eXKvHh#ODQp8w1-8kMFi7UyxPV1qAX>H`Z^17z zbd)t+3BjCGlTo)yWyq9Qwm$o7w2VtNh!UK2Sab|`^`_2mJpd7t63+>7YrzclUD(lr zvxLA9qIji4`8#7j;I_C9mc2ZoF7(e94ja3;X{WQHkr$o~jlA$|Xe5O0ToI|dEE^gL z=pKmb6;G027-pw0k&X`zV)4p=bGWjr!zQ6>koD)GR? zNm#0!rCXGbV@Z$lvSpmGb8`5UoS9zs;L@XUIKYLy$Krg3EJouZPtU`>*pCH^MMCVr zGG8DCZM&kmUsCeGC5oV5ViR9brGj$)U{xAX>Fen8cQ5u+(Lz=-hd!z{OptTUL}=1Y zd99oi$VAu+}LP+w?OcuB0Qg@29};h*z|l}fb@j;d8| zuOhUTMU@nt>8smhu+vHRTBn7yVK}|Kz-f$B*UDto3*C|d%WPLg;1;$pQ*LbU-_ouW z8}vr~zkS?RQi0pJwYW6Q$BKYtNGZ$$R#ZOi9>(KIkYOxUh=3VTWn&e6h$O};ATtq%93iCbnlR5Vsa>4pg-J%@ugqp|Rb&X%mU;S$R{kJm zhdx>U2B%Lj@GGyfU!W%LXjgxgM_zy`ZCd*}demxtKD@MFOqV^OoYoPhPo!yC=bMEZ zhnTB;uoxRYp)e!~)I9nbJ&Mam!=##m_g!}!GO`KBA7<~G#j}lgjF}{8MY3IO3Sbu@ z-?IL#5TYoM>}oDTESYT=Q3Maxy;YDR*lKeeBae)#Do#{dH*+4@AI-)~h3)VW;iKc2 z%VciV7t8r#gA*cooj7lGwN!b^R>t8SUAWFL3>D6k zE+!6rvEtB`hDtJiiH^3>e~HDRFG+Fz0UqMFain90xl|mwDGB*G&qHq&s3CQb>dP!N zJlJlD1j^Pfxvde8UZu6eA0~VBRdR*MrOz(@P01T2=n^+<%}6C_h7QnC<)#}>F{9QQ z@djz*mW${OHk3CoO&b-<2lSU=KNeKee}U>1LN%QO)d~fKe*)j0d%jqa;`WDS2H5sqxs@= zdFQ_t1iX7=q(d%rvXSW3x@h)SAPOcs=62aRq>vw@@vji6IQIQV56`mG;DnWn^z4}$ zhKF#=nB}cNsp>g`>5EKWc8E>Oxn=3yG8rbs?q|BCzTn5aJ0zK>YE*ls?AsBmdwTdzy{d z^ByH4K!->iUK=Fmd}P3Mk7*!Sa(hawP?a_>J8aKicJ1xoSp4eGhjZH3Rzn<=V`})esiIj;oQ=9ugYr{aCOUv;M=dutwa`ji|N$OEU3MO%bRspa#~YmM{%$RT?2iSHOY2s zEd`yTfD&Rx7JkjP64pKe-hJ8#wFf2SN(gwr1#%vwxpMA!S)_RZs+6U-L&nMVt=ECBCCnZ-WFiDHrYap z%e$HR9B9L$sX}-|*9?3C%T@Chsns;H#@e}8e18nD=y*3nCMbGAxl#0FeqNugW)lo5 z-vKG=2Z;+1qP8zzlM9?^O{uJyXh2Px9pRBD;>w@4zAha9ad7<-~EshCLMkNFDGd^b2H1cZU zcnoQ@=fQowvs=@EzWz21)>=Fs=e9r_-ew*5oj z_reW*FD4QVeP1#3Jy1S|e&B~c^n-JUzJJcp5590iKM;m~uo(J&nTuv?&^uoWXSEda zGm`7HP$pS3B9jK6ndb_h8}E6cR>k4D@Hte=Z>*fM%c?m1!og=Iz~^uQpF@JrvHA}7 z2O+CIwKlz~^ZPQq0}x9PK=V(NKqOG2j8k&}n~2ZTww$zxwLOu&C$_g(n_k?iWq~d& z-&YdIi$dSA21-3khxlmalkn)no z2m()&V#UdtWk<-J;)%ABYQ@QQpn5XZUrfxmp^O7qM~Xlqf)$GdnwC^!@D+J4l~jSS zM9)1{jsnG(BXrmg6y~J`F=tDH?bxPsdzxf#u(XeQJbAt48gH4zsFzuDOlNcJ_$|I8 zNvO7qv1HCtrLln>`JQH2M7-Dj4-rJDP-Hmfq!c5>5T zB>$X^o7FJ?IWJHBI+z)DuRb;#peo#9AAb9GzZEjuq~14kTLY!$*YV&5{10EndMUm=gWWL1+_wc9IP z7QI@O#F2$=uzQ|*4)^N8Io+#g#tttn+OjgXbNnVbzP2ctyIPg5z`)sW#td=HFY6w* z&Nh0XE#oGg0bD+$G@uL{BFW>xe)QU|{0XMJ)}MfYt(3&AyuB#M2FU!;XrM^-6*q1eO;_{g;2Pm4{QicFm0Folnc zv`SL7eJ%Hk0u(taiy2_~mW!HP??1GuSP;-uC+1*mH zCqJM6f%YNW>0;B)_^1U#%dzYILt{`?-Vb1T{05yj)E<`p{!uxv_ z3Pg4;oP*4=H@oc|_Z`q#UP3070uXiPhsr>bt$ylnM_7 z=`psDp_I-T!92S*Ti%P@7|zcdEfr~^010!#t1-xI@k%-B47kJjNy1@evF~>rs>#XQ z-lXkYOy(AXUirBRNbP=y4^VfzeEyP7oEFHB+s#oVx3Ru z1=|tqsdB$LdU(zPXP}yfM=&i`>@tIikyP;HS^_|rkAty{u7JR8nu zO-k5A4jv;TZ1{9?;_lBE0`OkpC4oN)z-#$MIJ7ma70dm3+cr4wB=A8|w3rB#VmA=H z3e?0z)U2|gdL9zKy6^zPgtV?>MP}8y5>sdave&Cf>uR%b>(rY7v`x$wE-kIi27DUZ zckXmM#y*;L!8qT~rDD$AesNHD?4Y_q#}j}<+R!993~cp1P3#qXvKHdgc^h^xVGgNT za$kbdwZ;fh*O_r1xvroF!XazK5e@8PQvDvO^Za))y~vn4g7G`G85I0DwgE;p^wM~b zPE@&tAL&zd?5Ikjq?OuI7_q)8l8)JBNNUY{%xl`Ws11$)ce~Yp+OB!XUPwKZy&r;R z`NEa(8M6_#B4wAA7QjY@GUGLdB`XnjJ&kF5?ZQ0e(66#ggS36-CH?tK$|kLCOB_v$ zsE{*jZ8f|10mGK;jn=l}YVFo~Wyhf*c(Mg6dX0(LXnDFL$6KHaP}I4LqF#qa$8O7e!tz;Q#&oXlg|FT*_oB}(5u2QmePr1ECLQY_|BRa(ylY;|27!D=XlVb zxmtD4TupoC8houibKKaTIZnF?d*N>s8z$;9$Ya283*QJ+w}SV!1!Cf}XBjl;<`TyT($Jv6P@1u8~zdLyrnhF9!s zXL9v~!)qw~8ct~9$zeS?7M`&8hr<~3gd=RM41tC+7SIo=h!eI^O*SHU*%3RFZ3hPt z0^|%dI%%U3^;|}x2EMnN-P%=*LHsFmn*ov_oeyX8Ro9g74v<% z3yA-L!Iuk3T7B#3eFK}+7k>FMWkm{Y{`rgZzv@ep^xewKzZiV^soyKVJU96AdzF{} zb@1iyf42PcF9%;g%WOD ziD@&1Wxpj){*Nvs1Z|(Urh-Yn(XqUaW5im@0|_6&tw2zkL~-O64gyLW7^E69dN)A5 ze)C>UL#vr@!Q?h5lFWyyMoUQ%B)aJHE!2wsTCx^p=um&Dy0mxmpfzyJ>|E2=WzS%M zAf+7XLH%h>$E>Lt=&8n=#@dhzz-V?yR@1(JbtDE%_jZo!fCx^7pc7r>KrfY1gvntY zLiW7JHdHGHz&Lrin$7D2Bl$vm4>bw+vfPVY$G5@YJdB)WcLyX}sH z_Ox^gs92)!X_>RO?GQ=MCG}tn=1yu;$S{C6g5ZCh{06@XdmH+y5iKJTKU~v{Jv7IT z0;{o6)QAO)qjlLYUsTFjO;pNb&|?a}QXDpQ>p70i1c*|fiiqGK&ZkWU5-Tt$h?2e! zO!#07rX|<7`w>@B}@EqshmqXTX zazgm-u$JNac8|?y;&-C#TwqEGYd3m}yt!uOXI> z&o0_$q6b*+uYzVwM(luP{vg?`ucbxzY86ebHjstKz>!o6BuDHlMlA}G_sZIFoa)-g ztujd=EljJk;h=dO6FDn{CbQw~T}jt05_P~KQfyZOS@8e%hi{EmWds%QRX-+r&hl|O z(gHOIVhwaeSUv?FF&#tOO^F;%+d$N6ce+v{!br7#gpRocW{x<*P5|=I6s?J1h-#^| zbd*_VA~`_Zi3cO~Jt*mN6#3|n21j5$a##*-l<$;<@Yie~ymK^+1dY(bKe z&Djuq4=cj$KXBaCvzXpH*2qv!90S%5&3qBX7no0AMPb*Qu*4hz+F&W}Sxt811c}$M z4t6!thk!MjM?YS)CCi=Cilcb;zZIIbnQZ?7!{9q$8twHq<7(Eb(Kgl<Ido0f85Q z8hH62`hpEH3|=<^wK}~Zs72ro#AZdNxb=w?32Z_qrx zdu#q#-4?QXUljO(pXh)2^YyreSYA>P8b>ome#DnydZ<T2<$56Gp-d+kF>wn0e~*$EJPRWtUX;$*@anoO%Zqy@t`-kR31pIAa*Lwilwbvj^V9p3T1&3*q1y}y}z-3uQf~9 z&IC5s%c8M@ax~yWcKmZ#o*i&;U-xV97#i@0S}5%qqJZ?-H9~HjpGf~)lBZPT12BB2%xQO|Nt z$}o=C-+McE%5+NarAu{2)s3&YMAEO&7^)aGFHU82YEB;r+bNg^7-WJ7jkBrIH*5DP zG(&W1jzVMN9S*WyjRaa%6dFVbDz$U2aF0@lhDBwpVTK#SEC4INA>CR!EWkq$VPcUqR!4{M z!&wPZoA}shTk*qVO%*+h-Ezp!Zo0;M&E*>qm*9Aw8_p}GLH1*6LQVI#SMByU8&I@T z^pDpVjV!S^;WHA!kgX5$rl3c=CD&eEPJV9nBaY63J#!aTC;8d0_qKgDjIxNLP@*B^ z$JFrFv|&_*{`IKJkjxN6`Ob}FGW@CyZi}n+YS>405B(Yi+W2N z1W%s0Yv8o;OHvlI{PoNogOr677jQVvySMsBE?Ia5c)NWwE~ zLqn{s9_;-_eE^`0Hze%43n<_kl`$jqms{#$fk*iVj)-4<`hEP(l9UphP2Kr|ZUt^& z)CDhkQrg?01t(YRphvu4qb*;KhTJWcFUYI zn#E?fm;CH8c9d zC*4l3)`7b*%IhTq5B9B-F+cJdfWcbZL_|{q^P`a3Sj$?BT>~xqA1HzPkDgUo-#-@mZw@+KEPIq6e08H^ikgJlcm{9owh*T@&XJGsB4 zvo*)vUPn!g46Z(R?lC@DiDlA8wJv(6ea@?&ZFjzHdL{1{7qxx z6TCQthMB*$JCtA7Z3T|fGLE8D#ozxTeK0LZ6Ef^v#a@?O1tM6Knuoaz4=O|0oM4gJ z5NZ$uhpmjD7Cmepx2$c%m`)v?$VSy)9uUGfRD=2HGtwkm`UbWIu_R2{Nk?9twPeC# zmjkttb}ns=TkT4>ESX0K5{~LK12vQ`wWAJLnRN-^$b+0Qyon1LD-oqAz;Ux&V%qtB zu;y~oyZ9ddu=88R9^T-4C#Z#9<6XMSy-S=Dj2o#LY+E%yc}pTBdqC}ln;`1d z7pHhpyFF5pT`xJy1Jj@Vfj;*0e}&7a)T5~$9d;w7Kf=$?>cqxyQMi!*KbjbV%)b1| zn>zomo~%?i_fE*;@v5l*RGX}=I?ow5&4nhpzwpb^Y1?9mrSYfp0*;BkimqJv~>rZ(LnSm<}u4k2^(;vl?y;$C=2yQC8&qN`8Jj%K51ynA zy$d1p$+r9}pZP~n={^e*k#=fdqXwCZnIAv>0HT+_jkUh~y>iT0fY`7~D%VWQT$@}` zxyeGTO5%hq{25|?YO9uXoWK7Tx2v!|4Qs#{6y2@r5?JCKz=W1{(dn%UjE?o}@vUG> z<8@;UIM|MnbMgy*45L!Y1>=e|7YKDxh>xI-r?#evmi8HbB}&ZP!oAe2w?UG1t+hV1 zIXTRA{?yhTP6m?ve0h1XyqvtHS77fg7#yXOCHWI&0S1$E(5#;Rf(qOcv1GV8d5h19 z5pT8e@GW|7xATaSHq?1VN$O%0CvRcd2(@Q#5wjG0<-TDj+13gaB?)|KYyR1TLanIt zKb%Xb*I(KyFBTVZ%0Fxa>xqnA-#Moe^efH}I#~vzBwa$|BwZpg(z<$pbP40q<>0=! zxzcg4N%aq@&v#Ay7fl}WCanTf)mg%_#h1z>9X$jE18C(W&)pyvWXy%kQnAjdJBZC+ zQu!ecfs=s%cyL0n;ytiOu)k#eKc^x3h_qNRnosQLim79}(n8c02>`(Vf!n@TB7~-d zjT#e7I;%4q4t$|Vd5tj>gu_77`JQ8y0Oo5^!~;4ZPM;>k8{5%h3z_rs|H~qxou*#A zIse=0K~4EbKO*e)mw%S!-_dQ4-Yz8eYlJ2!z*q7|Yz=rV`?`Y?meAoDBvAbx04?&1 zO`}F9CbX2Xu*PB8Bg-9}=#EhmAMgar0kL?qFeypLBtC$}CaxM~5D4=F=3p1Q>sQl& zH;byW^D!P!(=NG?5+Q$&HuNndlM?fT-lSC^>40UcQDkUO|G zETa89$oFdd4mq?tpOb9_ZX1=3?=!U;rrY~H$VO}`f)yFFi2^Gs7r0CkeY{KjEUQT$ z0ZnC~ffvg1b;Q`9Bo;S(84fp?`(RSPgz+nEJRO~5{0ho_AYc6|Yai6z_!YRYL0!f* z>li&R$rn$KZXAI6C2d~|kuS5VAK3`fVLmv@uVCP|FYg5qKnMUT^_M3&)n&91RAlkO zfc$c9%;I%;Bdb(w8ue8Q9vFkX|Irfq z{AkMB=saVjLf$xk*FIYYT17IZg%~P{jPihMdTss`?Jr&<1BbkgxD)6y|HFrB;zQVr z29}SXS!JvhCnY4zhH9F}g3E`O9wcRra5s-VNOhM8I@g}^oMNa5o~i?t!X5-6W{Jq& zBc2NN(UkG8*K;SyPKN!uID8(&R$A~l>?~)Z>-o+Iu ziDfD$aRGPGiwnUJ9p|SMEJuW2Paz*nHFfR}qWl3f=@usgddnZMmT6bQ+LL!tDbmt8 z{q`yc3;gxd7>vtaRfIZQWj*^K0!D>!SFK*E9RcjFcKfg6giuE!hAZ{0LtIl^NiIFq z4F5@~e_9*ppI?Yv0mX8U_L$^{EP0JIepe!KirRDcNMk^Wq9{K=K@X;vqm~Wy;Jy_i^3_BJ!P=<=@q9K}1}gY5yC; z5W{f!a51yyAZ@x+*oa|RPZVgy6O2i=#<}V6S`RznVe*preI$w3-?x}Ew*6bJ+6cU* z7k@I>6jg#2(OCv4UjY{o_|*1!j8IYMA8e?0Yf_UI>c#b1Japhfu{9N^R#Q^~?Be|4 zEjVU6&+0?fm_M~e=%|^K#r*LtJ35o^=AEQ1u?*@aZ@;ibweldzhfPzKzAKu3p&?Hx z#SU$Pxce7KrM{WCoA8wcHe@XVu>CFKnlIvJSj7B35K|kbb7|MuGwnyPwNuiSW2FZU zbG6xH4>e=A$rel*od?uwQITN{v3SrU3qjPQpX!_u2YTbS-_VVkccW}cM5orn7e{V2 zRx29=#mfg8z0=uHBO!4bSbvR;d1`D7))*e(g6>@KIaVTY5P^de*`ZL?!w+@yzn0us zAnt-D=u{}c))n|@HX=JQ#K(g_Yz}9)LP0T#nAEN*ozEN4H*YHqYbHY3%^L02ldPSd9*U!KTSYWF|?cwXQuM8B^7FbaX7sO00i+JKF&RAu3`iPqx&#@W|NE5PxC;Ea4vru# z_LKTPX^{#<^MccfAabLCeR^*1bvey*#hK^ibb@F}$UvAN{)^SZB&&JAqsjOo4mh`T zuI^|HW1h!-BzV3MW417{s{7~}WD3NQ@}D!Az^Y|O11wN5Nnc=LjYC+NeqiL3I2C#; z+#N|%mKHC(A}l_R#R6qglRXkSN$(u97+C5X4I^eBCPfSe5~A*<2r>=@HUv2rPPB&j zE>XgonKHo(5?m-44%$$Eo!RWci+>T5FC!*PFkR5taOmNZ9DIFs{=wg=kc0dax(l@8 z8-p(o{%-m5&jw$fsl5De122Cu2KykFNe{OrLCBWRgd$Q4uRt*K*qMkR{3@h-{E|rb ze2vuZc-IrvVS?JPmIh`>9f7&2uE!G7l$^GLb84?nP3?C3C!7c@*Up*q6~oBKviw&b zs)7TAvyE3!40Mv*LbKZ+jN@c?9-HGM&~wl*JX7 zSv-*P%nCwltzc9s7#*=-En|=GkcNv&ONcylng3rit`XMT?@y4K>&VP>GeR58(PHgp zvd&$ytp|0fm^P4eo;ck*`qL&|27e=~F5l<7OY8Co8bPw^fZm(K9fK8P*ze^$GN>3X z#irB7hq7j66wME$@c=QQrVQP?zJZrtYokvQ}Y z5bW&)i?aI7}UT0CF4ZM85_l-T~v%D=-E zmiGkez(@L5_SVWGgs^K_grAKg&L7M%qGMz!aA<=#j5ul%YDLf23(|NX>w`iD!FAEc zHW)a7rrt4|aGYnceBbGvcq6u(D%jwMy^aJ}INI3S#_x_l;XgmX$OSFMYc4b=ci5ct z>dPQ2PfSSc>474{JOK@q0DI03bLJ%@D4+h%iFW%}TMcXq7WA1EhNhY1Em_m8NoD;6 z%;P9;kt&h=he(UFwr6(SCKo4GK4aUc@D7@ZuiP0(`oO}gi@Pv?%+~j`A9pbr6VNV~ zCoW9+NV^0Zt|hM^8X^h5(4*VO$$mMa3^^k}ZIZZ3``JE@D-+@+g`2$4(}kt0LNM5` zvZ$ZKR`mKP1>v^!wY zE^GKb@1AV!q91H0_Z?Pv^AQFsga(( zmXF)MwOx3^z6RSnblN1bhNz^4#`EP9Z1kmw%ID;QWZ0B%=cJD`RiA;hJ&~0!whESK zz=(ua?eMiE^hMO{YmS}mB(z$Df=JDJTuo?YFSA7meS$LytrjVvRSX6(AQDj|w|a}!#%szC@!Xl1pL&%!3ue9N31*}!mR`q&d5uwI!M6gTcQ!2fAqZ;d85 zP_2Kj?=Ile>k5ptui3KJF!q&~Py1z_ULbawygStw2R{&#gL0;K9lV5<&Pk+ ze|Wg=8;~&nIH{-?+mKR>NM5iy=LLyvo8iUwJ_<2X%u3`@kXhA@RaybWHcV-82eu8y zvZrEcw-KUmB94ytts~n$EYmb6#t=L#atk)I42+meW$bK$(CO^y({a{P1Q}ABt>zD> zW?LCkhd)^bv| z(uj5lo%E~0qZ2zNUF(R?c8+ghs8&TYZ9k%euswu3V{7Zcv5WoRj_|G0)@T$@YP^a< zkFu-OGa|avtJFcg9yPs}Gi_LoD(sm}4TA6+U_{1Xp^BI6c8SOlS=gGF3)PBnp_oy~;3iL@C`BSaEGntFS7nszbGo{zF9`udNf9s<<5}W+Q!=%%&4(M_ zvXIj7UZxZa7#dPZe4`2&0*>Y-)v6RFz?X?95*bk4RPXNZ>)rC7qIdt4{i?kx^y^)+ z3FS6cl7p@AG+M-7MZqLN<6cRT3#p@B&C&F;xLxut0fk zl>kMP`x-oB3mQKDu#&Fwp%>?$t>Wo|UotllW-XzHx!8B)7{}BRqJ$N1e)h*jDMkwl ziK{~oZb<47l4M)iLr)GNtb=)Td>Lz}+Tl6*OBV7q|4yykIa75u11J=w9_m95t&My&!3K<2=%bd#5`p@|Jy>|(nkkvq-YW_6_(WoxaGepk9|n@W zgQOe1MVDx@Bq#^I!$jUo*hWp$4iB=v5D3G1V-bq=3_0r&W#}S$z^(h{UQ1B_JXm$8 z;5|kgwe!-cXn2mw04mnS(j@J%X_^%)Xc(V@jN%7l;reTYdVvWtTDLBOzpz9UBK%nv z;!veeb8=uVM4$dkY~JmKzy!QlbqQce94t(e$SlvHv!by8(8(8403;RGQ7kTGe?4#- zt1qBmRtuz>atuNW5{lE6jHf!-L1NsBI{PG7v_F)@O_{i2t>Se=dz%j>(zY}*fOR7q z4tvK?kCYLN5;DO7U)x4)+s=v&1WYxr2@D_HMqMA=IY3F_m~^kOEe%|GJEo^9(uJJ_ zjjosupkpV2r8nY1+B7%|H0&g(*-iqTo|$4t2F7Xp>a0#Dzy|uvP67;`1h@(Z7^%9C zjs{kRodkfJPt0Hin>Mpx`zuKe0~)re>#z+>BJTGBpbP~tw!KRlbX?}%<1*ylqghDh z8hpkeGmTOB0n?We+z;X8mH=Pm)f+6T16tgW>w95P^js(;GKLF`OBhhxu4!2UqUeob zC;Wi9S$_=3O#A4aVb|`C-F5_r$u0q1jN>5Iib*NYJJRo12)I;)~3ZHnmIn*MNh?0p2sI_s25F2 zeqF*k{kc2F^@j6Sg+$ydHp@ea+@aFFM*3USU8&ECf9aA%b)u>wpcY^5n`D@p!DHvTt_(pUlT9fJp z8W%5CGMSL+dKLTynD+-TKPPyK2QC>Gj=hMsmN4Dp(`oxnK>RAYv?NdN7*9KNZ%JO$ zn<2e193W2>i8C+{^Amw)wRDL=n|fAyJ$z4ss3m4yh^TysY*FboUX$01CQe?#c>p2` zzqhwTrD>GVjV zY4wrq227w}`5r&|0Y)uBTnpRTgbwSV0OXEBM`4$i$6fnIx!-3WDIuZ^{kRLU3P6s; zTQ4KNKM*HM&yZ~EbqFnG-$rPzbu6~3;2?XEAa`n-%%~j|dAgt%!G2%lyfpj0z;QaB zR>Gpm+dd5KRnI@5)$+PGiNGdDj?*urW5?V3lZJxn2nOSCuN!LK^F!FU1>fMy5(aQ< zJFmPSj2We%F18yotC8ExKtj>xuY0ZzlF-^C+f(MI6(kN@!9P8Q25eVmY!DGdwtqwf zsTjq!53?lk=2$6-Dt7TkQH0o1EHOj|seXlS>bR^*rK(c9gx$jEpxO3eBm9=BNy|Hk z;_VMM8cLW4JnqMb%S@PsK+N3#rIA38AMv;vzwwB6DSR8;gb?MoipGhAVTzDBga`=9 z0q#wMO=E$2RbV%FTWF@FERuZ_sYmTI&QD~fI&gr|GLbTip1oPaG~_USi}mYSB`^(O z1#bA>V{XDz!3QRE=4Q!3H$r=fxVstNEeZ$I;?g8Q)auR26QS;75<7|1>sRyO@H7px z%jXXjR}g(tL;-oemFmuEC^-3;0mA%2`3(&#KVeOl>Ky|;hEc9RDF4KudiT7-!(+uN zK8)}uP2mK;YD(%|(F?c~?)(Bg-V;S3C`+o~uRsKwRGc84Kb6?Wnp$Y9MgE&J5_)`l z#krgEM|E4oSml3QdHKzOm#1qr0&`fRJ!BN3c^5igSJRG|z>w!m-yHIF%nXsdhk#Wo zDR4MO--yDlj2;flEMPcnRv6%7w9coPpDpsZCca!jXQ8^U zMo=BpmyZKfXNl5CKy?b0sE$$JP~FDq;xpS99I5ds7^<64Z4L&d_A;){3IWw?j_Q%2 zy899g)oD~T-BA59g!%ucnLJRlAeX}E$T;wOAhu&+x8-FLEk1+<1xsM0ySp;2PzfOY z&ym4*+AC3aoD=Tty3BjNB;DJ3wl^=^8)m2UidOVH*v85 z+99!kP&YfiOaXaD-YDpU54n+TE!bgj&p{OUdLk=0uyuR1(HO*FbG{@xJ1&hN+M1bs zze8A>o}V73xi;mmZdl|wLTs*Unrl*ILD*ml^UQUP8eNaFZ6H;WEsVS5TfH{kZ4a{y&6#%!at^Q)%Bz>8kcTO-(F zZDu~9Cz{~>wf4SPIUxF_8w9N|y-@+sIb?G`qtrMp-PPphixi$l2z}_9X$ip68*Gxq zSuaCd{;n2}%vFII#{y>A!Vt^I+%P*N17l72V1`LHHdhU^1pl(=E+{0Xc_alYcdzK3V*Ve7tu(h{sXgAXgZ`isg@i?-m5Y%lA7Vd?RO@UM|*F~^5E-sp6uR_v(x3*D$)gOAA&~=Pwd-YYPN+%*;~xkFTq`w zDAF}B43?zj`X==~_jO9N|8;-knOU`|P6urj$-*HnN()g)(cF=x*2978N*y72OZ-I% z9>|y*M<+I5c znY2z26Sh_JCF`X)iKEm{WuYY~fiKlb$eNiEx;BOKe5|7~>@_dZ9y zCn3YL?Dv?|zK_&O%Jdy#o7tW^;RQBdPBm>bEjDfYwEmMns_QXE2Vd zs%IKV6UjE&t$$D1Re5!iDV}>EMu@neSn=GWD0=0+??Sdcb&I`-Lf8#S zeA(Y-8oH^}HU_h`5?Q3-hK!S*#nHTRK(-}u&H`Pk8LAQ41a?$+lOsX(pyj~O7-`65 zOxz9`8tnvuq)73?@+c465CaQ|C&lxF zrlmRH!x>k5VK8t?BT{uZW-w}CxUj=xP;_O`kTR;o2jTD-KN~tVn?z)aZhzL+%dE{f z4@W}VdQ*Tm^z~4!??`B#a3r(}l*@B&dXk{uG*zZY3;LI*StMsLX=r)sNN9)+3J1T6Nh83mD-iXh7T(ZK{pps6a~9j(L-NIo%Z0u6u$v(Job< zJm-S`4fdF5S1XiqM`tgPd%~x1}3;iq_H#=V`pN8Pp>5L5OGW6+( zF;IaYJxDh2XP^B!Y$$hKZadnO?U9DLIcv7BL25&v_}Pa#LJXRL+dH}hLzcz$6@ZiI zC<{*A;TI%UGcZb2fg~C{7z?Zub2qg~uG7XxbA%F~$u5S7Xj=5>LNb*&i5Jh`5KWne z@L`()(esX|sli`&OZ`rvv$VDXQIK~4UGLINn~pC5EaAbr=ms$gf;#m-M@~qeZ|Hxn zM;L&m<53ss&m$<;Lup(6ycnxeTeQ@n0BKHiqD?B{AQx!`aC1#pVlmQ*1odnbJg^S& zRD;A7shX|Ql=CjUykevii;?!Xw@3V!y2qx7k;aZ?rJkl1i;YbRn^2I0qN}c9d_k zjt@n19Rk5$g!dCyVjH)n0g$NgzCda6niU%;FgIJAYF8#Ho7PH1v@NW6(nBVeDC#6c zQOjx4Mo9zHY*s8zLUL+4;$1-mO)Y5MglZk~pt(U{wUdgt>;)W0>X!!`_vW}&gdF3- z4N`5Z2svh(U=V;oWkQa@*Ito_99xfjeWWbCNHBb#X2Dlb-tc73vg9G^L zhV-owaM{MuVjG2GRp@+lrxP_29_~bYsjrK!#|vr1txG68Wst^eii*FpnECEtLZRcv!d`B2!I+yz5%953ptOAI_fETJIw^Icl7EICxz=h3`lct zI*QLd>E(*KH;u;U-jo%ad#w`D0NS72po>_0;9v&O4|H%WGm+{mpTSCHuZc7a`%p0P zK4^OhNSWWndxdTrccVN+(DdC>cpCFoB5#k0y_aZm4X)AFZa!qak8z~ zxrkoi#LqMFp^4K^rXH{bXakh|9 z`^Kqy}vIgP8r0 zyf;+{2K+UE;YP8rZULt_cBKLwl!F2|cpB2W`&$h-P^NAKa4e(`_|^h&M2i}O(SiuR zjI{s`%R|aAf27(r`t@3})l-)wYp*&0hfa$m2smcn0fGf^c*SAx{knd#(u8I)wR zLN=419>nXzIT>ieK|(PCY7O~?y2=%T8$$FBb_FUZS#n@W2rWN*di!{Ih}lHZ8hRoV zId&DmuI_D=Rnn9YV4mc&y+{c=*s|t*Hr+%!V6`etHzClAgMi^o{Eu#8N!fwo5A zOnxw*i>l`fX>Xyq2n6Mn!c*JgNsPurmRoE{%-oA;OkfE5>Jb2G%Nw*Bsu!D>j3IqB|LI_9*fS!$d5zq|P z=!R%VQsLzyeYW`oa5hB#pmYwuhSrAx&W`Csf|N7PmXX0sDKSLJ41$<`cBf9E2;mc zZ8? zPc>WU9fkQlV9fN1O)%9VKdseV5i~9`%isVB!{;;DZQ>&+dYjeE{X!v_u@aEfkQy7dZ5YWbSUQuP&9%zFt@)#yY!g~jKxR0sKP*RbG1Sb!&=g$Lp?z+iXbS#- zfdU9cp};;BFa=rDQ4S`~M|Qe%g#_JzrhYGE7k$d~0VmS`5BvgHLVnNU}ETY{ZsdtulDo zaR2}_W}#Wk7#0ZT7P;AhfMVR@R1U(iIi9o%kWff~AVzkYKfE{dn_;M^SD0DlCoSRN zQw%l~Bb8L+BMnHIH7nB`F;cUHVoxRRBFW=oq)j3Rz*V2*oBAf%5Gjn)R#IBu=cBSE z!&rP&Y;FgaHCkI_I^|NAVN68P`&-!zKT=8YuxB>Pe^d88s&VF%+y#>I-P`g#eJ{WJ zNcr+he7f)R|Ky+koLSbBZRNCxYfivKJ@JAkJ~PHDq8hWX`iSZeLPci>)fX_LP$9*% z!3HsxkTgm!30vfEPzJ@75W+dhb5QYf`q)$a8Wr6J?epV{*9MS%ZpSsoNOGyD%Xe$rqAcrfxE>LdOB8T>(UT=sfxyw46O2 z5|L_;Z#jdoIWtydwQy)JfjF{DEEONgI2&4u1ol5LoTuT?-hs02AXiyP9c77Tx;^4Q zGGbZmir+NkByZnjHk2+4wCqD%@H$jD<8}PbZhobL6QlO)`;0D8SoCzq$Qv*?VJ`Jx zFFLakscJpmEOmpZSVbp{U-Sb$wP{6^Wi^Xx0x^l*>eZ_M;?0UEGJ%^m3qLK$OvFciByE@k~G7u2UiDsZBYLf zaNDao#gM!Zb+PdOxP$w3*zy6Azvt`fuu+6pAe}w)p2^AC+cqc1Vn%?+$31v$FqeljWZ_A9>o)$`x|%a6YQJ1_To@R+^g{h#{a-&Fd9cflTg{G0#%tD#RgOp2

>Bhjn{c0ogeoHU@k56c3x$ze>sT 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