-
-
Save mdsitton/aaa0835fdf68e9a3937a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -r a7b8b4febb49 papatcher.go | |
--- a/papatcher.go Fri Jul 25 14:08:36 2014 -0700 | |
+++ b/papatcher.go Tue Sep 01 00:39:36 2015 -0500 | |
@@ -32,26 +32,59 @@ | |
var cacerts_pem = ` | |
-Go Daddy Root Certificate Authority - G2 -- needed to talk to https://uberent.com | |
-======================================== | |
-----BEGIN CERTIFICATE----- | |
-MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT | |
-B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu | |
-MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 | |
-MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 | |
-b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G | |
-A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI | |
-hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq | |
-9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD | |
-+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd | |
-fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl | |
-NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC | |
-MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 | |
-BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac | |
-vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r | |
-5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV | |
-N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO | |
-LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 | |
+MIIEJTCCAw2gAwIBAgIDAjp3MA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlVT | |
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i | |
+YWwgQ0EwHhcNMTQwODI5MjEzOTMyWhcNMjIwNTIwMjEzOTMyWjBHMQswCQYDVQQG | |
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXUmFwaWRTU0wg | |
+U0hBMjU2IENBIC0gRzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv | |
+VJvZWF0eLFbG1eh/9H0WA//Qi1rkjqfdVC7UBMBdmJyNkA+8EGVf2prWRHzAn7Xp | |
+SowLBkMEu/SW4ib2YQGRZjEiwzQ0Xz8/kS9EX9zHFLYDn4ZLDqP/oIACg8PTH2lS | |
+1p1kD8mD5xvEcKyU58Okaiy9uJ5p2L4KjxZjWmhxgHsw3hUEv8zTvz5IBVV6s9cQ | |
+DAP8m/0Ip4yM26eO8R5j3LMBL3+vV8M8SKeDaCGnL+enP/C1DPz1hNFTvA5yT2AM | |
+QriYrRmIV9cE7Ie/fodOoyH5U/02mEiN1vi7SPIpyGTRzFRIU4uvt2UevykzKdkp | |
+YEj4/5G8V1jlNS67abZZAgMBAAGjggEdMIIBGTAfBgNVHSMEGDAWgBTAephojYn7 | |
+qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUw5zz/NNGCDS7zkZ/oHxb8+IIy1kwEgYD | |
+VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwNQYDVR0fBC4wLDAqoCig | |
+JoYkaHR0cDovL2cuc3ltY2IuY29tL2NybHMvZ3RnbG9iYWwuY3JsMC4GCCsGAQUF | |
+BwEBBCIwIDAeBggrBgEFBQcwAYYSaHR0cDovL2cuc3ltY2QuY29tMEwGA1UdIARF | |
+MEMwQQYKYIZIAYb4RQEHNjAzMDEGCCsGAQUFBwIBFiVodHRwOi8vd3d3Lmdlb3Ry | |
+dXN0LmNvbS9yZXNvdXJjZXMvY3BzMA0GCSqGSIb3DQEBCwUAA4IBAQCjWB7GQzKs | |
+rC+TeLfqrlRARy1+eI1Q9vhmrNZPc9ZE768LzFvB9E+aj0l+YK/CJ8cW8fuTgZCp | |
+fO9vfm5FlBaEvexJ8cQO9K8EWYOHDyw7l8NaEpt7BDV7o5UzCHuTcSJCs6nZb0+B | |
+kvwHtnm8hEqddwnxxYny8LScVKoSew26T++TGezvfU5ho452nFnPjJSxhJf3GrkH | |
+uLLGTxN5279PURt/aQ1RKsHWFf83UTRlUfQevjhq7A6rvz17OQV79PP7GqHQyH5O | |
+ZI3NjGFVkP46yl0lD/gdo0p0Vk8aVUBwdSWmMy66S6VdU5oNMOGNX2Esr8zvsJmh | |
+gP8L8mJMcCaY | |
+-----END CERTIFICATE----- | |
+ | |
+-----BEGIN CERTIFICATE----- | |
+MIIErjCCA5agAwIBAgIDAvgtMA0GCSqGSIb3DQEBCwUAMEcxCzAJBgNVBAYTAlVT | |
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMSAwHgYDVQQDExdSYXBpZFNTTCBTSEEy | |
+NTYgQ0EgLSBHMzAeFw0xNTAzMTIyMDMxNTNaFw0xNjAzMTQxNjM0NDlaMIGRMRMw | |
+EQYDVQQLEwpHVDIxMjE0MTg0MTEwLwYDVQQLEyhTZWUgd3d3LnJhcGlkc3NsLmNv | |
+bS9yZXNvdXJjZXMvY3BzIChjKTE1MS8wLQYDVQQLEyZEb21haW4gQ29udHJvbCBW | |
+YWxpZGF0ZWQgLSBSYXBpZFNTTChSKTEWMBQGA1UEAwwNKi51YmVyZW50LmNvbTCC | |
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKk9M3AfKTQG/p3nWzePUA/N | |
+Ee+nH6EKd0xa6+JpiZBKFHN5iu8oKRBZ4IMAVZHh0nCLk8CMWmKnzt3RKwIuFs5g | |
+S9YeIZKCmgxPMbKChyOc5WkVA5BwY4eODHaUtIivdEOxvHmFDjFXKKCUQ4/+Szzd | |
+xjbrkl+vMkQblL/AeyyUY+O2uaRnxydjDUtDF1xZrslJiztmED7Lg5cyfnjjkXGt | |
+uMLFgjF6IaWVL2Ur3jH/a0G34r3DAm9gPiwnOzsgsRhxQ/xamxGkNy6+K6T6zazV | |
+8t7WbUZoEcXjMYQbLQFxdyeD79nCR9jY+Lw+H0cekeudo9cCMumAGdRKaE7oXUcC | |
+AwEAAaOCAVYwggFSMB8GA1UdIwQYMBaAFMOc8/zTRgg0u85Gf6B8W/PiCMtZMFcG | |
+CCsGAQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL2d2LnN5bWNkLmNvbTAm | |
+BggrBgEFBQcwAoYaaHR0cDovL2d2LnN5bWNiLmNvbS9ndi5jcnQwDgYDVR0PAQH/ | |
+BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAlBgNVHREEHjAc | |
+gg0qLnViZXJlbnQuY29tggt1YmVyZW50LmNvbTArBgNVHR8EJDAiMCCgHqAchhpo | |
+dHRwOi8vZ3Yuc3ltY2IuY29tL2d2LmNybDAMBgNVHRMBAf8EAjAAMEUGA1UdIAQ+ | |
+MDwwOgYKYIZIAYb4RQEHNjAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5yYXBp | |
+ZHNzbC5jb20vbGVnYWwwDQYJKoZIhvcNAQELBQADggEBACng0yNR/lgKp2KkIzaf | |
+eoOu7LwVKsNAbzqK0WiwZigLoNUe5r+JHvYY8MDtwMrRN2ltc8QeR2kN0Nnyb83M | |
+LIxlgrjLHBk4G6bkPoxmNjHGwpPEZ0er/EDSDvFpUtKDCOKXye5n1WfgDPi0tusv | |
+HM7A9u7LEZWx14A0nHhXe9Tf5k55pCfNOQUYdIEt9ZHhVt19Wkp3sqi5EblFb1xN | |
+FRhPA0X6quKymp7lmenclVKIgZNGE/0Gx8FA2N9H1I7w8Jm1Z1/2pJhce9a15+PQ | |
+C3mogKQmamltwRGcHQkcczE16jLxqURDP+2WxBQL05qJL80WnCgAH+DZ0qR6Lxg6 | |
+/IE= | |
-----END CERTIFICATE----- | |
@@ -283,7 +316,7 @@ | |
} | |
urlroot = "https://uberentdev.com" | |
} else { | |
- urlroot = "http://uberent.com" | |
+ urlroot = "https://uberent.com" | |
} | |
stdin_reader := bufio.NewReader(os.Stdin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 Uber Entertainment. | |
// All rights reserved. | |
package main | |
import "bufio" | |
import "bytes" | |
import "compress/gzip" | |
import "crypto/sha1" | |
import _ "crypto/sha256" | |
import "crypto/tls" | |
import "crypto/x509" | |
import "encoding/hex" | |
import "encoding/json" | |
import "flag" | |
import "fmt" | |
import "io" | |
import "io/ioutil" | |
import "net/http" | |
import "os" | |
import "os/user" | |
import "os/exec" | |
import "path/filepath" | |
import "runtime" | |
import "sort" | |
import "strconv" | |
import "strings" | |
import "sync" | |
import "time" | |
var cacerts_pem = ` | |
-----BEGIN CERTIFICATE----- | |
MIIEJTCCAw2gAwIBAgIDAjp3MA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlVT | |
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i | |
YWwgQ0EwHhcNMTQwODI5MjEzOTMyWhcNMjIwNTIwMjEzOTMyWjBHMQswCQYDVQQG | |
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXUmFwaWRTU0wg | |
U0hBMjU2IENBIC0gRzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv | |
VJvZWF0eLFbG1eh/9H0WA//Qi1rkjqfdVC7UBMBdmJyNkA+8EGVf2prWRHzAn7Xp | |
SowLBkMEu/SW4ib2YQGRZjEiwzQ0Xz8/kS9EX9zHFLYDn4ZLDqP/oIACg8PTH2lS | |
1p1kD8mD5xvEcKyU58Okaiy9uJ5p2L4KjxZjWmhxgHsw3hUEv8zTvz5IBVV6s9cQ | |
DAP8m/0Ip4yM26eO8R5j3LMBL3+vV8M8SKeDaCGnL+enP/C1DPz1hNFTvA5yT2AM | |
QriYrRmIV9cE7Ie/fodOoyH5U/02mEiN1vi7SPIpyGTRzFRIU4uvt2UevykzKdkp | |
YEj4/5G8V1jlNS67abZZAgMBAAGjggEdMIIBGTAfBgNVHSMEGDAWgBTAephojYn7 | |
qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUw5zz/NNGCDS7zkZ/oHxb8+IIy1kwEgYD | |
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwNQYDVR0fBC4wLDAqoCig | |
JoYkaHR0cDovL2cuc3ltY2IuY29tL2NybHMvZ3RnbG9iYWwuY3JsMC4GCCsGAQUF | |
BwEBBCIwIDAeBggrBgEFBQcwAYYSaHR0cDovL2cuc3ltY2QuY29tMEwGA1UdIARF | |
MEMwQQYKYIZIAYb4RQEHNjAzMDEGCCsGAQUFBwIBFiVodHRwOi8vd3d3Lmdlb3Ry | |
dXN0LmNvbS9yZXNvdXJjZXMvY3BzMA0GCSqGSIb3DQEBCwUAA4IBAQCjWB7GQzKs | |
rC+TeLfqrlRARy1+eI1Q9vhmrNZPc9ZE768LzFvB9E+aj0l+YK/CJ8cW8fuTgZCp | |
fO9vfm5FlBaEvexJ8cQO9K8EWYOHDyw7l8NaEpt7BDV7o5UzCHuTcSJCs6nZb0+B | |
kvwHtnm8hEqddwnxxYny8LScVKoSew26T++TGezvfU5ho452nFnPjJSxhJf3GrkH | |
uLLGTxN5279PURt/aQ1RKsHWFf83UTRlUfQevjhq7A6rvz17OQV79PP7GqHQyH5O | |
ZI3NjGFVkP46yl0lD/gdo0p0Vk8aVUBwdSWmMy66S6VdU5oNMOGNX2Esr8zvsJmh | |
gP8L8mJMcCaY | |
-----END CERTIFICATE----- | |
-----BEGIN CERTIFICATE----- | |
MIIErjCCA5agAwIBAgIDAvgtMA0GCSqGSIb3DQEBCwUAMEcxCzAJBgNVBAYTAlVT | |
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMSAwHgYDVQQDExdSYXBpZFNTTCBTSEEy | |
NTYgQ0EgLSBHMzAeFw0xNTAzMTIyMDMxNTNaFw0xNjAzMTQxNjM0NDlaMIGRMRMw | |
EQYDVQQLEwpHVDIxMjE0MTg0MTEwLwYDVQQLEyhTZWUgd3d3LnJhcGlkc3NsLmNv | |
bS9yZXNvdXJjZXMvY3BzIChjKTE1MS8wLQYDVQQLEyZEb21haW4gQ29udHJvbCBW | |
YWxpZGF0ZWQgLSBSYXBpZFNTTChSKTEWMBQGA1UEAwwNKi51YmVyZW50LmNvbTCC | |
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKk9M3AfKTQG/p3nWzePUA/N | |
Ee+nH6EKd0xa6+JpiZBKFHN5iu8oKRBZ4IMAVZHh0nCLk8CMWmKnzt3RKwIuFs5g | |
S9YeIZKCmgxPMbKChyOc5WkVA5BwY4eODHaUtIivdEOxvHmFDjFXKKCUQ4/+Szzd | |
xjbrkl+vMkQblL/AeyyUY+O2uaRnxydjDUtDF1xZrslJiztmED7Lg5cyfnjjkXGt | |
uMLFgjF6IaWVL2Ur3jH/a0G34r3DAm9gPiwnOzsgsRhxQ/xamxGkNy6+K6T6zazV | |
8t7WbUZoEcXjMYQbLQFxdyeD79nCR9jY+Lw+H0cekeudo9cCMumAGdRKaE7oXUcC | |
AwEAAaOCAVYwggFSMB8GA1UdIwQYMBaAFMOc8/zTRgg0u85Gf6B8W/PiCMtZMFcG | |
CCsGAQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL2d2LnN5bWNkLmNvbTAm | |
BggrBgEFBQcwAoYaaHR0cDovL2d2LnN5bWNiLmNvbS9ndi5jcnQwDgYDVR0PAQH/ | |
BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAlBgNVHREEHjAc | |
gg0qLnViZXJlbnQuY29tggt1YmVyZW50LmNvbTArBgNVHR8EJDAiMCCgHqAchhpo | |
dHRwOi8vZ3Yuc3ltY2IuY29tL2d2LmNybDAMBgNVHRMBAf8EAjAAMEUGA1UdIAQ+ | |
MDwwOgYKYIZIAYb4RQEHNjAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5yYXBp | |
ZHNzbC5jb20vbGVnYWwwDQYJKoZIhvcNAQELBQADggEBACng0yNR/lgKp2KkIzaf | |
eoOu7LwVKsNAbzqK0WiwZigLoNUe5r+JHvYY8MDtwMrRN2ltc8QeR2kN0Nnyb83M | |
LIxlgrjLHBk4G6bkPoxmNjHGwpPEZ0er/EDSDvFpUtKDCOKXye5n1WfgDPi0tusv | |
HM7A9u7LEZWx14A0nHhXe9Tf5k55pCfNOQUYdIEt9ZHhVt19Wkp3sqi5EblFb1xN | |
FRhPA0X6quKymp7lmenclVKIgZNGE/0Gx8FA2N9H1I7w8Jm1Z1/2pJhce9a15+PQ | |
C3mogKQmamltwRGcHQkcczE16jLxqURDP+2WxBQL05qJL80WnCgAH+DZ0qR6Lxg6 | |
/IE= | |
-----END CERTIFICATE----- | |
ValiCert Class 2 VA -- needed to talk to https://uberentdev.com | |
=================== | |
-----BEGIN CERTIFICATE----- | |
MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh | |
MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE | |
YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 | |
MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo | |
ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg | |
MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN | |
ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA | |
PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w | |
wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi | |
EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY | |
avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ | |
YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE | |
sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h | |
/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 | |
IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj | |
YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD | |
ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy | |
OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P | |
TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ | |
HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER | |
dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf | |
ReYNnyicsbkqWletNw+vHX/bvZ8= | |
-----END CERTIFICATE----- | |
` | |
type LoginParams struct { | |
TitleId int | |
AuthMethod string | |
UberName string | |
Password string | |
} | |
type LoginResponse struct { | |
SessionTicket string | |
UberName string | |
UberId uint64 | |
DisplayName string | |
UberIdString string | |
} | |
type FailResponse struct { | |
ErrorCode int | |
Message string | |
} | |
type StreamsResponse struct { | |
Streams []StreamInfo | |
} | |
type StreamInfo struct { | |
TitleId int | |
StreamName string | |
BuildId string | |
Description string | |
DownloadUrl string | |
AuthSuffix string | |
ManifestName string | |
TitleFolder string | |
} | |
type WorkItem struct { | |
Download int64 | |
Validate int64 | |
Write int64 | |
} | |
type ManifestEntry struct { | |
Filename string | |
ChecksumStr string `json:"checksum"` | |
ChecksumZStr string `json:"checksumZ"` | |
SizeStr string `json:"size"` | |
SizeZStr string `json:"sizeZ"` | |
OffsetStr string `json:"offset"` | |
Executable bool | |
Checksum []byte `json:"-"` | |
ChecksumZ []byte `json:"-"` | |
Size int64 `json:"-"` | |
SizeZ int64 `json:"-"` | |
Offset int64 `json:"-"` | |
} | |
type ManifestBundle struct { | |
ChecksumStr string `json:"checksum"` | |
SizeStr string `json:"size"` | |
Entries []*ManifestEntry | |
Checksum []byte `json:"-"` | |
Size int64 `json:"-"` | |
} | |
type Manifest struct { | |
Version string | |
PatchesFrom string | |
Bundles []*ManifestBundle | |
} | |
type Throttle chan struct{} | |
func NewThrottle(cap int) Throttle { | |
return make(chan struct{}, cap) | |
} | |
func (t Throttle) Enter() { | |
t <- struct{}{} | |
} | |
func (t Throttle) Exit() { | |
<- t | |
} | |
func panicIf(err error) { | |
if err != nil { | |
panic(err) | |
} | |
} | |
func readLine(reader *bufio.Reader, prompt string) (result string, err error) { | |
os.Stdout.Write([]byte(prompt)) | |
result,err = reader.ReadString('\n') | |
if err == nil { | |
result = strings.TrimSuffix(result[:len(result)-1], "\r") | |
} else if err == io.EOF { | |
if result != "" { | |
err = nil | |
} else { | |
err = io.ErrUnexpectedEOF | |
} | |
} | |
return | |
} | |
var client *http.Client | |
func init() { | |
cacerts := x509.NewCertPool() | |
worked := cacerts.AppendCertsFromPEM([]byte(cacerts_pem)) | |
if !worked { | |
panic("could not parse CA certs") | |
} | |
tr := &http.Transport{ | |
TLSClientConfig: &tls.Config{ RootCAs: cacerts }, | |
} | |
client = &http.Client{ Transport: tr } | |
} | |
var platform_map = map[string]string { | |
"darwin": "OSX", | |
"linux": "Linux", | |
"windows": "Windows", | |
} | |
func main() { | |
os.Exit(run()) | |
} | |
func run() int { | |
var err error | |
start := time.Now() | |
platform,ok := platform_map[runtime.GOOS] | |
if !ok { | |
fmt.Fprintf(os.Stderr, "Unrecognied result from runtime.GOOS: %v.\n", runtime.GOOS) | |
return 1 | |
} | |
ncpus := runtime.NumCPU() | |
runtime.GOMAXPROCS(ncpus) | |
usr,err := user.Current() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "Could not figure out the current user: %v", err) | |
return 1 | |
} | |
var devenv bool | |
flag.BoolVar(&devenv, "dev", false, "Use ubernetdev.com environment") | |
var desired_stream string | |
flag.StringVar(&desired_stream, "stream", "stable", "Stream to download/update") | |
var quiet bool | |
flag.BoolVar(&quiet, "quiet", false, "No status updates") | |
var root_dir string | |
flag.StringVar(&root_dir, "dir", "", "Target directory to patch") | |
var update_only bool | |
flag.BoolVar(&update_only, "update-only", false, "Only do an update, don't launch") | |
flag.Parse() | |
var urlroot string | |
if (devenv) { | |
if !quiet { | |
fmt.Println("Using dev environment") | |
} | |
urlroot = "https://uberentdev.com" | |
} else { | |
urlroot = "https://uberent.com" | |
} | |
stdin_reader := bufio.NewReader(os.Stdin) | |
var username string; | |
if flag.NArg() < 1 { | |
username,err = readLine(stdin_reader, "Ubername: ") | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(1) | |
} | |
} else { | |
username = flag.Arg(0) | |
} | |
var password string; | |
if flag.NArg() < 2 { | |
password,err = readLine(stdin_reader, "Password: ") | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(1) | |
} | |
} else { | |
password = flag.Arg(1) | |
} | |
if !quiet { | |
fmt.Println("logging in..."); | |
} | |
login_response := login(username, password, urlroot) | |
if login_response == nil { | |
return 1 | |
} | |
if !quiet { | |
fmt.Println("requesting streams..."); | |
} | |
req,err := http.NewRequest("GET", urlroot + "/Launcher/ListStreams?Platform=" + platform, nil) | |
panicIf(err) | |
req.Header.Set("X-Authorization", login_response.SessionTicket) | |
resp,err := client.Do(req) | |
if err != nil { | |
fmt.Fprintln(os.Stderr, err) | |
return 1 | |
} | |
resp_bytes,err := ioutil.ReadAll(resp.Body) | |
panicIf(err) | |
panicIf(resp.Body.Close()) | |
if (resp.StatusCode != 200) { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", resp.Status) | |
os.Stderr.Write(resp_bytes) | |
return 1 | |
} | |
streams := make(map[string]StreamInfo) | |
var streams_response StreamsResponse | |
panicIf(json.Unmarshal(resp_bytes, &streams_response)) | |
for _,stream := range streams_response.Streams { | |
streams[stream.StreamName] = stream | |
} | |
stream,found := streams[desired_stream] | |
if !found { | |
fmt.Fprintf(os.Stderr, "Unknown stream: %v\nOptions:\n", desired_stream) | |
for _,stream = range streams_response.Streams { | |
fmt.Fprintf(os.Stderr, " %v\n", stream.StreamName) | |
} | |
return 1 | |
} | |
if root_dir == "" { | |
root_dir = filepath.Join(usr.HomeDir, ".local", "Uber Entertainment", "Planetary Annihilation") | |
} | |
if !quiet { | |
fmt.Printf("Using target directory %v\n", root_dir) | |
} | |
err = os.MkdirAll(root_dir, 0777) | |
if err != nil { | |
fmt.Fprint(os.Stderr, err) | |
return 1 | |
} | |
manifest_url := fmt.Sprintf("%v/%v/%v", stream.DownloadUrl, stream.TitleFolder, stream.ManifestName) | |
if !quiet { | |
fmt.Printf("downloading manifest %v\n", manifest_url) | |
} | |
resp,err = client.Get(manifest_url + stream.AuthSuffix) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", err) | |
return 1 | |
} | |
reader,err := gzip.NewReader(resp.Body) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", err) | |
return 1 | |
} | |
manifest_bytes,err := ioutil.ReadAll(reader) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", err) | |
return 1 | |
} | |
err = resp.Body.Close() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", err) | |
return 1 | |
} | |
if (resp.StatusCode != 200) { | |
fmt.Fprintf(os.Stderr, "download failed: %v\n", resp.Status) | |
os.Stderr.Write(manifest_bytes) | |
return 1 | |
} | |
var manifest Manifest | |
if err = json.Unmarshal(manifest_bytes, &manifest); err != nil { | |
fmt.Fprintf(os.Stderr, "Decoding manifest failed: %v\n", err) | |
return 1 | |
} | |
total_work, files, err := validateManifest(&manifest) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "Manifest broken: %v\n", err) | |
return 1 | |
} | |
download_prefix := fmt.Sprintf("%v/%v/hashed/", stream.DownloadUrl, stream.TitleFolder) | |
game_dir := filepath.Join(root_dir, stream.StreamName) | |
diag_chan := make(chan string, 3) | |
status_chan := make(chan string, 3) | |
diag_done := make(chan struct{}) | |
go func () { | |
dc := diag_chan | |
sc := status_chan | |
var status string | |
for dc != nil || sc != nil { | |
select { | |
case item,okay := <-dc: | |
if !okay { | |
dc = nil | |
} | |
if !quiet { | |
fmt.Printf("\r%v\n%v", item, status) | |
} | |
case new_status,okay := <-sc: | |
if !okay { | |
fmt.Printf("\r%-*s", len(status), "") | |
status = "" | |
sc = nil | |
} else if !quiet { | |
fmt.Printf("\r%-*s", len(status), new_status) | |
status = new_status | |
} | |
} | |
} | |
diag_done <- struct{}{} | |
return | |
}() | |
errors_chan := make(chan string, 3) | |
errors_done := make(chan []string) | |
go func () { | |
errors := make([]string, 0) | |
for err := range errors_chan { | |
errors = append(errors, err) | |
diag_chan <- "ERROR: " + err | |
} | |
errors_done <- errors | |
}() | |
progress_chan := make(chan WorkItem, 1) | |
progress_done := make(chan struct{}) | |
go func() { | |
ticker := time.NewTicker(time.Millisecond * 500) | |
var cum WorkItem | |
for { | |
select { | |
case item,okay := <-progress_chan: | |
if !okay { | |
diag_chan <- fmt.Sprintf("D: %5.1f%% V: %5.1f%% W: %5.1f%%", | |
float64(cum.Download) * 100.0 / float64(total_work.Download), | |
float64(cum.Validate) * 100.0 / float64(total_work.Validate), | |
float64(cum.Write) * 100.0 / float64(total_work.Write)) | |
close(status_chan) | |
progress_done <- struct{}{} | |
return | |
} | |
cum.Download += item.Download | |
cum.Validate += item.Validate | |
cum.Write += item.Write | |
case _ = <-ticker.C: | |
status_chan <- fmt.Sprintf("D: %5.1f%% V: %5.1f%% W: %5.1f%%", | |
float64(cum.Download) * 100.0 / float64(total_work.Download), | |
float64(cum.Validate) * 100.0 / float64(total_work.Validate), | |
float64(cum.Write) * 100.0 / float64(total_work.Write)) | |
} | |
} | |
}() | |
throttle := NewThrottle(ncpus) | |
var waitgroup sync.WaitGroup | |
waitgroup.Add(len(manifest.Bundles)) | |
cache_dir := filepath.Join(root_dir, ".cache") | |
go func () { | |
for _,bundle := range manifest.Bundles { | |
throttle.Enter() | |
go func (bundle *ManifestBundle) { | |
defer throttle.Exit() | |
processBundle(bundle, download_prefix, stream.AuthSuffix, cache_dir, game_dir, diag_chan, errors_chan, progress_chan) | |
waitgroup.Done() | |
}(bundle) | |
} | |
}() | |
var removes []string | |
filepath.Walk(game_dir, func (path string, info os.FileInfo, err error) error { | |
filename := strings.Replace(path[len(game_dir):], "\\", "/", -1) | |
if !files[filename] { | |
// diag_chan <- fmt.Sprintf("removing %v", filename) | |
removes = append(removes, path) | |
} | |
return nil | |
}) | |
for i := len(removes)-1; i >= 0; i-- { | |
path := removes[i] | |
err = os.Remove(path) | |
if err != nil { | |
errors_chan <- fmt.Sprintf("removal of extra file %v failed: %v", path, err) | |
} | |
} | |
waitgroup.Wait() | |
close(progress_chan) | |
<- progress_done | |
close(errors_chan) | |
errors := <- errors_done | |
close(diag_chan) | |
<- diag_done | |
end := time.Now() | |
elapsed := end.Sub(start) | |
if !quiet { | |
fmt.Printf("\nFinished in %v\n", elapsed) | |
} | |
if len(errors) > 0 { | |
fmt.Fprintln(os.Stderr, "\nUpdate failed:") | |
for _,err := range errors { | |
fmt.Fprintln(os.Stderr, " ", err) | |
} | |
return 1 | |
} | |
if update_only { | |
return 0 | |
} | |
var args = make([]string,0,5) | |
if devenv { | |
args = append(args, "--ubernetdev") | |
} | |
args = append(args, "--ticket", login_response.SessionTicket) | |
cmd := exec.Command(filepath.Join(game_dir, "PA"), args...) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
if err = cmd.Start(); err != nil { | |
fmt.Fprintln(os.Stderr, "\nLaunching PA failed:", err) | |
return 1 | |
} | |
if err = cmd.Wait(); err != nil { | |
fmt.Fprintln(os.Stderr, "\nPA exited with non-zero status: ", err) | |
return 1 | |
} | |
return 0 | |
} | |
func login(username, password, urlroot string) (result *LoginResponse) { | |
login_params := LoginParams{ | |
TitleId: 4, | |
AuthMethod: "UberCredentials", | |
UberName: username, | |
Password: password, | |
} | |
login_params_json,err := json.Marshal(login_params) | |
if err != nil { | |
panic(err); | |
} | |
resp,err := client.Post(urlroot + "/GC/Authenticate", "application/json", bytes.NewBuffer(login_params_json)) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "Could not contact %v:\n %v\n", urlroot, err) | |
return nil | |
} | |
resp_bytes,err := ioutil.ReadAll(resp.Body) | |
panicIf(err) | |
panicIf(resp.Body.Close()) | |
if (resp.StatusCode != 200) { | |
var fail_response FailResponse | |
err = json.Unmarshal(resp_bytes, &fail_response) | |
if err == nil { | |
fmt.Fprintf(os.Stderr, "Login failed: %v (code=%v)\n", fail_response.Message, fail_response.ErrorCode) | |
return nil | |
} | |
fmt.Fprintf(os.Stderr, "login failed with HTTP status %#v\n", resp.Status) | |
if (len(resp_bytes) > 0) { | |
fmt.Fprintf(os.Stderr, "response details:\n") | |
os.Stderr.Write(resp_bytes) | |
if resp_bytes[len(resp_bytes)-1] != '\n' { | |
fmt.Fprintln(os.Stderr) | |
} | |
} | |
return nil | |
} | |
result = new(LoginResponse) | |
panicIf(json.Unmarshal(resp_bytes, result)) | |
return | |
} | |
func validateManifest(manifest *Manifest) (work WorkItem, files map[string]bool, err error) { | |
files = make(map[string]bool) | |
seen := make(map[string]*ManifestBundle) | |
uniques := make([]*ManifestBundle, 0, len(manifest.Bundles)) | |
for _,bundle := range manifest.Bundles { | |
if bundle.Checksum,err = hex.DecodeString(bundle.ChecksumStr); err != nil { | |
return | |
} | |
if bundle.Size,err = strconv.ParseInt(bundle.SizeStr, 10, 64); err != nil { | |
return | |
} | |
existing,found := seen[bundle.ChecksumStr] | |
if found { | |
if existing.Size != bundle.Size { | |
err = fmt.Errorf("Bundle %v repeated but with different sizes (%v and %v)", bundle.Checksum, bundle.Size, existing.Size) | |
return | |
} | |
existing.Entries = append(existing.Entries, bundle.Entries...) | |
} else { | |
seen[bundle.ChecksumStr] = bundle | |
uniques = append(uniques, bundle) | |
work.Download += bundle.Size | |
work.Validate += bundle.Size | |
work.Write += bundle.Size | |
} | |
for _,entry := range bundle.Entries { | |
if entry.Checksum,err = hex.DecodeString(entry.ChecksumStr); err != nil { | |
return | |
} | |
if entry.Offset,err = strconv.ParseInt(entry.OffsetStr, 10, 64); err != nil { | |
return | |
} | |
if entry.Size,err = strconv.ParseInt(entry.SizeStr, 10, 64); err != nil { | |
return | |
} | |
if entry.SizeZ,err = strconv.ParseInt(entry.SizeZStr, 10, 64); err != nil { | |
return | |
} | |
filename := entry.Filename | |
for !files[filename] { | |
files[filename] = true | |
slash := strings.LastIndex(filename, "/") | |
if slash == -1 { | |
break | |
} | |
filename = filename[:slash] | |
} | |
work.Validate += entry.Size | |
work.Write += entry.Size | |
} | |
} | |
manifest.Bundles = uniques | |
return | |
} | |
type ByOffset []*ManifestEntry | |
func (a ByOffset) Len() int { | |
return len(a) | |
} | |
func (a ByOffset) Swap(i, j int) { | |
a[i], a[j] = a[j], a[i] | |
} | |
func (a ByOffset) Less(i, j int) bool { | |
return a[i].Offset < a[j].Offset | |
} | |
type DownloadWrapper struct { | |
file io.ReadCloser | |
progress_chan chan<- WorkItem | |
} | |
func (dw *DownloadWrapper) Read(bytes []byte) (n int, err error) { | |
n,err = dw.file.Read(bytes) | |
if n > 0 { | |
dw.progress_chan <- WorkItem{Download: int64(n), Write: int64(n)} | |
} | |
return | |
} | |
func (dw *DownloadWrapper) Close() error { | |
return dw.file.Close() | |
} | |
type WriteWrapper struct { | |
file io.ReadCloser | |
progress_chan chan<- WorkItem | |
} | |
func (ww *WriteWrapper) Read(bytes []byte) (n int, err error) { | |
n,err = ww.file.Read(bytes) | |
if n > 0 { | |
ww.progress_chan <- WorkItem{Write: int64(n)} | |
} | |
return | |
} | |
func (ww *WriteWrapper) Close() error { | |
return ww.file.Close() | |
} | |
func processBundle(bundle *ManifestBundle, download_prefix, auth_suffix, cache_dir, game_dir string, diag_chan, errors_chan chan<- string, progress_chan chan<- WorkItem) { | |
cache_file := filepath.Join(cache_dir, strings.ToLower(bundle.ChecksumStr)) | |
downloaded := false | |
download_factory := func () (io.ReadCloser) { | |
url := download_prefix + bundle.ChecksumStr | |
// diag_chan <- fmt.Sprintf("downloading %v", url) | |
resp,err := client.Get(url + auth_suffix) | |
if err != nil { | |
errors_chan <- fmt.Sprintf("download of %v failed: %v", url, err) | |
return nil | |
} | |
if resp.StatusCode != 200 { | |
resp.Body.Close() | |
errors_chan <- fmt.Sprintf("download of %v failed: %v", url, resp.Status) | |
return nil | |
} | |
downloaded = true | |
return &DownloadWrapper{file: resp.Body, progress_chan: progress_chan} | |
} | |
file := verifyOrRecreate(cache_file, bundle.Checksum, bundle.Size, false, download_factory, true, diag_chan, errors_chan, progress_chan) | |
if file == nil { | |
return | |
} | |
if !downloaded { | |
progress_chan <- WorkItem{Download: bundle.Size} | |
} | |
defer file.Close() | |
sort.Sort(ByOffset(bundle.Entries)) | |
for _,entry := range bundle.Entries { | |
dst_file := game_dir + entry.Filename | |
extract_factory := func () (io.ReadCloser) { | |
//diag_chan <- fmt.Sprintf("extracting %v from %v [offset %v]", dst_file, cache_file, entry.Offset) | |
var err error | |
if _,err = file.Seek(entry.Offset, os.SEEK_SET); err != nil { | |
errors_chan <- fmt.Sprintf("extraction of %v failed: %v", entry.Filename, err) | |
return nil | |
} | |
var reader io.ReadCloser | |
if entry.SizeZ != 0 { | |
reader,err = gzip.NewReader(&io.LimitedReader{R: file, N: entry.SizeZ}) | |
if err != nil { | |
errors_chan <- fmt.Sprintf("extraction of %v failed: %v", entry.Filename, err) | |
return nil | |
} | |
} else { | |
reader = ioutil.NopCloser(&io.LimitedReader{R: file, N: entry.Size}) | |
} | |
return &WriteWrapper{file: reader, progress_chan: progress_chan} | |
} | |
verifyOrRecreate(dst_file, entry.Checksum, entry.Size, entry.Executable, extract_factory, false, diag_chan, errors_chan, progress_chan) | |
} | |
} | |
func verifyOrRecreate(filename string, checksum []byte, length int64, executable bool, factory func () io.ReadCloser, leave_open bool, diag_chan, errors_chan chan<- string, progress_chan chan<- WorkItem) *os.File { | |
file,err := os.Open(filename) | |
if err == nil { | |
//diag_chan <- fmt.Sprintf("verifying %v (length %v)", filename, length) | |
hash := sha1.New() | |
_,err := io.Copy(hash, file) | |
progress_chan <- WorkItem{Validate: length} | |
if err != nil { | |
diag_chan <- fmt.Sprintf("%v: %v", filename, err) | |
file.Close() | |
} else if calculated := hash.Sum(make([]byte, 0, hash.Size())); bytes.Compare(checksum, calculated) != 0 { | |
diag_chan <- fmt.Sprintf("%v: checksum wrong, got %v wanted %v", filename, hex.EncodeToString(calculated), hex.EncodeToString(checksum)) | |
file.Close() | |
} else { | |
progress_chan <- WorkItem{Write: length} | |
fileinfo,err := file.Stat() | |
if err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
exe_bits := fileinfo.Mode() & 0111 | |
if executable { | |
read_bits := fileinfo.Mode() & 0444 | |
if (read_bits >> 2 != exe_bits) { | |
err = file.Chmod(fileinfo.Mode() | (read_bits >> 2)) | |
if err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
} | |
} else { | |
if exe_bits != 0 { | |
err = file.Chmod(fileinfo.Mode() ^ exe_bits); | |
if err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
} | |
} | |
if leave_open { | |
return file | |
} else { | |
file.Close() | |
return nil | |
} | |
} | |
} else { | |
progress_chan <- WorkItem{Validate: length} | |
} | |
if err = os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
src := factory() | |
if src == nil { | |
return nil | |
} | |
var mode os.FileMode | |
if executable { mode = 0777 } else { mode = 0666 } | |
dst,err := os.OpenFile(filename + ".new", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) | |
if err != nil { | |
src.Close() | |
errors_chan <- err.Error() | |
return nil | |
} | |
hash := sha1.New() | |
if _,err = io.Copy(io.MultiWriter(dst, hash), src); err != nil { | |
src.Close() | |
dst.Close() | |
errors_chan <- err.Error() | |
return nil | |
} | |
if err = dst.Close(); err != nil { | |
src.Close() | |
errors_chan <- err.Error() | |
return nil | |
} | |
src.Close() | |
calculated := hash.Sum(make([]byte, 0, hash.Size())) | |
if bytes.Compare(checksum, calculated) != 0 { | |
errors_chan <- fmt.Sprintf("%v: checksum wrong, got %v wanted %v", filename, hex.EncodeToString(calculated), hex.EncodeToString(checksum)) | |
return nil | |
} | |
if err = os.Rename(filename + ".new", filename); err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
if !leave_open { | |
return nil | |
} | |
result,err := os.Open(filename) | |
if err != nil { | |
errors_chan <- err.Error() | |
return nil | |
} | |
return result | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment