test_apache.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. """tests for passlib.apache -- (c) Assurance Technologies 2008-2011"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement
  6. # core
  7. from logging import getLogger
  8. import os
  9. import subprocess
  10. # site
  11. # pkg
  12. from passlib import apache, registry
  13. from passlib.exc import MissingBackendError
  14. from passlib.utils.compat import irange
  15. from passlib.tests.backports import unittest
  16. from passlib.tests.utils import TestCase, get_file, set_file, ensure_mtime_changed
  17. from passlib.utils.compat import u
  18. from passlib.utils import to_bytes
  19. from passlib.utils.handlers import to_unicode_for_identify
  20. # module
  21. log = getLogger(__name__)
  22. #=============================================================================
  23. # helpers
  24. #=============================================================================
  25. def backdate_file_mtime(path, offset=10):
  26. """backdate file's mtime by specified amount"""
  27. # NOTE: this is used so we can test code which detects mtime changes,
  28. # without having to actually *pause* for that long.
  29. atime = os.path.getatime(path)
  30. mtime = os.path.getmtime(path)-offset
  31. os.utime(path, (atime, mtime))
  32. #=============================================================================
  33. # detect external HTPASSWD tool
  34. #=============================================================================
  35. htpasswd_path = os.environ.get("PASSLIB_TEST_HTPASSWD_PATH") or "htpasswd"
  36. def _call_htpasswd(args, stdin=None):
  37. """
  38. helper to run htpasswd cmd
  39. """
  40. if stdin is not None:
  41. stdin = stdin.encode("utf-8")
  42. proc = subprocess.Popen([htpasswd_path] + args, stdout=subprocess.PIPE,
  43. stderr=subprocess.STDOUT, stdin=subprocess.PIPE if stdin else None)
  44. out, err = proc.communicate(stdin)
  45. rc = proc.wait()
  46. out = to_unicode_for_identify(out or "")
  47. return out, rc
  48. def _call_htpasswd_verify(path, user, password):
  49. """
  50. wrapper for htpasswd verify
  51. """
  52. out, rc = _call_htpasswd(["-vi", path, user], password)
  53. return not rc
  54. def _detect_htpasswd():
  55. """
  56. helper to check if htpasswd is present
  57. """
  58. try:
  59. out, rc = _call_htpasswd([])
  60. except OSError:
  61. # TODO: under py3, could trap the more specific FileNotFoundError
  62. # cmd not found
  63. return False, False
  64. # when called w/o args, it should print usage to stderr & return rc=2
  65. if not rc:
  66. log.warning("htpasswd test returned with rc=0")
  67. have_bcrypt = " -B " in out
  68. return True, have_bcrypt
  69. HAVE_HTPASSWD, HAVE_HTPASSWD_BCRYPT = _detect_htpasswd()
  70. requires_htpasswd_cmd = unittest.skipUnless(HAVE_HTPASSWD, "requires `htpasswd` cmdline tool")
  71. #=============================================================================
  72. # htpasswd
  73. #=============================================================================
  74. class HtpasswdFileTest(TestCase):
  75. """test HtpasswdFile class"""
  76. descriptionPrefix = "HtpasswdFile"
  77. # sample with 4 users
  78. sample_01 = (b'user2:2CHkkwa2AtqGs\n'
  79. b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
  80. b'user4:pass4\n'
  81. b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n')
  82. # sample 1 with user 1, 2 deleted; 4 changed
  83. sample_02 = b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\n'
  84. # sample 1 with user2 updated, user 1 first entry removed, and user 5 added
  85. sample_03 = (b'user2:pass2x\n'
  86. b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
  87. b'user4:pass4\n'
  88. b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
  89. b'user5:pass5\n')
  90. # standalone sample with 8-bit username
  91. sample_04_utf8 = b'user\xc3\xa6:2CHkkwa2AtqGs\n'
  92. sample_04_latin1 = b'user\xe6:2CHkkwa2AtqGs\n'
  93. sample_dup = b'user1:pass1\nuser1:pass2\n'
  94. # sample with bcrypt & sha256_crypt hashes
  95. sample_05 = (b'user2:2CHkkwa2AtqGs\n'
  96. b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
  97. b'user4:pass4\n'
  98. b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
  99. b'user5:$2a$12$yktDxraxijBZ360orOyCOePFGhuis/umyPNJoL5EbsLk.s6SWdrRO\n'
  100. b'user6:$5$rounds=110000$cCRp/xUUGVgwR4aP$'
  101. b'p0.QKFS5qLNRqw1/47lXYiAcgIjJK.WjCO8nrEKuUK.\n')
  102. def test_00_constructor_autoload(self):
  103. """test constructor autoload"""
  104. # check with existing file
  105. path = self.mktemp()
  106. set_file(path, self.sample_01)
  107. ht = apache.HtpasswdFile(path)
  108. self.assertEqual(ht.to_string(), self.sample_01)
  109. self.assertEqual(ht.path, path)
  110. self.assertTrue(ht.mtime)
  111. # check changing path
  112. ht.path = path + "x"
  113. self.assertEqual(ht.path, path + "x")
  114. self.assertFalse(ht.mtime)
  115. # check new=True
  116. ht = apache.HtpasswdFile(path, new=True)
  117. self.assertEqual(ht.to_string(), b"")
  118. self.assertEqual(ht.path, path)
  119. self.assertFalse(ht.mtime)
  120. # check autoload=False (deprecated alias for new=True)
  121. with self.assertWarningList("``autoload=False`` is deprecated"):
  122. ht = apache.HtpasswdFile(path, autoload=False)
  123. self.assertEqual(ht.to_string(), b"")
  124. self.assertEqual(ht.path, path)
  125. self.assertFalse(ht.mtime)
  126. # check missing file
  127. os.remove(path)
  128. self.assertRaises(IOError, apache.HtpasswdFile, path)
  129. # NOTE: "default_scheme" option checked via set_password() test, among others
  130. def test_00_from_path(self):
  131. path = self.mktemp()
  132. set_file(path, self.sample_01)
  133. ht = apache.HtpasswdFile.from_path(path)
  134. self.assertEqual(ht.to_string(), self.sample_01)
  135. self.assertEqual(ht.path, None)
  136. self.assertFalse(ht.mtime)
  137. def test_01_delete(self):
  138. """test delete()"""
  139. ht = apache.HtpasswdFile.from_string(self.sample_01)
  140. self.assertTrue(ht.delete("user1")) # should delete both entries
  141. self.assertTrue(ht.delete("user2"))
  142. self.assertFalse(ht.delete("user5")) # user not present
  143. self.assertEqual(ht.to_string(), self.sample_02)
  144. # invalid user
  145. self.assertRaises(ValueError, ht.delete, "user:")
  146. def test_01_delete_autosave(self):
  147. path = self.mktemp()
  148. sample = b'user1:pass1\nuser2:pass2\n'
  149. set_file(path, sample)
  150. ht = apache.HtpasswdFile(path)
  151. ht.delete("user1")
  152. self.assertEqual(get_file(path), sample)
  153. ht = apache.HtpasswdFile(path, autosave=True)
  154. ht.delete("user1")
  155. self.assertEqual(get_file(path), b"user2:pass2\n")
  156. def test_02_set_password(self):
  157. """test set_password()"""
  158. ht = apache.HtpasswdFile.from_string(
  159. self.sample_01, default_scheme="plaintext")
  160. self.assertTrue(ht.set_password("user2", "pass2x"))
  161. self.assertFalse(ht.set_password("user5", "pass5"))
  162. self.assertEqual(ht.to_string(), self.sample_03)
  163. # test legacy default kwd
  164. with self.assertWarningList("``default`` is deprecated"):
  165. ht = apache.HtpasswdFile.from_string(self.sample_01, default="plaintext")
  166. self.assertTrue(ht.set_password("user2", "pass2x"))
  167. self.assertFalse(ht.set_password("user5", "pass5"))
  168. self.assertEqual(ht.to_string(), self.sample_03)
  169. # invalid user
  170. self.assertRaises(ValueError, ht.set_password, "user:", "pass")
  171. # test that legacy update() still works
  172. with self.assertWarningList("update\(\) is deprecated"):
  173. ht.update("user2", "test")
  174. self.assertTrue(ht.check_password("user2", "test"))
  175. def test_02_set_password_autosave(self):
  176. path = self.mktemp()
  177. sample = b'user1:pass1\n'
  178. set_file(path, sample)
  179. ht = apache.HtpasswdFile(path)
  180. ht.set_password("user1", "pass2")
  181. self.assertEqual(get_file(path), sample)
  182. ht = apache.HtpasswdFile(path, default_scheme="plaintext", autosave=True)
  183. ht.set_password("user1", "pass2")
  184. self.assertEqual(get_file(path), b"user1:pass2\n")
  185. def test_02_set_password_default_scheme(self):
  186. """test set_password() -- default_scheme"""
  187. def check(scheme):
  188. ht = apache.HtpasswdFile(default_scheme=scheme)
  189. ht.set_password("user1", "pass1")
  190. return ht.context.identify(ht.get_hash("user1"))
  191. # explicit scheme
  192. self.assertEqual(check("sha256_crypt"), "sha256_crypt")
  193. self.assertEqual(check("des_crypt"), "des_crypt")
  194. # unknown scheme
  195. self.assertRaises(KeyError, check, "xxx")
  196. # alias resolution
  197. self.assertEqual(check("portable"), apache.htpasswd_defaults["portable"])
  198. self.assertEqual(check("portable_apache_22"), apache.htpasswd_defaults["portable_apache_22"])
  199. self.assertEqual(check("host_apache_22"), apache.htpasswd_defaults["host_apache_22"])
  200. # default
  201. self.assertEqual(check(None), apache.htpasswd_defaults["portable_apache_22"])
  202. def test_03_users(self):
  203. """test users()"""
  204. ht = apache.HtpasswdFile.from_string(self.sample_01)
  205. ht.set_password("user5", "pass5")
  206. ht.delete("user3")
  207. ht.set_password("user3", "pass3")
  208. self.assertEqual(sorted(ht.users()), ["user1", "user2", "user3", "user4", "user5"])
  209. def test_04_check_password(self):
  210. """test check_password()"""
  211. ht = apache.HtpasswdFile.from_string(self.sample_05)
  212. self.assertRaises(TypeError, ht.check_password, 1, 'pass9')
  213. self.assertTrue(ht.check_password("user9","pass9") is None)
  214. # users 1..6 of sample_01 run through all the main hash formats,
  215. # to make sure they're recognized.
  216. for i in irange(1, 7):
  217. i = str(i)
  218. try:
  219. self.assertTrue(ht.check_password("user"+i, "pass"+i))
  220. self.assertTrue(ht.check_password("user"+i, "pass9") is False)
  221. except MissingBackendError:
  222. if i == "5":
  223. # user5 uses bcrypt, which is apparently not available right now
  224. continue
  225. raise
  226. self.assertRaises(ValueError, ht.check_password, "user:", "pass")
  227. # test that legacy verify() still works
  228. with self.assertWarningList(["verify\(\) is deprecated"]*2):
  229. self.assertTrue(ht.verify("user1", "pass1"))
  230. self.assertFalse(ht.verify("user1", "pass2"))
  231. def test_05_load(self):
  232. """test load()"""
  233. # setup empty file
  234. path = self.mktemp()
  235. set_file(path, "")
  236. backdate_file_mtime(path, 5)
  237. ha = apache.HtpasswdFile(path, default_scheme="plaintext")
  238. self.assertEqual(ha.to_string(), b"")
  239. # make changes, check load_if_changed() does nothing
  240. ha.set_password("user1", "pass1")
  241. ha.load_if_changed()
  242. self.assertEqual(ha.to_string(), b"user1:pass1\n")
  243. # change file
  244. set_file(path, self.sample_01)
  245. ha.load_if_changed()
  246. self.assertEqual(ha.to_string(), self.sample_01)
  247. # make changes, check load() overwrites them
  248. ha.set_password("user5", "pass5")
  249. ha.load()
  250. self.assertEqual(ha.to_string(), self.sample_01)
  251. # test load w/ no path
  252. hb = apache.HtpasswdFile()
  253. self.assertRaises(RuntimeError, hb.load)
  254. self.assertRaises(RuntimeError, hb.load_if_changed)
  255. # test load w/ dups and explicit path
  256. set_file(path, self.sample_dup)
  257. hc = apache.HtpasswdFile()
  258. hc.load(path)
  259. self.assertTrue(hc.check_password('user1','pass1'))
  260. # NOTE: load_string() tested via from_string(), which is used all over this file
  261. def test_06_save(self):
  262. """test save()"""
  263. # load from file
  264. path = self.mktemp()
  265. set_file(path, self.sample_01)
  266. ht = apache.HtpasswdFile(path)
  267. # make changes, check they saved
  268. ht.delete("user1")
  269. ht.delete("user2")
  270. ht.save()
  271. self.assertEqual(get_file(path), self.sample_02)
  272. # test save w/ no path
  273. hb = apache.HtpasswdFile(default_scheme="plaintext")
  274. hb.set_password("user1", "pass1")
  275. self.assertRaises(RuntimeError, hb.save)
  276. # test save w/ explicit path
  277. hb.save(path)
  278. self.assertEqual(get_file(path), b"user1:pass1\n")
  279. def test_07_encodings(self):
  280. """test 'encoding' kwd"""
  281. # test bad encodings cause failure in constructor
  282. self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16")
  283. # check sample utf-8
  284. ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding="utf-8",
  285. return_unicode=True)
  286. self.assertEqual(ht.users(), [ u("user\u00e6") ])
  287. # test deprecated encoding=None
  288. with self.assertWarningList("``encoding=None`` is deprecated"):
  289. ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding=None)
  290. self.assertEqual(ht.users(), [ b'user\xc3\xa6' ])
  291. # check sample latin-1
  292. ht = apache.HtpasswdFile.from_string(self.sample_04_latin1,
  293. encoding="latin-1", return_unicode=True)
  294. self.assertEqual(ht.users(), [ u("user\u00e6") ])
  295. def test_08_get_hash(self):
  296. """test get_hash()"""
  297. ht = apache.HtpasswdFile.from_string(self.sample_01)
  298. self.assertEqual(ht.get_hash("user3"), b"{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=")
  299. self.assertEqual(ht.get_hash("user4"), b"pass4")
  300. self.assertEqual(ht.get_hash("user5"), None)
  301. with self.assertWarningList("find\(\) is deprecated"):
  302. self.assertEqual(ht.find("user4"), b"pass4")
  303. def test_09_to_string(self):
  304. """test to_string"""
  305. # check with known sample
  306. ht = apache.HtpasswdFile.from_string(self.sample_01)
  307. self.assertEqual(ht.to_string(), self.sample_01)
  308. # test blank
  309. ht = apache.HtpasswdFile()
  310. self.assertEqual(ht.to_string(), b"")
  311. def test_10_repr(self):
  312. ht = apache.HtpasswdFile("fakepath", autosave=True, new=True, encoding="latin-1")
  313. repr(ht)
  314. def test_11_malformed(self):
  315. self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
  316. b'realm:user1:pass1\n')
  317. self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
  318. b'pass1\n')
  319. def test_12_from_string(self):
  320. # forbid path kwd
  321. self.assertRaises(TypeError, apache.HtpasswdFile.from_string,
  322. b'', path=None)
  323. def test_13_whitespace(self):
  324. """whitespace & comment handling"""
  325. # per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c),
  326. # lines that match "^\s*(#.*)?$" should be ignored
  327. source = to_bytes(
  328. '\n'
  329. 'user2:pass2\n'
  330. 'user4:pass4\n'
  331. 'user7:pass7\r\n'
  332. ' \t \n'
  333. 'user1:pass1\n'
  334. ' # legacy users\n'
  335. '#user6:pass6\n'
  336. 'user5:pass5\n\n'
  337. )
  338. # loading should see all users (except user6, who was commented out)
  339. ht = apache.HtpasswdFile.from_string(source)
  340. self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"])
  341. # update existing user
  342. ht.set_hash("user4", "althash4")
  343. self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"])
  344. # add a new user
  345. ht.set_hash("user6", "althash6")
  346. self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6", "user7"])
  347. # delete existing user
  348. ht.delete("user7")
  349. self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6"])
  350. # re-serialization should preserve whitespace
  351. target = to_bytes(
  352. '\n'
  353. 'user2:pass2\n'
  354. 'user4:althash4\n'
  355. ' \t \n'
  356. 'user1:pass1\n'
  357. ' # legacy users\n'
  358. '#user6:pass6\n'
  359. 'user5:pass5\n'
  360. 'user6:althash6\n'
  361. )
  362. self.assertEqual(ht.to_string(), target)
  363. @requires_htpasswd_cmd
  364. def test_htpasswd_cmd_verify(self):
  365. """
  366. verify "htpasswd" command can read output
  367. """
  368. path = self.mktemp()
  369. ht = apache.HtpasswdFile(path=path, new=True)
  370. def hash_scheme(pwd, scheme):
  371. return ht.context.handler(scheme).hash(pwd)
  372. # base scheme
  373. ht.set_hash("user1", hash_scheme("password","apr_md5_crypt"))
  374. # 2.2-compat scheme
  375. host_no_bcrypt = apache.htpasswd_defaults["host_apache_22"]
  376. ht.set_hash("user2", hash_scheme("password", host_no_bcrypt))
  377. # 2.4-compat scheme
  378. host_best = apache.htpasswd_defaults["host"]
  379. ht.set_hash("user3", hash_scheme("password", host_best))
  380. # unsupported scheme -- should always fail to verify
  381. ht.set_hash("user4", "$xxx$foo$bar$baz")
  382. # make sure htpasswd properly recognizes hashes
  383. ht.save()
  384. self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
  385. self.assertFalse(_call_htpasswd_verify(path, "user2", "wrong"))
  386. self.assertFalse(_call_htpasswd_verify(path, "user3", "wrong"))
  387. self.assertFalse(_call_htpasswd_verify(path, "user4", "wrong"))
  388. self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
  389. self.assertTrue(_call_htpasswd_verify(path, "user2", "password"))
  390. self.assertTrue(_call_htpasswd_verify(path, "user3", "password"))
  391. @requires_htpasswd_cmd
  392. @unittest.skipUnless(registry.has_backend("bcrypt"), "bcrypt support required")
  393. def test_htpasswd_cmd_verify_bcrypt(self):
  394. """
  395. verify "htpasswd" command can read bcrypt format
  396. this tests for regression of issue 95, where we output "$2b$" instead of "$2y$";
  397. fixed in v1.7.2.
  398. """
  399. path = self.mktemp()
  400. ht = apache.HtpasswdFile(path=path, new=True)
  401. def hash_scheme(pwd, scheme):
  402. return ht.context.handler(scheme).hash(pwd)
  403. ht.set_hash("user1", hash_scheme("password", "bcrypt"))
  404. ht.save()
  405. self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
  406. if HAVE_HTPASSWD_BCRYPT:
  407. self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
  408. else:
  409. # apache2.2 should fail, acting like it's an unknown hash format
  410. self.assertFalse(_call_htpasswd_verify(path, "user1", "password"))
  411. #===================================================================
  412. # eoc
  413. #===================================================================
  414. #=============================================================================
  415. # htdigest
  416. #=============================================================================
  417. class HtdigestFileTest(TestCase):
  418. """test HtdigestFile class"""
  419. descriptionPrefix = "HtdigestFile"
  420. # sample with 4 users
  421. sample_01 = (b'user2:realm:549d2a5f4659ab39a80dac99e159ab19\n'
  422. b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
  423. b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
  424. b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')
  425. # sample 1 with user 1, 2 deleted; 4 changed
  426. sample_02 = (b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
  427. b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n')
  428. # sample 1 with user2 updated, user 1 first entry removed, and user 5 added
  429. sample_03 = (b'user2:realm:5ba6d8328943c23c64b50f8b29566059\n'
  430. b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
  431. b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
  432. b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n'
  433. b'user5:realm:03c55fdc6bf71552356ad401bdb9af19\n')
  434. # standalone sample with 8-bit username & realm
  435. sample_04_utf8 = b'user\xc3\xa6:realm\xc3\xa6:549d2a5f4659ab39a80dac99e159ab19\n'
  436. sample_04_latin1 = b'user\xe6:realm\xe6:549d2a5f4659ab39a80dac99e159ab19\n'
  437. def test_00_constructor_autoload(self):
  438. """test constructor autoload"""
  439. # check with existing file
  440. path = self.mktemp()
  441. set_file(path, self.sample_01)
  442. ht = apache.HtdigestFile(path)
  443. self.assertEqual(ht.to_string(), self.sample_01)
  444. # check without autoload
  445. ht = apache.HtdigestFile(path, new=True)
  446. self.assertEqual(ht.to_string(), b"")
  447. # check missing file
  448. os.remove(path)
  449. self.assertRaises(IOError, apache.HtdigestFile, path)
  450. # NOTE: default_realm option checked via other tests.
  451. def test_01_delete(self):
  452. """test delete()"""
  453. ht = apache.HtdigestFile.from_string(self.sample_01)
  454. self.assertTrue(ht.delete("user1", "realm"))
  455. self.assertTrue(ht.delete("user2", "realm"))
  456. self.assertFalse(ht.delete("user5", "realm"))
  457. self.assertFalse(ht.delete("user3", "realm5"))
  458. self.assertEqual(ht.to_string(), self.sample_02)
  459. # invalid user
  460. self.assertRaises(ValueError, ht.delete, "user:", "realm")
  461. # invalid realm
  462. self.assertRaises(ValueError, ht.delete, "user", "realm:")
  463. def test_01_delete_autosave(self):
  464. path = self.mktemp()
  465. set_file(path, self.sample_01)
  466. ht = apache.HtdigestFile(path)
  467. self.assertTrue(ht.delete("user1", "realm"))
  468. self.assertFalse(ht.delete("user3", "realm5"))
  469. self.assertFalse(ht.delete("user5", "realm"))
  470. self.assertEqual(get_file(path), self.sample_01)
  471. ht.autosave = True
  472. self.assertTrue(ht.delete("user2", "realm"))
  473. self.assertEqual(get_file(path), self.sample_02)
  474. def test_02_set_password(self):
  475. """test update()"""
  476. ht = apache.HtdigestFile.from_string(self.sample_01)
  477. self.assertTrue(ht.set_password("user2", "realm", "pass2x"))
  478. self.assertFalse(ht.set_password("user5", "realm", "pass5"))
  479. self.assertEqual(ht.to_string(), self.sample_03)
  480. # default realm
  481. self.assertRaises(TypeError, ht.set_password, "user2", "pass3")
  482. ht.default_realm = "realm2"
  483. ht.set_password("user2", "pass3")
  484. ht.check_password("user2", "realm2", "pass3")
  485. # invalid user
  486. self.assertRaises(ValueError, ht.set_password, "user:", "realm", "pass")
  487. self.assertRaises(ValueError, ht.set_password, "u"*256, "realm", "pass")
  488. # invalid realm
  489. self.assertRaises(ValueError, ht.set_password, "user", "realm:", "pass")
  490. self.assertRaises(ValueError, ht.set_password, "user", "r"*256, "pass")
  491. # test that legacy update() still works
  492. with self.assertWarningList("update\(\) is deprecated"):
  493. ht.update("user2", "realm2", "test")
  494. self.assertTrue(ht.check_password("user2", "test"))
  495. # TODO: test set_password autosave
  496. def test_03_users(self):
  497. """test users()"""
  498. ht = apache.HtdigestFile.from_string(self.sample_01)
  499. ht.set_password("user5", "realm", "pass5")
  500. ht.delete("user3", "realm")
  501. ht.set_password("user3", "realm", "pass3")
  502. self.assertEqual(sorted(ht.users("realm")), ["user1", "user2", "user3", "user4", "user5"])
  503. self.assertRaises(TypeError, ht.users, 1)
  504. def test_04_check_password(self):
  505. """test check_password()"""
  506. ht = apache.HtdigestFile.from_string(self.sample_01)
  507. self.assertRaises(TypeError, ht.check_password, 1, 'realm', 'pass5')
  508. self.assertRaises(TypeError, ht.check_password, 'user', 1, 'pass5')
  509. self.assertIs(ht.check_password("user5", "realm","pass5"), None)
  510. for i in irange(1,5):
  511. i = str(i)
  512. self.assertTrue(ht.check_password("user"+i, "realm", "pass"+i))
  513. self.assertIs(ht.check_password("user"+i, "realm", "pass5"), False)
  514. # default realm
  515. self.assertRaises(TypeError, ht.check_password, "user5", "pass5")
  516. ht.default_realm = "realm"
  517. self.assertTrue(ht.check_password("user1", "pass1"))
  518. self.assertIs(ht.check_password("user5", "pass5"), None)
  519. # test that legacy verify() still works
  520. with self.assertWarningList(["verify\(\) is deprecated"]*2):
  521. self.assertTrue(ht.verify("user1", "realm", "pass1"))
  522. self.assertFalse(ht.verify("user1", "realm", "pass2"))
  523. # invalid user
  524. self.assertRaises(ValueError, ht.check_password, "user:", "realm", "pass")
  525. def test_05_load(self):
  526. """test load()"""
  527. # setup empty file
  528. path = self.mktemp()
  529. set_file(path, "")
  530. backdate_file_mtime(path, 5)
  531. ha = apache.HtdigestFile(path)
  532. self.assertEqual(ha.to_string(), b"")
  533. # make changes, check load_if_changed() does nothing
  534. ha.set_password("user1", "realm", "pass1")
  535. ha.load_if_changed()
  536. self.assertEqual(ha.to_string(), b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')
  537. # change file
  538. set_file(path, self.sample_01)
  539. ha.load_if_changed()
  540. self.assertEqual(ha.to_string(), self.sample_01)
  541. # make changes, check load_if_changed overwrites them
  542. ha.set_password("user5", "realm", "pass5")
  543. ha.load()
  544. self.assertEqual(ha.to_string(), self.sample_01)
  545. # test load w/ no path
  546. hb = apache.HtdigestFile()
  547. self.assertRaises(RuntimeError, hb.load)
  548. self.assertRaises(RuntimeError, hb.load_if_changed)
  549. # test load w/ explicit path
  550. hc = apache.HtdigestFile()
  551. hc.load(path)
  552. self.assertEqual(hc.to_string(), self.sample_01)
  553. # change file, test deprecated force=False kwd
  554. ensure_mtime_changed(path)
  555. set_file(path, "")
  556. with self.assertWarningList(r"load\(force=False\) is deprecated"):
  557. ha.load(force=False)
  558. self.assertEqual(ha.to_string(), b"")
  559. def test_06_save(self):
  560. """test save()"""
  561. # load from file
  562. path = self.mktemp()
  563. set_file(path, self.sample_01)
  564. ht = apache.HtdigestFile(path)
  565. # make changes, check they saved
  566. ht.delete("user1", "realm")
  567. ht.delete("user2", "realm")
  568. ht.save()
  569. self.assertEqual(get_file(path), self.sample_02)
  570. # test save w/ no path
  571. hb = apache.HtdigestFile()
  572. hb.set_password("user1", "realm", "pass1")
  573. self.assertRaises(RuntimeError, hb.save)
  574. # test save w/ explicit path
  575. hb.save(path)
  576. self.assertEqual(get_file(path), hb.to_string())
  577. def test_07_realms(self):
  578. """test realms() & delete_realm()"""
  579. ht = apache.HtdigestFile.from_string(self.sample_01)
  580. self.assertEqual(ht.delete_realm("x"), 0)
  581. self.assertEqual(ht.realms(), ['realm'])
  582. self.assertEqual(ht.delete_realm("realm"), 4)
  583. self.assertEqual(ht.realms(), [])
  584. self.assertEqual(ht.to_string(), b"")
  585. def test_08_get_hash(self):
  586. """test get_hash()"""
  587. ht = apache.HtdigestFile.from_string(self.sample_01)
  588. self.assertEqual(ht.get_hash("user3", "realm"), "a500bb8c02f6a9170ae46af10c898744")
  589. self.assertEqual(ht.get_hash("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
  590. self.assertEqual(ht.get_hash("user5", "realm"), None)
  591. with self.assertWarningList("find\(\) is deprecated"):
  592. self.assertEqual(ht.find("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
  593. def test_09_encodings(self):
  594. """test encoding parameter"""
  595. # test bad encodings cause failure in constructor
  596. self.assertRaises(ValueError, apache.HtdigestFile, encoding="utf-16")
  597. # check sample utf-8
  598. ht = apache.HtdigestFile.from_string(self.sample_04_utf8, encoding="utf-8", return_unicode=True)
  599. self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
  600. self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
  601. # check sample latin-1
  602. ht = apache.HtdigestFile.from_string(self.sample_04_latin1, encoding="latin-1", return_unicode=True)
  603. self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
  604. self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
  605. def test_10_to_string(self):
  606. """test to_string()"""
  607. # check sample
  608. ht = apache.HtdigestFile.from_string(self.sample_01)
  609. self.assertEqual(ht.to_string(), self.sample_01)
  610. # check blank
  611. ht = apache.HtdigestFile()
  612. self.assertEqual(ht.to_string(), b"")
  613. def test_11_malformed(self):
  614. self.assertRaises(ValueError, apache.HtdigestFile.from_string,
  615. b'realm:user1:pass1:other\n')
  616. self.assertRaises(ValueError, apache.HtdigestFile.from_string,
  617. b'user1:pass1\n')
  618. #===================================================================
  619. # eoc
  620. #===================================================================
  621. #=============================================================================
  622. # eof
  623. #=============================================================================