diff --git a/anki/ankiweb.certs b/anki/ankiweb.certs new file mode 100644 index 000000000..390503e10 --- /dev/null +++ b/anki/ankiweb.certs @@ -0,0 +1,55 @@ +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw +cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV +BAMTDlBvc2l0aXZlU1NMIENBMB4XDTExMDkxNTAwMDAwMFoXDTE0MDkxNDIzNTk1 +OVowTzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMRQwEgYDVQQL +EwtQb3NpdGl2ZVNTTDEUMBIGA1UEAxMLYW5raXdlYi5uZXQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDEDfc2WFrF5mkg4yrrbu/bxqW5SLu1qPp3Vtlv ++he8Fzkn44Zc8in7Q/t8pOxRMNq5nRjsKNUr51Zv206Z8aDzV6Mi2LtdHhADTOQW +UGa2+ANcvGJg6lCJ4WvQwcDIktO9kGgmlkMvuPAskF+nhRi7TlTaU8lhHlhMV5zu +G0XfXjPGitK1m5egIY78PJoVK+M/k44NMxi0sb+XgErXW0k6QdCZvba0z5Heks7+ +aIFmLjx7bcEQxQS/+1nIK05nNrDPi7TymwZOM+b2T48t7+x9H9cjEeLUZsCiETja +SBLyj/2WLFYfH7jZuwylwcCvJ5PlnutAk7za3iASpGpUvWdFAgMBAAGjggHKMIIB +xjAfBgNVHSMEGDAWgBS4yhHpBjF528OUxugZKry7NRYxpDAdBgNVHQ4EFgQUGUTe +GYqlhmH/El7O10/hoPgIdqEwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEYGA1UdIAQ/MD0wOwYLKwYB +BAGyMQECAgcwLDAqBggrBgEFBQcCARYeaHR0cDovL3d3dy5wb3NpdGl2ZXNzbC5j +b20vQ1BTMGkGA1UdHwRiMGAwL6AtoCuGKWh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L1Bvc2l0aXZlU1NMQ0EuY3JsMC2gK6AphidodHRwOi8vY3JsLmNvbW9kby5uZXQv +UG9zaXRpdmVTU0xDQS5jcmwwawYIKwYBBQUHAQEEXzBdMDUGCCsGAQUFBzAChilo +dHRwOi8vY3J0LmNvbW9kb2NhLmNvbS9Qb3NpdGl2ZVNTTENBLmNydDAkBggrBgEF +BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMCcGA1UdEQQgMB6CC2Fua2l3 +ZWIubmV0gg93d3cuYW5raXdlYi5uZXQwDQYJKoZIhvcNAQEFBQADggEBAI6Mcuwd +OQTvTkeZ45j2VcI1hR/nqSf2VnisxRQxNRr+n8grjt1ulqYJWJyOrocUINW7XyoJ +jHcFpS30m/E4ZedaHXq++hJqjat140r5TRcBigAHnZj7u69hjAhwG/A6Er0JFbX+ +eCwe1SCBUgDLGhcNA4o3cykmgK6qG/drj5/CVwpTVKzQ65JGxEMgWehELraPzbx9 +mi3e9BMSC11eEsE6O0CpBL+ENcXngYpyi1R3GYFce9oFk+ps/1yYiUstgStq4obJ +8MdDZKB8qOLFPe097FGRcOz1JRDYDVD8JQ+d+o4T3/EHuxZ2EJXhAXJDc/FoKzru +Pb1JBxNB1O0QCmE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- diff --git a/anki/consts.py b/anki/consts.py index 2c4508dd9..cb8d898c8 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -43,6 +43,8 @@ SYNC_HOST = os.environ.get("SYNC_HOST") or "dev.ankiweb.net" SYNC_PORT = int(os.environ.get("SYNC_PORT") or 80) SYNC_URL = "http://%s:%d/sync/" % (SYNC_HOST, SYNC_PORT) SYNC_VER = 0 +HTTP_CERTS = os.path.join(os.path.basename(__file__), "ankiweb.certs") +HTTP_TIMEOUT = 60 # deck schema SCHEMA_VERSION = 1 diff --git a/anki/sync.py b/anki/sync.py index 5d40adb3e..688e8454f 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -7,7 +7,7 @@ from cStringIO import StringIO from datetime import date from anki.db import DB from anki.errors import * -from anki.utils import ids2str, checksum, intTime +from anki.utils import ids2str, checksum, intTime, httpCon from anki.consts import * from anki.lang import _ from hooks import runHook @@ -383,7 +383,7 @@ class HttpSyncer(object): # retrieving a host key for future operations def hostKey(self, pw): - h = httplib2.Http(timeout=60) + h = httpCon() resp, cont = h.request( SYNC_URL+"hostKey?" + urllib.urlencode(dict(u=self.user,p=pw))) if resp['status'] != '200': @@ -452,11 +452,10 @@ class RemoteServer(Syncer, HttpSyncer): def __init__(self, user, hkey): self.user = user self.hkey = hkey - self.con = None + self.con = httpCon() def meta(self): - h = httplib2.Http(timeout=60) - resp, cont = h.request( + resp, cont = self.con.request( SYNC_URL+"meta?" + urllib.urlencode(dict(u=self.user,v=SYNC_VER))) # fixme: convert these into easily-catchable errors if resp['status'] in ('503', '504'): @@ -470,7 +469,6 @@ class RemoteServer(Syncer, HttpSyncer): return simplejson.loads(cont) def applyChanges(self, **kw): - self.con = httplib2.Http(timeout=60) return self._run("applyChanges", kw) def chunk(self, **kw): @@ -495,16 +493,14 @@ class RemoteServer(Syncer, HttpSyncer): class FullSyncer(HttpSyncer): - def __init__(self, col, hkey): + def __init__(self, col, hkey, con): self.col = col self.hkey = hkey - - def _con(self): - return httplib2.Http(timeout=60) + self.con = con def download(self): self.col.close() - resp, cont = self._con().request( + resp, cont = self.con.request( SYNC_URL+"download?" + urllib.urlencode(self._vars())) if resp['status'] != '200': raise Exception("Invalid response code: %s" % resp['status']) @@ -518,7 +514,7 @@ class FullSyncer(HttpSyncer): def upload(self): self.col.beforeUpload() - assert self.postData(self._con(), "upload", open(self.col.path, "rb"), + assert self.postData(self.con, "upload", open(self.col.path, "rb"), self._vars(), comp=6) == "OK" # Media syncing @@ -589,9 +585,9 @@ class MediaSyncer(object): class RemoteMediaServer(MediaSyncer, HttpSyncer): - def __init__(self, hkey): + def __init__(self, hkey, con): self.hkey = hkey - self.con = httplib2.Http(timeout=60) + self.con = con def remove(self, **kw): return simplejson.loads( diff --git a/anki/utils.py b/anki/utils.py index b6e375175..772fa3487 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -3,9 +3,10 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import re, os, random, time, types, math, htmlentitydefs, subprocess, \ - tempfile, shutil, string + tempfile, shutil, string, httplib2 from hashlib import sha1 from anki.lang import _, ngettext +from anki.consts import * import locale, sys if sys.version_info[1] < 5: @@ -297,3 +298,13 @@ def call(argv, wait=True, **kwargs): isMac = sys.platform.startswith("darwin") isWin = sys.platform.startswith("win32") + +# OS helpers +############################################################################## + +def httpCon(): + disable = os.environ.get("SSL_NOVALIDATE") or False + return httplib2.Http( + timeout=HTTP_TIMEOUT, + disable_ssl_certificate_validation=disable, + ca_certs=HTTP_CERTS) diff --git a/tests/test_remote_sync.py b/tests/test_remote_sync.py index d34df11a2..2f9b0d3f8 100644 --- a/tests/test_remote_sync.py +++ b/tests/test_remote_sync.py @@ -4,7 +4,7 @@ import nose, os, tempfile, shutil, time from tests.shared import assertException from anki.errors import * -from anki.utils import intTime +from anki.utils import intTime, httpCon from anki.sync import Syncer, FullSyncer, LocalServer, RemoteServer, \ MediaSyncer, RemoteMediaServer from anki.notes import Note @@ -65,7 +65,7 @@ def test_hkey(): def test_download(): if not TEST_REMOTE: return - f = FullSyncer(ts.client.col, "abc") + f = FullSyncer(ts.client.col, "abc", ts.server.con) assertException(Exception, f.download) f.hkey = TEST_HKEY f.download() @@ -77,7 +77,7 @@ def test_remoteSync(): # not yet associated, so will require a full sync assert ts.client.sync() == "fullSync" # upload - f = FullSyncer(ts.client.col, TEST_HKEY) + f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con) f.upload() ts.client.col.reopen() # should report no changes @@ -89,7 +89,7 @@ def test_remoteSync(): assert ts.client.sync() == "noChanges" # downloading the remote col should give us the same mod lmod = ts.client.col.mod - f = FullSyncer(ts.client.col, TEST_HKEY) + f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con) f.download() d = aopen(ts.client.col.path) assert d.mod == lmod @@ -101,7 +101,8 @@ def test_remoteSync(): def setup_remoteMedia(): setup_basic() - ts.server = RemoteMediaServer(TEST_HKEY) + con = httpCon() + ts.server = RemoteMediaServer(TEST_HKEY, con) ts.server2 = RemoteServer(TEST_USER, TEST_HKEY) ts.client = MediaSyncer(ts.deck1, ts.server)