commit f0681b77bdc2629f0e80b350bd35ef81b45b245c Author: wenyifan Date: Fri Jun 2 15:41:51 2023 +0800 init commit 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 0000000..a11ad16 Binary files /dev/null and b/webapp/favicon.ico differ diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 0000000..29732f2 --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + 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 0000000..7706edd Binary files /dev/null and b/webapp/parsecd differ diff --git a/webapp/static/lib/matoya.js b/webapp/static/lib/matoya.js new file mode 100644 index 0000000..bb4e3c0 --- /dev/null +++ b/webapp/static/lib/matoya.js @@ -0,0 +1,1512 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT License was not distributed with this file, +// You can obtain one at https://spdx.org/licenses/MIT.html. + +// Global state + +const MTY = { + module: null, + alloc: 0, + free: 0, + audio: null, + cbuf: null, + kbMap: null, + keysRev: {}, + wakeLock: null, + reqs: {}, + reqIndex: 0, + endFunc: () => {}, + cursorId: 0, + cursorCache: {}, + cursorClass: '', + defaultCursor: false, + synthesizeEsc: true, + relative: false, + gps: [false, false, false, false], + action: null, + lastX: 0, + lastY: 0, + keys: {}, + clip: null, + + // GL + gl: null, + glver: 'webgl', + glIndex: 0, + glObj: {}, + + // WASI + arg0: '', + fds: {}, + fdIndex: 64, + preopen: false, +}; + + +// Private helpers + +function mty_mem() { + return MTY.module.instance.exports.memory.buffer; +} + +function mty_mem_view() { + return new DataView(mty_mem()); +} + +function mty_buf_to_js_str(buf) { + return (new TextDecoder()).decode(buf); +} + +function mty_b64_to_buf(str) { + return Uint8Array.from(atob(str), c => c.charCodeAt(0)) +} + +function mty_buf_to_b64(buf) { + let str = ''; + for (let x = 0; x < buf.length; x++) + str += String.fromCharCode(buf[x]); + + return btoa(str); +} + +function mty_copy_str(ptr, buf) { + const heap = new Uint8Array(mty_mem(), ptr); + heap.set(buf); + heap[buf.length] = 0; +} + +function mty_strlen(buf) { + let len = 0; + for (; len < 0x7FFFFFFF && buf[len] != 0; len++); + + return len; +} + + +// WASM utility + +function MTY_CFunc(ptr) { + return MTY.module.instance.exports.__indirect_function_table.get(ptr); +} + +function MTY_Alloc(size, el) { + return MTY_CFunc(MTY.alloc)(size, el ? el : 1); +} + +function MTY_Free(ptr) { + MTY_CFunc(MTY.free)(ptr); +} + +function MTY_SetUint32(ptr, value) { + mty_mem_view().setUint32(ptr, value, true); +} + +function MTY_SetUint16(ptr, value) { + mty_mem_view().setUint16(ptr, value, true); +} + +function MTY_SetInt32(ptr, value) { + mty_mem_view().setInt32(ptr, value, true); +} + +function MTY_SetInt8(ptr, value) { + mty_mem_view().setInt8(ptr, value); +} + +function MTY_SetFloat(ptr, value) { + mty_mem_view().setFloat32(ptr, value, true); +} + +function MTY_SetUint64(ptr, value) { + mty_mem_view().setBigUint64(ptr, BigInt(value), true); +} + +function MTY_GetUint32(ptr) { + return mty_mem_view().getUint32(ptr, true); +} + +function MTY_Memcpy(cptr, abuffer) { + const heap = new Uint8Array(mty_mem(), cptr, abuffer.length); + heap.set(abuffer); +} + +function MTY_StrToJS(ptr) { + const len = mty_strlen(new Uint8Array(mty_mem(), ptr)); + const slice = new Uint8Array(mty_mem(), ptr, len) + + return (new TextDecoder()).decode(slice); +} + +function MTY_StrToC(js_str, ptr, size) { + if (size == 0) + return; + + const buf = (new TextEncoder()).encode(js_str); + const copy_size = buf.length < size ? buf.length : size - 1; + mty_copy_str(ptr, new Uint8Array(buf, 0, copy_size)); + + return ptr; +} + +function MTY_StrToCD(js_str) { + const buf = (new TextEncoder()).encode(js_str); + const ptr = MTY_Alloc(buf.length); + mty_copy_str(ptr, buf); + + return ptr; +} + + +// stubs + +const MTY_UNISTD_API = { + flock: function (fd, flags) { + return 0; + }, +}; + + +// GL + +function mty_gl_new(obj) { + MTY.glObj[MTY.glIndex] = obj; + + return MTY.glIndex++; +} + +function mty_gl_del(index) { + let obj = MTY.glObj[index]; + + MTY.glObj[index] = undefined; + delete MTY.glObj[index]; + + return obj; +} + +function mty_gl_obj(index) { + return MTY.glObj[index]; +} + +const MTY_GL_API = { + glGenFramebuffers: function (n, ids) { + for (let x = 0; x < n; x++) + MTY_SetUint32(ids + x * 4, mty_gl_new(MTY.gl.createFramebuffer())); + }, + glDeleteFramebuffers: function (n, ids) { + for (let x = 0; x < n; x++) + MTY.gl.deleteFramebuffer(mty_gl_del(MTY_GetUint32(ids + x * 4))); + }, + glBindFramebuffer: function (target, fb) { + MTY.gl.bindFramebuffer(target, fb ? mty_gl_obj(fb) : null); + }, + glBlitFramebuffer: function (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) { + MTY.gl.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + }, + glFramebufferTexture2D: function (target, attachment, textarget, texture, level) { + MTY.gl.framebufferTexture2D(target, attachment, textarget, mty_gl_obj(texture), level); + }, + glEnable: function (cap) { + MTY.gl.enable(cap); + }, + glIsEnabled: function (cap) { + return MTY.gl.isEnabled(cap); + }, + glDisable: function (cap) { + MTY.gl.disable(cap); + }, + glViewport: function (x, y, width, height) { + MTY.gl.viewport(x, y, width, height); + }, + glGetIntegerv: function (name, data) { + const p = MTY.gl.getParameter(name); + + switch (name) { + // object + case MTY.gl.READ_FRAMEBUFFER_BINDING: + case MTY.gl.DRAW_FRAMEBUFFER_BINDING: + case MTY.gl.ARRAY_BUFFER_BINDING: + case MTY.gl.TEXTURE_BINDING_2D: + case MTY.gl.CURRENT_PROGRAM: + MTY_SetUint32(data, mty_gl_new(p)); + break; + + // int32[4] + case MTY.gl.VIEWPORT: + case MTY.gl.SCISSOR_BOX: + for (let x = 0; x < 4; x++) + MTY_SetUint32(data + x * 4, p[x]); + break; + + // int + case MTY.gl.ACTIVE_TEXTURE: + case MTY.gl.BLEND_SRC_RGB: + case MTY.gl.BLEND_DST_RGB: + case MTY.gl.BLEND_SRC_ALPHA: + case MTY.gl.BLEND_DST_ALPHA: + case MTY.gl.BLEND_EQUATION_RGB: + case MTY.gl.BLEND_EQUATION_ALPHA: + MTY_SetUint32(data, p); + break; + } + + MTY_SetUint32(data, p); + }, + glGetFloatv: function (name, data) { + switch (name) { + case MTY.gl.COLOR_CLEAR_VALUE: + const p = MTY.gl.getParameter(name); + + for (let x = 0; x < 4; x++) + MTY_SetFloat(data + x * 4, p[x]); + break; + } + }, + glBindTexture: function (target, texture) { + MTY.gl.bindTexture(target, texture ? mty_gl_obj(texture) : null); + }, + glDeleteTextures: function (n, ids) { + for (let x = 0; x < n; x++) + MTY.gl.deleteTexture(mty_gl_del(MTY_GetUint32(ids + x * 4))); + }, + glTexParameteri: function (target, pname, param) { + MTY.gl.texParameteri(target, pname, param); + }, + glGenTextures: function (n, ids) { + for (let x = 0; x < n; x++) + MTY_SetUint32(ids + x * 4, mty_gl_new(MTY.gl.createTexture())); + }, + glTexImage2D: function (target, level, internalformat, width, height, border, format, type, data) { + MTY.gl.texImage2D(target, level, internalformat, width, height, border, format, type, + new Uint8Array(mty_mem(), data)); + }, + glTexSubImage2D: function (target, level, xoffset, yoffset, width, height, format, type, pixels) { + MTY.gl.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, + new Uint8Array(mty_mem(), pixels)); + }, + glDrawElements: function (mode, count, type, indices) { + MTY.gl.drawElements(mode, count, type, indices); + }, + glGetAttribLocation: function (program, c_name) { + return MTY.gl.getAttribLocation(mty_gl_obj(program), MTY_StrToJS(c_name)); + }, + glShaderSource: function (shader, count, c_strings, c_len) { + let source = ''; + for (let x = 0; x < count; x++) + source += MTY_StrToJS(MTY_GetUint32(c_strings + x * 4)); + + MTY.gl.shaderSource(mty_gl_obj(shader), source); + }, + glBindBuffer: function (target, buffer) { + MTY.gl.bindBuffer(target, buffer ? mty_gl_obj(buffer) : null); + }, + glVertexAttribPointer: function (index, size, type, normalized, stride, pointer) { + MTY.gl.vertexAttribPointer(index, size, type, normalized, stride, pointer); + }, + glCreateProgram: function () { + return mty_gl_new(MTY.gl.createProgram()); + }, + glUniform1i: function (loc, v0) { + MTY.gl.uniform1i(mty_gl_obj(loc), v0); + }, + glUniform1f: function (loc, v0) { + MTY.gl.uniform1f(mty_gl_obj(loc), v0); + }, + glUniform4i: function (loc, v0, v1, v2, v3) { + MTY.gl.uniform4i(mty_gl_obj(loc), v0, v1, v2, v3); + }, + glUniform4f: function (loc, v0, v1, v2, v3) { + MTY.gl.uniform4f(mty_gl_obj(loc), v0, v1, v2, v3); + }, + glActiveTexture: function (texture) { + MTY.gl.activeTexture(texture); + }, + glDeleteBuffers: function (n, ids) { + for (let x = 0; x < n; x++) + MTY.gl.deleteBuffer(mty_gl_del(MTY_GetUint32(ids + x * 4))); + }, + glEnableVertexAttribArray: function (index) { + MTY.gl.enableVertexAttribArray(index); + }, + glBufferData: function (target, size, data, usage) { + MTY.gl.bufferData(target, new Uint8Array(mty_mem(), data, size), usage); + }, + glDeleteShader: function (shader) { + MTY.gl.deleteShader(mty_gl_del(shader)); + }, + glGenBuffers: function (n, ids) { + for (let x = 0; x < n; x++) + MTY_SetUint32(ids + x * 4, mty_gl_new(MTY.gl.createBuffer())); + }, + glCompileShader: function (shader) { + MTY.gl.compileShader(mty_gl_obj(shader)); + }, + glLinkProgram: function (program) { + MTY.gl.linkProgram(mty_gl_obj(program)); + }, + glGetUniformLocation: function (program, name) { + return mty_gl_new(MTY.gl.getUniformLocation(mty_gl_obj(program), MTY_StrToJS(name))); + }, + glCreateShader: function (type) { + return mty_gl_new(MTY.gl.createShader(type)); + }, + glAttachShader: function (program, shader) { + MTY.gl.attachShader(mty_gl_obj(program), mty_gl_obj(shader)); + }, + glUseProgram: function (program) { + MTY.gl.useProgram(program ? mty_gl_obj(program) : null); + }, + glGetShaderiv: function (shader, pname, params) { + if (pname == 0x8B81) { + let ok = MTY.gl.getShaderParameter(mty_gl_obj(shader), MTY.gl.COMPILE_STATUS); + MTY_SetUint32(params, ok); + + if (!ok) + console.warn(MTY.gl.getShaderInfoLog(mty_gl_obj(shader))); + + } else { + MTY_SetUint32(params, 0); + } + }, + glDetachShader: function (program, shader) { + MTY.gl.detachShader(mty_gl_obj(program), mty_gl_obj(shader)); + }, + glDeleteProgram: function (program) { + MTY.gl.deleteProgram(mty_gl_del(program)); + }, + glClear: function (mask) { + MTY.gl.clear(mask); + }, + glClearColor: function (red, green, blue, alpha) { + MTY.gl.clearColor(red, green, blue, alpha); + }, + glGetError: function () { + return MTY.gl.getError(); + }, + glGetShaderInfoLog: function () { + // FIXME Logged automatically as part of glGetShaderiv + }, + glFinish: function () { + MTY.gl.finish(); + }, + glScissor: function (x, y, width, height) { + MTY.gl.scissor(x, y, width, height); + }, + glBlendFunc: function (sfactor, dfactor) { + MTY.gl.blendFunc(sfactor, dfactor); + }, + glBlendEquation: function (mode) { + MTY.gl.blendEquation(mode); + }, + glUniformMatrix4fv: function (loc, count, transpose, value) { + MTY.gl.uniformMatrix4fv(mty_gl_obj(loc), transpose, new Float32Array(mty_mem(), value, 4 * 4 * count)); + }, + glBlendEquationSeparate: function (modeRGB, modeAlpha) { + MTY.gl.blendEquationSeparate(modeRGB, modeAlpha); + }, + glBlendFuncSeparate: function (srcRGB, dstRGB, srcAlpha, dstAlpha) { + MTY.gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + }, + glGetProgramiv: function (program, pname, params) { + MTY_SetUint32(params, MTY.gl.getProgramParameter(mty_gl_obj(program), pname)); + }, + glPixelStorei: function (pname, param) { + // GL_UNPACK_ROW_LENGTH is not compatible with WebGL 1 + if (MTY.glver == 'webgl' && pname == 0x0CF2) + return; + + MTY.gl.pixelStorei(pname, param); + }, + web_gl_flush: function () { + MTY.gl.flush(); + }, +}; + + +// Audio + +function mty_audio_queued_ms() { + let queued_ms = Math.round((MTY.audio.next_time - MTY.audio.ctx.currentTime) * 1000.0); + let buffered_ms = Math.round((MTY.audio.offset / 4) / MTY.audio.frames_per_ms); + + return (queued_ms < 0 ? 0 : queued_ms) + buffered_ms; +} + +const MTY_AUDIO_API = { + MTY_AudioCreate: function (sampleRate, minBuffer, maxBuffer, channels, deviceID, fallback) { + MTY.audio = {}; + MTY.audio.flushing = false; + MTY.audio.playing = false; + MTY.audio.sample_rate = sampleRate; + MTY.audio.channels = channels; + + MTY.audio.frames_per_ms = Math.round(sampleRate / 1000.0); + MTY.audio.min_buffer = minBuffer * MTY.audio.frames_per_ms; + MTY.audio.max_buffer = maxBuffer * MTY.audio.frames_per_ms; + + MTY.audio.offset = 0; + MTY.audio.buf = MTY_Alloc(sampleRate * 2 * MTY.audio.channels); + + return 0xCDD; + }, + MTY_AudioDestroy: function (audio) { + MTY_Free(MTY.audio.buf); + MTY_SetUint32(audio, 0); + MTY.audio = null; + }, + MTY_AudioQueue: function (ctx, frames, count) { + // Initialize on first queue otherwise the browser may complain about user interaction + if (!MTY.audio.ctx) + MTY.audio.ctx = new AudioContext(); + + let queued_frames = MTY.audio.frames_per_ms * mty_audio_queued_ms(); + + // Stop playing and flush if we've exceeded the maximum buffer + if (queued_frames > MTY.audio.max_buffer) { + MTY.audio.playing = false; + MTY.audio.flushing = true; + } + + // Stop flushing when the queue reaches zero + if (queued_frames == 0) { + MTY.audio.flushing = false; + MTY.audio.playing = false; + } + + // Convert PCM int16_t to float + if (!MTY.audio.flushing) { + let size = count * 2 * MTY.audio.channels; + MTY_Memcpy(MTY.audio.buf + MTY.audio.offset, new Uint8Array(mty_mem(), frames, size)); + MTY.audio.offset += size; + } + + // Begin playing again if the buffer has accumulated past the min + if (!MTY.audio.playing && !MTY.audio.flushing && + MTY.audio.offset / (2 * MTY.audio.channels) > MTY.audio.min_buffer) + { + MTY.audio.next_time = MTY.audio.ctx.currentTime; + MTY.audio.playing = true; + } + + // Queue the audio if playing + if (MTY.audio.playing) { + const src = new Int16Array(mty_mem(), MTY.audio.buf); + const bcount = MTY.audio.offset / (2 * MTY.audio.channels); + + const buf = MTY.audio.ctx.createBuffer(MTY.audio.channels, bcount, MTY.audio.sample_rate); + + const chans = []; + for (let x = 0; x < MTY.audio.channels; x++) + chans[x] = buf.getChannelData(x); + + let offset = 0; + for (let x = 0; x < bcount * MTY.audio.channels; x += MTY.audio.channels) { + for (y = 0; y < MTY.audio.channels; y++) { + chans[y][offset] = src[x + y] / 32768; + offset++; + } + } + + const source = MTY.audio.ctx.createBufferSource(); + source.buffer = buf; + source.connect(MTY.audio.ctx.destination); + source.start(MTY.audio.next_time); + + MTY.audio.next_time += buf.duration; + MTY.audio.offset = 0; + } + }, + MTY_AudioReset: function (ctx) { + MTY.audio.playing = false; + MTY.audio.flushing = false; + MTY.audio.offset = 0; + }, + MTY_AudioGetQueued: function (ctx) { + if (MTY.audio.ctx) + return mty_audio_queued_ms(); + + return 0; + }, +}; + + +// Net + +const MTY_ASYNC_OK = 0; +const MTY_ASYNC_DONE = 1; +const MTY_ASYNC_CONTINUE = 2; +const MTY_ASYNC_ERROR = 3; + +function mty_decompress_image(input, func) { + const img = new Image(); + img.src = URL.createObjectURL(new Blob([input])); + + img.decode().then(() => { + const width = img.naturalWidth; + const height = img.naturalHeight; + + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + const imgData = ctx.getImageData(0, 0, width, height); + + func(imgData.data, width, height); + }); +} + +const MTY_NET_API = { + MTY_HttpAsyncCreate: function (num_threads) { + }, + MTY_HttpAsyncDestroy: function () { + }, + MTY_HttpSetProxy: function (proxy) { + }, + MTY_HttpParseUrl: function (url_c, host_c_out, host_size, path_c_out, path_size) { + const url = MTY_StrToJS(url_c); + + try { + const url_obj = new URL(url); + const path = url_obj.pathname + url_obj.search; + + MTY_StrToC(url_obj.host, host_c_out, host_size); + MTY_StrToC(path, path_c_out, path_size); + + return true; + + } catch (err) { + console.error(err); + } + + return false; + }, + MTY_HttpEncodeUrl: function(src, dst, dst_len) { + // No-op, automatically converted in fetch + MTY_StrToC(MTY_StrToJS(src), dst, dst_len); + }, + MTY_HttpAsyncRequest: function(index, chost, port, secure, cmethod, + cpath, cheaders, cbody, bodySize, timeout, image) + { + const req = ++MTY.reqIndex; + MTY_SetUint32(index, req); + + MTY.reqs[req] = { + async: MTY_ASYNC_CONTINUE, + image: image, + }; + + const jport = port != 0 ? ':' + port.toString() : ''; + const scheme = secure ? 'https' : 'http'; + const method = MTY_StrToJS(cmethod); + const host = MTY_StrToJS(chost); + const path = MTY_StrToJS(cpath); + const headers_str = MTY_StrToJS(cheaders); + const body = cbody ? MTY_StrToJS(cbody) : undefined; + const url = scheme + '://' + host + jport + path; + + const headers = {}; + const headers_nl = headers_str.split('\n'); + for (let x = 0; x < headers_nl.length; x++) { + const pair = headers_nl[x]; + const pair_split = pair.split(':'); + + if (pair_split[0] && pair_split[1]) + headers[pair_split[0]] = pair_split[1]; + } + + fetch(url, { + method: method, + headers: headers, + body: body + + }).then((response) => { + const data = MTY.reqs[req]; + data.status = response.status; + + return response.arrayBuffer(); + + }).then((body) => { + const data = MTY.reqs[req]; + data.response = new Uint8Array(body); + data.async = MTY_ASYNC_OK; + + }).catch((err) => { + const data = MTY.reqs[req]; + console.error(err); + data.status = 0; + data.async = MTY_ASYNC_ERROR; + }); + }, + MTY_HttpAsyncPoll: function(index, response, responseSize, code) { + const data = MTY.reqs[index]; + + // Unknown index or request has already been polled + if (data == undefined || data.async == MTY_ASYNC_DONE) + return MTY_ASYNC_DONE; + + // Request is in progress + if (data.async == MTY_ASYNC_CONTINUE) + return MTY_ASYNC_CONTINUE; + + // Request is has completed asynchronously, check if there is a response + if (data.response != undefined) { + + // Optionally decompress an image on a successful response + const res_ok = data.status >= 200 && data.status < 300; + const req_ok = data.async == MTY_ASYNC_OK; + + if (data.image && req_ok && res_ok) { + data.async = MTY_ASYNC_CONTINUE; + data.image = false; + + mty_decompress_image(data.response, (image, width, height) => { + data.width = width; + data.height = height; + data.response = image + data.async = MTY_ASYNC_OK; + }); + + return MTY_ASYNC_CONTINUE; + } + + // Set C status code + MTY_SetUint32(code, data.status); + + // Set C response size + if (data.width && data.height) { + MTY_SetUint32(responseSize, data.width | data.height << 16); + + } else { + MTY_SetUint32(responseSize, data.response.length); + } + + // Allocate C buffer and set return pointer + if (data.buf == undefined) { + data.buf = MTY_Alloc(data.response.length + 1); + MTY_Memcpy(data.buf, data.response); + } + + MTY_SetUint32(response, data.buf); + } + + const r = data.async; + data.async = MTY_ASYNC_DONE; + + return r; + }, + MTY_HttpAsyncClear: function (index) { + const req = MTY_GetUint32(index); + const data = MTY.reqs[req]; + + if (data == undefined) + return; + + MTY_Free(data.buf); + delete MTY.reqs[req]; + + MTY_SetUint32(index, 0); + }, +}; + + +// Image + +const MTY_IMAGE_API = { + MTY_DecompressImageAsync: function (input, size, func, opaque) { + const jinput = new Uint8Array(mty_mem(), input, size); + + mty_decompress_image(jinput, (image, width, height) => { + const cimage = MTY_Alloc(width * height * 4); + MTY_Memcpy(cimage, image); + + MTY_CFunc(func)(cimage, width, height, opaque); + }); + }, +}; + + +// Crypto + +const MTY_CRYPTO_API = { + MTY_CryptoHash: function (algo, input, inputSize, key, keySize, output, outputSize) { + }, + MTY_GetRandomBytes: function (buf, size) { + const jbuf = new Uint8Array(mty_mem(), buf, size); + crypto.getRandomValues(jbuf); + }, +}; + + +// System + +const MTY_SYSTEM_API = { + MTY_HandleProtocol: function (uri, token) { + MTY_SetAction(() => { + window.open(MTY_StrToJS(uri), '_blank'); + }); + }, +}; + + +// Web API (mostly used in app.c) + +function mty_get_mods(ev) { + let mods = 0; + + if (ev.shiftKey) mods |= 0x01; + if (ev.ctrlKey) mods |= 0x02; + if (ev.altKey) mods |= 0x04; + if (ev.metaKey) mods |= 0x08; + + if (ev.getModifierState("CapsLock")) mods |= 0x10; + if (ev.getModifierState("NumLock") ) mods |= 0x20; + + return mods; +} + +function mty_run_action() { + setTimeout(() => { + if (MTY.action) { + MTY.action(); + MTY.action = null; + } + }, 100); +} + +function MTY_SetAction(action) { + MTY.action = action; + + // In case click handler doesn't happen + mty_run_action(); +} + +function mty_scaled(num) { + return Math.round(num * window.devicePixelRatio); +} + +function mty_correct_relative() { + if (!document.pointerLockElement && MTY.relative) + MTY.gl.canvas.requestPointerLock(); +} + +function mty_poll_gamepads(app, controller) { + const gps = navigator.getGamepads(); + + for (let x = 0; x < 4; x++) { + const gp = gps[x]; + + if (gp) { + let state = 0; + + // Connected + if (!MTY.gps[x]) { + MTY.gps[x] = true; + state = 1; + } + + let lx = 0; + let ly = 0; + let rx = 0; + let ry = 0; + let lt = 0; + let rt = 0; + let buttons = 0; + + if (gp.buttons) { + if (gp.buttons[6]) lt = gp.buttons[6].value; + if (gp.buttons[7]) rt = gp.buttons[7].value; + + for (let i = 0; i < gp.buttons.length && i < 32; i++) + if (gp.buttons[i].pressed) + buttons |= 1 << i; + } + + if (gp.axes) { + if (gp.axes[0]) lx = gp.axes[0]; + if (gp.axes[1]) ly = gp.axes[1]; + if (gp.axes[2]) rx = gp.axes[2]; + if (gp.axes[3]) ry = gp.axes[3]; + } + + MTY_CFunc(controller)(app, x, state, buttons, lx, ly, rx, ry, lt, rt); + + // Disconnected + } else if (MTY.gps[x]) { + MTY_CFunc(controller)(app, x, 2, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + MTY.gps[x] = false; + } + } +} + +const MTY_WEB_API = { + web_alert: function (title, msg) { + alert(MTY_StrToJS(title) + '\n\n' + MTY_StrToJS(msg)); + }, + web_platform: function (platform, size) { + MTY_StrToC(navigator.platform, platform, size); + }, + web_set_fullscreen: function (fullscreen) { + if (fullscreen && !document.fullscreenElement) { + if (navigator.keyboard) + navigator.keyboard.lock(["Escape"]); + + document.documentElement.requestFullscreen(); + + } else if (!fullscreen && document.fullscreenElement) { + document.exitFullscreen(); + + if (navigator.keyboard) + navigator.keyboard.unlock(); + } + }, + web_get_fullscreen: function () { + return document.fullscreenElement != null; + }, + web_set_mem_funcs: function (alloc, free) { + MTY.alloc = alloc; + MTY.free = free; + + // Global buffers for scratch heap space + MTY.cbuf = MTY_Alloc(1024); + }, + web_set_key: function (reverse, code, key) { + const str = MTY_StrToJS(code); + MTY.keys[str] = key; + + if (reverse) + MTY.keysRev[key] = str; + }, + web_get_key: function (key, cbuf, len) { + const code = MTY.keysRev[key]; + + if (code != undefined) { + if (MTY.kbMap) { + const text = MTY.kbMap.get(code); + if (text) { + MTY_StrToC(text.toUpperCase(), cbuf, len); + return true; + } + } + + MTY_StrToC(code, cbuf, len); + return true; + } + + return false; + }, + web_wake_lock: async function (enable) { + try { + if (enable && !MTY.wakeLock) { + MTY.wakeLock = await navigator.wakeLock.request('screen'); + + } else if (!enable && MTY.wakeLock) { + MTY.wakeLock.release(); + MTY.wakeLock = undefined; + } + } catch (e) { + MTY.wakeLock = undefined; + } + }, + web_rumble_gamepad: function (id, low, high) { + const gps = navigator.getGamepads(); + const gp = gps[id]; + + if (gp && gp.vibrationActuator) + gp.vibrationActuator.playEffect('dual-rumble', { + startDelay: 0, + duration: 2000, + weakMagnitude: low, + strongMagnitude: high, + }); + }, + web_show_cursor: function (show) { + MTY.gl.canvas.style.cursor = show ? '': 'none'; + }, + web_get_hostname: function () { + return MTY_StrToCD(location.hostname); + }, + web_get_clipboard: function () { + MTY.clip.focus(); + MTY.clip.select(); + document.execCommand('paste'); + + return MTY_StrToCD(MTY.clip.value); + }, + web_set_clipboard: function (text_c) { + MTY.clip.value = MTY_StrToJS(text_c); + MTY.clip.focus(); + MTY.clip.select(); + document.execCommand('copy'); + }, + web_set_pointer_lock: function (enable) { + if (enable && !document.pointerLockElement) { + MTY.gl.canvas.requestPointerLock(); + + } else if (!enable && document.pointerLockElement) { + MTY.synthesizeEsc = false; + document.exitPointerLock(); + } + + MTY.relative = enable; + }, + web_get_relative: function () { + return MTY.relative; + }, + web_has_focus: function () { + return document.hasFocus(); + }, + web_is_visible: function () { + if (document.hidden != undefined) { + return !document.hidden; + + } else if (document.webkitHidden != undefined) { + return !document.webkitHidden; + } + + return true; + }, + web_get_size: function (c_width, c_height) { + MTY_SetUint32(c_width, MTY.gl.drawingBufferWidth); + MTY_SetUint32(c_height, MTY.gl.drawingBufferHeight); + }, + web_get_position: function (c_x, c_y) { + MTY_SetInt32(c_x, MTY.lastX); + MTY_SetInt32(c_y, MTY.lastY); + }, + web_get_screen_size: function (c_width, c_height) { + MTY_SetUint32(c_width, screen.width); + MTY_SetUint32(c_height, screen.height); + }, + web_set_title: function (title) { + document.title = MTY_StrToJS(title); + }, + web_use_default_cursor: function (use_default) { + if (MTY.cursorClass.length > 0) { + if (use_default) { + MTY.gl.canvas.classList.remove(MTY.cursorClass); + + } else { + MTY.gl.canvas.classList.add(MTY.cursorClass); + } + } + + MTY.defaultCursor = use_default; + }, + web_set_png_cursor: function (buffer, size, hot_x, hot_y) { + if (buffer) { + const buf = new Uint8Array(mty_mem(), buffer, size); + const b64_png = mty_buf_to_b64(buf); + + if (!MTY.cursorCache[b64_png]) { + MTY.cursorCache[b64_png] = `cursor-x-${MTY.cursorId}`; + + const style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = `.cursor-x-${MTY.cursorId++} ` + + `{cursor: url(data:image/png;base64,${b64_png}) ${hot_x} ${hot_y}, auto;}`; + document.querySelector('head').appendChild(style); + } + + if (MTY.cursorClass.length > 0) + MTY.gl.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = MTY.cursorCache[b64_png]; + + if (!MTY.defaultCursor) + MTY.gl.canvas.classList.add(MTY.cursorClass); + + } else { + if (!MTY.defaultCursor && MTY.cursorClass.length > 0) + MTY.gl.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = ''; + } + }, + web_get_pixel_ratio: function () { + return window.devicePixelRatio; + }, + web_attach_events: function (app, mouse_motion, mouse_button, mouse_wheel, keyboard, focus, drop, resize) { + MTY.gl.canvas.addEventListener('mousemove', (ev) => { + let x = mty_scaled(ev.clientX); + let y = mty_scaled(ev.clientY); + + if (MTY.relative) { + x = ev.movementX; + y = ev.movementY; + } + + MTY_CFunc(mouse_motion)(app, MTY.relative, x, y); + }); + + document.addEventListener('pointerlockchange', (ev) => { + // Left relative via the ESC key, which swallows a natural ESC keypress + if (!document.pointerLockElement && MTY.synthesizeEsc) { + MTY_CFunc(keyboard)(app, true, MTY.keys['Escape'], 0, 0); + MTY_CFunc(keyboard)(app, false, MTY.keys['Escape'], 0, 0); + } + + MTY.synthesizeEsc = true; + }); + + window.addEventListener('click', (ev) => { + // Popup blockers can interfere with window.open if not called from within the 'click' listener + mty_run_action(); + ev.preventDefault(); + }); + + window.addEventListener('mousedown', (ev) => { + mty_correct_relative(); + ev.preventDefault(); + MTY_CFunc(mouse_button)(app, true, ev.button, mty_scaled(ev.clientX), mty_scaled(ev.clientY)); + }); + + window.addEventListener('mouseup', (ev) => { + ev.preventDefault(); + MTY_CFunc(mouse_button)(app, false, ev.button, mty_scaled(ev.clientX), mty_scaled(ev.clientY)); + }); + + MTY.gl.canvas.addEventListener('contextmenu', (ev) => { + ev.preventDefault(); + }); + + MTY.gl.canvas.addEventListener('wheel', (ev) => { + let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0; + let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0; + MTY_CFunc(mouse_wheel)(app, x, y); + }, {passive: true}); + + window.addEventListener('keydown', (ev) => { + mty_correct_relative(); + const key = MTY.keys[ev.code]; + + if (key != undefined) { + const text = ev.key.length == 1 ? MTY_StrToC(ev.key, MTY.cbuf, 1024) : 0; + + if (MTY_CFunc(keyboard)(app, true, key, text, mty_get_mods(ev))) + ev.preventDefault(); + } + }); + + window.addEventListener('keyup', (ev) => { + const key = MTY.keys[ev.code]; + + if (key != undefined) + if (MTY_CFunc(keyboard)(app, false, key, 0, mty_get_mods(ev))) + ev.preventDefault(); + }); + + MTY.gl.canvas.addEventListener('dragover', (ev) => { + ev.preventDefault(); + }); + + window.addEventListener('blur', (ev) => { + MTY_CFunc(focus)(app, false); + }); + + window.addEventListener('focus', (ev) => { + MTY_CFunc(focus)(app, true); + }); + + window.addEventListener('resize', (ev) => { + MTY_CFunc(resize)(app); + }); + + MTY.gl.canvas.addEventListener('drop', (ev) => { + ev.preventDefault(); + + if (!ev.dataTransfer.items) + return; + + for (let x = 0; x < ev.dataTransfer.items.length; x++) { + if (ev.dataTransfer.items[x].kind == 'file') { + let file = ev.dataTransfer.items[x].getAsFile(); + + const reader = new FileReader(); + reader.addEventListener('loadend', (fev) => { + if (reader.readyState == 2) { + let buf = new Uint8Array(reader.result); + let cmem = MTY_Alloc(buf.length); + MTY_Memcpy(cmem, buf); + MTY_CFunc(drop)(app, MTY_StrToC(file.name, MTY.cbuf, 1024), cmem, buf.length); + MTY_Free(cmem); + } + }); + reader.readAsArrayBuffer(file); + break; + } + } + }); + }, + web_raf: function (app, func, controller, move, opaque) { + // Init position + MTY.lastX = window.screenX; + MTY.lastY = window.screenY; + + const step = () => { + // Poll gamepads + if (document.hasFocus()) + mty_poll_gamepads(app, controller); + + // Poll position changes + if (MTY.lastX != window.screenX || MTY.lastY != window.screenY) { + MTY.lastX = window.screenX; + MTY.lastY = window.screenY; + MTY_CFunc(move)(app); + } + + // Poll size changes and resize the canvas + const rect = MTY.gl.canvas.getBoundingClientRect(); + + MTY.gl.canvas.width = mty_scaled(rect.width); + MTY.gl.canvas.height = mty_scaled(rect.height); + + // Keep looping recursively or end based on AppFunc return value + if (MTY_CFunc(func)(opaque)) { + window.requestAnimationFrame(step); + + } else { + MTY.endFunc(); + } + }; + + window.requestAnimationFrame(step); + throw 'MTY_AppRun halted execution'; + }, +}; + + +// WASI API + +// https://github.com/WebAssembly/WASI/blob/master/phases/snapshot/docs.md + +function mty_append_buf_to_b64(b64, buf) { + // FIXME This is a crude way to handle appending to an open file, + // complex seek operations will break this + + const cur_buf = mty_b64_to_buf(b64); + const new_buf = new Uint8Array(cur_buf.length + buf.length); + + new_buf.set(cur_buf); + new_buf.set(buf, cur_buf.length); + + return mty_buf_to_b64(new_buf); +} + +function mty_arg_list() { + const params = new URLSearchParams(window.location.search); + const qs = params.toString(); + + let plist = [MTY.arg0]; + + // TODO This would put each key/val pair as a separate arg + // for (let p of params) + // plist.push(p[0] + '=' + p[1]); + + //return plist; + + + // For now treat the entire query string as argv[1] + if (qs) + plist.push(qs); + + return plist; +} + +const MTY_WASI_API = { + // Command line arguments + args_get: function (argv, argv_buf) { + const args = mty_arg_list(); + for (let x = 0; x < args.length; x++) { + MTY_StrToC(args[x], argv_buf, 32 * 1024); // FIXME what is the real size of this buffer + MTY_SetUint32(argv + x * 4, argv_buf); + argv_buf += args[x].length + 1; + } + + return 0; + }, + args_sizes_get: function (argc, argv_buf_size) { + const args = mty_arg_list(); + + MTY_SetUint32(argc, args.length); + MTY_SetUint32(argv_buf_size, args.join(' ').length + 1); + return 0; + }, + + // WASI preopened directory (/) + fd_prestat_get: function (fd, path) { + return !MTY.preopen ? 0 : 8; + }, + fd_prestat_dir_name: function (fd, path, path_len) { + if (!MTY.preopen) { + MTY_StrToC('/', path, path_len); + MTY.preopen = true; + + return 0; + } + + return 28; + }, + + // Paths + path_filestat_get: function (fd, flags, cpath, _0, filestat_out) { + const path = MTY_StrToJS(cpath); + if (localStorage[path]) { + // We only need to return the size + const buf = mty_b64_to_buf(localStorage[path]); + MTY_SetUint64(filestat_out + 32, buf.byteLength); + } + + return 0; + }, + path_open: function (fd, dir_flags, path, o_flags, _0, _1, _2, mode, fd_out) { + const new_fd = MTY.fdIndex++; + MTY_SetUint32(fd_out, new_fd); + + MTY.fds[new_fd] = { + path: MTY_StrToJS(path), + append: mode == 1, + offset: 0, + }; + + return 0; + }, + path_create_directory: function () { + return 0; + }, + path_remove_directory: function () { + return 0; + }, + path_unlink_file: function () { + return 0; + }, + path_readlink: function () { + }, + path_rename: function () { + console.log('path_rename', arguments); + return 0; + }, + + // File descriptors + fd_close: function (fd) { + delete MTY.fds[fd]; + }, + fd_fdstat_get: function () { + return 0; + }, + fd_fdstat_set_flags: function () { + }, + fd_readdir: function () { + return 8; + }, + fd_seek: function (fd, offset, whence, offset_out) { + return 0; + }, + fd_read: function (fd, iovs, iovs_len, nread) { + const finfo = MTY.fds[fd]; + + if (finfo && localStorage[finfo.path]) { + const full_buf = mty_b64_to_buf(localStorage[finfo.path]); + let total = 0; + + for (let x = 0; x < iovs_len; x++) { + let ptr = iovs + x * 8; + let cbuf = MTY_GetUint32(ptr); + let cbuf_len = MTY_GetUint32(ptr + 4); + let len = cbuf_len < full_buf.length - total ? cbuf_len : full_buf.length - total; + + let view = new Uint8Array(mty_mem(), cbuf, cbuf_len); + let slice = new Uint8Array(full_buf.buffer, total, len); + view.set(slice); + + total += len; + } + + MTY_SetUint32(nread, total); + } + + return 0; + }, + fd_write: function (fd, iovs, iovs_len, nwritten) { + // Calculate full write size + let len = 0; + for (let x = 0; x < iovs_len; x++) + len += MTY_GetUint32(iovs + x * 8 + 4); + + MTY_SetUint32(nwritten, len); + + // Create a contiguous buffer + let offset = 0; + let full_buf = new Uint8Array(len); + for (let x = 0; x < iovs_len; x++) { + let ptr = iovs + x * 8; + let cbuf = MTY_GetUint32(ptr); + let cbuf_len = MTY_GetUint32(ptr + 4); + + full_buf.set(new Uint8Array(mty_mem(), cbuf, cbuf_len), offset); + offset += cbuf_len; + } + + // stdout + if (fd == 1) { + const str = mty_buf_to_js_str(full_buf); + if (str != '\n') + console.log(str); + + // stderr + } else if (fd == 2) { + const str = mty_buf_to_js_str(full_buf) + if (str != '\n') + console.error(str); + + // Filesystem + } else if (MTY.fds[fd]) { + const finfo = MTY.fds[fd]; + const cur_b64 = localStorage[finfo.path]; + + if (cur_b64 && finfo.append) { + localStorage[finfo.path] = mty_append_buf_to_b64(cur_b64, full_buf); + + } else { + localStorage[finfo.path] = mty_buf_to_b64(full_buf, len); + } + + finfo.offet += len; + } + + return 0; + }, + + // Misc + clock_time_get: function (id, precision, time_out) { + MTY_SetUint64(time_out, Math.round(performance.now() * 1000.0 * 1000.0)); + return 0; + }, + poll_oneoff: function (sin, sout, nsubscriptions, nevents) { + MTY_SetUint32(sout + 8, 0); + return 0; + }, + proc_exit: function () { + }, + environ_get: function () { + }, + environ_sizes_get: function () { + }, +}; + + +// Entry + +function mty_supports_wasm() { + try { + if (typeof WebAssembly == 'object' && typeof WebAssembly.instantiate == 'function') { + const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); + + if (module instanceof WebAssembly.Module) + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } catch (e) {} + + return false; +} + +function mty_supports_web_gl() { + try { + return document.createElement('canvas').getContext('webgl'); + } catch (e) {} + + return false; +} + +async function MTY_Start(bin, userEnv, endFunc, glver) { + MTY.arg0 = bin; + + if (!mty_supports_wasm() || !mty_supports_web_gl()) + return false; + + if (!userEnv) + userEnv = {}; + + if (endFunc) + MTY.endFunc = endFunc; + + // Set up full window canvas and webgl context + const html = document.querySelector('html'); + html.style.width = '100%'; + html.style.height = '100%'; + html.style.margin = 0; + + const body = document.querySelector('body'); + body.style.width = '100%'; + body.style.height = '100%'; + body.style.background = 'black'; + body.style.overflow = 'hidden'; + body.style.margin = 0; + + const canvas = document.createElement('canvas'); + canvas.style.width = '100%'; + canvas.style.height = '100%'; + document.body.appendChild(canvas); + + if (glver) + MTY.glver = glver; + + MTY.gl = canvas.getContext(MTY.glver, { + depth: false, + antialias: false, + premultipliedAlpha: true, + }); + + // Set up the clipboard + MTY.clip = document.createElement('textarea'); + MTY.clip.style.position = 'absolute'; + MTY.clip.style.left = '-9999px'; + MTY.clip.autofocus = true; + document.body.appendChild(MTY.clip); + + // Load keyboard map + if (navigator.keyboard) + MTY.kbMap = await navigator.keyboard.getLayoutMap(); + + // Fetch the wasm file as an ArrayBuffer + const res = await fetch(bin); + const buf = await res.arrayBuffer(); + + // Create wasm instance (module) from the ArrayBuffer + MTY.module = await WebAssembly.instantiate(buf, { + // Custom imports + env: { + ...MTY_UNISTD_API, + ...MTY_GL_API, + ...MTY_AUDIO_API, + ...MTY_NET_API, + ...MTY_IMAGE_API, + ...MTY_CRYPTO_API, + ...MTY_SYSTEM_API, + ...MTY_WEB_API, + ...userEnv, + }, + + // Current version of WASI we're compiling against, 'wasi_snapshot_preview1' + wasi_snapshot_preview1: { + ...MTY_WASI_API, + }, + }); + + // Execute the '_start' entry point, this will fetch args and execute the 'main' function + try { + MTY.module.instance.exports._start(); + + // We expect to catch the 'MTY_AppRun halted execution' exception + // Otherwise look for an indication of unsupported WASM features + } catch (e) { + estr = e.toString(); + + if (estr.search('MTY_AppRun') == -1) + console.error(e); + + // This probably means the browser does not support WASM 64 + return estr.search('i64 not allowed') == -1; + } + + return true; +} diff --git a/webapp/static/lib/parsec.js b/webapp/static/lib/parsec.js new file mode 100644 index 0000000..d0b6900 --- /dev/null +++ b/webapp/static/lib/parsec.js @@ -0,0 +1,46 @@ +'use strict';var h;function q(a){var b=0;return function(){return bb||1342177279>>=1)d+=d;return c}});y("String.prototype.padStart",function(a){return a?a:function(b,d){var c=K(this,"padStart");b-=c.length;d=void 0!==d?String(d):" ";return(0d&&(g=d);0>g&&(g=0);a.x=g;d=Math.round(c/e*(a.y-k));d===c-1&&(d=c);d>c&&(d=c);0>d&&(d=0);a.y=d}return O(3,a.relative?1:0,a.x,a.y);case 8:return c=new ArrayBuffer(28),N(c,23,a.id,0,0),d=new DataView(c), +d.setUint16(16,a.buttons),d.setInt16(18,a.thumbLX),d.setInt16(20,a.thumbLY),d.setInt16(22,a.thumbRX),d.setInt16(24,a.thumbRY),d.setUint8(26,a.leftTrigger),d.setUint8(27,a.rightTrigger),c;case 2:return O(1,a.button,a.pressed?1:0,0);case 1:return O(0,a.code,a.mod,a.pressed?1:0);case 3:return O(2,a.x,a.y,0);case 5:return O(4,a.button,a.pressed?1:0,a.id);case 6:return O(5,a.axis,a.value,a.id);case 7:return O(6,0,0,a.id);case 9:return O(24,0,0,0)}}var P={},la=1; +function ma(a){var b=la++;P[b]=a;return b}function na(a){var b=a.getInt16(32),d=a.getInt32(16),c=01.5*T?c.i+1:0,603*T?10:1.25):k3*U?c.b.currentTime=g+1E3:k>1.5*U&&1==c.b.playbackRate?V(c.b,10):kd;d++)crypto.getRandomValues(b),a.push(Array.from(b).map(function(c){return c.toString(16).padStart(2,"0")}).join(""));return a.join("-")} +function Y(a,b){var d=this;this.status=L.PARSEC_NOT_RUNNING;this.o=performance.now();this.s={encodeLatency:0,decodeLatency:0,networkLatency:0};this.c=[];this.signal=this.b=null;this.h=[];this.f="";this.video=a;this.l=[];this.j={};this.m=0;this.i=new W(b,'audio/mp4; codecs="opus"',null);this.g=new W(a,'video/mp4; codecs="avc1.64001e"',function(){console.warn("Attempting to recover stream...");d.b&&d.b.send(O(13,0,0,1),0)});this.h.push(M(window,"beforeunload",function(){d.C();return null},null))} +function Z(a,b){a.signal&&(a.signal.close(1E3),a.signal=null);b!=L.PARSEC_OK&&(a.b&&a.status==L.PARSEC_OK&&a.b.send(O(10,0,0,0),0),a.b&&(a.b.close(),a.b=null),X(a.g),X(a.i));a.status=b;a.l=[];a.j={};a.m=0}h=Y.prototype;h.C=function(){ha(this.h);this.D()};h.X=function(){};h.S=function(){return this.f};h.T=function(a){var b=P[a];void 0!=b&&delete P[a];return void 0==b?null:b}; +h.G=function(a,b,d){var c=this,g,k,e,f,m,p;return J(function(t){if(1==t.b){g=/Chrome/.test(navigator.userAgent)&&/Google Inc/.test(navigator.vendor);if(!g)return c.status=L.PARSEC_WRN_BROWSER,t.return();if(c.status!=L.PARSEC_NOT_RUNNING)return t.return();c.status=L.PARSEC_CONNECTING;P={};la=1;X(c.i);X(c.g);c.c=[];c.f=xa();c.signal=new S(c.f,function(l){Z(c,l)});k=function(l,n,u,v,A){c.c.push({type:8,attemptID:c.f,ip:l,port:n,lan:A,fromStun:v,sync:u})};e=function(){c.h.push(M(document,"visibilitychange", +function(){document.hidden?(X(c.g),c.b&&c.b.send(O(19,0,0,0),0)):c.b&&c.b.send(O(13,0,0,0),0)},null));var l=window.screen.width,n=window.screen.height;if(800>l||600>n||1920 { + if (!PARSEC) { + setTimeout(connect, 100); + + } else { + PARSEC.clientConnect(session_id, peer_id, secret); + PARSEC.setPeerID(PARSEC.getAttemptID(), peer_id); + } + }; + + connect(); + + } else { + window.location.assign('parsec://peer_id=' + peer_id + '&host_secret=' + secret); + } + + return 0; + }, + signal_init: function (s_out, role, host, port, loader_v, service_v, device_id, cbs, opaque) { + const role_js = MTY_StrToJS(role); + const host_js = MTY_StrToJS(host); + + // XXX TODO create context, but don't yet connect to websocket + }, + signal_destroy: function (s_out) { + // XXX TODO full clean up and null out context + }, + signal_update: function (s, host, session_id) { + const host_js = MTY_StrToJS(host); + const session_id_js = MTY_StrToJS(session_id); + + // XXX TODO reconnect websocket with new session_id and websocket host + + return false; + }, + signal_stop: function (s) { + // XXX TODO Disconnect from websocket, clear message queue + }, + + + // parsec SDK + parsec_web_init: function () { + if (PARSEC) + return; + + AUDIO = document.createElement('audio'); + document.body.appendChild(AUDIO); + + const container = document.createElement('div'); + container.style.zIndex = -1; + container.style.background = 'black'; + container.style.position = 'fixed'; + container.style.top = 0; + container.style.right = 0; + container.style.bottom = 0; + container.style.left = 0; + document.body.appendChild(container); + + const video = document.createElement('video'); + video.muted = true; + video.style.width = '100%'; + video.style.height = '100%'; + container.appendChild(video); + + PARSEC = new Parsec(video, AUDIO); + }, + parsec_web_destroy: function () { + if (!PARSEC) + return; + + PARSEC.destroy(); + PARSEC = undefined; + }, + parsec_web_disconnect: function () { + PARSEC.clientDisconnect(); + }, + parsec_web_get_status: function () { + return DAEMON_WAITING ? PARSEC.Status.PARSEC_CONNECTING : PARSEC.clientGetStatus(); + }, + parsec_web_send_user_data: function (id, msg_c) { + PARSEC.clientSendUserData(id, MTY_StrToJS(msg_c)); + }, + parsec_web_get_guests: function (jstr_c, len) { + MTY_StrToC(JSON.stringify(PARSEC.clientGetGuests()), jstr_c, len); + }, + parsec_web_get_attempt_id: function (attempt_id_c, len) { + MTY_StrToC(PARSEC.getAttemptID(), attempt_id_c, len); + }, + parsec_web_poll_events: function (event_str_c, len) { + const event = PARSEC.clientPollEvents(); + + if (event) { + MTY_StrToC(JSON.stringify(event), event_str_c, len); + return true; + } + + return false; + }, + parsec_web_get_buffer: function (key) { + const buffer = PARSEC.getBuffer(key); + + if (buffer) { + const ptr = MTY_Alloc(buffer.length); + MTY_Memcpy(ptr, buffer); + + return ptr; + } + + return 0; + }, + parsec_web_set_log_callback: function (callback_c, opaque) { + PARSEC.setLogCallback((level, msg) => { + const msg_c = MTY_Alloc(1024); + MTY_StrToC(msg, msg_c, 1024); + + MTY_CFunc(callback_c)(level, msg_c, opaque); + MTY_Free(msg_c); + }); + }, + parsec_web_send_message: function (msg_c) { + const msg = JSON.parse(MTY_StrToJS(msg_c)); + + PARSEC.clientSendMessage(msg); + }, + parsec_web_get_metrics: function (decode_ptr, encode_ptr, network_ptr) { + const metrics = PARSEC.clientGetMetrics(); + + MTY_SetFloat(decode_ptr, metrics['decodeLatency']); + MTY_SetFloat(encode_ptr, metrics['encodeLatency']); + MTY_SetFloat(network_ptr, metrics['networkLatency']); + }, + parsec_web_get_network_failure: function () { + return PARSEC.clientNetworkFailure(); + }, + parsec_web_get_self: function (owner_ptr, id_ptr) { + const me = PARSEC.clientGetSelf(); + + MTY_SetInt8(owner_ptr, me['owner']); + MTY_SetInt32(id_ptr, me['id']); + }, + parsec_web_get_host_mode: function () { + return PARSEC.clientGetHostMode(); + }, +};