mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 14:21:39 +00:00
Compare commits
559 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b6824ee1 | ||
|
|
2902fc8931 | ||
|
|
1a6ec398b2 | ||
|
|
7d849e0cc0 | ||
|
|
2958bd9f86 | ||
|
|
15d93c9e5d | ||
|
|
082c34b453 | ||
|
|
32e7932050 | ||
|
|
6becd08f3c | ||
|
|
26fbf9c524 | ||
|
|
d9124f3ffa | ||
|
|
aa1db89bd3 | ||
|
|
ff3a8644ec | ||
|
|
4721469b1d | ||
|
|
c4a3d29964 | ||
|
|
1454526e65 | ||
|
|
2c0026512d | ||
|
|
3c85da292e | ||
|
|
c7b5251b03 | ||
|
|
5307a560bd | ||
|
|
059e6a1813 | ||
|
|
395a561b8c | ||
|
|
6c3a744ed3 | ||
|
|
907126d642 | ||
|
|
9e4506141e | ||
|
|
5deac72484 | ||
|
|
9def6e6d73 | ||
|
|
fefe9de384 | ||
|
|
f8341be9ea | ||
|
|
3c50f464cc | ||
|
|
6961a0e1b3 | ||
|
|
3fa6cde6b0 | ||
|
|
4129e05f5e | ||
|
|
42137297a1 | ||
|
|
bc64e9a67c | ||
|
|
e1ec8b8649 | ||
|
|
b9ec06807b | ||
|
|
e3d826cdb3 | ||
|
|
4306dba9f1 | ||
|
|
cf397c228c | ||
|
|
a2a6719333 | ||
|
|
7cef626a6f | ||
|
|
be44ae4322 | ||
|
|
4c00b54ad4 | ||
|
|
8508ee4afa | ||
|
|
17b2c4091d | ||
|
|
925f4532bc | ||
|
|
786bbe5609 | ||
|
|
6be52c8b3c | ||
|
|
2c2046a784 | ||
|
|
20de452685 | ||
|
|
df603937ee | ||
|
|
315a1db144 | ||
|
|
968c096a99 | ||
|
|
6703519d36 | ||
|
|
9dd8696c1e | ||
|
|
df7c12b737 | ||
|
|
7cfba0ada1 | ||
|
|
2ee5109424 | ||
|
|
721f787f0f | ||
|
|
bc62f7a9f6 | ||
|
|
4eee744321 | ||
|
|
0dbd460b71 | ||
|
|
5c27695613 | ||
|
|
e52e939afa | ||
|
|
60a97b3e26 | ||
|
|
fb41698e5f | ||
|
|
9397c1c07c | ||
|
|
071231310c | ||
|
|
1c92ad3d6d | ||
|
|
facd10d882 | ||
|
|
e634a04dc7 | ||
|
|
f291e9c4d3 | ||
|
|
ed48e5ecd9 | ||
|
|
34e55a492e | ||
|
|
abf3d8fa1c | ||
|
|
6143d00476 | ||
|
|
a2fe11fd70 | ||
|
|
78ce646eee | ||
|
|
3115c578a0 | ||
|
|
5eb029baf1 | ||
|
|
cf0af20d89 | ||
|
|
ce067c3e2d | ||
|
|
801078e618 | ||
|
|
dabb3362c8 | ||
|
|
8d2af5ca66 | ||
|
|
31cdf55af8 | ||
|
|
c4dffd7584 | ||
|
|
14c86e3780 | ||
|
|
cc1126ff2f | ||
|
|
6b0fb9e54d | ||
|
|
8435d41d44 | ||
|
|
e92b9690d4 | ||
|
|
953ea36177 | ||
|
|
36d0d7da7b | ||
|
|
e101231f9b | ||
|
|
3090c59582 | ||
|
|
1de7e32f25 | ||
|
|
2bf058b089 | ||
|
|
c507714ab6 | ||
|
|
cdc72d2f23 | ||
|
|
2368adfebd | ||
|
|
46c000f6d1 | ||
|
|
4254ed4d86 | ||
|
|
3233807c17 | ||
|
|
b3501a2d4a | ||
|
|
3d5ac7c448 | ||
|
|
11e2a3845d | ||
|
|
9812c23513 | ||
|
|
5c02c1b50c | ||
|
|
f3f3f91fcb | ||
|
|
f8be56efdc | ||
|
|
dcae0155b2 | ||
|
|
7c21c670dc | ||
|
|
44c0d4bf15 | ||
|
|
39159d3ce7 | ||
|
|
62565d7c97 | ||
|
|
b8b268f0ad | ||
|
|
4182b3cd1e | ||
|
|
24c0390174 | ||
|
|
2b336d8c9f | ||
|
|
5b02a22d77 | ||
|
|
b680a6bf12 | ||
|
|
2b65d87bfa | ||
|
|
e214ad8154 | ||
|
|
3bf2ecc6e3 | ||
|
|
594e10d0a4 | ||
|
|
656f87f89c | ||
|
|
a5dd9b69e3 | ||
|
|
ae8949c6d6 | ||
|
|
eeb29355f1 | ||
|
|
4dea7cb650 | ||
|
|
da4177f0e4 | ||
|
|
c7b44ae8d9 | ||
|
|
37726c493d | ||
|
|
83af5ee757 | ||
|
|
c35df829b0 | ||
|
|
1539c37576 | ||
|
|
34b9029e90 | ||
|
|
50abefd5f9 | ||
|
|
0508554e2c | ||
|
|
67ab67c81b | ||
|
|
7bf263fd35 | ||
|
|
88067e64e6 | ||
|
|
8254b1040c | ||
|
|
fdaf6508b1 | ||
|
|
bbc9b8fb2b | ||
|
|
f229ab7de2 | ||
|
|
3b5b5be881 | ||
|
|
b9155f42d6 | ||
|
|
aa48edc0ff | ||
|
|
2f306d1181 | ||
|
|
0e01a32c64 | ||
|
|
a1b2e3b63b | ||
|
|
219509853f | ||
|
|
f66aa71ec1 | ||
|
|
e1e49f8edf | ||
|
|
bbb70421ed | ||
|
|
7a2785ba5c | ||
|
|
2c15230756 | ||
|
|
232755590d | ||
|
|
ddd040e395 | ||
|
|
a9dc255979 | ||
|
|
785073eb87 | ||
|
|
368a6d1217 | ||
|
|
13d2f9dd96 | ||
|
|
e85e54b86a | ||
|
|
e39d441d01 | ||
|
|
f6f15c8801 | ||
|
|
0dc8accb64 | ||
|
|
79964892f6 | ||
|
|
b648ddbfdf | ||
|
|
cab068357a | ||
|
|
acb4b39953 | ||
|
|
74481b1c31 | ||
|
|
aa9cd3f1ca | ||
|
|
7f67a3043a | ||
|
|
734413266b | ||
|
|
159d4cf085 | ||
|
|
4b380be637 | ||
|
|
0f84ee1e07 | ||
|
|
38df869afc | ||
|
|
c05c2ffaa2 | ||
|
|
bb86db3cd3 | ||
|
|
9e2e9632c2 | ||
|
|
2b686ebe74 | ||
|
|
15043d1de8 | ||
|
|
e00151ef39 | ||
|
|
160ea6aa5b | ||
|
|
7e3297e8c7 | ||
|
|
880a9c8939 | ||
|
|
64f9cbd54f | ||
|
|
6b97662f8a | ||
|
|
71f37acdf4 | ||
|
|
a58c300e84 | ||
|
|
b1ce6544f5 | ||
|
|
d5daf24c15 | ||
|
|
9b9aea9841 | ||
|
|
1145faef56 | ||
|
|
e42ca916fe | ||
|
|
24611614d6 | ||
|
|
6df45551e5 | ||
|
|
7c211c664f | ||
|
|
f07a2ef2be | ||
|
|
3df6d1f833 | ||
|
|
f6396d5e4a | ||
|
|
8db8c48fd9 | ||
|
|
8cc4710874 | ||
|
|
d42f4d764a | ||
|
|
21715f079b | ||
|
|
b345cb063f | ||
|
|
d6fdaa2874 | ||
|
|
daf2735eb3 | ||
|
|
68ad322f41 | ||
|
|
f688404ac6 | ||
|
|
13a9024dd1 | ||
|
|
2fdfc3750d | ||
|
|
9246aed660 | ||
|
|
0efb736b42 | ||
|
|
ef7fcb2114 | ||
|
|
52f4c049e1 | ||
|
|
ee00a41ff9 | ||
|
|
289fda94df | ||
|
|
49d1845129 | ||
|
|
d850f5575b | ||
|
|
1f2ffcf97a | ||
|
|
4ea63c3167 | ||
|
|
f2887abb49 | ||
|
|
3ada129e7f | ||
|
|
6c3a0e2b71 | ||
|
|
457feac4ac | ||
|
|
45728fbbda | ||
|
|
0d507855bd | ||
|
|
5fa2f3d955 | ||
|
|
bfc734138d | ||
|
|
fdfa830aa0 | ||
|
|
d78ea8efeb | ||
|
|
d47f5967e9 | ||
|
|
2debf9507a | ||
|
|
e527cd42a1 | ||
|
|
38399c0e1e | ||
|
|
43782ca3f8 | ||
|
|
b5f911e259 | ||
|
|
4596accf5a | ||
|
|
d4cad7a242 | ||
|
|
9ba732e0bf | ||
|
|
0034704b3f | ||
|
|
b6caa8a5ba | ||
|
|
cca9684ffb | ||
|
|
c00259a5b6 | ||
|
|
455730dad8 | ||
|
|
b17b80ee12 | ||
|
|
4fadf68da4 | ||
|
|
fb9aebf123 | ||
|
|
087c6775e3 | ||
|
|
a3c509ce61 | ||
|
|
6b0fce21a5 | ||
|
|
7b8b4674e7 | ||
|
|
4956873357 | ||
|
|
4dd1d6a244 | ||
|
|
65367947e0 | ||
|
|
a83414f831 | ||
|
|
bba894bdc3 | ||
|
|
190c4f212d | ||
|
|
d8a78d96ae | ||
|
|
1d30eb7d91 | ||
|
|
ac86758e79 | ||
|
|
afee6c32a3 | ||
|
|
7412236679 | ||
|
|
3ef433687a | ||
|
|
82d43d0b62 | ||
|
|
bf31e72384 | ||
|
|
2a37589a9f | ||
|
|
a334645910 | ||
|
|
6519a5b007 | ||
|
|
cafa01248a | ||
|
|
5ab14fef05 | ||
|
|
6b6ada5b2c | ||
|
|
18420275af | ||
|
|
8f283acf66 | ||
|
|
f27df74339 | ||
|
|
ca059a62a6 | ||
|
|
6dae2302c0 | ||
|
|
bae5f20ec4 | ||
|
|
51a4d92a90 | ||
|
|
d527f4104f | ||
|
|
2d74916ca5 | ||
|
|
0aabe4ae9b | ||
|
|
0a41b4ec68 | ||
|
|
9cf4a151aa | ||
|
|
10fcf566b8 | ||
|
|
2704a8b695 | ||
|
|
09814b7dcd | ||
|
|
4f2ce2625d | ||
|
|
9c368b7d10 | ||
|
|
f9bd5506c7 | ||
|
|
4a168d16a3 | ||
|
|
b817bd04ec | ||
|
|
43adae4e70 | ||
|
|
77ebba9c62 | ||
|
|
ee517c1800 | ||
|
|
154099c3f4 | ||
|
|
1746845651 | ||
|
|
d07ab2d7e1 | ||
|
|
f3b970ae14 | ||
|
|
16d8cddf12 | ||
|
|
671f7d810c | ||
|
|
0d94dd3fa5 | ||
|
|
eb5cfde630 | ||
|
|
aa04e3ec1d | ||
|
|
0333e29eef | ||
|
|
8929ee534f | ||
|
|
8eb347488f | ||
|
|
f8642a18df | ||
|
|
12ccd58eae | ||
|
|
95bb288e38 | ||
|
|
5c64f0825f | ||
|
|
25e97b97d4 | ||
|
|
d258d4da63 | ||
|
|
cb79688a73 | ||
|
|
dae75f6234 | ||
|
|
0209b51c4d | ||
|
|
f35c188496 | ||
|
|
c00d820c75 | ||
|
|
69689e286b | ||
|
|
5697580bd0 | ||
|
|
2288e99e56 | ||
|
|
c8fd6e76af | ||
|
|
23cb9afec7 | ||
|
|
20e8175ae1 | ||
|
|
3a38dceb5f | ||
|
|
c885cdefbb | ||
|
|
b7d5374718 | ||
|
|
59a0aadd72 | ||
|
|
a3f218a98d | ||
|
|
0424ced649 | ||
|
|
9f75968684 | ||
|
|
90fd503838 | ||
|
|
a7a3f2eef6 | ||
|
|
008a65329e | ||
|
|
2390c4284e | ||
|
|
75483185d6 | ||
|
|
acb21cb926 | ||
|
|
c0ee674060 | ||
|
|
7d69c8e3bd | ||
|
|
38a37c49de | ||
|
|
a36478b1f5 | ||
|
|
bcb17cd0a5 | ||
|
|
1ec164a25a | ||
|
|
571a9dcb3e | ||
|
|
a0ac6265e9 | ||
|
|
ea6f49f7be | ||
|
|
0470680a4d | ||
|
|
e0c52c8660 | ||
|
|
3182ce031c | ||
|
|
f6dd0ccd12 | ||
|
|
775b0c8c60 | ||
|
|
1c4424dd0b | ||
|
|
8501aec7bc | ||
|
|
05a36d3245 | ||
|
|
2bf8f9164e | ||
|
|
56732ea3e8 | ||
|
|
7a4b32aadb | ||
|
|
16add1bf24 | ||
|
|
433cdfe87d | ||
|
|
a3d0a0250a | ||
|
|
b037333d2b | ||
|
|
bf6c2ef266 | ||
|
|
abde922b49 | ||
|
|
eca89ca5e9 | ||
|
|
a91c987107 | ||
|
|
12166e2245 | ||
|
|
e612c20141 | ||
|
|
d2039e5566 | ||
|
|
20bba75e41 | ||
|
|
2d26b647c8 | ||
|
|
ab0bec7a7b | ||
|
|
b6c5f1b1e7 | ||
|
|
881cc4d255 | ||
|
|
3d61973071 | ||
|
|
5ae1f3c441 | ||
|
|
6ae4cf495d | ||
|
|
900fc9c4e3 | ||
|
|
0207e84551 | ||
|
|
3a2d663c86 | ||
|
|
7aaaaf9125 | ||
|
|
e051b9bffa | ||
|
|
df0bcda952 | ||
|
|
d871378336 | ||
|
|
f99add7a3f | ||
|
|
7515700b1a | ||
|
|
99db4d50d3 | ||
|
|
0cd8246bdb | ||
|
|
29ee81ef18 | ||
|
|
9e09c06770 | ||
|
|
65603ca314 | ||
|
|
a983d23f91 | ||
|
|
3545306559 | ||
|
|
08163cc5cd | ||
|
|
c629c3424c | ||
|
|
ef403119d9 | ||
|
|
798c126881 | ||
|
|
813d503bb8 | ||
|
|
7e71a06c5f | ||
|
|
68475a00c1 | ||
|
|
4d71b6943c | ||
|
|
42caddb8a3 | ||
|
|
52d8604099 | ||
|
|
2df3aef52d | ||
|
|
9773e25932 | ||
|
|
cd766d90e4 | ||
|
|
8f69fc84a8 | ||
|
|
a8f0882220 | ||
|
|
d79c28d2d3 | ||
|
|
ba756d12b2 | ||
|
|
48e6872233 | ||
|
|
5037a9bbfd | ||
|
|
3fcde95fe8 | ||
|
|
a58e5e4276 | ||
|
|
2235c10df7 | ||
|
|
327e09291b | ||
|
|
1dd36424be | ||
|
|
ade2d0ae54 | ||
|
|
ac3dbd25f3 | ||
|
|
2e6811d2d4 | ||
|
|
61a9d0c0a6 | ||
|
|
4cc775bcae | ||
|
|
96dfa52dba | ||
|
|
1f1329c536 | ||
|
|
3bb54f875d | ||
|
|
84b6c1cb87 | ||
|
|
4f4bb316d0 | ||
|
|
fe6430edc6 | ||
|
|
6c421de8c4 | ||
|
|
c04ae91dc5 | ||
|
|
14bc340e56 | ||
|
|
6fd107c230 | ||
|
|
5f2c2103a5 | ||
|
|
a06828dbcf | ||
|
|
b260cb8f50 | ||
|
|
2fc3355388 | ||
|
|
24d5a169f6 | ||
|
|
2bc0dd5fbe | ||
|
|
d76e5008a7 | ||
|
|
21f01757a3 | ||
|
|
4da936344f | ||
|
|
05920cc7d7 | ||
|
|
edb17ad06e | ||
|
|
ab6f8fa7bf | ||
|
|
b822608b15 | ||
|
|
7a9bda9b1b | ||
|
|
3edfce202f | ||
|
|
c5b4a822d9 | ||
|
|
e3ae862732 | ||
|
|
74b1127f6e | ||
|
|
2c3f12b38c | ||
|
|
94ee718aa9 | ||
|
|
f34620aa73 | ||
|
|
a43bb56a43 | ||
|
|
a4ed95b81b | ||
|
|
ddd8348bdd | ||
|
|
d32095f3fe | ||
|
|
a958bf8be7 | ||
|
|
2d643a551c | ||
|
|
b16d75ec43 | ||
|
|
f40af555c3 | ||
|
|
c82672d77b | ||
|
|
867488bf77 | ||
|
|
9c485334f1 | ||
|
|
04ff83fc2d | ||
|
|
d61e2751ef | ||
|
|
87b64572db | ||
|
|
50a33a5083 | ||
|
|
2dc72ab262 | ||
|
|
6dec0ea0f1 | ||
|
|
fdc4e867c2 | ||
|
|
525731fe33 | ||
|
|
66d86b8d4d | ||
|
|
2443d5d1cf | ||
|
|
8863e7337e | ||
|
|
27b31ff1fb | ||
|
|
58335025c4 | ||
|
|
c1225178d6 | ||
|
|
8b19040e45 | ||
|
|
6ba62b66b4 | ||
|
|
e1bd7d7ae9 | ||
|
|
c9b2c1d8d6 | ||
|
|
1ae2b960c7 | ||
|
|
0c2d4ab5cb | ||
|
|
b9460cbcea | ||
|
|
886a26cc5d | ||
|
|
94dcd98e8d | ||
|
|
c3038807b9 | ||
|
|
7acb8a9e85 | ||
|
|
2d47622a0e | ||
|
|
bbfe5d36e8 | ||
|
|
0ecb732d60 | ||
|
|
8b5ac30030 | ||
|
|
0c7bf10355 | ||
|
|
319273eb03 | ||
|
|
199c49ff5e | ||
|
|
8d967b1125 | ||
|
|
277b5ac261 | ||
|
|
0b035deff0 | ||
|
|
73c3cb013f | ||
|
|
f2b2c90586 | ||
|
|
2c21aac0d6 | ||
|
|
ce1efd6cb9 | ||
|
|
5b72c7d713 | ||
|
|
2861b739c9 | ||
|
|
e5e9cd1367 | ||
|
|
0fd9ab303d | ||
|
|
db0dd231b1 | ||
|
|
a2e8d17a69 | ||
|
|
e73eb0453d | ||
|
|
d9a911cf56 | ||
|
|
95c8b7ab16 | ||
|
|
825f16ecc7 | ||
|
|
a9993ad361 | ||
|
|
e2f717a46a | ||
|
|
ec1b59066f | ||
|
|
3354bb386d | ||
|
|
11bac44de6 | ||
|
|
ee35a41d03 | ||
|
|
baf2e67744 | ||
|
|
0542a09b88 | ||
|
|
e37e6935c4 | ||
|
|
cbe848faff | ||
|
|
6b2aa1c532 | ||
|
|
9939fa0198 | ||
|
|
518b820ad5 | ||
|
|
fd9a6b6737 | ||
|
|
3e8bb878c8 | ||
|
|
25cd11e3a9 | ||
|
|
6e7c15d101 | ||
|
|
bc48432a8d | ||
|
|
723e8c042a | ||
|
|
995f4db93b | ||
|
|
ac401bd1f2 | ||
|
|
9e2198e115 | ||
|
|
eaf99c682f | ||
|
|
447a807f69 | ||
|
|
a88dde7d7a | ||
|
|
2e28793663 | ||
|
|
dd90f6c0ad | ||
|
|
9c67b6b8b4 | ||
|
|
206b6864c7 | ||
|
|
d65a2494bf | ||
|
|
d806d55a64 | ||
|
|
05f5ed338b | ||
|
|
a119f77237 | ||
|
|
227985e8eb | ||
|
|
0ca14a918b | ||
|
|
71ade81064 | ||
|
|
c6012f049a | ||
|
|
d05eab9f3f | ||
|
|
09816fa817 | ||
|
|
3da941d8b4 | ||
|
|
8f2bc384bd |
14
.github/ISSUE_TEMPLATE.txt
vendored
Normal file
14
.github/ISSUE_TEMPLATE.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
|
||||
|
||||
Please confirm the following:
|
||||
* I have upgraded to the latest GAM release from https://git.io/gamreleases and I still have this issue.
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
|
||||
|
||||
Full steps to reproduce the issue:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Expected outcome (what are you trying to do?):
|
||||
|
||||
Actual outcome (what errors or bad behavior do you see instead?):
|
||||
26
README.md
26
README.md
@@ -1,23 +1,10 @@
|
||||
GAM
|
||||
============================
|
||||
GAM is a free, open source command line tool for
|
||||
Google Apps Administrators to manage
|
||||
domain and user settings quickly and easily. GAM supports
|
||||
|
||||
* creating, deleting, and updating users, aliases, groups,
|
||||
organizations, and resource calendars
|
||||
* modifying user email settings such as IMAP, signatures,
|
||||
vacation messages, profile sharing, email forwarding,
|
||||
send as address, labels, and features.
|
||||
* modifying calendar access rights for users and resource calendars.
|
||||
* generating detailed reports for users, groups, resources,
|
||||
account activity, email clients, and quotas.
|
||||
* and many more commands
|
||||
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily.
|
||||
|
||||
Downloads
|
||||
---------
|
||||
You can download the current GAM release from
|
||||
the [GitHub Releases] page.
|
||||
You can download the current GAM release from the [GitHub Releases] page.
|
||||
|
||||
Documentation
|
||||
------------------
|
||||
@@ -25,18 +12,13 @@ The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
Mailing List / Discussion group
|
||||
-------------------------------
|
||||
The GAM mailing list / discussion group is hosted
|
||||
on [Google Groups]. You can join the list and interact
|
||||
via email, or just post from the web itself.
|
||||
|
||||
Source Repository
|
||||
-----------------
|
||||
The official GAM source repository is on [GitHub] in the master branch.
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
Author
|
||||
------
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
[GitHub]: https://github.com/jay0lee/GAM/tree/master
|
||||
[GitHub Wiki]: https://github.com/jay0lee/GAM/wiki/
|
||||
|
||||
72
src/.gitignore
vendored
Normal file
72
src/.gitignore
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Rope
|
||||
.ropeproject
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
*.pot
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# GAM specific
|
||||
client_secrets.json
|
||||
oauth2.txt*
|
||||
oauth2service*
|
||||
debug.gam
|
||||
lastupdatecheck.txt
|
||||
noupdatecheck.txt
|
||||
nodito.txt
|
||||
nobrowser.txt
|
||||
nocache.txt
|
||||
noverifyssl.txt
|
||||
gamcache/
|
||||
gam/
|
||||
gam-64/
|
||||
*.zip
|
||||
*.msi
|
||||
*.wixobj
|
||||
*.wixpdb
|
||||
803
src/GamCommands.txt
Normal file
803
src/GamCommands.txt
Normal file
@@ -0,0 +1,803 @@
|
||||
This document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form
|
||||
Items on the command line are space separated, when an actual space character is required, it will be indicated by <Space>.
|
||||
If an item contains spaces, it should be surrounded by " or '.
|
||||
|
||||
[] optional item
|
||||
() group items
|
||||
* item may appear zero or more times
|
||||
+ item may appear one or more times
|
||||
| separates alternative items
|
||||
|
||||
# Primatives
|
||||
<Digit> ::= 0|1|2|3|4|5|6|7|8|9
|
||||
<Number> ::= <Digit>+
|
||||
<Hex> ::= <Digit>|a|b|c|d|e|f|A|B|C|D|E|F
|
||||
<Space> ::= an actual space character
|
||||
<String> ::= a string of characters, surrounded by " or ' if it contains spaces
|
||||
<TrueValues> ::= true|on|yes|enabled|1
|
||||
<FalseValues>= false|off|no|disabled|0
|
||||
<DataTransferService> ::= googledrive|gdrive|drive|"drive and docs"
|
||||
<ProductID> ::= Google-Apps|Google-Coordinate|Google-Drive-storage|Google-Vault
|
||||
<SKUID> ::= apps|gafb|gafw|gams|gau|unlimited|d4w|dfw|coordinate|vault|vfe|
|
||||
drive-20gb|drive20gb|20gb|drive-50gb|drive50gb|50gb|drive-200gb|drive200gb|200gb|drive-400gb|drive400gb|400gb|
|
||||
drive-1tb|drive1tb|1tb|drive-2tb|drive2tb|2tb|drive-4tb|drive4tb|4tb|drive-8tb|drive8tb|8tb|drive-16tb|drive16tb|16tb
|
||||
<Charset> ::= ascii|mbcs|utf-8|utf-8-sig|utf-16|<String>
|
||||
<FileFormat> ::= csv|html|txt|tsv|jpeg|jpg|png|svg|pdf|rtf|pptx|xlsx|docx|odt|ods|openoffice|ms|microsoft|micro$oft
|
||||
<Language> ::= ar|bn|bg|ca|zh-CN|zh-TW|hr|cs|da|nl|en|en-GB|et|fi|fr|de|el|gu|iw|is|in|it|ja|kn|ko|lv|lt|ms|ml|mr|no|or|fa|pl|pt-BR|pt-PT|ro|ru|sr|sk|sl|es|sv|tl|ta|te|th|tr|uk|ur|vi
|
||||
|
||||
# Basic items built from primatives
|
||||
<Boolean> ::= <TrueValues>|<FalseValues>
|
||||
<ByteCount> ::= <Number>[m|k|b]
|
||||
<CIDRnetmask> ::= <Number>.<Number>.<Number>.<Number>/<Number>
|
||||
<ColorHex> ::= #<Hex><Hex><Hex><Hex><Hex><Hex>
|
||||
<DomainName> ::= <String>(.<String>)+
|
||||
<EmailAddress> ::= <String>@<DomainName>
|
||||
<Year> ::= <Digit><Digit><Digit><Digit>
|
||||
<Month> ::= <Digit><Digit>
|
||||
<Day> ::= <Digit><Digit>
|
||||
<Hour> ::= <Digit><Digit>
|
||||
<Minute> ::= <Digit><Digit>
|
||||
<Second> ::= <Digit><Digit>
|
||||
<MilliSeconds> ::= <Digit><Digit><Digit>
|
||||
<Date> ::= <Year>-<Month>-<Day>
|
||||
<DateTime> ::= <Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>
|
||||
<Time> ::= <Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>[:<Second>[.<MilliSeconds>[Z]]]
|
||||
<RegularExpression> ::= <Python Regular Expression, see: https://docs.python.org/2/library/re.html>
|
||||
<Tag> ::= <String>
|
||||
<UniqueID> ::= uid:<String>
|
||||
|
||||
# Named items
|
||||
<AccessToken> ::= <String>
|
||||
<ACLScope> ::= [user:]<EmailAddress>|group:<EmailAddress>|domain[:<DomainName>]|default
|
||||
<CalendarACLRole> ::= editor|freebusy|freebusyreader|owner|reader|writer
|
||||
<CalendarColorIndex> ::== <Number in range 1-24>
|
||||
<CalendarItem> ::= <EmailAddress>|<String>
|
||||
<ClientID> ::= <String>
|
||||
<CourseAlias> ::= <String>
|
||||
<CourseID> ::= <Number>|d:<CourseAlias>
|
||||
<CourseParticipantType> ::= teacher|teachers|student|students
|
||||
<CrOSID> ::= <String>
|
||||
<CrOSItem> ::= <CrOSID>|(query:<QueryCrOS>)|(query <QueryCrOS>)
|
||||
<DestEmailAddress> ::= <EmailAddress>
|
||||
<DomainAlias> ::= <String>
|
||||
<DriveFileACLRole> :: =commenter|editor|owner|reader|writer
|
||||
<DriveFileID> ::= <String>
|
||||
<DriveFileURL> :: = https://docs.google.com/a/<DomainName>/document/d/<DriveFileID>/<String>
|
||||
<DriveFileItem> ::= <DriveFileID>|<DriveFileURL>
|
||||
<DriveFileName> ::= <String>
|
||||
<DriveFolderID> ::= <String>
|
||||
<DriveFolderName> ::= <String>
|
||||
<EmailItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<EventColorIndex> ::== <Number in range 1-11>
|
||||
<EventID> ::= <String>
|
||||
<FieldName> ::= <String>
|
||||
<FileName> ::= <String>
|
||||
<FileNamePattern> ::= <String>
|
||||
<FilterID> ::= <Sttring>
|
||||
<FolderName> ::= <String>
|
||||
<GroupItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<GuardianItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<GuardianInvitationID> ::= <String>
|
||||
<HostName> ::= <String>
|
||||
<Key> ::= <String>
|
||||
<LabelID> ::= <String>
|
||||
<LabelName> ::= <String>
|
||||
<LabelReplacement> ::= <String>
|
||||
<Marker> ::= <String>
|
||||
<MobileID> ::= <String>
|
||||
<MobileItem> ::= <MobileID>|(query:<QueryMobile>)|(query <QueryMobile>)
|
||||
<Name> ::= <String>
|
||||
<NotificationID> ::= <String>
|
||||
<OrgUnitID> ::= <String>
|
||||
<OrgUnitPath> ::= /|(/<String)+
|
||||
<ParameterKey> ::= <String>
|
||||
<ParameterValue> ::= <String>
|
||||
<PermissionID> ::= id:<String>|<EmailAddress>|anyone|anyonewithlink
|
||||
<PrinterID> ::= <String>
|
||||
<PrintJobAge> ::= <Number>[m|h|d]
|
||||
<PrintJobID> ::= <String>
|
||||
<PrintJobStatus> ::= done|error|held|in_progress|queued|submitted
|
||||
<PropertyKey> ::= <String>
|
||||
<PropertyValue> ::= <String>
|
||||
<QueryContact> ::= <String> See: https://developers.google.com/google-apps/contacts/v3/reference#contacts-query-parameters-reference
|
||||
<QueryCrOS> ::= <String> See: https://support.google.com/chrome/a/answer/1698333?hl=en
|
||||
<QueryDriveFile> :: = <String> See: https://developers.google.com/drive/v2/web/search-parameters
|
||||
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
|
||||
<QueryMobile> ::= <String> See: https://support.google.com/a/answer/1408863?hl=en#search
|
||||
<QueryPrinter> ::= <String> See: https://developers.google.com/cloud-print/docs/appInterfaces#search
|
||||
<QueryPrintJob> ::= <String> See: https://developers.google.com/cloud-print/docs/appInterfaces#parameters_3
|
||||
<QueryUser> :: = <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
|
||||
<RequestID> ::= <String>
|
||||
<ResourceID> ::= <String>
|
||||
<RoleItem> ::= <UniqueID>|<String>
|
||||
<RoleAssignmentID> ::= <String>
|
||||
<SchemaName> ::= <String>
|
||||
<Section> ::= <String>
|
||||
<StudentItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<Timezone> ::= <String>
|
||||
<Title> ::= <String>
|
||||
<URI> ::= <String>
|
||||
<URL> ::= <String>
|
||||
<UserItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
|
||||
|
||||
<CrOSFieldName> ::=
|
||||
activetimeranges|timeranges|
|
||||
annotatedassetid|assedid|asset|
|
||||
annotatedlocation|location|
|
||||
annotateduser|user|
|
||||
bootmode|
|
||||
deviceid|
|
||||
ethernetmacaddress|
|
||||
firmwareversion|
|
||||
lastenrollmenttime|
|
||||
lastsync|
|
||||
macaddress|
|
||||
meid|
|
||||
model|
|
||||
notes|
|
||||
ordernumber|
|
||||
orgunitpath|org|ou|
|
||||
osversion|
|
||||
platformversion|
|
||||
recentusers|
|
||||
serialnumber|
|
||||
status|
|
||||
supportenddate|
|
||||
willautorenew
|
||||
|
||||
<CrOSOrderByFieldName> ::=
|
||||
lastsync|location|notes|serialnumber|status|supportenddate|user
|
||||
|
||||
<DriveFieldName> ::=
|
||||
appdatacontents|
|
||||
cancomment|
|
||||
canreadrevisions|
|
||||
copyable|
|
||||
createddate|createdtime|
|
||||
description|
|
||||
editable|
|
||||
explicitlytrashed|
|
||||
fileextension|
|
||||
filesize|
|
||||
foldercolorrgb|
|
||||
fullfileextension|
|
||||
headrevisionid|
|
||||
iconlink|
|
||||
id|
|
||||
lastmodifyinguser|
|
||||
lastmodifyingusername|
|
||||
lastviewedbyme|lastviewedbymedate|lastviewedbymetime|lastviewedbyuser|
|
||||
md5|md5checksum|md5sum|
|
||||
mime|mimetype|
|
||||
modifiedbyme|modifiedbymedate|modifiedbymetime|modifiedbyuser|
|
||||
modifieddate|modifiedtime|
|
||||
name|
|
||||
originalfilename|
|
||||
ownedbyme|
|
||||
ownernames|
|
||||
owners|
|
||||
parents|
|
||||
permissions|
|
||||
quotabytesused|quotaused|
|
||||
restricted|
|
||||
shareable|
|
||||
shared|
|
||||
sharedwithmedate|sharedwithmetime|
|
||||
sharinguser|
|
||||
size|
|
||||
spaces|
|
||||
starred|
|
||||
thumbnaillink|
|
||||
title|
|
||||
trashed
|
||||
userpermission|
|
||||
version|
|
||||
viewed|
|
||||
viewerscancopycontent|
|
||||
webcontentlink|
|
||||
webviewlink|
|
||||
writerscanshare
|
||||
|
||||
<DriveOrderByFieldName> ::=
|
||||
createddate|
|
||||
folder|
|
||||
lastviewedbyme|lastviewedbymedate|lastviewedbyuser|
|
||||
modifiedbyme|modifiedbymedate|modifiedbyuser|
|
||||
modifieddate|
|
||||
name|
|
||||
quotabytesused|quotaused|
|
||||
recency|
|
||||
sharedwithmedate|
|
||||
starred|
|
||||
title|
|
||||
viewedbymedate
|
||||
|
||||
<GroupFieldName> ::=
|
||||
admincreated|
|
||||
aliases|
|
||||
allowexternalmembers|
|
||||
allowgooglecommunication|
|
||||
allowwebposting|
|
||||
archiveonly|
|
||||
customreplyto|
|
||||
defaultmessagedenynotificationtext|
|
||||
description|
|
||||
email|
|
||||
id|
|
||||
includeinglobaladdresslist|gal|
|
||||
isarchived|
|
||||
maxmessagebytes|
|
||||
memberscanpostasthegroup|
|
||||
messagedisplayfont|
|
||||
messagemoderationlevel|
|
||||
name
|
||||
primarylanguage|
|
||||
replyto|
|
||||
sendmessagedenynotification|
|
||||
showingroupdirectory|
|
||||
spammoderationlevel|
|
||||
whocanadd|
|
||||
whocancontactowner|
|
||||
whocaninvite|
|
||||
whocanjoin|
|
||||
whocanleavegroup|
|
||||
whocanpostmessage|
|
||||
whocanviewgroup|
|
||||
whocanviewmembership
|
||||
|
||||
<GuardianState> ::=
|
||||
complete|
|
||||
pending
|
||||
|
||||
<MembersFieldName> ::=
|
||||
email|
|
||||
id|
|
||||
name|
|
||||
role|
|
||||
type
|
||||
|
||||
<MobileOrderByFieldName> ::=
|
||||
deviceid|email|lastsync|model|name|os|status|type
|
||||
|
||||
<OrgUnitFieldName> ::=
|
||||
description|id|inherit|name|orgunitpath|parent|parentid|inherit
|
||||
|
||||
<PrintJobOrderByFieldName> ::=
|
||||
create_time|status|title
|
||||
|
||||
<UserFieldName> ::=
|
||||
addresses|address|
|
||||
agreedtoterms|agreed2terms|
|
||||
changepasswordatnextlogin|changepassword|
|
||||
creationtime|
|
||||
deletiontime|
|
||||
email|emails|otheremail|otheremails|
|
||||
externalids|externalid|
|
||||
familyname|firstname|fullname|givenname|lastname|name|
|
||||
id|
|
||||
ims|im|
|
||||
includeinglobaladdresslist|gal|
|
||||
ipwhitelisted|
|
||||
isdelegatedadmin|admin|isadmin|
|
||||
ismailboxsetup|
|
||||
lastlogintime|
|
||||
noneditablealiases|aliases|nicknames|
|
||||
notes|note|
|
||||
organizations|organization|
|
||||
orgunitpath|org|ou|
|
||||
phones|phone|
|
||||
primaryemail|username|
|
||||
relations|relation|
|
||||
suspended|
|
||||
thumbnailphotourl|photo|photourl|
|
||||
websites|website|
|
||||
|
||||
<UserOrderByFieldName> ::=
|
||||
familyname|lastname|givenname|firstname|email
|
||||
|
||||
# Named Lists
|
||||
# Lists can be in the following formats
|
||||
# Items, separated by commas, without spaces or commas in the items themselves: item(,item)*
|
||||
# Items, separated by spaces, without spaces or commas in the items themselves: "item( item)*"
|
||||
# Items, separated by commas, with spaces or commas in the items themselves: "'it em'(,'it em')*"
|
||||
# Items, separated by spaces, with spaces or commas in the items themselves: "'it em'( 'it em')*"
|
||||
|
||||
<ACLList> ::== '<ACLScope>(,<ACLScope>)*'
|
||||
<CalendarList> ::= '<CalendarItem>(,<CalendarItem>)*'
|
||||
<CourseAliasList> ::= '<CourseAlias>(,<CourseAlias>)*'
|
||||
<CourseIDList> ::= '<CourseID>(,<CourseID>)*'
|
||||
<CrOSFieldNameList> ::= '<CrOSFieldName>(,<CrOSFieldName>)*'
|
||||
<CrOSList> ::= '<CrOSID>(,<CrOSID>)*'
|
||||
<DriveFileList> ::= '<DriveFileItem>(,<DriveFileItem>)*'
|
||||
<EmailAddressList> ::= '<EmailAddress>(,<EmailAddress>)*'
|
||||
<EventIDList> ::= '<EventID>(,<EventID>)*'
|
||||
<FileFormatList> ::= '<FileFormat>(,<FileFormat)*'
|
||||
<FilterIDList> ::= '<FilterID>(,<FilterID>)*'
|
||||
<GroupFieldNameList> ::= '<GroupFieldName>(,<GroupFieldName>)*'
|
||||
<GroupList> ::= '<GroupItem>(,<GroupItem>)*'
|
||||
<GuardianStateList> ::= '<GuardianState>(,<GuardianState>)*'
|
||||
<LabelNameList> ::= '<LabelName>(,<LabelName)*'
|
||||
<MembersFieldNameList> ::= '<MembersFieldName>(,<MembersFieldName>)*'
|
||||
<MobileList> ::= '<MobileId>(,<MobileId>)*'
|
||||
<OrgUnitList> ::== '<OrgUnitPath>(,<OrgUnitPath>)*'
|
||||
<PrinterIDList> ::= '<PrinterID>(,<PrinterID>)*'
|
||||
<ProductIDList> ::= '(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*'
|
||||
<PrintJobIDList> ::= '<PrintJobID>(,<PrintJobID>)*'
|
||||
<ResourceIDList> ::= '<ResourceID>(,<ResourceID>)*'
|
||||
<SKUIDList> ='<SKUID>(,<SKUID>)*'
|
||||
<SchemaNameList> ::= '<SchemaName>(,<SchemaName>)*'
|
||||
<UserFieldNameList> ::= '<UserFieldName>(,<UserFieldName>)*'
|
||||
<UserList> ::= '<UserItem>(,<UserItem>)*'
|
||||
|
||||
# Specify a collection of ChromeOS devices by directly specifying them
|
||||
<CrOSTypeEntity> ::=
|
||||
(all cros)|
|
||||
(cros <CrOSList>)|
|
||||
# Specify a collection of Users by directly specifying them or by specifiying items that will yield a list of users
|
||||
<UserTypeEntity> ::=
|
||||
(all users)|
|
||||
(user <UserItem>)|
|
||||
(users <UserList>)|
|
||||
(group <GroupItem)|
|
||||
(ou|org <OrgUnitPath)|
|
||||
(ou_and_children|ou_and_child <OrgUnitPath>)|
|
||||
(courseparticipants <CourseID>)|
|
||||
(students <CourseID>)|
|
||||
(teachers <CourseID>)|
|
||||
(file <FileName>)|
|
||||
(csvfile <FileName>:<FieldName>)|
|
||||
(license|licenses|licence|licences <SKUIDList>)|
|
||||
(query <QueryUser>)
|
||||
|
||||
|
||||
# Item attributes
|
||||
<CalendarAttributes> ::=
|
||||
(selected <Boolean>)|(hidden <Boolean>)|(summary <String>)|(colorindex|colorid <CalendarColorIndex>)|(backgroundcolor <ColorHex>)|(foregroundcolor <ColorHex>)|
|
||||
(reminder clear|(email|sms|pop <Number>))|
|
||||
(notification clear|(email|sms eventcreation|eventchange|eventcancellation|eventresponse|agenda))
|
||||
|
||||
<CalendarSettings> ::==
|
||||
(summary <String>)|(description <String>)|(location <String>)|(timezone <String>)
|
||||
|
||||
<CourseAttributes> ::=
|
||||
(name <String>)|(section <string>)|(heading <String>)|(room <String>)|(description <String>)|
|
||||
(state|status active|archived|provisioned|declined)
|
||||
|
||||
<CrOSAttributes> ::=
|
||||
(asset|assetid|tag <String>)|
|
||||
(location <String>)|
|
||||
(notes <String>)|
|
||||
(org|ou <OrgUnitPath>)|
|
||||
(user <Name>)
|
||||
|
||||
<DriveFileAddAttributes> ::=
|
||||
(localfile <FileName>)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|(restricted|restrict)|(starred|star)|(trashed|trash)|(viewed|view)|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype gdoc|gdocument|gdrawing|gfolder|gdirectory|gform|gfusion|gpresentation|gscript|gsite|gsheet|gspreadsheet)|
|
||||
(parentid <DriveFolderID>)|(parentname <FolderName>)|writerscantshare
|
||||
<DriveFileUpdateAttributes> ::=
|
||||
(localfile <FileName>)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype gdoc|gdocument|gdrawing|gfolder|gdirectory|gform|gfusion|gpresentation|gscript|gsite|gsheet|gspreadsheet)|
|
||||
(parentid <DriveFolderID>)|(parentname <FolderName>)|writerscantshare
|
||||
<EventAttributes> ::=
|
||||
(anyonecanaddself)|(guestscantinviteothers)|(guestscantseeothers)|(notifyattendees)|(available)|(visibility default|public|prvate)|(tentative)|
|
||||
(attendee <EmailAddress>)|(optionalattendee <EmailAddress>)|
|
||||
(description <String>)|(summary <String>)|(location <String>)|(id <String>)|
|
||||
(source <String> <URL>)|(privateproperty <PropertyKey> <PropertyValue>)|(sharedproperty <PropertyKey> <PropertyValue>)|
|
||||
(recurrence <RRULE, EXRULE, RDATE and EXDATE line>)|
|
||||
(start allday <Date>)|(start <Time>)|(end allday <Date>)|(end <Time>)|(timezone <Timezone>)|
|
||||
(noreminders|(reminder email|popup|sms <Number>))|
|
||||
(colorindex|colorid <EventColorIndex>)
|
||||
|
||||
<GroupAttributes> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
(allowgooglecommunication <Boolean>)|
|
||||
(allowwebposting <Boolean>)|
|
||||
(archiveonly <Boolean>)|
|
||||
(customfootertext <String>)|
|
||||
(customreplyto <EmailAddress>)|
|
||||
(defaultmessagedenynotificationtext <String>)|
|
||||
(description <String>)|
|
||||
(gal|includeInGlobalAddressList <Boolean>)|
|
||||
(includecustomfooter <Boolean>)|
|
||||
(isarchived <Boolean>)|
|
||||
(maxmessagebytes <ByteCount>)|
|
||||
(memberscanpostasthegroup <Boolean>)|
|
||||
(messagedisplayfont DEFAULT_FONT|FIXED_WIDTH_FONT)|
|
||||
(messagemoderationlevel MODERATE_ALL_MESSAGES|MODERATE_NON_MEMBERS|MODERATE_NEW_MEMBERS|MODERATE_NONE)|
|
||||
(name <String>)|
|
||||
(primarylanguage <Language>)|
|
||||
(replyto REPLY_TO_CUSTOM|REPLY_TO_SENDER|REPLY_TO_LIST|REPLY_TO_OWNER|REPLY_TO_IGNORE|REPLY_TO_MANAGERS)|
|
||||
(sendmessagedenynotification <Boolean>)|
|
||||
(showingroupdirectory <Boolean>)|
|
||||
(spammoderationlevel ALLOW|MODERATE|SILENTLY_MODERATE|REJECT)|
|
||||
(whocanadd ALL_MEMBERS_CAN_ADD|ALL_MANAGERS_CAN_ADD|NONE_CAN_ADD)|
|
||||
(whocancontactowner ANYONE_CAN_CONTACT|ALL_IN_DOMAIN_CAN_CONTACT|ALL_MEMBERS_CAN_CONTACT|ALL_MANAGERS_CAN_CONTACT)|
|
||||
(whocaninvite ALL_MEMBERS_CAN_INVITE|ALL_MANAGERS_CAN_INVITE|NONE_CAN_INVITE)|
|
||||
(whocanjoin ANYONE_CAN_JOIN|ALL_IN_DOMAIN_CAN_JOIN|INVITED_CAN_JOIN|CAN_REQUEST_TO_JOIN)|
|
||||
(whocanleavegroup ALL_MANAGERS_CAN_LEAVE|ALL_MEMBERS_CAN_LEAVE|NONE_CAN_LEAVE)|
|
||||
(whocanpostmessage NONE_CAN_POST|ALL_MANAGERS_CAN_POST|ALL_MEMBERS_CAN_POST|ALL_IN_DOMAIN_CAN_POST|ANYONE_CAN_POST)|
|
||||
(whocanviewgroup ANYONE_CAN_VIEW|ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)|
|
||||
(whocanviewmembership ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)
|
||||
|
||||
<MobileAttributes> ::=
|
||||
(model <String>)|(os <String>)|(useragent <String>)|
|
||||
(action admin_remote_wipe|wipe|admin_account_wipe|accountwipe|wipeaccount|approve|block|cancel_remote_wipe_then_activate|cancel_remote_wipe_then_block)
|
||||
|
||||
<PrinterAttributes> ::= (currentquota <Number>)|(dailyquota <Number>)|
|
||||
(defaultdisplayname <String>)|(description <String>)|(displayname <String>)|(firmware <String>)|(gcpversion <String>)|
|
||||
(istosaccepted <Boolean>)|(manufacturer <String>)|(model <String>)|(name <String>)|(ownerid <EmailAddress>)|(proxy <String>)|(public <Boolean>)|
|
||||
(quotaenabled <Boolean>)|(status <Number>)|(type <String>)|(uuid <String>)|
|
||||
(setupurl <URL>)|(supporturl <URL>)|(updateurl <URL>)
|
||||
|
||||
<SchemaFieldDefinition> ::=
|
||||
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
|
||||
|
||||
<UserAttributes> ::=
|
||||
(address|addresses clear|(type work|home|other|(custom <String>) [unstructured|formatted <String>] [pobox <String>] [extendedaddress <String>] [streetaddress <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country <String>] [countrycode <String>] notprimary|primary))|
|
||||
(admin <Boolean>)|
|
||||
(agreed2terms|agreedtoterms <Boolean>)|
|
||||
(changepassword|changepasswordatnextlogin <Boolean>)|
|
||||
(crypt|sha|sha1|sha-1|md5|nohash)|
|
||||
(customerid <String>)|
|
||||
(email|primaryemail|username <EmailAddress>)|
|
||||
(emails|otheremail|otheremails clear|(work|home|other|<String> <String>))|
|
||||
(externalid|externalids clear|(account|customer|network|organization|<String> <String>))|
|
||||
(firstname|givenname <String>)|
|
||||
(gal|includeinglobaladdresslist <Boolean>)|
|
||||
(im|ims clear|(type work|home|other|(custom <String>) protocol aim|gtalk|icq|jabber|msn|net_meeting|qq|skype|yahoo|(custom_protocol <String>) <String> [notprimary|primary]))|
|
||||
(ipwhitelisted <Boolean>)|
|
||||
(lastname|familyname <String>)|
|
||||
(note|notes clear|([text_plain|text_html] <String>|(file <FileName>)))|
|
||||
(organization|organizations clear|([type domain_only|school|unknown|work] [customtype <String>] [name <String>] [title <String>] [department <String>] [symbol <String>]
|
||||
[costcenter <String>] [location <String>] [description <String>] [domain <String>] notprimary|primary))|
|
||||
(org|ou|orgunitpath <OrgUnitPath>)
|
||||
(password random|<String>)|
|
||||
(phone|phones clear|([type work|home|other|work_fax|home_fax|other_fax|main|company_main|assistant|mobile|work_mobile|pager|work_pager|car|radio|callback|isdn|telex|tty_tdd|grand_central|(custom <String>)
|
||||
[value <String>] notprimary|primary))|
|
||||
(relation|relations clear|(spouse|child|mother|father|parent|brother|sister|friend|relative|domestic_partner|manager|assistant|referred_by|partner|<String> <String>))|
|
||||
(suspended <Boolean>)|
|
||||
(website|websites clear|(home_page|blog|profile|work|home|other|ftp|reservations|app_install_page|<String> <URL> [notprimary|primary]))|
|
||||
(<SchemaName>.<FieldName> [multivalued|multivalue|value [type work|home|other|(custom <String>)]] <String>)
|
||||
|
||||
gam version [check]
|
||||
gam help
|
||||
|
||||
gam batch <FileName>|- [charset <Charset>]
|
||||
gam csv <FileName>|- [charset <Charset>] gam <GAM argument list>
|
||||
|
||||
# You can make substitutions in <GAMArgumentList> with values from the CSV file.
|
||||
# An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file
|
||||
# An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file
|
||||
|
||||
# Example: gam csv Users.csv gam update user '~primaryEmail' address type work unstructured '~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~'
|
||||
# Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
|
||||
|
||||
gam oauth|oauth2 create|request
|
||||
gam oauth|oauth2 delete|revoke
|
||||
gam oauth|oauth2 info|verify [<AccessToken>]
|
||||
|
||||
gam whatis <EmailItem>
|
||||
|
||||
gam report users|user [todrive]
|
||||
[date <Date>] [(user all|<UserItem>)] [filter|filters <String>] [fields|parameters <String>]
|
||||
gam report customers|customer|domain [todrive]
|
||||
[date <Date>] [fields|parameters <String>]
|
||||
gam report admin|calendar|calendars|drive|docs|doc|groups|group|logins|login|mobile|tokens|token [todrive]
|
||||
[start <Time>] [end <Time>] [(user all|<UserItem>)] [event <String>] [filter|filters <String>] [ip <String>]
|
||||
|
||||
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
|
||||
gam delete admin <RoleAssignmentId>
|
||||
gam print admins [todrive] [user <UserItem>] [role <RoleItem>]
|
||||
gam print adminroles|roles [todrive]
|
||||
|
||||
gam create domain <DomainName>
|
||||
gam update domain <DomainName> primary
|
||||
gam delete domain <DomainName>
|
||||
gam info domain [<DomainName>]
|
||||
gam print domains [todrive]
|
||||
|
||||
gam create domainalias|aliasdomain <DomainAlias> <DomainName>
|
||||
gam delete domainalias|aliasdomain <DomainAlias>
|
||||
gam info domainalias|aliasdomain <DomainAlias>
|
||||
gam print domainaliases|aliasdomains [todrive]
|
||||
|
||||
|
||||
gam info customer
|
||||
gam update customer [adminsecondaryemail|alternateemail <EmailAddress>] [language <LanguageCode] [phone|phonenumber <String>]
|
||||
[contact|contactname <String>] [name|organizationname <String>]
|
||||
[address1|addressline1 <String>] [address2|addressline2 <String>] [address3|addressline3 <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country|countrycode <String>]
|
||||
|
||||
gam create datatransfer|transfer <OldOwnerID> <DataTransferService> <NewOwnerID> (<ParameterKey> <ParameterValue>)*
|
||||
gam info datatransfer|transfer <TransferID>
|
||||
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>] [status <String>]
|
||||
|
||||
gam print transferapps
|
||||
|
||||
gam create org|ou <Name> [description <String>] [parent <OrgUnitPath>] [inherit|noinherit]
|
||||
gam update org|ou <OrgUnitPath> [name <Name>] [description <String>] [parent <OrgUnitPath>] [inherit|noinherit]
|
||||
gam update org|ou <OrgUnitPath> add|move <CrOSTypeEntity>|<UserTypeEntity>
|
||||
gam delete org|ou <OrgUnitPath>
|
||||
gam info org|ou <OrgUnitPath> [nousers] [children|child]
|
||||
gam print orgs|ous [todrive] [toplevelonly] [from_parent <OrgUnitPath>] [allfields|<OrgUnitFieldName>*]
|
||||
|
||||
gam create alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
|
||||
gam update alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
|
||||
gam delete alias|nickname [user|group|target] <UniqueID>|<EmailAddress>
|
||||
gam info alias|nickname <EmailAddress>
|
||||
gam print aliases|nicknames [todrive]
|
||||
|
||||
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> del|delete <CalendarACLRole> <EmailAddress>|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> showacl
|
||||
|
||||
gam calendar <CalendarItem> addevent <EventAttributes>+
|
||||
gam calendar <CalendarItem> wipe
|
||||
|
||||
gam update cros <CrOSItem> (<CrOSAttributes>+)|(action deprovision_same_model_replace|deprovision_different_model_replace|deprovision_retiring_device|disable|reenable [acknowledge_device_touch_requirement])
|
||||
gam info cros <CrOSItem> [nolists] [listlimit <Number>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
||||
|
||||
gam print cros [todrive] [query <QueryCrOS>]
|
||||
[orderby <CrOSOrderByFieldName> [ascending|descending]] [nolists] [listlimit <Number>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
||||
gam <CrOSTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
gam print cros
|
||||
Prints a header row and deviceId for all CrOS devices.
|
||||
gam <CrOSTypeEntity> print cros
|
||||
Prints no header row and deviceId for specified CrOS devices.
|
||||
gam print cros ... basic|full
|
||||
Prints a header row and selected fields for specified CrOS devices.
|
||||
|
||||
The basic argument yields these column headers: deviceId,annotatedAssetId,annotatedLocation,annotatedUser,lastSync,notes,serialNumber,status
|
||||
The full argument yields all column headers including two headers, recentUsers and activeTimeRanges,
|
||||
that repeat with two subvalues each, yielding a large number of columns that make the output hard to process.
|
||||
The nolists argument suppresses these two headers; if you want these headers in a more manageable form use the following arguments.
|
||||
The listlimit <Number> argument limits the number of repetitions to <Number>. If <Number> equals zero, there is no limit.
|
||||
If recentusers is specified as a field, each pair of values for recentUsers is put on a separate row with all of the other headers.
|
||||
If timeranges is specified as a field, each pair of values for activeTimeRanges is put on a separate row with all of the other headers.
|
||||
|
||||
gam update mobile <MobileItem> <MobileAttributes>+
|
||||
gam delete mobile <MobileItem>
|
||||
gam info mobile <MobileItem>
|
||||
gam print mobile [todrive] [query <QueryMobile>] [basic|full] [orderby <MobileOrderByFieldName> [ascending|descending]]
|
||||
|
||||
gam create group <EmailAddress> <GroupAttributes>*
|
||||
gam update group <GroupItem> [admincreated <Boolean>] [email <EmailAddress>] <GroupAttributes>*
|
||||
gam update group <GroupItem> add [member|manager|owner] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> delete|remove [member|manager|owner] <UserTypeEntity>
|
||||
gam update group <GroupItem> sync [member|manager|owner] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> update [member|manager|owner] <UserTypeEntity>
|
||||
gam delete group <GroupItem>
|
||||
gam info group <GroupItem> [nousers] [noaliases] [groups]
|
||||
gam update group <GroupItem> clear [member] [manager] [owner]
|
||||
|
||||
gam print groups [todrive] ([domain <DomainName>] [member <UserItem>])
|
||||
[maxresults <Number>] [delimiter <String>]
|
||||
[members] [managers] [owners] [settings] <GroupFieldName>* [fields <GroupFieldNameList>]
|
||||
|
||||
gam print group-members|groups-members [todrive] ([domain <DomainName>] [member <UserItem>])|[group <GroupItem>]
|
||||
[membernames] [fields <MembersFieldNameList>]
|
||||
|
||||
gam print license|licenses|licence|licences [todrive] [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
||||
|
||||
gam update notification|notifications [(id all)|(id <NotificationID>)*] unread|read
|
||||
gam delete notification|notifications [(id all)|(id <NotificationID>)*]
|
||||
gam info notification|notifications [unreadonly]
|
||||
|
||||
gam create resource <ResourceID> <Name> [description <String>] [type <String>]
|
||||
gam update resource <ResourceID> [name <Name>] [description <String>] [type <String>]
|
||||
gam delete resource <ResourceID>
|
||||
gam info resource <ResourceID>
|
||||
gam print resources [todrive] [allfields] [id] [name] [description] [email] [type]
|
||||
|
||||
gam create schema|schemas <SchemaName> <SchemaFieldDefinition>+
|
||||
gam update schema <SchemaName> <SchemaFieldDefinition>* (deletefield <FieldName>)*
|
||||
gam delete schema <SchemaName>
|
||||
gam info schema <SchemaName>
|
||||
gam show schema|schemas
|
||||
gam print schema|schemas
|
||||
|
||||
gam create user <EmailAddress> <UserAttrubutes>*
|
||||
gam update user <UserItem> <UserAttributes>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>]
|
||||
gam delete user <UserItem>
|
||||
gam undelete user <UserItem> [org|ou <OrgUnitPath>]
|
||||
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas|custom <SchemaNameList>] [userview] [skus|sku <SKUIDList>]
|
||||
|
||||
gam print users [todrive] ([domain <DomainName>] [query <QueryUser>] [deleted_only|only_deleted])
|
||||
[groups] [license|licenses|licence|licences] [emailpart|emailparts|username]
|
||||
[orderby <UserOrderByFieldName> [ascending|descending]] [userview]
|
||||
[basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>] [schemas|custom all|<SchemaNameList>]
|
||||
gam <UserTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
gam print users
|
||||
Prints a header row and primaryEmail for all users.
|
||||
gam <UserTypeEntity> print
|
||||
Prints no header row and primaryEmail for specified users.
|
||||
|
||||
gam create verify|verification <DomainName>
|
||||
gam update verify|verification <DomainName> cname|txt|text|site|file
|
||||
gam info verify|verification
|
||||
|
||||
gam create course id|alias <CourseAlias> [teacher <UserItem>] <CourseAttributes>*
|
||||
gam update course <CourseID> <CourseAttributes>+
|
||||
gam delete course <CourseID>
|
||||
gam info course <CourseID>
|
||||
gam print courses [todrive] [teacher] [student] [alias|aliases] [delimiter <String>]
|
||||
|
||||
gam course <CourseID> add alias <CourseAlias>
|
||||
gam course <CourseID> delete alias <CourseAlias>
|
||||
gam course <CourseID> add teachers|students <UserItem>
|
||||
gam course <CourseID> delete|remove teachers|students <UserItem>
|
||||
gam course <CourseID> sync teachers|students <UserTypeEntity>
|
||||
gam print course-participants [todrive] (course|class <CourseID>)*|([teacher <UserItem>] [student <UserItem>]) [show all|students|teachers]
|
||||
|
||||
gam create guardian|guardianinvite|inviteguardian <EmailAddress> <StudentItem>
|
||||
gam delete guardian|guardians <GuardianItem> <StudentItem> [invitation]
|
||||
gam show guardian|guardians [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations] [states <GuardianStateList>] [<UserTypeEntity>]
|
||||
gam print guardian|guardians [todrive] [invitedguardian <EmailAddress>] [student <StudentItem>] [invitations [states <GuardianStateList>]] [<UserTypeEntity>]
|
||||
gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
|
||||
|
||||
gam printer register
|
||||
gam update printer <PrinterID> <PrinterAttributes>+
|
||||
gam delete printer <PrinterID>
|
||||
gam info printer <PrinterID> [everything]
|
||||
gam print printers [todrive] [query <QueryPrinter>] [type <String>] [status <String>] [extrafields <String>]
|
||||
|
||||
gam printer <PrinterID> add user|manager|owner <EmailAddress>|[domain:]<DomainName>|public
|
||||
gam printer <PrinterID> delete <EmailAddress>|[domain:]<DomainName>|public
|
||||
gam printer <PrinterID> showacl
|
||||
gam printjob <PrintJobID> cancel
|
||||
gam printjob <PrintJobID> delete
|
||||
gam printjob <PrintJobID> resubmit <PrinterID>
|
||||
|
||||
gam printjob <PrinterID>|any fetch
|
||||
[olderthan|newerthan <PrintJobAge>] [query <QueryPrintJob>]
|
||||
[status <PrintJobStatus>]
|
||||
[orderby <PrintJobOrderByFieldName> [ascending|descending]]
|
||||
[owner|user <EmailAddress>]
|
||||
[limit <Number>] [drivedir|(targetfolder <FilePath>)]
|
||||
gam printjob <PrinterID> submit <FileName>|<URL> [name|title <String>] (tag <String>)*
|
||||
gam print printjobs [todrive] [printer|printerid <PrinterID>]
|
||||
[olderthan|newerthan <PrintJobAge>] [query <QueryPrintJob>]
|
||||
[status <PrintJobStatus>]
|
||||
[orderby <PrintJobOrderByFieldName> [ascending|descending]]
|
||||
[owner|user <EmailAddress>]
|
||||
[limit <Number>]
|
||||
|
||||
gam <UserTypeEntity> delete|del asp|asps|applicationspecificpasswords <AspID>
|
||||
gam <UserTypeEntity> show asps|asp|applicationspecificpasswords
|
||||
|
||||
gam <UserTypeEntity> update backupcodes|backupcode|verificationcodes
|
||||
gam <UserTypeEntity> delete|del backupcodes|backupcode|verificationcodes
|
||||
gam <UserTypeEntity> show backupcodes|backupcode|verificationcodes
|
||||
|
||||
gam <UserTypeEntity> add calendar <CalendarItem> <CalendarAttributes>*
|
||||
gam <UserTypeEntity> update calendar <CalendarItem> <CalendarAttributes>+
|
||||
gam <UserTypeEntity> delete|del calendar <CalendarItem>
|
||||
gam <UserTypeEntity> show calendars
|
||||
gam <UserTypeEntity> info calendar <CalendarItem>|primary
|
||||
gam <UserTypeEntity> print calendars [todrive]
|
||||
|
||||
gam <UserTypeEntity> show calsettings
|
||||
gam <UserTypeEntity> update calattendees csv <FileName> [dryrun] [start <Date>] [end <Date>] [allevents]
|
||||
gam <UserTypeEntity> transfer seccals <UserItem> [keepuser]
|
||||
|
||||
gam <UserTypeEntity> print|show driveactivity [todrive] [fileid <DriveFileID>] [folderid <DriveFolderID>]
|
||||
gam <UserTypeEntity> print|show drivesettings [todrive]
|
||||
gam <UserTypeEntity> print|show filelist [todrive] [anyowner] [query <QueryDriveFile>] [fullquery <QueryDriveFile>]
|
||||
[allfields|<DriveFieldName>*] (orderby <DriveOrderByFieldName> [ascending|descending])*
|
||||
|
||||
gam <UserTypeEntity> show fileinfo <DriveFileID> [allfields|<DriveFieldName>*]
|
||||
gam <UserTypeEntity> show filerevisions <DriveFileID>
|
||||
gam <UserTypeEntity> show filetree [anyowner] (orderby <DriveOrderByFieldName> [ascending|descending])*
|
||||
|
||||
gam <UserTypeEntity> add drivefile [drivefilename <DriveFileName>] <DriveFileAddAttributes>*
|
||||
gam <UserTypeEntity> update drivefile (id <DriveFileID)|(drivefilename <DriveFileName>)|(query <QueryDriveFile) [copy] [newfilename <DriveFileName>] <DriveFileUpdateAttributes>*
|
||||
gam <UserTypeEntity> get drivefile (id <DriveFileID>)|(drivefilename <DriveFileName>)|(query <QueryDriveFile>) [format <FileFormatList>] [targetfolder <FilePath>] [revision <Number>]
|
||||
gam <UserTypeEntity> delete|del drivefile <DriveFileID>|<DriveFileURL>|(query:<QueryDriveFile>) [purge|untrash]
|
||||
gam <UserTypeEntity> transfer drive <UserItem> [keepuser]
|
||||
gam <UserTypeEntity> delete|del emptydrivefolders
|
||||
gam <UserTypeEntity> empty drivetrash
|
||||
|
||||
gam <UserTypeEntity> add drivefileacl <DriveFileID> anyone|(user <UserItem>)|(group <GroupItem>)|(domain <DomainName>)
|
||||
(role <DriveFileACLRole>) [withlink] [sendmail] [emailmessage <String>]
|
||||
gam <UserTypeEntity> update drivefileacl <DriveFileID> <PermissionID>
|
||||
(role <DriveFileACLRole>) [withlink] [transferownership <Boolean>]
|
||||
gam <UserTypeEntity> delete|del drivefileacl <DriveFileID> <PermissionID>
|
||||
gam <UserTypeEntity> show drivefileacl <DriveFileID>
|
||||
|
||||
gam <UserTypeEntity> delete|del alias|aliases
|
||||
|
||||
gam <UserTypeEntity> delete|del group|groups
|
||||
|
||||
gam <UserTypeEntity> add license <SKUID>
|
||||
gam <UserTypeEntity> update license <SKUID> [from] <SKUID>
|
||||
gam <UserTypeEntity> delete|del license <SKUID>
|
||||
|
||||
gam <UserTypeEntity> update photo <FileNamePattern>
|
||||
gam <UserTypeEntity> delete|del photo
|
||||
gam <UserTypeEntity> get photo [drivedir|(targetfolder <FilePath>)] [noshow]
|
||||
|
||||
gam <UserTypeEntity> profile share|shared|unshare|unshared
|
||||
gam <UserTypeEntity> show profile
|
||||
|
||||
gam <UserTypeEntity> delete|del token|tokens clientid <ClientID>
|
||||
gam <UserTypeEntity> show tokens|token [clientid <ClientID>]
|
||||
gam <UserTypeEntity> print tokens|token [todrive] [clientid <ClientID>]
|
||||
gam print tokens|token [todrive] [clientid <ClientID>] [<UserTypeEntity>]
|
||||
|
||||
gam <UserTypeEntity> update user <UserAttrubutes>
|
||||
|
||||
gam <UserTypeEntity> deprovision|deprov
|
||||
#
|
||||
# Update user Gmail mailbox
|
||||
#
|
||||
gam <UserTypeEntity> [add] label|labels <Name> [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
||||
gam <UserTypeEntity> update labelsettings <LabelName> [name <Name>] [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
||||
gam <UserTypeEntity> update label|labels [search <RegularExpression>] [replace <LabelReplacement>] [merge]
|
||||
gam <UserTypeEntity> delete|del label|labels <LabelName>|regex:<RegularExpression>|--ALL_LABELS--
|
||||
gam <UserTypeEntity> show labels|label [onlyuser] [showcounts]
|
||||
|
||||
gam <UserTypeEntity> delete messages query <QueryGmail> [doit] [max_to_delete|max_to_process <Number>]
|
||||
gam <UserTypeEntity> modify messages query <QueryGmail> [doit] [max_to_modify|max_to_process <Number>] (addlabel <LabelName>)* (removelabel <LabelName>)*
|
||||
gam <UserTypeEntity> trash messages query <QueryGmail> [doit] [max_to_trash|max_to_process <Number>]
|
||||
gam <UserTypeEntity> untrash messages query <QueryGmail> [doit] [max_to_untrash|max_to_process <Number>]
|
||||
#
|
||||
# Update user Gmail settings
|
||||
#
|
||||
gam <UserTypeEntity> show gmailprofile [todrive]
|
||||
gam <UserTypeEntity> show gplusprofile [todrive]
|
||||
|
||||
gam <UserTypeEntity> add delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> delegate|delegates to <EmailAddress>
|
||||
gam <UserTypeEntity> delete|del delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> show delegates|delegate [csv]
|
||||
gam <UserTypeEntity> print delegates [todrive]
|
||||
|
||||
gam <UserTypeEntity> [add] filter [from <EmailAddress>] [to <EmailAddress>] [subject <String>] [haswords|query <List>] [nowords|negatedquery <List>] [musthaveattachment|hasattachment] [excludechats] [size larger|smaller <ByteCount>]
|
||||
[label <LabelID>] [important|notimportant] [star] [trash] [markread] [archive] [neverspam] [forward <EmailAddress>]
|
||||
gam <UserTypeEntity> delete filters <FilterIDEntity>
|
||||
gam <UserTypeEntity> show filters
|
||||
gam <UserTypeEntity> info filters <FilterIDEntity>
|
||||
gam <UserTypeEntity> print filters [todrive]
|
||||
|
||||
gam <UserTypeEntity> forward <FalseValues>
|
||||
gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress>
|
||||
gam <UserTypeEntity> show forward
|
||||
gam <UserTypeEntity> print forward [todrive]
|
||||
|
||||
gam <UserTypeEntity> add forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> delete forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> show forwardingaddress|forwardingaddresses
|
||||
gam <UserTypeEntity> info forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> print forwardingaddress|forwardingaddresses [todrive]
|
||||
|
||||
gam <UserTypeEntity> imap|imap4 <Boolean> [noautoexpunge] [expungebehavior archive|deleteforever|trash] [maxfoldersize 0|1000|2000|5000|10000]
|
||||
gam <UserTypeEntity> show imap|imap4
|
||||
|
||||
gam <UserTypeEntity> pop|pop3 <Boolean> [for allmail|newmail|mailfromnowon|fromnowown] [action keep|leaveininbox|archive|delete|trash|markread]
|
||||
gam <UserTypeEntity> show pop|pop3
|
||||
|
||||
gam <UserTypeEntity> [add] sendas <EmailAddress> <Name> [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)*] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
gam <UserTypeEntity> update sendas <EmailAddress> [name <Name>] [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)*] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
gam <UserTypeEntity> delete sendas <EmailAddress>
|
||||
gam <UserTypeEntity> show sendas [format]
|
||||
gam <UserTypeEntity> info sendas <EmailAddress> [format]
|
||||
gam <UserTypeEntity> print sendas [todrive]
|
||||
|
||||
gam <UserTypeEntity> signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)* [name <String>] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
||||
gam <UserTypeEntity> show signature|sig [format]
|
||||
|
||||
gam <UserTypeEntity> vacation <FalseValues>
|
||||
gam <UserTypeEntity> vacation <TrueValues> subject <String> (message <String>)|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)* [html]
|
||||
[contactsonly] [domainonly] [startdate <Date>] [enddate <Date>]
|
||||
gam <UserTypeEntity> show vacation [format]
|
||||
547
src/LICENSE
Normal file
547
src/LICENSE
Normal file
@@ -0,0 +1,547 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
APACHE HTTP SERVER SUBCOMPONENTS:
|
||||
|
||||
The Apache HTTP Server includes a number of subcomponents with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for the these subcomponents is subject to the terms and
|
||||
conditions of the following licenses.
|
||||
|
||||
For the mod_mime_magic component:
|
||||
|
||||
/*
|
||||
* mod_mime_magic: MIME type lookup via file magic numbers
|
||||
* Copyright (c) 1996-1997 Cisco Systems, Inc.
|
||||
*
|
||||
* This software was submitted by Cisco Systems to the Apache Group in July
|
||||
* 1997. Future revisions and derivatives of this source code must
|
||||
* acknowledge Cisco Systems as the original contributor of this module.
|
||||
* All other licensing and usage conditions are those of the Apache Group.
|
||||
*
|
||||
* Some of this code is derived from the free version of the file command
|
||||
* originally posted to comp.sources.unix. Copyright info for that program
|
||||
* is included below as required.
|
||||
* ---------------------------------------------------------------------------
|
||||
* - Copyright (c) Ian F. Darwin, 1987. Written by Ian F. Darwin.
|
||||
*
|
||||
* This software is not subject to any license of the American Telephone and
|
||||
* Telegraph Company or of the Regents of the University of California.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose on any
|
||||
* computer system, and to alter it and redistribute it freely, subject to
|
||||
* the following restrictions:
|
||||
*
|
||||
* 1. The author is not responsible for the consequences of use of this
|
||||
* software, no matter how awful, even if they arise from flaws in it.
|
||||
*
|
||||
* 2. The origin of this software must not be misrepresented, either by
|
||||
* explicit claim or by omission. Since few users ever read sources, credits
|
||||
* must appear in the documentation.
|
||||
*
|
||||
* 3. Altered versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software. Since few users ever read
|
||||
* sources, credits must appear in the documentation.
|
||||
*
|
||||
* 4. This notice may not be removed or altered.
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
For the modules\mappers\mod_imagemap.c component:
|
||||
|
||||
"macmartinized" polygon code copyright 1992 by Eric Haines, erich@eye.com
|
||||
|
||||
For the server\util_md5.c component:
|
||||
|
||||
/************************************************************************
|
||||
* NCSA HTTPd Server
|
||||
* Software Development Group
|
||||
* National Center for Supercomputing Applications
|
||||
* University of Illinois at Urbana-Champaign
|
||||
* 605 E. Springfield, Champaign, IL 61820
|
||||
* httpd@ncsa.uiuc.edu
|
||||
*
|
||||
* Copyright (C) 1995, Board of Trustees of the University of Illinois
|
||||
*
|
||||
************************************************************************
|
||||
*
|
||||
* md5.c: NCSA HTTPd code which uses the md5c.c RSA Code
|
||||
*
|
||||
* Original Code Copyright (C) 1994, Jeff Hostetler, Spyglass, Inc.
|
||||
* Portions of Content-MD5 code Copyright (C) 1993, 1994 by Carnegie Mellon
|
||||
* University (see Copyright below).
|
||||
* Portions of Content-MD5 code Copyright (C) 1991 Bell Communications
|
||||
* Research, Inc. (Bellcore) (see Copyright below).
|
||||
* Portions extracted from mpack, John G. Myers - jgm+@cmu.edu
|
||||
* Content-MD5 Code contributed by Martin Hamilton (martin@net.lut.ac.uk)
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/* these portions extracted from mpack, John G. Myers - jgm+@cmu.edu */
|
||||
/* (C) Copyright 1993,1994 by Carnegie Mellon University
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software
|
||||
* and its documentation for any purpose is hereby granted without
|
||||
* fee, provided that the above copyright notice appear in all copies
|
||||
* and that both that copyright notice and this permission notice
|
||||
* appear in supporting documentation, and that the name of Carnegie
|
||||
* Mellon University not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. Carnegie Mellon University makes no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
|
||||
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 1991 Bell Communications Research, Inc. (Bellcore)
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this material
|
||||
* for any purpose and without fee is hereby granted, provided
|
||||
* that the above copyright notice and this permission notice
|
||||
* appear in all copies, and that the name of Bellcore not be
|
||||
* used in advertising or publicity pertaining to this
|
||||
* material without the specific, prior written permission
|
||||
* of an authorized representative of Bellcore. BELLCORE
|
||||
* MAKES NO REPRESENTATIONS ABOUT THE ACCURACY OR SUITABILITY
|
||||
* OF THIS MATERIAL FOR ANY PURPOSE. IT IS PROVIDED "AS IS",
|
||||
* WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
|
||||
*/
|
||||
|
||||
For the srclib\apr\include\apr_md5.h component:
|
||||
/*
|
||||
* This is work is derived from material Copyright RSA Data Security, Inc.
|
||||
*
|
||||
* The RSA copyright statement and Licence for that original material is
|
||||
* included below. This is followed by the Apache copyright statement and
|
||||
* licence for the modifications made to that material.
|
||||
*/
|
||||
|
||||
/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
rights reserved.
|
||||
|
||||
License to copy and use this software is granted provided that it
|
||||
is identified as the "RSA Data Security, Inc. MD5 Message-Digest
|
||||
Algorithm" in all material mentioning or referencing this software
|
||||
or this function.
|
||||
|
||||
License is also granted to make and use derivative works provided
|
||||
that such works are identified as "derived from the RSA Data
|
||||
Security, Inc. MD5 Message-Digest Algorithm" in all material
|
||||
mentioning or referencing the derived work.
|
||||
|
||||
RSA Data Security, Inc. makes no representations concerning either
|
||||
the merchantability of this software or the suitability of this
|
||||
software for any particular purpose. It is provided "as is"
|
||||
without express or implied warranty of any kind.
|
||||
|
||||
These notices must be retained in any copies of any part of this
|
||||
documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr\passwd\apr_md5.c component:
|
||||
|
||||
/*
|
||||
* This is work is derived from material Copyright RSA Data Security, Inc.
|
||||
*
|
||||
* The RSA copyright statement and Licence for that original material is
|
||||
* included below. This is followed by the Apache copyright statement and
|
||||
* licence for the modifications made to that material.
|
||||
*/
|
||||
|
||||
/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm
|
||||
*/
|
||||
|
||||
/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
rights reserved.
|
||||
|
||||
License to copy and use this software is granted provided that it
|
||||
is identified as the "RSA Data Security, Inc. MD5 Message-Digest
|
||||
Algorithm" in all material mentioning or referencing this software
|
||||
or this function.
|
||||
|
||||
License is also granted to make and use derivative works provided
|
||||
that such works are identified as "derived from the RSA Data
|
||||
Security, Inc. MD5 Message-Digest Algorithm" in all material
|
||||
mentioning or referencing the derived work.
|
||||
|
||||
RSA Data Security, Inc. makes no representations concerning either
|
||||
the merchantability of this software or the suitability of this
|
||||
software for any particular purpose. It is provided "as is"
|
||||
without express or implied warranty of any kind.
|
||||
|
||||
These notices must be retained in any copies of any part of this
|
||||
documentation and/or software.
|
||||
*/
|
||||
/*
|
||||
* The apr_md5_encode() routine uses much code obtained from the FreeBSD 3.0
|
||||
* MD5 crypt() function, which is licenced as follows:
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* <phk@login.dknet.dk> wrote this file. As long as you retain this notice you
|
||||
* can do whatever you want with this stuff. If we meet some day, and you think
|
||||
* this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\crypto\apr_md4.c component:
|
||||
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
* rights reserved.
|
||||
*
|
||||
* License to copy and use this software is granted provided that it
|
||||
* is identified as the "RSA Data Security, Inc. MD4 Message-Digest
|
||||
* Algorithm" in all material mentioning or referencing this software
|
||||
* or this function.
|
||||
*
|
||||
* License is also granted to make and use derivative works provided
|
||||
* that such works are identified as "derived from the RSA Data
|
||||
* Security, Inc. MD4 Message-Digest Algorithm" in all material
|
||||
* mentioning or referencing the derived work.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\include\apr_md4.h component:
|
||||
|
||||
*
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
* rights reserved.
|
||||
*
|
||||
* License to copy and use this software is granted provided that it
|
||||
* is identified as the "RSA Data Security, Inc. MD4 Message-Digest
|
||||
* Algorithm" in all material mentioning or referencing this software
|
||||
* or this function.
|
||||
*
|
||||
* License is also granted to make and use derivative works provided
|
||||
* that such works are identified as "derived from the RSA Data
|
||||
* Security, Inc. MD4 Message-Digest Algorithm" in all material
|
||||
* mentioning or referencing the derived work.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
|
||||
For the srclib\apr-util\test\testmd4.c component:
|
||||
|
||||
*
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1990-2, RSA Data Security, Inc. Created 1990. All
|
||||
* rights reserved.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\xml\expat\conftools\install-sh component:
|
||||
|
||||
#
|
||||
# install - install a program, script, or datafile
|
||||
# This comes from X11R5 (mit/util/scripts/install.sh).
|
||||
#
|
||||
# Copyright 1991 by the Massachusetts Institute of Technology
|
||||
#
|
||||
# Permission to use, copy, modify, distribute, and sell this software and its
|
||||
# documentation for any purpose is hereby granted without fee, provided that
|
||||
# the above copyright notice appear in all copies and that both that
|
||||
# copyright notice and this permission notice appear in supporting
|
||||
# documentation, and that the name of M.I.T. not be used in advertising or
|
||||
# publicity pertaining to distribution of the software without specific,
|
||||
# written prior permission. M.I.T. makes no representations about the
|
||||
# suitability of this software for any purpose. It is provided "as is"
|
||||
# without express or implied warranty.
|
||||
#
|
||||
|
||||
For the test\zb.c component:
|
||||
|
||||
/* ZeusBench V1.01
|
||||
===============
|
||||
|
||||
This program is Copyright (C) Zeus Technology Limited 1996.
|
||||
|
||||
This program may be used and copied freely providing this copyright notice
|
||||
is not removed.
|
||||
|
||||
This software is provided "as is" and any express or implied waranties,
|
||||
including but not limited to, the implied warranties of merchantability and
|
||||
fitness for a particular purpose are disclaimed. In no event shall
|
||||
Zeus Technology Ltd. be liable for any direct, indirect, incidental, special,
|
||||
exemplary, or consequential damaged (including, but not limited to,
|
||||
procurement of substitute good or services; loss of use, data, or profits;
|
||||
or business interruption) however caused and on theory of liability. Whether
|
||||
in contract, strict liability or tort (including negligence or otherwise)
|
||||
arising in any way out of the use of this software, even if advised of the
|
||||
possibility of such damage.
|
||||
|
||||
Written by Adam Twiss (adam@zeus.co.uk). March 1996
|
||||
|
||||
Thanks to the following people for their input:
|
||||
Mike Belshe (mbelshe@netscape.com)
|
||||
Michael Campanella (campanella@stevms.enet.dec.com)
|
||||
|
||||
*/
|
||||
|
||||
For the expat xml parser component:
|
||||
|
||||
Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd
|
||||
and Clark Cooper
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====================================================================
|
||||
486
src/cloudprint-v2.json
Normal file
486
src/cloudprint-v2.json
Normal file
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "cloudprint:v2",
|
||||
"name": "cloudprint",
|
||||
"version": "v2",
|
||||
"revision": "20150605",
|
||||
"title": "Cloud Print API",
|
||||
"description": "Lets you access Cloud Print Printers",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/cloud-print",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://www.google.com/",
|
||||
"basePath": "/cloudprint/",
|
||||
"rootUrl": "https://www.google.com/",
|
||||
"servicePath": "/cloudprint/",
|
||||
"parameters": {
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/cloudprint": {
|
||||
"description": "Manage Cloud Print"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Job": {
|
||||
"id": "Job",
|
||||
"type": "object",
|
||||
"description": "Job Object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Job Title"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jobs": {
|
||||
"id": "Jobs",
|
||||
"type": "object",
|
||||
"description": "List of Jobs.",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "array",
|
||||
"description": "List of job objects.",
|
||||
"items": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printer": {
|
||||
"id": "Printer",
|
||||
"type": "object",
|
||||
"description": "Printer Object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Display Name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printers": {
|
||||
"id": "Printers",
|
||||
"type": "object",
|
||||
"description": "List of Printers.",
|
||||
"properties": {
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"description": "List of printer objects.",
|
||||
"items": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"jobs": {
|
||||
"methods": {
|
||||
"delete": {
|
||||
"id": "cloudprint.jobs.delete",
|
||||
"path": "deletejob",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"required": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"id": "cloudprint.jobs.fetch",
|
||||
"path": "fetch",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"getticket": {
|
||||
"id": "cloudprint.jobs.getticket",
|
||||
"path": "ticket",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"use_cjt": {
|
||||
"type": "boolean",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.jobs.list",
|
||||
"path": "jobs",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"q": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"offset": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"sortorder": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.jobs.update",
|
||||
"path": "control",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"semantic_state_diff": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"resubmit": {
|
||||
"id": "cloudprint.jobs.resubmit",
|
||||
"path": "resubmit",
|
||||
"httpMethod": "POST",
|
||||
"description": "resubmit a job to new printer.",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"id": "cloudprint.jobs.submit",
|
||||
"path": "submit",
|
||||
"httpMethod": "POST",
|
||||
"description": "Send a print job to cloud print.",
|
||||
"request": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"contentType": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"printers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "cloudprint.printers.get",
|
||||
"path": "printer",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.printers.list",
|
||||
"path": "search",
|
||||
"httpMethod": "GET",
|
||||
"description": "List all printers",
|
||||
"parameters": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Query list of printers",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers of type",
|
||||
"location": "query"
|
||||
},
|
||||
"connection_status": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers with this status",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"description": "include extra fields",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printers"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"id": "cloudprint.printers.share",
|
||||
"path": "share",
|
||||
"httpMethod": "GET",
|
||||
"description": "Share printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"skip_notification": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unshare": {
|
||||
"id": "cloudprint.printers.unshare",
|
||||
"path": "unshare",
|
||||
"httpMethod": "GET",
|
||||
"description": "unshare printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "cloudprint.printers.delete",
|
||||
"path": "delete",
|
||||
"httpMethod": "GET",
|
||||
"description": "delete a printer",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.printers.update",
|
||||
"path": "update",
|
||||
"httpMethod": "GET",
|
||||
"description": "update a printer",
|
||||
"parameters": {
|
||||
"isTosAccepted": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"gcpVersion": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"setupUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"supportUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"firmware": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"currentQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"proxy": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"defaultDisplayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"updateUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"quotaEnabled": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"dailyQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/email-settings-v2.json
Normal file
151
src/email-settings-v2.json
Normal file
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "email-settings:v2",
|
||||
"name": "email-settings",
|
||||
"version": "v2",
|
||||
"revision": "20161013",
|
||||
"title": "Email Settings API",
|
||||
"description": "Lets you manage Google Apps Email Settings",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/email-settings",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/emailsettings/2.0/",
|
||||
"parameters": {
|
||||
"v": {
|
||||
"type": "string",
|
||||
"description": "GData Version",
|
||||
"default": "2.0",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"GData 2.0"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"description": "Data format for the response.",
|
||||
"default": "json",
|
||||
"enum": [
|
||||
"json"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"quotaUser": {
|
||||
"type": "string",
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
|
||||
"location": "query"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://apps-apis.google.com/a/feeds/emailsettings/2.0/": {
|
||||
"description": "Manage email settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Delegate": {
|
||||
"id": "Delegate",
|
||||
"type": "object",
|
||||
"description": "a delegate.",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "organization name value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Delegates": {
|
||||
"id": "feed",
|
||||
"type": "object",
|
||||
"description": "List of delegates.",
|
||||
"properties": {
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"description": "list of delegates",
|
||||
"items": {
|
||||
"$ref": "Delegate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"delegates": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "email-settings.delegates.get",
|
||||
"path": "{domainName}/{delegator}/delegation",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Delegates"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "email-settings.delegates.delete",
|
||||
"path": "{domainName}/{delegator}/delegation/{delegate}",
|
||||
"httpMethod": "DELETE",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegate": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
src/gam-install.sh
Executable file
280
src/gam-install.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
usage()
|
||||
{
|
||||
cat << EOF
|
||||
GAM installation script.
|
||||
|
||||
OPTIONS:
|
||||
-h show help.
|
||||
-d Directory where gam folder will be installed. Default is \$HOME/bin/
|
||||
-a Architecture to install (i386, x86_64, arm). Default is to detect your arch with "uname -m".
|
||||
-o OS we are running (linux, macos). Default is to detect your OS with "uname -s".
|
||||
-p Profile update (true, false). Should script add gam command to environment. Default is true.
|
||||
-u Admin user email address to use with GAM. Default is to prompt.
|
||||
-r Regular user email address. Used to test service account access to user data. Default is to prompt.
|
||||
-v Version to install (latest, prerelease, draft, 3.8, etc). Default is latest.
|
||||
EOF
|
||||
}
|
||||
|
||||
target_dir="$HOME/bin"
|
||||
gamarch=$(uname -m)
|
||||
gamos=$(uname -s)
|
||||
update_profile=true
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
while getopts "hd:a:o:p:u:r:v:" OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
h) usage; exit;;
|
||||
d) target_dir=$OPTARG;;
|
||||
a) gamarch=$OPTARG;;
|
||||
o) gamos=$OPTARG;;
|
||||
p) update_profile=$OPTARG;;
|
||||
u) adminuser=$OPTARG;;
|
||||
r) regularuser=$OPTARG;;
|
||||
v) gamversion=$OPTARG;;
|
||||
?) usage; exit;;
|
||||
esac
|
||||
done
|
||||
|
||||
update_profile() {
|
||||
[ -f "$1" ] || return 1
|
||||
|
||||
grep -F "$alias_line" "$1" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo_yellow "Adding gam alias to profile file $1."
|
||||
echo -e "\n$alias_line" >> "$1"
|
||||
else
|
||||
echo_yellow "gam alias already exists in profile file $1. Skipping add."
|
||||
fi
|
||||
}
|
||||
|
||||
echo_red()
|
||||
{
|
||||
echo -e "\x1B[1;31m$1"
|
||||
echo -e '\x1B[0m'
|
||||
}
|
||||
|
||||
echo_green()
|
||||
{
|
||||
echo -e "\x1B[1;32m$1"
|
||||
echo -e '\x1B[0m'
|
||||
}
|
||||
|
||||
echo_yellow()
|
||||
{
|
||||
echo -e "\x1B[1;33m$1"
|
||||
echo -e '\x1B[0m'
|
||||
}
|
||||
|
||||
case $gamos in
|
||||
[lL]inux)
|
||||
gamos="linux"
|
||||
case $gamarch in
|
||||
x86_64) gamfile="linux-x86_64.tar.xz";;
|
||||
i?86) gamfile="linux-i686.tar.xz";;
|
||||
arm*) gamfile="linux-armv7l.tar.xz";;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports i386, x86_64 and arm Linux. Looks like you're running on $gamarch. Exiting."
|
||||
exit
|
||||
esac
|
||||
;;
|
||||
[Mm]ac[Oo][sS]|[Dd]arwin)
|
||||
osver=$(sw_vers -productVersion | awk -F'.' '{print $2}')
|
||||
if (( $osver < 10 )); then
|
||||
echo_red "ERROR: GAM currently requires MacOS 10.10 or newer. You are running MacOS 10.$osver. Please upgrade."
|
||||
exit
|
||||
else
|
||||
echo_green "Good, you're running MacOS 10.$osver..."
|
||||
fi
|
||||
gamos="macos"
|
||||
gamfile="macos.tar.xz"
|
||||
;;
|
||||
*)
|
||||
echo_red "Sorry, this installer currently only supports Linux and MacOS. Looks like you're runnning on $gamos. Exiting."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$gamversion" == "latest" -o "$gamversion" == "prerelease" -o "$gamversion" == "draft" ]; then
|
||||
release_url="https://api.github.com/repos/jay0lee/GAM/releases"
|
||||
else
|
||||
release_url="https://api.github.com/repos/jay0lee/GAM/releases/tags/v$gamversion"
|
||||
fi
|
||||
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release..."
|
||||
release_json=$(curl -s $release_url 2>&1 /dev/null)
|
||||
|
||||
echo_yellow "Getting file and download URL..."
|
||||
# Python is sadly the nearest to universal way to safely handle JSON with Bash
|
||||
# At least this code should be compatible with just about any Python version ever
|
||||
# unlike GAM itself. If some users don't have Python we can try grep / sed / etc
|
||||
# but that gets really ugly
|
||||
pycode="import json
|
||||
import sys
|
||||
|
||||
attrib = sys.argv[1]
|
||||
gamversion = sys.argv[2]
|
||||
|
||||
release = json.load(sys.stdin)
|
||||
if type(release) is list:
|
||||
for a_release in release:
|
||||
if a_release['prerelease'] and gamversion != 'prerelease':
|
||||
continue
|
||||
elif a_release['draft'] and gamversion != 'draft':
|
||||
continue
|
||||
release = a_release
|
||||
break
|
||||
for asset in release['assets']:
|
||||
if asset[sys.argv[1]].endswith('$gamfile'):
|
||||
print asset[sys.argv[1]]
|
||||
break"
|
||||
browser_download_url=$(echo "$release_json" | python -c "$pycode" browser_download_url $gamversion)
|
||||
name=$(echo "$release_json" | python -c "$pycode" name $gamversion)
|
||||
# Temp dir for archive
|
||||
temp_archive_dir=$(mktemp -d)
|
||||
|
||||
echo_yellow "Downloading file $name from $browser_download_url to $temp_archive_dir."
|
||||
# Save archive to temp w/o losing our path
|
||||
(cd $temp_archive_dir && curl -O -L $browser_download_url)
|
||||
|
||||
mkdir -p $target_dir
|
||||
|
||||
echo_yellow "Extracting archive to $target_dir"
|
||||
tar xf $temp_archive_dir/$name -C $target_dir
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: extracting the GAM archive with tar failed with error $rc. Exiting."
|
||||
exit
|
||||
else
|
||||
echo_green "Finished extracting GAM archive."
|
||||
fi
|
||||
|
||||
# Update profile to add gam command
|
||||
if [ "$update_profile" = true ]; then
|
||||
alias_line="alias gam=\"$target_dir/gam/gam\""
|
||||
if [ "$gamos" == "linux" ]; then
|
||||
update_profile "$HOME/.bashrc" || update_profile "$HOME/.bash_profile"
|
||||
elif [ "$gamos" == "macos" ]; then
|
||||
update_profile "$HOME/.profile" || update_profile "$HOME/.bash_profile"
|
||||
fi
|
||||
else
|
||||
echo_yellow "skipping profile update."
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -p "Can you run a full browser on this machine? (usually Y for MacOS, N for Linux if you SSH into this machine) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
break
|
||||
;;
|
||||
[Nn]*)
|
||||
touch $target_dir/gam/nobrowser.txt > /dev/null 2>&1
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo_red "Please answer yes or no."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo
|
||||
|
||||
project_created=false
|
||||
while true; do
|
||||
read -p "GAM is now installed. Are you ready to set up a Google API project for GAM? (yes or no) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
if [ "$adminuser" == "" ]; then
|
||||
read -p "Please enter your G Suite admin email address: " adminuser
|
||||
fi
|
||||
$target_dir/gam/gam create project $adminuser
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Project creation complete."
|
||||
project_created=true
|
||||
break
|
||||
else
|
||||
echo_red "Projection creation failed. Trying again. Say N to skip projection creation."
|
||||
fi
|
||||
;;
|
||||
[Nn]*)
|
||||
echo -e "\nYou can create an API project later by running:\n\ngam create project\n"
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo_red "Please answer yes or no."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
admin_authorized=false
|
||||
while $project_created; do
|
||||
read -p "Are you ready to authorize GAM to perform G Suite management operations as your admin account? (yes or no) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
$target_dir/gam/gam oauth create $adminuser
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Admin authorization complete."
|
||||
admin_authorized=true
|
||||
break
|
||||
else
|
||||
echo_red "Admin authorization failed. Trying again. Say N to skip admin authorization."
|
||||
fi
|
||||
;;
|
||||
[Nn]*)
|
||||
echo -e "\nYou can authorize an admin later by running:\n\ngam oauth create\n"
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo_red "Please answer yes or no."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
service_account_authorized=false
|
||||
while $project_created; do
|
||||
read -p "Are you ready to authorize GAM to manage G Suite user data and settings? (yes or no) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
if [ "$regularuser" == "" ]; then
|
||||
read -p "Please enter the email address of a regular G Suite user: " regularuser
|
||||
fi
|
||||
echo_yellow "Great! Checking service account scopes.This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console."
|
||||
$target_dir/gam/gam user $adminuser check serviceaccount
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Service account authorization complete."
|
||||
service_account_authorized=true
|
||||
break
|
||||
else
|
||||
echo_red "Service account authorization failed. Confirm you entered the scopes correctly in the admin console. It can take a few minutes for scopes to PASS after they are entered in the admin console so if you're sure you entered them correctly, go grab a coffee and then hit Y to try again. Say N to skip admin authorization."
|
||||
fi
|
||||
;;
|
||||
[Nn]*)
|
||||
echo -e "\nYou can authorize a service account later by running:\n\ngam check serviceaccount\n"
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo_red "Please answer yes or no."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo_green "Here's information about your new GAM installation:"
|
||||
$target_dir/gam/gam version
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: Failed running GAM for the first time with $rc. Please report this error to GAM mailing list. Exiting."
|
||||
exit
|
||||
fi
|
||||
|
||||
echo_green "GAM installation and setup complete!"
|
||||
if [ "$update_profile" = true ]; then
|
||||
echo_green "Please restart your terminal shell or to get started right away run:\n\n$alias_line"
|
||||
fi
|
||||
|
||||
# Clean up after ourselves even if we are killed with CTRL-C
|
||||
trap "rm -rf $temp_archive_dir" EXIT
|
||||
75
src/gam-setup.bat
Normal file
75
src/gam-setup.bat
Normal file
@@ -0,0 +1,75 @@
|
||||
@echo(
|
||||
@set /p adminemail= "Please enter your G Suite admin email address: "
|
||||
|
||||
:createproject
|
||||
@echo(
|
||||
@set /p yn= "Are you ready to set up a Google API project for GAM? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo You can create an API project later by running:
|
||||
@ echo(
|
||||
@ echo gam create project
|
||||
@ goto alldone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo Please answer y or n.
|
||||
@ goto createproject
|
||||
)
|
||||
@gam create project %adminemail%
|
||||
@if not ERRORLEVEL 1 goto projectdone
|
||||
@echo(
|
||||
@echo Projection creation failed. Trying again. Say n to skip projection creation.
|
||||
@goto createproject
|
||||
:projectdone
|
||||
|
||||
:adminauth
|
||||
@echo(
|
||||
@set /p yn= "Are you ready to authorize GAM to perform G Suite management operations as your admin account? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo You can authorize an admin later by running:
|
||||
@ echo(
|
||||
@ echo gam oauth create %adminemail%
|
||||
@ goto admindone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo Please answer y or n.
|
||||
@ goto adminauth
|
||||
)
|
||||
@gam oauth create %adminemail%
|
||||
@if not ERRORLEVEL 1 goto admindone
|
||||
@echo(
|
||||
@echo Admin authorization failed. Trying again. Say n to skip admin authorization.
|
||||
@goto adminauth
|
||||
:admindone
|
||||
|
||||
:saauth
|
||||
@echo(
|
||||
@set /p yn= "Are you ready to authorize GAM to manage G Suite user data and settings? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo You can authorize a service account later by running:
|
||||
@ echo(
|
||||
@ echo gam user %adminemail% check serviceaccount
|
||||
@ goto sadone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo Please answer y or n.
|
||||
@ goto saauth
|
||||
)
|
||||
@echo(
|
||||
@set /p regularuser= "Please enter the email address of a regular G Suite user: "
|
||||
@echo Great! Checking service account scopes. This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console.
|
||||
@gam user %regularuser% check serviceaccount
|
||||
@if not ERRORLEVEL 1 goto sadone
|
||||
@echo(
|
||||
@echo Service account authorization failed. Confirm you entered the scopes correctly in the admin console. It can take a few minutes for scopes to PASS after they are entered in the admin console so if you're sure you entered them correctly, go grab a coffee and then hit Y to try again. Say N to skip admin authorization.
|
||||
@goto saauth
|
||||
:sadone
|
||||
|
||||
@echo GAM installation and setup complete!
|
||||
:alldone
|
||||
@pause
|
||||
10879
src/gam.py
Executable file
10879
src/gam.py
Executable file
File diff suppressed because it is too large
Load Diff
74
src/gam.wxs
Normal file
74
src/gam.wxs
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" >
|
||||
<Product
|
||||
Id="*"
|
||||
Name="GAM"
|
||||
Language="1033"
|
||||
Version="$(env.GAMVERSION)"
|
||||
Manufacturer="Jay Lee - jay0lee@gmail.com"
|
||||
UpgradeCode="15C5FD61-B04C-4E04-A26D-CD8424C19D9F">
|
||||
<Package
|
||||
InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
|
||||
|
||||
<MajorUpgrade
|
||||
DowngradeErrorMessage=
|
||||
"A newer version of [ProductName] is already installed."
|
||||
Schedule="afterInstallExecute" />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
||||
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Feature
|
||||
Id="gam"
|
||||
Title="GAM"
|
||||
Level="1">
|
||||
<ComponentGroupRef Id="ProductComponents" />
|
||||
</Feature>
|
||||
</Product>
|
||||
|
||||
<Fragment>
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ROOTDRIVE">
|
||||
<Directory Id="INSTALLFOLDER" Name="GAM" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Fragment>
|
||||
|
||||
<Fragment>
|
||||
<!-- Group of components that are our main application items -->
|
||||
<ComponentGroup
|
||||
Id="ProductComponents"
|
||||
Directory="INSTALLFOLDER"
|
||||
Source="gam-64">
|
||||
<Component Id="gam_exe" Guid="886abc07-73c5-4acc-9f71-58daf62aabc1">
|
||||
<File Name="gam.exe" KeyPath="yes" />
|
||||
<Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]" Permanent="yes" Part="last" Action="set" System="yes" />
|
||||
</Component>
|
||||
<Component Id="license" Guid="7a15de2e-fb91-4d0a-b8bf-c8b19c68f569">
|
||||
<File Name="LICENSE" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="whatsnew_txt" Guid="6aa9863c-90d9-412f-9b73-fda82549a950">
|
||||
<File Name="whatsnew.txt" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="gam_setup_bat" Guid="ef01f93a-4b50-488a-9c04-ec5e13e66218">
|
||||
<File Name="gam-setup.bat" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="gamcommands_txt" Guid="58ff9c45-a7c9-4e22-8845-a9a92610c1f3">
|
||||
<File Name="gamcommands.txt" KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
|
||||
<Fragment>
|
||||
<InstallUISequence>
|
||||
<ExecuteAction />
|
||||
<Show Dialog="WelcomeDlg" Before="ProgressDlg" />
|
||||
</InstallUISequence>
|
||||
<CustomAction Id="setup_gam" ExeCommand="[INSTALLFOLDER]gam-setup.bat" Directory="INSTALLFOLDER" Execute="commit" Impersonate="yes" Return="asyncWait"/>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="setup_gam" After="InstallFiles" >NOT Installed AND NOT UPGRADINGPRODUCTCODE AND NOT WIX_UPGRADE_DETECTED</Custom>
|
||||
</InstallExecuteSequence>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
27
src/googleapiclient/__init__.py
Normal file
27
src/googleapiclient/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "1.5.2"
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
294
src/googleapiclient/channel.py
Normal file
294
src/googleapiclient/channel.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""Channel notifications support.
|
||||
|
||||
Classes and functions to support channel subscriptions and notifications
|
||||
on those channels.
|
||||
|
||||
Notes:
|
||||
- This code is based on experimental APIs and is subject to change.
|
||||
- Notification does not do deduplication of notification ids, that's up to
|
||||
the receiver.
|
||||
- Storing the Channel between calls is up to the caller.
|
||||
|
||||
|
||||
Example setting up a channel:
|
||||
|
||||
# Create a new channel that gets notifications via webhook.
|
||||
channel = new_webhook_channel("https://example.com/my_web_hook")
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
||||
# watch method because notifications may start arriving before the watch
|
||||
# method returns.
|
||||
...
|
||||
|
||||
resp = service.objects().watchAll(
|
||||
bucket="some_bucket_id", body=channel.body()).execute()
|
||||
channel.update(resp)
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
||||
# since the resource_id value will now be correct, and that's needed to
|
||||
# stop a subscription.
|
||||
...
|
||||
|
||||
|
||||
An example Webhook implementation using webapp2. Note that webapp2 puts
|
||||
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
||||
always be upper case.
|
||||
|
||||
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
||||
|
||||
# Retrieve the channel by id.
|
||||
channel = ...
|
||||
|
||||
# Parse notification from the headers, including validating the id.
|
||||
n = notification_from_headers(channel, self.request.headers)
|
||||
|
||||
# Do app specific stuff with the notification here.
|
||||
if n.resource_state == 'sync':
|
||||
# Code to handle sync state.
|
||||
elif n.resource_state == 'exists':
|
||||
# Code to handle the exists state.
|
||||
elif n.resource_state == 'not_exists':
|
||||
# Code to handle the not exists state.
|
||||
|
||||
|
||||
Example of unsubscribing.
|
||||
|
||||
service.channels().stop(channel.body())
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from googleapiclient import errors
|
||||
from oauth2client import util
|
||||
import six
|
||||
|
||||
# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
|
||||
# in '_helpers'.
|
||||
try:
|
||||
from oauth2client import util
|
||||
except ImportError:
|
||||
from oauth2client import _helpers as util
|
||||
|
||||
|
||||
# The unix time epoch starts at midnight 1970.
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
|
||||
# Map the names of the parameters in the JSON channel description to
|
||||
# the parameter names we use in the Channel class.
|
||||
CHANNEL_PARAMS = {
|
||||
'address': 'address',
|
||||
'id': 'id',
|
||||
'expiration': 'expiration',
|
||||
'params': 'params',
|
||||
'resourceId': 'resource_id',
|
||||
'resourceUri': 'resource_uri',
|
||||
'type': 'type',
|
||||
'token': 'token',
|
||||
}
|
||||
|
||||
X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID'
|
||||
X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER'
|
||||
X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE'
|
||||
X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI'
|
||||
X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID'
|
||||
|
||||
|
||||
def _upper_header_keys(headers):
|
||||
new_headers = {}
|
||||
for k, v in six.iteritems(headers):
|
||||
new_headers[k.upper()] = v
|
||||
return new_headers
|
||||
|
||||
|
||||
class Notification(object):
|
||||
"""A Notification from a Channel.
|
||||
|
||||
Notifications are not usually constructed directly, but are returned
|
||||
from functions like notification_from_headers().
|
||||
|
||||
Attributes:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored.
|
||||
uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The unique identifier of the version of the resource at
|
||||
this event.
|
||||
"""
|
||||
@util.positional(5)
|
||||
def __init__(self, message_number, state, resource_uri, resource_id):
|
||||
"""Notification constructor.
|
||||
|
||||
Args:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored. Can be one
|
||||
of "exists", "not_exists", or "sync".
|
||||
resource_uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The identifier of the watched resource.
|
||||
"""
|
||||
self.message_number = message_number
|
||||
self.state = state
|
||||
self.resource_uri = resource_uri
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class Channel(object):
|
||||
"""A Channel for notifications.
|
||||
|
||||
Usually not constructed directly, instead it is returned from helper
|
||||
functions like new_webhook_channel().
|
||||
|
||||
Attributes:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(self, type, id, token, address, expiration=None,
|
||||
params=None, resource_id="", resource_uri=""):
|
||||
"""Create a new Channel.
|
||||
|
||||
In user code, this Channel constructor will not typically be called
|
||||
manually since there are functions for creating channels for each specific
|
||||
type with a more customized set of arguments to pass.
|
||||
|
||||
Args:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.token = token
|
||||
self.address = address
|
||||
self.expiration = expiration
|
||||
self.params = params
|
||||
self.resource_id = resource_id
|
||||
self.resource_uri = resource_uri
|
||||
|
||||
def body(self):
|
||||
"""Build a body from the Channel.
|
||||
|
||||
Constructs a dictionary that's appropriate for passing into watch()
|
||||
methods as the value of body argument.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the channel.
|
||||
"""
|
||||
result = {
|
||||
'id': self.id,
|
||||
'token': self.token,
|
||||
'type': self.type,
|
||||
'address': self.address
|
||||
}
|
||||
if self.params:
|
||||
result['params'] = self.params
|
||||
if self.resource_id:
|
||||
result['resourceId'] = self.resource_id
|
||||
if self.resource_uri:
|
||||
result['resourceUri'] = self.resource_uri
|
||||
if self.expiration:
|
||||
result['expiration'] = self.expiration
|
||||
|
||||
return result
|
||||
|
||||
def update(self, resp):
|
||||
"""Update a channel with information from the response of watch().
|
||||
|
||||
When a request is sent to watch() a resource, the response returned
|
||||
from the watch() request is a dictionary with updated channel information,
|
||||
such as the resource_id, which is needed when stopping a subscription.
|
||||
|
||||
Args:
|
||||
resp: dict, The response from a watch() method.
|
||||
"""
|
||||
for json_name, param_name in six.iteritems(CHANNEL_PARAMS):
|
||||
value = resp.get(json_name)
|
||||
if value is not None:
|
||||
setattr(self, param_name, value)
|
||||
|
||||
|
||||
def notification_from_headers(channel, headers):
|
||||
"""Parse a notification from the webhook request headers, validate
|
||||
the notification, and return a Notification object.
|
||||
|
||||
Args:
|
||||
channel: Channel, The channel that the notification is associated with.
|
||||
headers: dict, A dictionary like object that contains the request headers
|
||||
from the webhook HTTP request.
|
||||
|
||||
Returns:
|
||||
A Notification object.
|
||||
|
||||
Raises:
|
||||
errors.InvalidNotificationError if the notification is invalid.
|
||||
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
||||
"""
|
||||
headers = _upper_header_keys(headers)
|
||||
channel_id = headers[X_GOOG_CHANNEL_ID]
|
||||
if channel.id != channel_id:
|
||||
raise errors.InvalidNotificationError(
|
||||
'Channel id mismatch: %s != %s' % (channel.id, channel_id))
|
||||
else:
|
||||
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
||||
state = headers[X_GOOG_RESOURCE_STATE]
|
||||
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
||||
resource_id = headers[X_GOOG_RESOURCE_ID]
|
||||
return Notification(message_number, state, resource_uri, resource_id)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
||||
"""Create a new webhook Channel.
|
||||
|
||||
Args:
|
||||
url: str, URL to post notifications to.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each notification delivered
|
||||
over this channel.
|
||||
expiration: datetime.datetime, A time in the future when the channel
|
||||
should expire. Can also be None if the subscription should use the
|
||||
default expiration. Note that different services may have different
|
||||
limits on how long a subscription lasts. Check the response from the
|
||||
watch() method to see the value the service has set for an expiration
|
||||
time.
|
||||
params: dict, Extra parameters to pass on channel creation. Currently
|
||||
not used for webhook channels.
|
||||
"""
|
||||
expiration_ms = 0
|
||||
if expiration:
|
||||
delta = expiration - EPOCH
|
||||
expiration_ms = delta.microseconds/1000 + (
|
||||
delta.seconds + delta.days*24*3600)*1000
|
||||
if expiration_ms < 0:
|
||||
expiration_ms = 0
|
||||
|
||||
return Channel('web_hook', str(uuid.uuid4()),
|
||||
token, url, expiration=expiration_ms,
|
||||
params=params)
|
||||
|
||||
1109
src/googleapiclient/discovery.py
Normal file
1109
src/googleapiclient/discovery.py
Normal file
File diff suppressed because it is too large
Load Diff
45
src/googleapiclient/discovery_cache/__init__.py
Normal file
45
src/googleapiclient/discovery_cache/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Caching utility for the discovery document."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
|
||||
|
||||
|
||||
def autodetect():
|
||||
"""Detects an appropriate cache module and returns it.
|
||||
|
||||
Returns:
|
||||
googleapiclient.discovery_cache.base.Cache, a cache object which
|
||||
is auto detected, or None if no cache object is available.
|
||||
"""
|
||||
try:
|
||||
from google.appengine.api import memcache
|
||||
from . import appengine_memcache
|
||||
return appengine_memcache.cache
|
||||
except Exception:
|
||||
try:
|
||||
from . import file_cache
|
||||
return file_cache.cache
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
return None
|
||||
55
src/googleapiclient/discovery_cache/appengine_memcache.py
Normal file
55
src/googleapiclient/discovery_cache/appengine_memcache.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""App Engine memcache based cache for the discovery document."""
|
||||
|
||||
import logging
|
||||
|
||||
# This is only an optional dependency because we only import this
|
||||
# module when google.appengine.api.memcache is available.
|
||||
from google.appengine.api import memcache
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NAMESPACE = 'google-api-client'
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A cache with app engine memcache API."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
|
||||
def get(self, url):
|
||||
try:
|
||||
return memcache.get(url, namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
def set(self, url, content):
|
||||
try:
|
||||
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
45
src/googleapiclient/discovery_cache/base.py
Normal file
45
src/googleapiclient/discovery_cache/base.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""An abstract class for caching the discovery document."""
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""A base abstract cache class."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, url):
|
||||
"""Gets the content from the memcache with a given key.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
|
||||
Returns:
|
||||
object, the value in the cache for the given key, or None if the key is
|
||||
not in the cache.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, url, content):
|
||||
"""Sets the given key and content in the cache.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
content: string, the discovery document.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
136
src/googleapiclient/discovery_cache/file_cache.py
Normal file
136
src/googleapiclient/discovery_cache/file_cache.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""File based cache for the discovery document.
|
||||
|
||||
The cache is stored in a single file so that multiple processes can
|
||||
share the same cache. It locks the file whenever accesing to the
|
||||
file. When the cache content is corrupted, it will be initialized with
|
||||
an empty cache.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
try:
|
||||
from oauth2client.contrib.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client < 2.0.0
|
||||
from oauth2client.locked_file import LockedFile
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FILENAME = 'google-api-python-client-discovery-doc.cache'
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
|
||||
|
||||
def _to_timestamp(date):
|
||||
try:
|
||||
return (date - EPOCH).total_seconds()
|
||||
except AttributeError:
|
||||
# The following is the equivalent of total_seconds() in Python2.6.
|
||||
# See also: https://docs.python.org/2/library/datetime.html
|
||||
delta = date - EPOCH
|
||||
return ((delta.microseconds + (delta.seconds + delta.days * 24 * 3600)
|
||||
* 10**6) / 10**6)
|
||||
|
||||
|
||||
def _read_or_initialize_cache(f):
|
||||
f.file_handle().seek(0)
|
||||
try:
|
||||
cache = json.load(f.file_handle())
|
||||
except Exception:
|
||||
# This means it opens the file for the first time, or the cache is
|
||||
# corrupted, so initializing the file with an empty dict.
|
||||
cache = {}
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
return cache
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A file based cache for the discovery documents."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
self._file = os.path.join(tempfile.gettempdir(), FILENAME)
|
||||
f = LockedFile(self._file, 'a+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
_read_or_initialize_cache(f)
|
||||
# If we can not obtain the lock, other process or thread must
|
||||
# have initialized the file.
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def get(self, url):
|
||||
f = LockedFile(self._file, 'r+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
if url in cache:
|
||||
content, t = cache.get(url, (None, 0))
|
||||
if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
|
||||
return content
|
||||
return None
|
||||
else:
|
||||
LOGGER.debug('Could not obtain a lock for the cache file.')
|
||||
return None
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def set(self, url, content):
|
||||
f = LockedFile(self._file, 'r+', 'r')
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
cache[url] = (content, _to_timestamp(datetime.datetime.now()))
|
||||
# Remove stale cache.
|
||||
for k, (_, timestamp) in list(cache.items()):
|
||||
if _to_timestamp(datetime.datetime.now()) >= timestamp + self._max_age:
|
||||
del cache[k]
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
else:
|
||||
LOGGER.debug('Could not obtain a lock for the cache file.')
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
146
src/googleapiclient/errors.py
Normal file
146
src/googleapiclient/errors.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Errors for the library.
|
||||
|
||||
All exceptions defined by the library
|
||||
should be defined in this file.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
|
||||
# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
|
||||
# in '_helpers'.
|
||||
try:
|
||||
from oauth2client import util
|
||||
except ImportError:
|
||||
from oauth2client import _helpers as util
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class HttpError(Error):
|
||||
"""HTTP data was invalid or unexpected."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, resp, content, uri=None):
|
||||
self.resp = resp
|
||||
if not isinstance(content, bytes):
|
||||
raise TypeError("HTTP content should be bytes")
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
|
||||
def _get_reason(self):
|
||||
"""Calculate the reason for the error from the response content."""
|
||||
reason = self.resp.reason
|
||||
try:
|
||||
data = json.loads(self.content.decode('utf-8'))
|
||||
reason = data['error']['message']
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
if reason is None:
|
||||
reason = ''
|
||||
return reason
|
||||
|
||||
def __repr__(self):
|
||||
if self.uri:
|
||||
return '<HttpError %s when requesting %s returned "%s">' % (
|
||||
self.resp.status, self.uri, self._get_reason().strip())
|
||||
else:
|
||||
return '<HttpError %s "%s">' % (self.resp.status, self._get_reason())
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class InvalidJsonError(Error):
|
||||
"""The JSON returned could not be parsed."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownFileType(Error):
|
||||
"""File type unknown or unexpected."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownLinkType(Error):
|
||||
"""Link type unknown or unexpected."""
|
||||
pass
|
||||
|
||||
|
||||
class UnknownApiNameOrVersion(Error):
|
||||
"""No API with that name and version exists."""
|
||||
pass
|
||||
|
||||
|
||||
class UnacceptableMimeTypeError(Error):
|
||||
"""That is an unacceptable mimetype for this operation."""
|
||||
pass
|
||||
|
||||
|
||||
class MediaUploadSizeError(Error):
|
||||
"""Media is larger than the method can accept."""
|
||||
pass
|
||||
|
||||
|
||||
class ResumableUploadError(HttpError):
|
||||
"""Error occured during resumable upload."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChunkSizeError(Error):
|
||||
"""The given chunksize is not valid."""
|
||||
pass
|
||||
|
||||
class InvalidNotificationError(Error):
|
||||
"""The channel Notification is invalid."""
|
||||
pass
|
||||
|
||||
class BatchError(HttpError):
|
||||
"""Error occured during batch operations."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, reason, resp=None, content=None):
|
||||
self.resp = resp
|
||||
self.content = content
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
return '<BatchError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class UnexpectedMethodError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected calls."""
|
||||
|
||||
@util.positional(1)
|
||||
def __init__(self, methodId=None):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedMethodError, self).__init__(
|
||||
'Received unexpected call %s' % methodId)
|
||||
|
||||
|
||||
class UnexpectedBodyError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected bodies."""
|
||||
|
||||
def __init__(self, expected, provided):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedBodyError, self).__init__(
|
||||
'Expected: [%s] - Provided: [%s]' % (expected, provided))
|
||||
1730
src/googleapiclient/http.py
Normal file
1730
src/googleapiclient/http.py
Normal file
File diff suppressed because it is too large
Load Diff
175
src/googleapiclient/mimeparse.py
Normal file
175
src/googleapiclient/mimeparse.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# Copyright 2014 Joe Gregorio
|
||||
#
|
||||
# Licensed under the MIT License
|
||||
|
||||
"""MIME-Type Parser
|
||||
|
||||
This module provides basic functions for handling mime-types. It can handle
|
||||
matching mime-types against a list of media-ranges. See section 14.1 of the
|
||||
HTTP specification [RFC 2616] for a complete explanation.
|
||||
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
||||
|
||||
Contents:
|
||||
- parse_mime_type(): Parses a mime-type into its component parts.
|
||||
- parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q'
|
||||
quality parameter.
|
||||
- quality(): Determines the quality ('q') of a mime-type when
|
||||
compared against a list of media-ranges.
|
||||
- quality_parsed(): Just like quality() except the second parameter must be
|
||||
pre-parsed.
|
||||
- best_match(): Choose the mime-type with the highest quality ('q')
|
||||
from a list of candidates.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from functools import reduce
|
||||
import six
|
||||
|
||||
__version__ = '0.1.3'
|
||||
__author__ = 'Joe Gregorio'
|
||||
__email__ = 'joe@bitworking.org'
|
||||
__license__ = 'MIT License'
|
||||
__credits__ = ''
|
||||
|
||||
|
||||
def parse_mime_type(mime_type):
|
||||
"""Parses a mime-type into its component parts.
|
||||
|
||||
Carves up a mime-type and returns a tuple of the (type, subtype, params)
|
||||
where 'params' is a dictionary of all the parameters for the media range.
|
||||
For example, the media range 'application/xhtml;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', 'xhtml', {'q', '0.5'})
|
||||
"""
|
||||
parts = mime_type.split(';')
|
||||
params = dict([tuple([s.strip() for s in param.split('=', 1)])\
|
||||
for param in parts[1:]
|
||||
])
|
||||
full_type = parts[0].strip()
|
||||
# Java URLConnection class sends an Accept header that includes a
|
||||
# single '*'. Turn it into a legal wildcard.
|
||||
if full_type == '*':
|
||||
full_type = '*/*'
|
||||
(type, subtype) = full_type.split('/')
|
||||
|
||||
return (type.strip(), subtype.strip(), params)
|
||||
|
||||
|
||||
def parse_media_range(range):
|
||||
"""Parse a media-range into its component parts.
|
||||
|
||||
Carves up a media range and returns a tuple of the (type, subtype,
|
||||
params) where 'params' is a dictionary of all the parameters for the media
|
||||
range. For example, the media range 'application/*;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', '*', {'q', '0.5'})
|
||||
|
||||
In addition this function also guarantees that there is a value for 'q'
|
||||
in the params dictionary, filling it in with a proper default if
|
||||
necessary.
|
||||
"""
|
||||
(type, subtype, params) = parse_mime_type(range)
|
||||
if 'q' not in params or not params['q'] or \
|
||||
not float(params['q']) or float(params['q']) > 1\
|
||||
or float(params['q']) < 0:
|
||||
params['q'] = '1'
|
||||
|
||||
return (type, subtype, params)
|
||||
|
||||
|
||||
def fitness_and_quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns a tuple of
|
||||
the fitness value and the value of the 'q' quality parameter of the best
|
||||
match, or (-1, 0) if no match was found. Just as for quality_parsed(),
|
||||
'parsed_ranges' must be a list of parsed media ranges.
|
||||
"""
|
||||
best_fitness = -1
|
||||
best_fit_q = 0
|
||||
(target_type, target_subtype, target_params) =\
|
||||
parse_media_range(mime_type)
|
||||
for (type, subtype, params) in parsed_ranges:
|
||||
type_match = (type == target_type or\
|
||||
type == '*' or\
|
||||
target_type == '*')
|
||||
subtype_match = (subtype == target_subtype or\
|
||||
subtype == '*' or\
|
||||
target_subtype == '*')
|
||||
if type_match and subtype_match:
|
||||
param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \
|
||||
six.iteritems(target_params) if key != 'q' and \
|
||||
key in params and value == params[key]], 0)
|
||||
fitness = (type == target_type) and 100 or 0
|
||||
fitness += (subtype == target_subtype) and 10 or 0
|
||||
fitness += param_matches
|
||||
if fitness > best_fitness:
|
||||
best_fitness = fitness
|
||||
best_fit_q = params['q']
|
||||
|
||||
return best_fitness, float(best_fit_q)
|
||||
|
||||
|
||||
def quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns the 'q'
|
||||
quality parameter of the best match, 0 if no match was found. This function
|
||||
bahaves the same as quality() except that 'parsed_ranges' must be a list of
|
||||
parsed media ranges.
|
||||
"""
|
||||
|
||||
return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
|
||||
|
||||
|
||||
def quality(mime_type, ranges):
|
||||
"""Return the quality ('q') of a mime-type against a list of media-ranges.
|
||||
|
||||
Returns the quality 'q' of a mime-type when compared against the
|
||||
media-ranges in ranges. For example:
|
||||
|
||||
>>> quality('text/html','text/*;q=0.3, text/html;q=0.7,
|
||||
text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
|
||||
0.7
|
||||
|
||||
"""
|
||||
parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
|
||||
|
||||
return quality_parsed(mime_type, parsed_ranges)
|
||||
|
||||
|
||||
def best_match(supported, header):
|
||||
"""Return mime-type with the highest quality ('q') from list of candidates.
|
||||
|
||||
Takes a list of supported mime-types and finds the best match for all the
|
||||
media-ranges listed in header. The value of header must be a string that
|
||||
conforms to the format of the HTTP Accept: header. The value of 'supported'
|
||||
is a list of mime-types. The list of supported mime-types should be sorted
|
||||
in order of increasing desirability, in case of a situation where there is
|
||||
a tie.
|
||||
|
||||
>>> best_match(['application/xbel+xml', 'text/xml'],
|
||||
'text/*;q=0.5,*/*; q=0.1')
|
||||
'text/xml'
|
||||
"""
|
||||
split_header = _filter_blank(header.split(','))
|
||||
parsed_header = [parse_media_range(r) for r in split_header]
|
||||
weighted_matches = []
|
||||
pos = 0
|
||||
for mime_type in supported:
|
||||
weighted_matches.append((fitness_and_quality_parsed(mime_type,
|
||||
parsed_header), pos, mime_type))
|
||||
pos += 1
|
||||
weighted_matches.sort()
|
||||
|
||||
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ''
|
||||
|
||||
|
||||
def _filter_blank(i):
|
||||
for s in i:
|
||||
if s.strip():
|
||||
yield s
|
||||
389
src/googleapiclient/model.py
Normal file
389
src/googleapiclient/model.py
Normal file
@@ -0,0 +1,389 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Model objects for requests and responses.
|
||||
|
||||
Each API may support one or more serializations, such
|
||||
as JSON, Atom, etc. The model classes are responsible
|
||||
for converting between the wire format and the Python
|
||||
object representation.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import six
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from googleapiclient import __version__
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
dump_request_response = False
|
||||
|
||||
|
||||
def _abstract():
|
||||
raise NotImplementedError('You need to override this function')
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""Model base class.
|
||||
|
||||
All Model classes should implement this interface.
|
||||
The Model serializes and de-serializes between a wire
|
||||
format such as JSON and a Python object representation.
|
||||
"""
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized in the desired wire format.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
"""Base model class.
|
||||
|
||||
Subclasses should provide implementations for the "serialize" and
|
||||
"deserialize" methods, as well as values for the following class attributes.
|
||||
|
||||
Attributes:
|
||||
accept: The value to use for the HTTP Accept header.
|
||||
content_type: The value to use for the HTTP Content-type header.
|
||||
no_content_response: The value to return when deserializing a 204 "No
|
||||
Content" response.
|
||||
alt_param: The value to supply as the "alt" query parameter for requests.
|
||||
"""
|
||||
|
||||
accept = None
|
||||
content_type = None
|
||||
no_content_response = None
|
||||
alt_param = None
|
||||
|
||||
def _log_request(self, headers, path_params, query, body):
|
||||
"""Logs debugging information about the request if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info('--request-start--')
|
||||
LOGGER.info('-headers-start-')
|
||||
for h, v in six.iteritems(headers):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
LOGGER.info('-headers-end-')
|
||||
LOGGER.info('-path-parameters-start-')
|
||||
for h, v in six.iteritems(path_params):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
LOGGER.info('-path-parameters-end-')
|
||||
LOGGER.info('body: %s', body)
|
||||
LOGGER.info('query: %s', query)
|
||||
LOGGER.info('--request-end--')
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable by json.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized as JSON
|
||||
"""
|
||||
query = self._build_query(query_params)
|
||||
headers['accept'] = self.accept
|
||||
headers['accept-encoding'] = 'gzip, deflate'
|
||||
if 'user-agent' in headers:
|
||||
headers['user-agent'] += ' '
|
||||
else:
|
||||
headers['user-agent'] = ''
|
||||
headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__
|
||||
|
||||
if body_value is not None:
|
||||
headers['content-type'] = self.content_type
|
||||
body_value = self.serialize(body_value)
|
||||
self._log_request(headers, path_params, query, body_value)
|
||||
return (headers, path_params, query, body_value)
|
||||
|
||||
def _build_query(self, params):
|
||||
"""Builds a query string.
|
||||
|
||||
Args:
|
||||
params: dict, the query parameters
|
||||
|
||||
Returns:
|
||||
The query parameters properly encoded into an HTTP URI query string.
|
||||
"""
|
||||
if self.alt_param is not None:
|
||||
params.update({'alt': self.alt_param})
|
||||
astuples = []
|
||||
for key, value in six.iteritems(params):
|
||||
if type(value) == type([]):
|
||||
for x in value:
|
||||
x = x.encode('utf-8')
|
||||
astuples.append((key, x))
|
||||
else:
|
||||
if isinstance(value, six.text_type) and callable(value.encode):
|
||||
value = value.encode('utf-8')
|
||||
astuples.append((key, value))
|
||||
return '?' + urlencode(astuples)
|
||||
|
||||
def _log_response(self, resp, content):
|
||||
"""Logs debugging information about the response if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info('--response-start--')
|
||||
for h, v in six.iteritems(resp):
|
||||
LOGGER.info('%s: %s', h, v)
|
||||
if content:
|
||||
LOGGER.info(content)
|
||||
LOGGER.info('--response-end--')
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
self._log_response(resp, content)
|
||||
# Error handling is TBD, for example, do we retry
|
||||
# for some operation/error combinations?
|
||||
if resp.status < 300:
|
||||
if resp.status == 204:
|
||||
# A 204: No Content response should be treated differently
|
||||
# to all the other success states
|
||||
return self.no_content_response
|
||||
return self.deserialize(content)
|
||||
else:
|
||||
LOGGER.debug('Content from bad request was: %s' % content)
|
||||
raise HttpError(resp, content)
|
||||
|
||||
def serialize(self, body_value):
|
||||
"""Perform the actual Python object serialization.
|
||||
|
||||
Args:
|
||||
body_value: object, the request body as a Python object.
|
||||
|
||||
Returns:
|
||||
string, the body in serialized form.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def deserialize(self, content):
|
||||
"""Perform the actual deserialization from response string to Python
|
||||
object.
|
||||
|
||||
Args:
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class JsonModel(BaseModel):
|
||||
"""Model class for JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request and response bodies.
|
||||
"""
|
||||
accept = 'application/json'
|
||||
content_type = 'application/json'
|
||||
alt_param = 'json'
|
||||
|
||||
def __init__(self, data_wrapper=False):
|
||||
"""Construct a JsonModel.
|
||||
|
||||
Args:
|
||||
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
||||
"""
|
||||
self._data_wrapper = data_wrapper
|
||||
|
||||
def serialize(self, body_value):
|
||||
if (isinstance(body_value, dict) and 'data' not in body_value and
|
||||
self._data_wrapper):
|
||||
body_value = {'data': body_value}
|
||||
return json.dumps(body_value)
|
||||
|
||||
def deserialize(self, content):
|
||||
try:
|
||||
content = content.decode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
body = json.loads(content)
|
||||
if self._data_wrapper and isinstance(body, dict) and 'data' in body:
|
||||
body = body['data']
|
||||
return body
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RawModel(JsonModel):
|
||||
"""Model class for requests that don't return JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
accept = '*/*'
|
||||
content_type = 'application/json'
|
||||
alt_param = None
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ''
|
||||
|
||||
|
||||
class MediaModel(JsonModel):
|
||||
"""Model class for requests that return Media.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
accept = '*/*'
|
||||
content_type = 'application/json'
|
||||
alt_param = 'media'
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ''
|
||||
|
||||
|
||||
class ProtocolBufferModel(BaseModel):
|
||||
"""Model class for protocol buffers.
|
||||
|
||||
Serializes and de-serializes the binary protocol buffer sent in the HTTP
|
||||
request and response bodies.
|
||||
"""
|
||||
accept = 'application/x-protobuf'
|
||||
content_type = 'application/x-protobuf'
|
||||
alt_param = 'proto'
|
||||
|
||||
def __init__(self, protocol_buffer):
|
||||
"""Constructs a ProtocolBufferModel.
|
||||
|
||||
The serialzed protocol buffer returned in an HTTP response will be
|
||||
de-serialized using the given protocol buffer class.
|
||||
|
||||
Args:
|
||||
protocol_buffer: The protocol buffer class used to de-serialize a
|
||||
response from the API.
|
||||
"""
|
||||
self._protocol_buffer = protocol_buffer
|
||||
|
||||
def serialize(self, body_value):
|
||||
return body_value.SerializeToString()
|
||||
|
||||
def deserialize(self, content):
|
||||
return self._protocol_buffer.FromString(content)
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return self._protocol_buffer()
|
||||
|
||||
|
||||
def makepatch(original, modified):
|
||||
"""Create a patch object.
|
||||
|
||||
Some methods support PATCH, an efficient way to send updates to a resource.
|
||||
This method allows the easy construction of patch bodies by looking at the
|
||||
differences between a resource before and after it was modified.
|
||||
|
||||
Args:
|
||||
original: object, the original deserialized resource
|
||||
modified: object, the modified deserialized resource
|
||||
Returns:
|
||||
An object that contains only the changes from original to modified, in a
|
||||
form suitable to pass to a PATCH method.
|
||||
|
||||
Example usage:
|
||||
item = service.activities().get(postid=postid, userid=userid).execute()
|
||||
original = copy.deepcopy(item)
|
||||
item['object']['content'] = 'This is updated.'
|
||||
service.activities.patch(postid=postid, userid=userid,
|
||||
body=makepatch(original, item)).execute()
|
||||
"""
|
||||
patch = {}
|
||||
for key, original_value in six.iteritems(original):
|
||||
modified_value = modified.get(key, None)
|
||||
if modified_value is None:
|
||||
# Use None to signal that the element is deleted
|
||||
patch[key] = None
|
||||
elif original_value != modified_value:
|
||||
if type(original_value) == type({}):
|
||||
# Recursively descend objects
|
||||
patch[key] = makepatch(original_value, modified_value)
|
||||
else:
|
||||
# In the case of simple types or arrays we just replace
|
||||
patch[key] = modified_value
|
||||
else:
|
||||
# Don't add anything to patch if there's no change
|
||||
pass
|
||||
for key in modified:
|
||||
if key not in original:
|
||||
patch[key] = modified[key]
|
||||
|
||||
return patch
|
||||
103
src/googleapiclient/sample_tools.py
Normal file
103
src/googleapiclient/sample_tools.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for making samples.
|
||||
|
||||
Consolidates a lot of code commonly repeated in sample applications.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
__all__ = ['init']
|
||||
|
||||
|
||||
import argparse
|
||||
import httplib2
|
||||
import os
|
||||
|
||||
from googleapiclient import discovery
|
||||
from oauth2client import client
|
||||
from oauth2client import file
|
||||
from oauth2client import tools
|
||||
|
||||
|
||||
def init(argv, name, version, doc, filename, scope=None, parents=[], discovery_filename=None):
|
||||
"""A common initialization routine for samples.
|
||||
|
||||
Many of the sample applications do the same initialization, which has now
|
||||
been consolidated into this function. This function uses common idioms found
|
||||
in almost all the samples, i.e. for an API with name 'apiname', the
|
||||
credentials are stored in a file named apiname.dat, and the
|
||||
client_secrets.json file is stored in the same directory as the application
|
||||
main file.
|
||||
|
||||
Args:
|
||||
argv: list of string, the command-line parameters of the application.
|
||||
name: string, name of the API.
|
||||
version: string, version of the API.
|
||||
doc: string, description of the application. Usually set to __doc__.
|
||||
file: string, filename of the application. Usually set to __file__.
|
||||
parents: list of argparse.ArgumentParser, additional command-line flags.
|
||||
scope: string, The OAuth scope used.
|
||||
discovery_filename: string, name of local discovery file (JSON). Use when discovery doc not available via URL.
|
||||
|
||||
Returns:
|
||||
A tuple of (service, flags), where service is the service object and flags
|
||||
is the parsed command-line flags.
|
||||
"""
|
||||
if scope is None:
|
||||
scope = 'https://www.googleapis.com/auth/' + name
|
||||
|
||||
# Parser command-line arguments.
|
||||
parent_parsers = [tools.argparser]
|
||||
parent_parsers.extend(parents)
|
||||
parser = argparse.ArgumentParser(
|
||||
description=doc,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=parent_parsers)
|
||||
flags = parser.parse_args(argv[1:])
|
||||
|
||||
# Name of a file containing the OAuth 2.0 information for this
|
||||
# application, including client_id and client_secret, which are found
|
||||
# on the API Access tab on the Google APIs
|
||||
# Console <http://code.google.com/apis/console>.
|
||||
client_secrets = os.path.join(os.path.dirname(filename),
|
||||
'client_secrets.json')
|
||||
|
||||
# Set up a Flow object to be used if we need to authenticate.
|
||||
flow = client.flow_from_clientsecrets(client_secrets,
|
||||
scope=scope,
|
||||
message=tools.message_if_missing(client_secrets))
|
||||
|
||||
# Prepare credentials, and authorize HTTP object with them.
|
||||
# If the credentials don't exist or are invalid run through the native client
|
||||
# flow. The Storage object will ensure that if successful the good
|
||||
# credentials will get written back to a file.
|
||||
storage = file.Storage(name + '.dat')
|
||||
credentials = storage.get()
|
||||
if credentials is None or credentials.invalid:
|
||||
credentials = tools.run_flow(flow, storage, flags)
|
||||
http = credentials.authorize(http = httplib2.Http())
|
||||
|
||||
if discovery_filename is None:
|
||||
# Construct a service object via the discovery service.
|
||||
service = discovery.build(name, version, http=http)
|
||||
else:
|
||||
# Construct a service object using a local discovery document file.
|
||||
with open(discovery_filename) as discovery_file:
|
||||
service = discovery.build_from_document(
|
||||
discovery_file.read(),
|
||||
base='https://www.googleapis.com/',
|
||||
http=http)
|
||||
return (service, flags)
|
||||
318
src/googleapiclient/schema.py
Normal file
318
src/googleapiclient/schema.py
Normal file
@@ -0,0 +1,318 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Schema processing for discovery based APIs
|
||||
|
||||
Schemas holds an APIs discovery schemas. It can return those schema as
|
||||
deserialized JSON objects, or pretty print them as prototype objects that
|
||||
conform to the schema.
|
||||
|
||||
For example, given the schema:
|
||||
|
||||
schema = \"\"\"{
|
||||
"Foo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"description": "ETag of the collection."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Type of the collection ('calendar#acl').",
|
||||
"default": "calendar#acl"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"description": "Token used to access the next
|
||||
page of this result. Omitted if no further results are available."
|
||||
}
|
||||
}
|
||||
}
|
||||
}\"\"\"
|
||||
|
||||
s = Schemas(schema)
|
||||
print s.prettyPrintByName('Foo')
|
||||
|
||||
Produces the following output:
|
||||
|
||||
{
|
||||
"nextPageToken": "A String", # Token used to access the
|
||||
# next page of this result. Omitted if no further results are available.
|
||||
"kind": "A String", # Type of the collection ('calendar#acl').
|
||||
"etag": "A String", # ETag of the collection.
|
||||
},
|
||||
|
||||
The constructor takes a discovery document in which to look up named schema.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import six
|
||||
|
||||
# TODO(jcgregorio) support format, enum, minimum, maximum
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import copy
|
||||
|
||||
# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
|
||||
# in '_helpers'.
|
||||
try:
|
||||
from oauth2client import util
|
||||
except ImportError:
|
||||
from oauth2client import _helpers as util
|
||||
|
||||
|
||||
class Schemas(object):
|
||||
"""Schemas for an API."""
|
||||
|
||||
def __init__(self, discovery):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
discovery: object, Deserialized discovery document from which we pull
|
||||
out the named schema.
|
||||
"""
|
||||
self.schemas = discovery.get('schemas', {})
|
||||
|
||||
# Cache of pretty printed schemas.
|
||||
self.pretty = {}
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintByName(self, name, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
if name in seen:
|
||||
# Do not fall into an infinite loop over recursive definitions.
|
||||
return '# Object with schema name: %s' % name
|
||||
seen.append(name)
|
||||
|
||||
if name not in self.pretty:
|
||||
self.pretty[name] = _SchemaToStruct(self.schemas[name],
|
||||
seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
seen.pop()
|
||||
|
||||
return self.pretty[name]
|
||||
|
||||
def prettyPrintByName(self, name):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintSchema(self, schema, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
def prettyPrintSchema(self, schema):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintSchema(schema, dent=1)[:-2]
|
||||
|
||||
def get(self, name):
|
||||
"""Get deserialized JSON schema from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Schema name.
|
||||
"""
|
||||
return self.schemas[name]
|
||||
|
||||
|
||||
class _SchemaToStruct(object):
|
||||
"""Convert schema to a prototype object."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, schema, seen, dent=0):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list, List of names of schema already seen while parsing. Used to
|
||||
handle recursive definitions.
|
||||
dent: int, Initial indentation depth.
|
||||
"""
|
||||
# The result of this parsing kept as list of strings.
|
||||
self.value = []
|
||||
|
||||
# The final value of the parsing.
|
||||
self.string = None
|
||||
|
||||
# The parsed JSON schema.
|
||||
self.schema = schema
|
||||
|
||||
# Indentation level.
|
||||
self.dent = dent
|
||||
|
||||
# Method that when called returns a prototype object for the schema with
|
||||
# the given name.
|
||||
self.from_cache = None
|
||||
|
||||
# List of names of schema already seen while parsing.
|
||||
self.seen = seen
|
||||
|
||||
def emit(self, text):
|
||||
"""Add text as a line to the output.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text, '\n'])
|
||||
|
||||
def emitBegin(self, text):
|
||||
"""Add text to the output, but with no line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text])
|
||||
|
||||
def emitEnd(self, text, comment):
|
||||
"""Add text and comment to the output with line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
comment: string, Python comment.
|
||||
"""
|
||||
if comment:
|
||||
divider = '\n' + ' ' * (self.dent + 2) + '# '
|
||||
lines = comment.splitlines()
|
||||
lines = [x.rstrip() for x in lines]
|
||||
comment = divider.join(lines)
|
||||
self.value.extend([text, ' # ', comment, '\n'])
|
||||
else:
|
||||
self.value.extend([text, '\n'])
|
||||
|
||||
def indent(self):
|
||||
"""Increase indentation level."""
|
||||
self.dent += 1
|
||||
|
||||
def undent(self):
|
||||
"""Decrease indentation level."""
|
||||
self.dent -= 1
|
||||
|
||||
def _to_str_impl(self, schema):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema file.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
"""
|
||||
stype = schema.get('type')
|
||||
if stype == 'object':
|
||||
self.emitEnd('{', schema.get('description', ''))
|
||||
self.indent()
|
||||
if 'properties' in schema:
|
||||
for pname, pschema in six.iteritems(schema.get('properties', {})):
|
||||
self.emitBegin('"%s": ' % pname)
|
||||
self._to_str_impl(pschema)
|
||||
elif 'additionalProperties' in schema:
|
||||
self.emitBegin('"a_key": ')
|
||||
self._to_str_impl(schema['additionalProperties'])
|
||||
self.undent()
|
||||
self.emit('},')
|
||||
elif '$ref' in schema:
|
||||
schemaName = schema['$ref']
|
||||
description = schema.get('description', '')
|
||||
s = self.from_cache(schemaName, seen=self.seen)
|
||||
parts = s.splitlines()
|
||||
self.emitEnd(parts[0], description)
|
||||
for line in parts[1:]:
|
||||
self.emit(line.rstrip())
|
||||
elif stype == 'boolean':
|
||||
value = schema.get('default', 'True or False')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'string':
|
||||
value = schema.get('default', 'A String')
|
||||
self.emitEnd('"%s",' % str(value), schema.get('description', ''))
|
||||
elif stype == 'integer':
|
||||
value = schema.get('default', '42')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'number':
|
||||
value = schema.get('default', '3.14')
|
||||
self.emitEnd('%s,' % str(value), schema.get('description', ''))
|
||||
elif stype == 'null':
|
||||
self.emitEnd('None,', schema.get('description', ''))
|
||||
elif stype == 'any':
|
||||
self.emitEnd('"",', schema.get('description', ''))
|
||||
elif stype == 'array':
|
||||
self.emitEnd('[', schema.get('description'))
|
||||
self.indent()
|
||||
self.emitBegin('')
|
||||
self._to_str_impl(schema['items'])
|
||||
self.undent()
|
||||
self.emit('],')
|
||||
else:
|
||||
self.emit('Unknown type! %s' % stype)
|
||||
self.emitEnd('', '')
|
||||
|
||||
self.string = ''.join(self.value)
|
||||
return self.string
|
||||
|
||||
def to_str(self, from_cache):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
from_cache: callable(name, seen), Callable that retrieves an object
|
||||
prototype for a schema with the given name. Seen is a list of schema
|
||||
names already seen as we recursively descend the schema definition.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
The lines of the code will all be properly indented.
|
||||
"""
|
||||
self.from_cache = from_cache
|
||||
return self._to_str_impl(self.schema)
|
||||
1741
src/httplib2/__init__.py
Normal file
1741
src/httplib2/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
2167
src/httplib2/cacerts.txt
Normal file
2167
src/httplib2/cacerts.txt
Normal file
File diff suppressed because it is too large
Load Diff
110
src/httplib2/iri2uri.py
Normal file
110
src/httplib2/iri2uri.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
iri2uri
|
||||
|
||||
Converts an IRI to a URI.
|
||||
|
||||
"""
|
||||
__author__ = "Joe Gregorio (joe@bitworking.org)"
|
||||
__copyright__ = "Copyright 2006, Joe Gregorio"
|
||||
__contributors__ = []
|
||||
__version__ = "1.0.0"
|
||||
__license__ = "MIT"
|
||||
__history__ = """
|
||||
"""
|
||||
|
||||
import urlparse
|
||||
|
||||
|
||||
# Convert an IRI to a URI following the rules in RFC 3987
|
||||
#
|
||||
# The characters we need to enocde and escape are defined in the spec:
|
||||
#
|
||||
# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
|
||||
# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
|
||||
# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
|
||||
# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
|
||||
# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
|
||||
# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
|
||||
# / %xD0000-DFFFD / %xE1000-EFFFD
|
||||
|
||||
escape_range = [
|
||||
(0xA0, 0xD7FF),
|
||||
(0xE000, 0xF8FF),
|
||||
(0xF900, 0xFDCF),
|
||||
(0xFDF0, 0xFFEF),
|
||||
(0x10000, 0x1FFFD),
|
||||
(0x20000, 0x2FFFD),
|
||||
(0x30000, 0x3FFFD),
|
||||
(0x40000, 0x4FFFD),
|
||||
(0x50000, 0x5FFFD),
|
||||
(0x60000, 0x6FFFD),
|
||||
(0x70000, 0x7FFFD),
|
||||
(0x80000, 0x8FFFD),
|
||||
(0x90000, 0x9FFFD),
|
||||
(0xA0000, 0xAFFFD),
|
||||
(0xB0000, 0xBFFFD),
|
||||
(0xC0000, 0xCFFFD),
|
||||
(0xD0000, 0xDFFFD),
|
||||
(0xE1000, 0xEFFFD),
|
||||
(0xF0000, 0xFFFFD),
|
||||
(0x100000, 0x10FFFD),
|
||||
]
|
||||
|
||||
def encode(c):
|
||||
retval = c
|
||||
i = ord(c)
|
||||
for low, high in escape_range:
|
||||
if i < low:
|
||||
break
|
||||
if i >= low and i <= high:
|
||||
retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
|
||||
break
|
||||
return retval
|
||||
|
||||
|
||||
def iri2uri(uri):
|
||||
"""Convert an IRI to a URI. Note that IRIs must be
|
||||
passed in a unicode strings. That is, do not utf-8 encode
|
||||
the IRI before passing it into the function."""
|
||||
if isinstance(uri ,unicode):
|
||||
(scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
|
||||
authority = authority.encode('idna')
|
||||
# For each character in 'ucschar' or 'iprivate'
|
||||
# 1. encode as utf-8
|
||||
# 2. then %-encode each octet of that utf-8
|
||||
uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
|
||||
uri = "".join([encode(c) for c in uri])
|
||||
return uri
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def test_uris(self):
|
||||
"""Test that URIs are invariant under the transformation."""
|
||||
invariant = [
|
||||
u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
|
||||
u"http://www.ietf.org/rfc/rfc2396.txt",
|
||||
u"ldap://[2001:db8::7]/c=GB?objectClass?one",
|
||||
u"mailto:John.Doe@example.com",
|
||||
u"news:comp.infosystems.www.servers.unix",
|
||||
u"tel:+1-816-555-1212",
|
||||
u"telnet://192.0.2.16:80/",
|
||||
u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
|
||||
for uri in invariant:
|
||||
self.assertEqual(uri, iri2uri(uri))
|
||||
|
||||
def test_iri(self):
|
||||
""" Test that the right type of escaping is done for each part of the URI."""
|
||||
self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
|
||||
self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
|
||||
self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}"))
|
||||
self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
|
||||
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
|
||||
self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
|
||||
self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
|
||||
|
||||
unittest.main()
|
||||
|
||||
|
||||
448
src/httplib2/socks.py
Normal file
448
src/httplib2/socks.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""SocksiPy - Python SOCKS module.
|
||||
Version 1.00
|
||||
|
||||
Copyright 2006 Dan-Haim. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of Dan Haim nor the names of his contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
|
||||
|
||||
|
||||
This module provides a standard socket-like interface for Python
|
||||
for tunneling connections through SOCKS proxies.
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
Minor modifications made by Christopher Gilbert (http://motomastyle.com/)
|
||||
for use in PyLoris (http://pyloris.sourceforge.net/)
|
||||
|
||||
Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
|
||||
mainly to merge bug fixes found in Sourceforge
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
if getattr(socket, 'socket', None) is None:
|
||||
raise ImportError('socket.socket missing, proxy support unusable')
|
||||
|
||||
PROXY_TYPE_SOCKS4 = 1
|
||||
PROXY_TYPE_SOCKS5 = 2
|
||||
PROXY_TYPE_HTTP = 3
|
||||
PROXY_TYPE_HTTP_NO_TUNNEL = 4
|
||||
|
||||
_defaultproxy = None
|
||||
_orgsocket = socket.socket
|
||||
|
||||
class ProxyError(Exception): pass
|
||||
class GeneralProxyError(ProxyError): pass
|
||||
class Socks5AuthError(ProxyError): pass
|
||||
class Socks5Error(ProxyError): pass
|
||||
class Socks4Error(ProxyError): pass
|
||||
class HTTPError(ProxyError): pass
|
||||
|
||||
_generalerrors = ("success",
|
||||
"invalid data",
|
||||
"not connected",
|
||||
"not available",
|
||||
"bad proxy type",
|
||||
"bad input")
|
||||
|
||||
_socks5errors = ("succeeded",
|
||||
"general SOCKS server failure",
|
||||
"connection not allowed by ruleset",
|
||||
"Network unreachable",
|
||||
"Host unreachable",
|
||||
"Connection refused",
|
||||
"TTL expired",
|
||||
"Command not supported",
|
||||
"Address type not supported",
|
||||
"Unknown error")
|
||||
|
||||
_socks5autherrors = ("succeeded",
|
||||
"authentication is required",
|
||||
"all offered authentication methods were rejected",
|
||||
"unknown username or invalid password",
|
||||
"unknown error")
|
||||
|
||||
_socks4errors = ("request granted",
|
||||
"request rejected or failed",
|
||||
"request rejected because SOCKS server cannot connect to identd on the client",
|
||||
"request rejected because the client program and identd report different user-ids",
|
||||
"unknown error")
|
||||
|
||||
def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
|
||||
"""setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets a default proxy which all further socksocket objects will use,
|
||||
unless explicitly changed.
|
||||
"""
|
||||
global _defaultproxy
|
||||
_defaultproxy = (proxytype, addr, port, rdns, username, password)
|
||||
|
||||
def wrapmodule(module):
|
||||
"""wrapmodule(module)
|
||||
Attempts to replace a module's socket library with a SOCKS socket. Must set
|
||||
a default proxy using setdefaultproxy(...) first.
|
||||
This will only work on modules that import socket directly into the namespace;
|
||||
most of the Python Standard Library falls into this category.
|
||||
"""
|
||||
if _defaultproxy != None:
|
||||
module.socket.socket = socksocket
|
||||
else:
|
||||
raise GeneralProxyError((4, "no proxy specified"))
|
||||
|
||||
class socksocket(socket.socket):
|
||||
"""socksocket([family[, type[, proto]]]) -> socket object
|
||||
Open a SOCKS enabled socket. The parameters are the same as
|
||||
those of the standard socket init. In order for SOCKS to work,
|
||||
you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
|
||||
"""
|
||||
|
||||
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
|
||||
_orgsocket.__init__(self, family, type, proto, _sock)
|
||||
if _defaultproxy != None:
|
||||
self.__proxy = _defaultproxy
|
||||
else:
|
||||
self.__proxy = (None, None, None, None, None, None)
|
||||
self.__proxysockname = None
|
||||
self.__proxypeername = None
|
||||
self.__httptunnel = True
|
||||
|
||||
def __recvall(self, count):
|
||||
"""__recvall(count) -> data
|
||||
Receive EXACTLY the number of bytes requested from the socket.
|
||||
Blocks until the required number of bytes have been received.
|
||||
"""
|
||||
data = self.recv(count)
|
||||
while len(data) < count:
|
||||
d = self.recv(count-len(data))
|
||||
if not d: raise GeneralProxyError((0, "connection closed unexpectedly"))
|
||||
data = data + d
|
||||
return data
|
||||
|
||||
def sendall(self, content, *args):
|
||||
""" override socket.socket.sendall method to rewrite the header
|
||||
for non-tunneling proxies if needed
|
||||
"""
|
||||
if not self.__httptunnel:
|
||||
content = self.__rewriteproxy(content)
|
||||
return super(socksocket, self).sendall(content, *args)
|
||||
|
||||
def __rewriteproxy(self, header):
|
||||
""" rewrite HTTP request headers to support non-tunneling proxies
|
||||
(i.e. those which do not support the CONNECT method).
|
||||
This only works for HTTP (not HTTPS) since HTTPS requires tunneling.
|
||||
"""
|
||||
host, endpt = None, None
|
||||
hdrs = header.split("\r\n")
|
||||
for hdr in hdrs:
|
||||
if hdr.lower().startswith("host:"):
|
||||
host = hdr
|
||||
elif hdr.lower().startswith("get") or hdr.lower().startswith("post"):
|
||||
endpt = hdr
|
||||
if host and endpt:
|
||||
hdrs.remove(host)
|
||||
hdrs.remove(endpt)
|
||||
host = host.split(" ")[1]
|
||||
endpt = endpt.split(" ")
|
||||
if (self.__proxy[4] != None and self.__proxy[5] != None):
|
||||
hdrs.insert(0, self.__getauthheader())
|
||||
hdrs.insert(0, "Host: %s" % host)
|
||||
hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2]))
|
||||
return "\r\n".join(hdrs)
|
||||
|
||||
def __getauthheader(self):
|
||||
auth = self.__proxy[4] + ":" + self.__proxy[5]
|
||||
return "Proxy-Authorization: Basic " + base64.b64encode(auth)
|
||||
|
||||
def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None, headers=None):
|
||||
"""setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets the proxy to be used.
|
||||
proxytype - The type of the proxy to be used. Three types
|
||||
are supported: PROXY_TYPE_SOCKS4 (including socks4a),
|
||||
PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
|
||||
addr - The address of the server (IP or DNS).
|
||||
port - The port of the server. Defaults to 1080 for SOCKS
|
||||
servers and 8080 for HTTP proxy servers.
|
||||
rdns - Should DNS queries be preformed on the remote side
|
||||
(rather than the local side). The default is True.
|
||||
Note: This has no effect with SOCKS4 servers.
|
||||
username - Username to authenticate with to the server.
|
||||
The default is no authentication.
|
||||
password - Password to authenticate with to the server.
|
||||
Only relevant when username is also provided.
|
||||
headers - Additional or modified headers for the proxy connect request.
|
||||
"""
|
||||
self.__proxy = (proxytype, addr, port, rdns, username, password, headers)
|
||||
|
||||
def __negotiatesocks5(self, destaddr, destport):
|
||||
"""__negotiatesocks5(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS5 server.
|
||||
"""
|
||||
# First we'll send the authentication packages we support.
|
||||
if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
|
||||
# The username/password details were supplied to the
|
||||
# setproxy method so we support the USERNAME/PASSWORD
|
||||
# authentication (in addition to the standard none).
|
||||
self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
|
||||
else:
|
||||
# No username/password were entered, therefore we
|
||||
# only support connections with no authentication.
|
||||
self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00))
|
||||
# We'll receive the server's response to determine which
|
||||
# method was selected
|
||||
chosenauth = self.__recvall(2)
|
||||
if chosenauth[0:1] != chr(0x05).encode():
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
# Check the chosen authentication method
|
||||
if chosenauth[1:2] == chr(0x00).encode():
|
||||
# No authentication is required
|
||||
pass
|
||||
elif chosenauth[1:2] == chr(0x02).encode():
|
||||
# Okay, we need to perform a basic username/password
|
||||
# authentication.
|
||||
self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5])
|
||||
authstat = self.__recvall(2)
|
||||
if authstat[0:1] != chr(0x01).encode():
|
||||
# Bad response
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
if authstat[1:2] != chr(0x00).encode():
|
||||
# Authentication failed
|
||||
self.close()
|
||||
raise Socks5AuthError((3, _socks5autherrors[3]))
|
||||
# Authentication succeeded
|
||||
else:
|
||||
# Reaching here is always bad
|
||||
self.close()
|
||||
if chosenauth[1] == chr(0xFF).encode():
|
||||
raise Socks5AuthError((2, _socks5autherrors[2]))
|
||||
else:
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
# Now we can request the actual connection
|
||||
req = struct.pack('BBB', 0x05, 0x01, 0x00)
|
||||
# If the given destination address is an IP address, we'll
|
||||
# use the IPv4 address request even if remote resolving was specified.
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
req = req + chr(0x01).encode() + ipaddr
|
||||
except socket.error:
|
||||
# Well it's not an IP number, so it's probably a DNS name.
|
||||
if self.__proxy[3]:
|
||||
# Resolve remotely
|
||||
ipaddr = None
|
||||
req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr
|
||||
else:
|
||||
# Resolve locally
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
req = req + chr(0x01).encode() + ipaddr
|
||||
req = req + struct.pack(">H", destport)
|
||||
self.sendall(req)
|
||||
# Get the response
|
||||
resp = self.__recvall(4)
|
||||
if resp[0:1] != chr(0x05).encode():
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
elif resp[1:2] != chr(0x00).encode():
|
||||
# Connection failed
|
||||
self.close()
|
||||
if ord(resp[1:2])<=8:
|
||||
raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
|
||||
else:
|
||||
raise Socks5Error((9, _socks5errors[9]))
|
||||
# Get the bound address/port
|
||||
elif resp[3:4] == chr(0x01).encode():
|
||||
boundaddr = self.__recvall(4)
|
||||
elif resp[3:4] == chr(0x03).encode():
|
||||
resp = resp + self.recv(1)
|
||||
boundaddr = self.__recvall(ord(resp[4:5]))
|
||||
else:
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
boundport = struct.unpack(">H", self.__recvall(2))[0]
|
||||
self.__proxysockname = (boundaddr, boundport)
|
||||
if ipaddr != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr, destport)
|
||||
|
||||
def getproxysockname(self):
|
||||
"""getsockname() -> address info
|
||||
Returns the bound IP address and port number at the proxy.
|
||||
"""
|
||||
return self.__proxysockname
|
||||
|
||||
def getproxypeername(self):
|
||||
"""getproxypeername() -> address info
|
||||
Returns the IP and port number of the proxy.
|
||||
"""
|
||||
return _orgsocket.getpeername(self)
|
||||
|
||||
def getpeername(self):
|
||||
"""getpeername() -> address info
|
||||
Returns the IP address and port number of the destination
|
||||
machine (note: getproxypeername returns the proxy)
|
||||
"""
|
||||
return self.__proxypeername
|
||||
|
||||
def __negotiatesocks4(self,destaddr,destport):
|
||||
"""__negotiatesocks4(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS4 server.
|
||||
"""
|
||||
# Check if the destination address provided is an IP address
|
||||
rmtrslv = False
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
except socket.error:
|
||||
# It's a DNS name. Check where it should be resolved.
|
||||
if self.__proxy[3]:
|
||||
ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
|
||||
rmtrslv = True
|
||||
else:
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
# Construct the request packet
|
||||
req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
|
||||
# The username parameter is considered userid for SOCKS4
|
||||
if self.__proxy[4] != None:
|
||||
req = req + self.__proxy[4]
|
||||
req = req + chr(0x00).encode()
|
||||
# DNS name if remote resolving is required
|
||||
# NOTE: This is actually an extension to the SOCKS4 protocol
|
||||
# called SOCKS4A and may not be supported in all cases.
|
||||
if rmtrslv:
|
||||
req = req + destaddr + chr(0x00).encode()
|
||||
self.sendall(req)
|
||||
# Get the response from the server
|
||||
resp = self.__recvall(8)
|
||||
if resp[0:1] != chr(0x00).encode():
|
||||
# Bad data
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
if resp[1:2] != chr(0x5A).encode():
|
||||
# Server returned an error
|
||||
self.close()
|
||||
if ord(resp[1:2]) in (91, 92, 93):
|
||||
self.close()
|
||||
raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
|
||||
else:
|
||||
raise Socks4Error((94, _socks4errors[4]))
|
||||
# Get the bound address/port
|
||||
self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0])
|
||||
if rmtrslv != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr, destport)
|
||||
|
||||
def __negotiatehttp(self, destaddr, destport):
|
||||
"""__negotiatehttp(self,destaddr,destport)
|
||||
Negotiates a connection through an HTTP server.
|
||||
"""
|
||||
# If we need to resolve locally, we do this now
|
||||
if not self.__proxy[3]:
|
||||
addr = socket.gethostbyname(destaddr)
|
||||
else:
|
||||
addr = destaddr
|
||||
headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"]
|
||||
wrote_host_header = False
|
||||
wrote_auth_header = False
|
||||
if self.__proxy[6] != None:
|
||||
for key, val in self.__proxy[6].iteritems():
|
||||
headers += [key, ": ", val, "\r\n"]
|
||||
wrote_host_header = (key.lower() == "host")
|
||||
wrote_auth_header = (key.lower() == "proxy-authorization")
|
||||
if not wrote_host_header:
|
||||
headers += ["Host: ", destaddr, "\r\n"]
|
||||
if not wrote_auth_header:
|
||||
if (self.__proxy[4] != None and self.__proxy[5] != None):
|
||||
headers += [self.__getauthheader(), "\r\n"]
|
||||
headers.append("\r\n")
|
||||
self.sendall("".join(headers).encode())
|
||||
# We read the response until we get the string "\r\n\r\n"
|
||||
resp = self.recv(1)
|
||||
while resp.find("\r\n\r\n".encode()) == -1:
|
||||
resp = resp + self.recv(1)
|
||||
# We just need the first line to check if the connection
|
||||
# was successful
|
||||
statusline = resp.splitlines()[0].split(" ".encode(), 2)
|
||||
if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
try:
|
||||
statuscode = int(statusline[1])
|
||||
except ValueError:
|
||||
self.close()
|
||||
raise GeneralProxyError((1, _generalerrors[1]))
|
||||
if statuscode != 200:
|
||||
self.close()
|
||||
raise HTTPError((statuscode, statusline[2]))
|
||||
self.__proxysockname = ("0.0.0.0", 0)
|
||||
self.__proxypeername = (addr, destport)
|
||||
|
||||
def connect(self, destpair):
|
||||
"""connect(self, despair)
|
||||
Connects to the specified destination through a proxy.
|
||||
destpar - A tuple of the IP/DNS address and the port number.
|
||||
(identical to socket's connect).
|
||||
To select the proxy server use setproxy().
|
||||
"""
|
||||
# Do a minimal input check first
|
||||
if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int):
|
||||
raise GeneralProxyError((5, _generalerrors[5]))
|
||||
if self.__proxy[0] == PROXY_TYPE_SOCKS5:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self, (self.__proxy[1], portnum))
|
||||
self.__negotiatesocks5(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self,(self.__proxy[1], portnum))
|
||||
self.__negotiatesocks4(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_HTTP:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 8080
|
||||
_orgsocket.connect(self,(self.__proxy[1], portnum))
|
||||
self.__negotiatehttp(destpair[0], destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 8080
|
||||
_orgsocket.connect(self,(self.__proxy[1],portnum))
|
||||
if destpair[1] == 443:
|
||||
self.__negotiatehttp(destpair[0],destpair[1])
|
||||
else:
|
||||
self.__httptunnel = False
|
||||
elif self.__proxy[0] == None:
|
||||
_orgsocket.connect(self, (destpair[0], destpair[1]))
|
||||
else:
|
||||
raise GeneralProxyError((4, _generalerrors[4]))
|
||||
0
src/httplib2/test/__init__.py
Normal file
0
src/httplib2/test/__init__.py
Normal file
1
src/httplib2/test/brokensocket/socket.py
Normal file
1
src/httplib2/test/brokensocket/socket.py
Normal file
@@ -0,0 +1 @@
|
||||
from realsocket import gaierror, error, getaddrinfo, SOCK_STREAM
|
||||
88
src/httplib2/test/functional/test_proxies.py
Normal file
88
src/httplib2/test/functional/test_proxies.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import unittest
|
||||
import errno
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import nose
|
||||
|
||||
import httplib2
|
||||
from httplib2 import socks
|
||||
from httplib2.test import miniserver
|
||||
|
||||
tinyproxy_cfg = """
|
||||
User "%(user)s"
|
||||
Port %(port)s
|
||||
Listen 127.0.0.1
|
||||
PidFile "%(pidfile)s"
|
||||
LogFile "%(logfile)s"
|
||||
MaxClients 2
|
||||
StartServers 1
|
||||
LogLevel Info
|
||||
"""
|
||||
|
||||
|
||||
class FunctionalProxyHttpTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not socks:
|
||||
raise nose.SkipTest('socks module unavailable')
|
||||
if not subprocess:
|
||||
raise nose.SkipTest('subprocess module unavailable')
|
||||
|
||||
# start a short-lived miniserver so we can get a likely port
|
||||
# for the proxy
|
||||
self.httpd, self.proxyport = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
self.httpd.shutdown()
|
||||
self.httpd, self.port = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
|
||||
self.pidfile = tempfile.mktemp()
|
||||
self.logfile = tempfile.mktemp()
|
||||
fd, self.conffile = tempfile.mkstemp()
|
||||
f = os.fdopen(fd, 'w')
|
||||
our_cfg = tinyproxy_cfg % {'user': os.getlogin(),
|
||||
'pidfile': self.pidfile,
|
||||
'port': self.proxyport,
|
||||
'logfile': self.logfile}
|
||||
f.write(our_cfg)
|
||||
f.close()
|
||||
try:
|
||||
# TODO use subprocess.check_call when 2.4 is dropped
|
||||
ret = subprocess.call(['tinyproxy', '-c', self.conffile])
|
||||
self.assertEqual(0, ret)
|
||||
except OSError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise nose.SkipTest('tinyproxy not available')
|
||||
raise
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
try:
|
||||
pid = int(open(self.pidfile).read())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError, e:
|
||||
if e.errno == errno.ESRCH:
|
||||
print '\n\n\nTinyProxy Failed to start, log follows:'
|
||||
print open(self.logfile).read()
|
||||
print 'end tinyproxy log\n\n\n'
|
||||
raise
|
||||
map(os.unlink, (self.pidfile,
|
||||
self.logfile,
|
||||
self.conffile))
|
||||
|
||||
def testSimpleProxy(self):
|
||||
proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP,
|
||||
'localhost', self.proxyport)
|
||||
client = httplib2.Http(proxy_info=proxy_info)
|
||||
src = 'miniserver.py'
|
||||
response, body = client.request('http://localhost:%d/%s' %
|
||||
(self.port, src))
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, open(os.path.join(miniserver.HERE, src)).read())
|
||||
lf = open(self.logfile).read()
|
||||
expect = ('Established connection to host "127.0.0.1" '
|
||||
'using file descriptor')
|
||||
self.assertTrue(expect in lf,
|
||||
'tinyproxy did not proxy a request for miniserver')
|
||||
113
src/httplib2/test/miniserver.py
Normal file
113
src/httplib2/test/miniserver.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import SimpleHTTPServer
|
||||
import socket
|
||||
import SocketServer
|
||||
import threading
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThisDirHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def translate_path(self, path):
|
||||
path = path.split('?', 1)[0].split('#', 1)[0]
|
||||
return os.path.join(HERE, *filter(None, path.split('/')))
|
||||
|
||||
def log_message(self, s, *args):
|
||||
# output via logging so nose can catch it
|
||||
logger.info(s, *args)
|
||||
|
||||
|
||||
class ShutdownServer(SocketServer.TCPServer):
|
||||
"""Mixin that allows serve_forever to be shut down.
|
||||
|
||||
The methods in this mixin are backported from SocketServer.py in the Python
|
||||
2.6.4 standard library. The mixin is unnecessary in 2.6 and later, when
|
||||
BaseServer supports the shutdown method directly.
|
||||
"""
|
||||
|
||||
def __init__(self, use_tls, *args, **kwargs):
|
||||
self.__use_tls = use_tls
|
||||
SocketServer.TCPServer.__init__(self, *args, **kwargs)
|
||||
self.__is_shut_down = threading.Event()
|
||||
self.__serving = False
|
||||
|
||||
def server_bind(self):
|
||||
SocketServer.TCPServer.server_bind(self)
|
||||
if self.__use_tls:
|
||||
import ssl
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
os.path.join(os.path.dirname(__file__), 'server.key'),
|
||||
os.path.join(os.path.dirname(__file__), 'server.pem'),
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
def serve_forever(self, poll_interval=0.1):
|
||||
"""Handle one request at a time until shutdown.
|
||||
|
||||
Polls for shutdown every poll_interval seconds. Ignores
|
||||
self.timeout. If you need to do periodic tasks, do them in
|
||||
another thread.
|
||||
"""
|
||||
self.__serving = True
|
||||
self.__is_shut_down.clear()
|
||||
while self.__serving:
|
||||
r, w, e = select.select([self.socket], [], [], poll_interval)
|
||||
if r:
|
||||
self._handle_request_noblock()
|
||||
self.__is_shut_down.set()
|
||||
|
||||
def shutdown(self):
|
||||
"""Stops the serve_forever loop.
|
||||
|
||||
Blocks until the loop has finished. This must be called while
|
||||
serve_forever() is running in another thread, or it will deadlock.
|
||||
"""
|
||||
self.__serving = False
|
||||
self.__is_shut_down.wait()
|
||||
|
||||
def handle_request(self):
|
||||
"""Handle one request, possibly blocking.
|
||||
|
||||
Respects self.timeout.
|
||||
"""
|
||||
# Support people who used socket.settimeout() to escape
|
||||
# handle_request before self.timeout was available.
|
||||
timeout = self.socket.gettimeout()
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
elif self.timeout is not None:
|
||||
timeout = min(timeout, self.timeout)
|
||||
fd_sets = select.select([self], [], [], timeout)
|
||||
if not fd_sets[0]:
|
||||
self.handle_timeout()
|
||||
return
|
||||
self._handle_request_noblock()
|
||||
|
||||
def _handle_request_noblock(self):
|
||||
"""Handle one request, without blocking.
|
||||
|
||||
I assume that select.select has returned that the socket is
|
||||
readable before this function was called, so there should be
|
||||
no risk of blocking in get_request().
|
||||
"""
|
||||
try:
|
||||
request, client_address = self.get_request()
|
||||
except socket.error:
|
||||
return
|
||||
if self.verify_request(request, client_address):
|
||||
try:
|
||||
self.process_request(request, client_address)
|
||||
except:
|
||||
self.handle_error(request, client_address)
|
||||
self.close_request(request)
|
||||
|
||||
|
||||
def start_server(handler, use_tls=False):
|
||||
httpd = ShutdownServer(use_tls, ("", 0), handler)
|
||||
threading.Thread(target=httpd.serve_forever).start()
|
||||
_, port = httpd.socket.getsockname()
|
||||
return httpd, port
|
||||
19
src/httplib2/test/other_cacerts.txt
Normal file
19
src/httplib2/test/other_cacerts.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBzCCAe+gAwIBAgIJAIw94zvO7fk1MA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
|
||||
BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNjA2MDQwMjMxMTRaFw0yNjA2MDIwMjMx
|
||||
MTRaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
|
||||
BQADggEPADCCAQoCggEBAK3YNcDIwK/wlTa0/iBARvDFOncQ6Jkk+Ymql1HXny7v
|
||||
mWPFWeLXEW+Zw1NrQEx/SIUGvxpRA+QyhTOhu2Gcwvtqilix/dHgaKgqWEcRYu8m
|
||||
L70uVDPVgB/kfNI8bpXM1Mz8Crjo0tHw5oUSD3wny8SyT6CYlXVmF923L8c2zdN9
|
||||
n9blFgYwxBq2+q+mqOiDErMFbwHES8FNBSWGBXdE1xjBdITtlfeHezmJhj/ylPW1
|
||||
7v8HInsv/WqU9DcJYlFxSnK0SZCLFBM/31Ez8O1gCfMlDUFvJoo59GyFqukUjuO1
|
||||
uB85wpu27gtcLm/J9X1Md71IxbDupV7a0dDoTvbhO4kCAwEAAaNQME4wHQYDVR0O
|
||||
BBYEFIHgAmwppZSKLz2peyFSO2kwVobNMB8GA1UdIwQYMBaAFIHgAmwppZSKLz2p
|
||||
eyFSO2kwVobNMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJxz+AU/
|
||||
Iq8fMEStJ0BgPP1N86W9Jpb7aPMFCYTEZ+nd8hFPhPs4//55J0yIve+1I43MNFFz
|
||||
yflwwCzrIIhZdkvbsyea6CmlTo4jBc4+ihaDGobYnoNzFhavC47n5kYqJ8Ikyb2W
|
||||
OMrmNRiaTeSBl0wQmftnnQCbonenjmE1LDuJtE6bCwfFjfLbMxwdWtp/ymOlXsb5
|
||||
80XcWwcqc12UHWexYwHFzEJmDfncak/8tjHBsLWMJg5p2sVTY9kVt7TYgSIl+mFb
|
||||
4WVGrqZd2uTlJkRQQ4pCl+D+PKwadHuV6YI7oxkeajjcHCgbK/ANwW28MXYho6t6
|
||||
aWVIN4bWHrZ38kE=
|
||||
-----END CERTIFICATE-----
|
||||
146
src/httplib2/test/server.key
Normal file
146
src/httplib2/test/server.key
Normal file
@@ -0,0 +1,146 @@
|
||||
Private TLS server key file used for HTTPS-related unit tests
|
||||
-------------------------------------------------------------
|
||||
|
||||
Public Key Info:
|
||||
Public Key Algorithm: RSA
|
||||
Key Security Level: Medium (2048 bits)
|
||||
|
||||
modulus:
|
||||
00:cc:fb:f1:c5:de:29:29:40:3f:c4:9f:af:da:f6:be
|
||||
27:f4:6a:00:ae:5a:f2:99:c3:5f:7a:e6:9b:cf:d9:08
|
||||
34:01:9b:ea:fb:da:b5:d0:b5:b2:4e:60:b4:0d:8d:05
|
||||
57:e4:2e:04:d4:57:1a:58:3c:0b:3a:ed:67:a3:13:31
|
||||
58:0a:c2:eb:fd:d6:27:ee:07:95:30:35:b5:98:91:c7
|
||||
a5:9b:be:a9:7e:ae:fd:73:c3:6b:21:bc:52:f8:ef:71
|
||||
db:d3:b1:cd:51:df:b3:37:b3:fd:7d:ae:7e:02:38:be
|
||||
8e:6f:45:55:e5:6d:8a:02:cb:36:c4:17:7a:ea:24:9a
|
||||
72:8d:1e:75:03:3a:6f:c4:cb:a0:3a:50:56:32:bb:4c
|
||||
e2:ea:74:f0:96:31:74:b2:c1:03:e8:c3:d4:a3:59:fc
|
||||
7a:cc:68:35:c4:97:eb:aa:46:fa:64:c3:f9:55:59:22
|
||||
b5:2b:3c:96:84:c6:d2:7d:b4:9f:b9:9c:af:d1:20:30
|
||||
7c:e8:60:4e:ee:0a:60:a0:9d:4e:8a:d8:34:74:bd:f2
|
||||
40:bc:d7:c2:b3:1a:b2:bb:d7:a5:4a:4c:65:94:43:82
|
||||
16:9a:8f:76:2a:05:b0:9e:3d:a7:fb:e2:c7:78:25:f7
|
||||
df:ca:08:ee:ec:4f:cd:1a:3c:03:41:ec:91:c5:50:70
|
||||
4b:
|
||||
|
||||
public exponent:
|
||||
01:00:01:
|
||||
|
||||
private exponent:
|
||||
00:ad:01:83:b8:7d:dd:fd:ab:f5:66:2d:64:ce:08:ec
|
||||
cb:6a:15:41:87:e6:c8:d5:10:39:78:d0:43:f7:73:f4
|
||||
e1:77:ee:31:b0:e9:92:04:9a:25:e8:d2:e3:84:80:5e
|
||||
5f:24:fd:d6:23:a5:74:5d:be:27:b8:4f:80:e5:f9:1f
|
||||
ef:6f:fd:be:12:1a:7a:cf:02:65:5f:30:25:99:a4:88
|
||||
7d:74:ea:c1:c1:63:4e:15:33:7d:2b:16:f8:6c:94:23
|
||||
63:e6:d3:2d:38:89:f6:87:f0:08:e5:d7:ad:10:90:f5
|
||||
fb:df:5c:04:b8:43:f0:74:95:31:1e:e5:b6:5f:02:0f
|
||||
bb:55:cb:e1:b5:48:9f:1f:d3:1b:55:a7:bc:39:2b:8e
|
||||
6d:14:64:3b:bf:e8:ca:6b:af:a9:f3:13:9a:c6:df:15
|
||||
ef:6d:17:4e:8e:67:6c:41:20:dc:6b:08:0d:b9:14:cd
|
||||
83:10:62:15:e6:b0:89:5d:37:fb:f6:fd:f0:bf:3b:9c
|
||||
0b:e9:fd:b8:de:e4:64:90:bf:81:d5:59:2c:30:43:07
|
||||
b9:60:8c:d0:ac:4f:95:87:aa:38:62:bd:c7:06:a7:c4
|
||||
2d:08:c1:3c:86:10:c7:8e:1e:df:58:bf:95:ad:39:84
|
||||
a0:2b:13:e2:18:e6:4a:80:f0:bc:04:50:bd:7d:cf:23
|
||||
a1:
|
||||
|
||||
prime1:
|
||||
00:f0:8f:ad:2f:c9:64:f3:0d:2c:aa:06:17:05:8f:2f
|
||||
d5:cb:92:22:90:05:66:3c:78:75:9d:7b:4c:6a:af:a9
|
||||
1e:d6:28:4f:13:0e:3a:e7:31:49:3d:87:ef:2c:17:70
|
||||
be:69:b3:42:82:6d:9c:b4:13:0a:e4:bc:8c:0f:1a:bd
|
||||
04:b6:a0:be:ba:12:15:bf:04:db:91:1c:26:91:d6:d7
|
||||
f2:ff:2f:0e:5f:96:a1:7c:4b:90:a8:2f:07:2a:cb:dc
|
||||
40:a0:0b:1d:2a:1d:48:98:bd:4a:6b:9d:5c:69:b0:2b
|
||||
6e:9b:2c:b2:a9:cb:28:fe:fa:7f:93:eb:20:c8:59:d0
|
||||
11:
|
||||
|
||||
prime2:
|
||||
00:da:23:c0:3e:82:4c:88:7c:d4:fb:de:24:45:eb:9c
|
||||
ae:2c:80:2d:52:a6:95:05:33:b9:d8:c1:7b:52:01:62
|
||||
11:e6:b6:c6:0d:56:a3:68:39:26:9a:90:08:95:12:a9
|
||||
1c:59:f6:0b:1d:af:6d:c0:c6:9b:2e:7a:62:98:21:36
|
||||
e1:15:4c:e6:6d:a4:08:ac:90:af:57:86:71:78:2e:0e
|
||||
cf:59:0f:35:79:cb:6a:a2:e2:30:2a:a8:f2:84:68:bc
|
||||
8a:f2:48:3b:07:d5:a5:34:f3:d3:ec:25:61:38:f1:0a
|
||||
07:f7:7e:29:61:e4:15:01:80:e3:7b:bd:63:9c:2e:16
|
||||
9b:
|
||||
|
||||
coefficient:
|
||||
00:cb:b4:d2:9f:b4:04:db:8c:54:e6:ae:a9:28:a0:c9
|
||||
70:ad:7a:94:72:5e:86:33:91:d9:43:61:2b:4d:55:e8
|
||||
b7:25:d2:cd:db:1e:c4:56:95:68:85:e2:9b:4f:31:24
|
||||
3a:40:06:41:1c:aa:7a:31:13:fa:07:e0:a6:59:c3:d1
|
||||
d2:c5:2c:6a:82:98:bb:a1:59:c0:6f:ad:d7:2e:ed:5a
|
||||
64:5f:e6:ea:4a:ee:45:29:d9:0f:96:b3:39:f7:ab:57
|
||||
97:aa:c9:f7:b6:9c:c0:51:5d:9f:01:2c:ec:58:8d:06
|
||||
6a:19:d0:33:74:11:6a:25:7c:8f:b7:31:d2:97:05:02
|
||||
6f:
|
||||
|
||||
exp1:
|
||||
00:87:60:43:95:1d:e0:0a:8b:82:74:18:43:42:64:a7
|
||||
05:c8:ae:ef:76:5f:23:7e:aa:47:7e:1d:52:0e:c3:d6
|
||||
07:bd:7b:27:ac:d0:98:43:5c:d0:1b:a9:70:e6:3e:36
|
||||
bb:61:5e:78:f2:4f:5f:1d:53:8e:10:d5:2e:78:9d:92
|
||||
7b:a1:8e:ea:66:6a:21:04:c3:66:10:ce:67:c2:30:c6
|
||||
8c:40:21:2a:14:8e:ff:47:a4:7a:be:ba:e0:6c:ac:16
|
||||
c1:e3:8e:fd:95:a2:af:25:0d:79:61:00:48:6e:4d:ae
|
||||
d3:6a:ce:07:a9:57:e4:35:41:a1:24:0b:f1:01:ee:d1
|
||||
11:
|
||||
|
||||
exp2:
|
||||
00:ca:ca:bd:a7:de:fe:43:4c:b9:bb:c4:d2:37:e6:47
|
||||
ec:6c:16:65:0c:17:2d:26:7e:e5:e1:2a:4d:f8:f8:ac
|
||||
31:34:28:ea:89:ef:e7:4d:b7:03:ba:60:f8:79:8d:b5
|
||||
85:53:e4:b6:84:cc:57:de:05:44:b2:ba:b7:f9:f1:b6
|
||||
d1:1d:3a:36:65:eb:3e:dd:1e:4c:c3:b3:8a:bd:4d:24
|
||||
1b:83:11:ee:86:e1:a2:aa:f6:58:0c:f0:af:34:85:21
|
||||
f2:92:36:b0:1a:22:75:c9:7a:7b:a3:67:44:b0:e8:f4
|
||||
88:5f:7e:fb:fd:b3:4a:0b:f1:c4:89:7e:91:a1:d9:fe
|
||||
cd:
|
||||
|
||||
|
||||
Public Key ID: 92:D5:B4:2A:B6:A8:64:67:2C:2A:08:DB:51:B8:97:86:5E:44:CD:6C
|
||||
Public key's random art:
|
||||
+--[ RSA 2048]----+
|
||||
| + . |
|
||||
| . E o . |
|
||||
| o . . o |
|
||||
| . o o . |
|
||||
| = .= S |
|
||||
|. +.=o + |
|
||||
|o++== . |
|
||||
|++o= |
|
||||
|o . |
|
||||
+-----------------+
|
||||
|
||||
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpgIBAAKCAQEAzPvxxd4pKUA/xJ+v2va+J/RqAK5a8pnDX3rmm8/ZCDQBm+r7
|
||||
2rXQtbJOYLQNjQVX5C4E1FcaWDwLOu1noxMxWArC6/3WJ+4HlTA1tZiRx6Wbvql+
|
||||
rv1zw2shvFL473Hb07HNUd+zN7P9fa5+Aji+jm9FVeVtigLLNsQXeuokmnKNHnUD
|
||||
Om/Ey6A6UFYyu0zi6nTwljF0ssED6MPUo1n8esxoNcSX66pG+mTD+VVZIrUrPJaE
|
||||
xtJ9tJ+5nK/RIDB86GBO7gpgoJ1Oitg0dL3yQLzXwrMasrvXpUpMZZRDghaaj3Yq
|
||||
BbCePaf74sd4Jfffygju7E/NGjwDQeyRxVBwSwIDAQABAoIBAQCtAYO4fd39q/Vm
|
||||
LWTOCOzLahVBh+bI1RA5eNBD93P04XfuMbDpkgSaJejS44SAXl8k/dYjpXRdvie4
|
||||
T4Dl+R/vb/2+Ehp6zwJlXzAlmaSIfXTqwcFjThUzfSsW+GyUI2Pm0y04ifaH8Ajl
|
||||
160QkPX731wEuEPwdJUxHuW2XwIPu1XL4bVInx/TG1WnvDkrjm0UZDu/6Mprr6nz
|
||||
E5rG3xXvbRdOjmdsQSDcawgNuRTNgxBiFeawiV03+/b98L87nAvp/bje5GSQv4HV
|
||||
WSwwQwe5YIzQrE+Vh6o4Yr3HBqfELQjBPIYQx44e31i/la05hKArE+IY5kqA8LwE
|
||||
UL19zyOhAoGBAPCPrS/JZPMNLKoGFwWPL9XLkiKQBWY8eHWde0xqr6ke1ihPEw46
|
||||
5zFJPYfvLBdwvmmzQoJtnLQTCuS8jA8avQS2oL66EhW/BNuRHCaR1tfy/y8OX5ah
|
||||
fEuQqC8HKsvcQKALHSodSJi9SmudXGmwK26bLLKpyyj++n+T6yDIWdARAoGBANoj
|
||||
wD6CTIh81PveJEXrnK4sgC1SppUFM7nYwXtSAWIR5rbGDVajaDkmmpAIlRKpHFn2
|
||||
Cx2vbcDGmy56YpghNuEVTOZtpAiskK9XhnF4Lg7PWQ81ectqouIwKqjyhGi8ivJI
|
||||
OwfVpTTz0+wlYTjxCgf3filh5BUBgON7vWOcLhabAoGBAIdgQ5Ud4AqLgnQYQ0Jk
|
||||
pwXIru92XyN+qkd+HVIOw9YHvXsnrNCYQ1zQG6lw5j42u2FeePJPXx1TjhDVLnid
|
||||
knuhjupmaiEEw2YQzmfCMMaMQCEqFI7/R6R6vrrgbKwWweOO/ZWiryUNeWEASG5N
|
||||
rtNqzgepV+Q1QaEkC/EB7tERAoGBAMrKvafe/kNMubvE0jfmR+xsFmUMFy0mfuXh
|
||||
Kk34+KwxNCjqie/nTbcDumD4eY21hVPktoTMV94FRLK6t/nxttEdOjZl6z7dHkzD
|
||||
s4q9TSQbgxHuhuGiqvZYDPCvNIUh8pI2sBoidcl6e6NnRLDo9Ihffvv9s0oL8cSJ
|
||||
fpGh2f7NAoGBAMu00p+0BNuMVOauqSigyXCtepRyXoYzkdlDYStNVei3JdLN2x7E
|
||||
VpVoheKbTzEkOkAGQRyqejET+gfgplnD0dLFLGqCmLuhWcBvrdcu7VpkX+bqSu5F
|
||||
KdkPlrM596tXl6rJ97acwFFdnwEs7FiNBmoZ0DN0EWolfI+3MdKXBQJv
|
||||
-----END RSA PRIVATE KEY-----
|
||||
50
src/httplib2/test/server.pem
Normal file
50
src/httplib2/test/server.pem
Normal file
@@ -0,0 +1,50 @@
|
||||
Public, self-signed TLS server key file used for HTTPS-related unit tests
|
||||
-------------------------------------------------------------------------
|
||||
|
||||
Public Key Information:
|
||||
Public Key Algorithm: RSA
|
||||
Algorithm Security Level: Medium (2048 bits)
|
||||
Modulus (bits 2048):
|
||||
00:cc:fb:f1:c5:de:29:29:40:3f:c4:9f:af:da:f6:be
|
||||
27:f4:6a:00:ae:5a:f2:99:c3:5f:7a:e6:9b:cf:d9:08
|
||||
34:01:9b:ea:fb:da:b5:d0:b5:b2:4e:60:b4:0d:8d:05
|
||||
57:e4:2e:04:d4:57:1a:58:3c:0b:3a:ed:67:a3:13:31
|
||||
58:0a:c2:eb:fd:d6:27:ee:07:95:30:35:b5:98:91:c7
|
||||
a5:9b:be:a9:7e:ae:fd:73:c3:6b:21:bc:52:f8:ef:71
|
||||
db:d3:b1:cd:51:df:b3:37:b3:fd:7d:ae:7e:02:38:be
|
||||
8e:6f:45:55:e5:6d:8a:02:cb:36:c4:17:7a:ea:24:9a
|
||||
72:8d:1e:75:03:3a:6f:c4:cb:a0:3a:50:56:32:bb:4c
|
||||
e2:ea:74:f0:96:31:74:b2:c1:03:e8:c3:d4:a3:59:fc
|
||||
7a:cc:68:35:c4:97:eb:aa:46:fa:64:c3:f9:55:59:22
|
||||
b5:2b:3c:96:84:c6:d2:7d:b4:9f:b9:9c:af:d1:20:30
|
||||
7c:e8:60:4e:ee:0a:60:a0:9d:4e:8a:d8:34:74:bd:f2
|
||||
40:bc:d7:c2:b3:1a:b2:bb:d7:a5:4a:4c:65:94:43:82
|
||||
16:9a:8f:76:2a:05:b0:9e:3d:a7:fb:e2:c7:78:25:f7
|
||||
df:ca:08:ee:ec:4f:cd:1a:3c:03:41:ec:91:c5:50:70
|
||||
4b
|
||||
Exponent (bits 24):
|
||||
01:00:01
|
||||
|
||||
Public Key Usage:
|
||||
|
||||
Public Key ID: 92d5b42ab6a864672c2a08db51b897865e44cd6c
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+zCCAeOgAwIBAgIJAISbkoXpX75CMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDAeFw0xNjA2MTcxNDA1NTlaFw0yNjA2MTUxNDA1NTlaMBQx
|
||||
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAMz78cXeKSlAP8Sfr9r2vif0agCuWvKZw1965pvP2Qg0AZvq+9q10LWyTmC0
|
||||
DY0FV+QuBNRXGlg8CzrtZ6MTMVgKwuv91ifuB5UwNbWYkcelm76pfq79c8NrIbxS
|
||||
+O9x29OxzVHfszez/X2ufgI4vo5vRVXlbYoCyzbEF3rqJJpyjR51AzpvxMugOlBW
|
||||
MrtM4up08JYxdLLBA+jD1KNZ/HrMaDXEl+uqRvpkw/lVWSK1KzyWhMbSfbSfuZyv
|
||||
0SAwfOhgTu4KYKCdTorYNHS98kC818KzGrK716VKTGWUQ4IWmo92KgWwnj2n++LH
|
||||
eCX338oI7uxPzRo8A0HskcVQcEsCAwEAAaNQME4wHQYDVR0OBBYEFFdXD8Z8k0et
|
||||
ZNyM4e4WypNnGlcCMB8GA1UdIwQYMBaAFFdXD8Z8k0etZNyM4e4WypNnGlcCMAwG
|
||||
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAC4FsnK1Ph/JpdoqSRTCJiVM
|
||||
MPFaaavKEEyYdAKPk/Acmb9vf07sqsT+OZg/0obsZG9LxJb7x0iAnhfM3aS+CmO9
|
||||
Ym2lXeFDaJ2bHooB9MsG2C3+n8lJUMwxm7Cqpff/lpCK6Z+6MGPx3GRs6HUEl34k
|
||||
BB5pue2vqhtFQ03UdHMpAK0M7n3TloAWbFb1a/JmqzTbsQ0oaMHGoECQEAbaBl+a
|
||||
/up6vA3iZHq+ZPYS1KIx+xuT/SapLcyUtjfhmq1bROVZP4+6EHMsBMnhJBYKxxHy
|
||||
0qKvqJL9X3NQLMgMKKUKzX+BuG2u5aRRyVIqewT/ORjaUr9Y8lU7WlXPf7Ljm6s=
|
||||
-----END CERTIFICATE-----
|
||||
23
src/httplib2/test/smoke_test.py
Normal file
23
src/httplib2/test/smoke_test.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import httplib2
|
||||
|
||||
from httplib2.test import miniserver
|
||||
|
||||
|
||||
class HttpSmokeTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.httpd, self.port = miniserver.start_server(
|
||||
miniserver.ThisDirHandler)
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
|
||||
def testGetFile(self):
|
||||
client = httplib2.Http()
|
||||
src = 'miniserver.py'
|
||||
response, body = client.request('http://localhost:%d/%s' %
|
||||
(self.port, src))
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(body, open(os.path.join(miniserver.HERE, src)).read())
|
||||
24
src/httplib2/test/test_no_socket.py
Normal file
24
src/httplib2/test/test_no_socket.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Tests for httplib2 when the socket module is missing.
|
||||
|
||||
This helps ensure compatibility with environments such as AppEngine.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import httplib2
|
||||
|
||||
class MissingSocketTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._oldsocks = httplib2.socks
|
||||
httplib2.socks = None
|
||||
|
||||
def tearDown(self):
|
||||
httplib2.socks = self._oldsocks
|
||||
|
||||
def testProxyDisabled(self):
|
||||
proxy_info = httplib2.ProxyInfo('blah',
|
||||
'localhost', 0)
|
||||
client = httplib2.Http(proxy_info=proxy_info)
|
||||
self.assertRaises(httplib2.ProxiesUnavailableError,
|
||||
client.request, 'http://localhost:-1/')
|
||||
66
src/httplib2/test/test_ssl_context.py
Normal file
66
src/httplib2/test/test_ssl_context.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/python2
|
||||
import BaseHTTPServer
|
||||
import logging
|
||||
import os.path
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
import httplib2
|
||||
|
||||
from httplib2.test import miniserver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""
|
||||
Request handler that keeps the HTTP connection open, so that the test can
|
||||
inspect the resulting SSL connection object
|
||||
"""
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
self.end_headers()
|
||||
|
||||
self.close_connection = 0
|
||||
|
||||
def log_message(self, s, *args):
|
||||
# output via logging so nose can catch it
|
||||
logger.info(s, *args)
|
||||
|
||||
class HttpsContextTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if sys.version_info < (2, 7, 9):
|
||||
return
|
||||
|
||||
self.httpd, self.port = miniserver.start_server(
|
||||
KeepAliveHandler, True)
|
||||
|
||||
def tearDown(self):
|
||||
self.httpd.shutdown()
|
||||
|
||||
def testHttpsContext(self):
|
||||
if sys.version_info < (2, 7, 9):
|
||||
if hasattr(unittest, "skipTest"):
|
||||
self.skipTest("SSLContext requires Python 2.7.9")# Python 2.7.0
|
||||
else:
|
||||
return
|
||||
import ssl
|
||||
|
||||
client = httplib2.Http(
|
||||
ca_certs=os.path.join(os.path.dirname(__file__), 'server.pem'))
|
||||
|
||||
# Establish connection to local server
|
||||
client.request('https://localhost:%d/' % (self.port))
|
||||
|
||||
# Verify that connection uses a TLS context with the correct hostname
|
||||
conn = client.connections['https:localhost:%d' % self.port]
|
||||
|
||||
self.assertIsInstance(conn.sock, ssl.SSLSocket)
|
||||
self.assertTrue(hasattr(conn.sock, 'context'))
|
||||
self.assertIsInstance(conn.sock.context, ssl.SSLContext)
|
||||
self.assertTrue(conn.sock.context.check_hostname)
|
||||
self.assertEqual(conn.sock.server_hostname, 'localhost')
|
||||
self.assertEqual(conn.sock.context.check_hostname, True)
|
||||
self.assertEqual(conn.sock.context.verify_mode, ssl.CERT_REQUIRED)
|
||||
self.assertEqual(conn.sock.context.protocol, ssl.PROTOCOL_SSLv23)
|
||||
BIN
src/license.rtf
Normal file
BIN
src/license.rtf
Normal file
Binary file not shown.
11
src/linux-build.sh
Executable file
11
src/linux-build.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
rm -rf gam
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf gam-$1-linux-$(arch).tar.xz
|
||||
|
||||
export LD_LIBRARY_PATH=/usr/local/lib
|
||||
pyinstaller --clean -F --distpath=gam linux-gam.spec
|
||||
cp LICENSE gam
|
||||
cp whatsnew.txt gam
|
||||
|
||||
tar cfJ gam-$1-linux-$(arch).tar.xz gam/
|
||||
24
src/linux-gam.spec
Normal file
24
src/linux-gam.spec
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('httplib2/cacerts.txt', 'httplib2/cacerts.txt', 'DATA')]
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
a.datas += [('email-settings-v2.json', 'email-settings-v2.json', 'DATA')]
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
10
src/macos-build.sh
Executable file
10
src/macos-build.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
rmdir /q /s gam
|
||||
rmdir /q /s build
|
||||
rmdir /q /s dist
|
||||
rm -rf gam-$1-macos.tar.xz
|
||||
|
||||
/Library/Frameworks/Python.framework/Versions/2.7/bin/pyinstaller --clean -F --distpath=gam macos-gam.spec
|
||||
cp LICENSE gam
|
||||
cp whatsnew.txt gam
|
||||
|
||||
tar cfJ gam-$1-macos.tar.xz gam/
|
||||
24
src/macos-gam.spec
Normal file
24
src/macos-gam.spec
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('httplib2/cacerts.txt', 'httplib2/cacerts.txt', 'DATA')]
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
a.datas += [('email-settings-v2.json', 'email-settings-v2.json', 'DATA')]
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
console=True )
|
||||
23
src/oauth2client/__init__.py
Normal file
23
src/oauth2client/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '3.0.0'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
|
||||
105
src/oauth2client/_helpers.py
Normal file
105
src/oauth2client/_helpers.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def _parse_pem_key(raw_key_input):
|
||||
"""Identify and extract PEM keys.
|
||||
|
||||
Determines whether the given key is in the format of PEM key, and extracts
|
||||
the relevant part of the key if it is.
|
||||
|
||||
Args:
|
||||
raw_key_input: The contents of a private key file (either PEM or
|
||||
PKCS12).
|
||||
|
||||
Returns:
|
||||
string, The actual key if the contents are from a PEM file, or
|
||||
else None.
|
||||
"""
|
||||
offset = raw_key_input.find(b'-----BEGIN ')
|
||||
if offset != -1:
|
||||
return raw_key_input[offset:]
|
||||
|
||||
|
||||
def _json_encode(data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
|
||||
def _to_bytes(value, encoding='ascii'):
|
||||
"""Converts a string value to bytes, if necessary.
|
||||
|
||||
Unfortunately, ``six.b`` is insufficient for this task since in
|
||||
Python2 it does not modify ``unicode`` objects.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
encoding: The encoding to use to convert unicode to bytes. Defaults
|
||||
to "ascii", which will not allow any characters from ordinals
|
||||
larger than 127. Other useful values are "latin-1", which
|
||||
which will only allows byte ordinals (up to 255) and "utf-8",
|
||||
which will encode any unicode that needs to be.
|
||||
|
||||
Returns:
|
||||
The original value converted to bytes (if unicode) or as passed in
|
||||
if it started out as bytes.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to bytes.
|
||||
"""
|
||||
result = (value.encode(encoding)
|
||||
if isinstance(value, six.text_type) else value)
|
||||
if isinstance(result, six.binary_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('{0!r} could not be converted to bytes'.format(value))
|
||||
|
||||
|
||||
def _from_bytes(value):
|
||||
"""Converts bytes to a string value, if necessary.
|
||||
|
||||
Args:
|
||||
value: The string/bytes value to be converted.
|
||||
|
||||
Returns:
|
||||
The original value converted to unicode (if bytes) or as passed in
|
||||
if it started out as unicode.
|
||||
|
||||
Raises:
|
||||
ValueError if the value could not be converted to unicode.
|
||||
"""
|
||||
result = (value.decode('utf-8')
|
||||
if isinstance(value, six.binary_type) else value)
|
||||
if isinstance(result, six.text_type):
|
||||
return result
|
||||
else:
|
||||
raise ValueError(
|
||||
'{0!r} could not be converted to unicode'.format(value))
|
||||
|
||||
|
||||
def _urlsafe_b64encode(raw_bytes):
|
||||
raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
|
||||
return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
|
||||
|
||||
|
||||
def _urlsafe_b64decode(b64string):
|
||||
# Guard against unicode strings, which base64 can't handle.
|
||||
b64string = _to_bytes(b64string)
|
||||
padded = b64string + b'=' * (4 - len(b64string) % 4)
|
||||
return base64.urlsafe_b64decode(padded)
|
||||
136
src/oauth2client/_openssl_crypt.py
Normal file
136
src/oauth2client/_openssl_crypt.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""OpenSSL Crypto-related routines for oauth2client."""
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class OpenSSLVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey, The public key to verify with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If string,
|
||||
will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
signature = _helpers._to_bytes(signature, encoding='utf-8')
|
||||
try:
|
||||
crypto.verify(self._pubkey, signature, message, 'sha256')
|
||||
return True
|
||||
except crypto.Error:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error: if the key_pem can't be parsed.
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
||||
else:
|
||||
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
||||
return OpenSSLVerifier(pubkey)
|
||||
|
||||
|
||||
class OpenSSLSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return crypto.sign(self._key, message, 'sha256')
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password=b'notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PKCS12 or PEM format.
|
||||
password: string, password for the private key file.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key can't be parsed.
|
||||
"""
|
||||
key = _helpers._to_bytes(key)
|
||||
parsed_pem_key = _helpers._parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
||||
else:
|
||||
password = _helpers._to_bytes(password, encoding='utf-8')
|
||||
pkey = crypto.load_pkcs12(key, password).get_privatekey()
|
||||
return OpenSSLSigner(pkey)
|
||||
|
||||
|
||||
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
|
||||
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
|
||||
|
||||
Args:
|
||||
private_key_bytes: Bytes. PKCS#12 key in DER format.
|
||||
private_key_password: String. Password for PKCS#12 key.
|
||||
|
||||
Returns:
|
||||
String. PEM contents of ``private_key_bytes``.
|
||||
"""
|
||||
private_key_password = _helpers._to_bytes(private_key_password)
|
||||
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkcs12.get_privatekey())
|
||||
184
src/oauth2client/_pure_python_crypt.py
Normal file
184
src/oauth2client/_pure_python_crypt.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Pure Python crypto-related routines for oauth2client.
|
||||
|
||||
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
||||
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
||||
certificates.
|
||||
"""
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import pem
|
||||
from pyasn1_modules.rfc2459 import Certificate
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
import six
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
_PKCS12_ERROR = r"""\
|
||||
PKCS12 format is not supported by the RSA library.
|
||||
Either install PyOpenSSL, or please convert .p12 format
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
||||
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----END RSA PRIVATE KEY-----')
|
||||
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
||||
'-----END PRIVATE KEY-----')
|
||||
_PKCS8_SPEC = PrivateKeyInfo()
|
||||
|
||||
|
||||
def _bit_list_to_bytes(bit_list):
|
||||
"""Converts an iterable of 1's and 0's to bytes.
|
||||
|
||||
Combines the list 8 at a time, treating each group of 8 bits
|
||||
as a single byte.
|
||||
"""
|
||||
num_bits = len(bit_list)
|
||||
byte_vals = bytearray()
|
||||
for start in six.moves.xrange(0, num_bits, 8):
|
||||
curr_bits = bit_list[start:start + 8]
|
||||
char_val = sum(val * digit
|
||||
for val, digit in zip(_POW2, curr_bits))
|
||||
byte_vals.append(char_val)
|
||||
return bytes(byte_vals)
|
||||
|
||||
|
||||
class RsaVerifier(object):
|
||||
"""Verifies the signature on a message.
|
||||
|
||||
Args:
|
||||
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
|
||||
"""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message. If
|
||||
string, will be encoded to bytes as utf-8.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
try:
|
||||
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
||||
except (ValueError, rsa.pkcs1.VerificationError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key_pem, is_x509_cert):
|
||||
"""Construct an RsaVerifier instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
RsaVerifier instance.
|
||||
|
||||
Raises:
|
||||
ValueError: if the key_pem can't be parsed. In either case, error
|
||||
will begin with 'No PEM start marker'. If
|
||||
``is_x509_cert`` is True, will fail to find the
|
||||
"-----BEGIN CERTIFICATE-----" error, otherwise fails
|
||||
to find "-----BEGIN RSA PUBLIC KEY-----".
|
||||
"""
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
if is_x509_cert:
|
||||
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
|
||||
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
|
||||
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
||||
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
||||
else:
|
||||
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
|
||||
return cls(pubkey)
|
||||
|
||||
|
||||
class RsaSigner(object):
|
||||
"""Signs messages with a private key.
|
||||
|
||||
Args:
|
||||
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
|
||||
def __init__(self, pkey):
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, key, password='notasecret'):
|
||||
"""Construct an RsaSigner instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
RsaSigner instance.
|
||||
|
||||
Raises:
|
||||
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
|
||||
PEM format.
|
||||
"""
|
||||
key = _helpers._from_bytes(key) # pem expects str in Py3
|
||||
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
||||
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
||||
|
||||
if marker_id == 0:
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
|
||||
format='DER')
|
||||
elif marker_id == 1:
|
||||
key_info, remaining = decoder.decode(
|
||||
key_bytes, asn1Spec=_PKCS8_SPEC)
|
||||
if remaining != b'':
|
||||
raise ValueError('Unused bytes', remaining)
|
||||
pkey_info = key_info.getComponentByName('privateKey')
|
||||
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
|
||||
format='DER')
|
||||
else:
|
||||
raise ValueError('No key could be detected.')
|
||||
|
||||
return cls(pkey)
|
||||
124
src/oauth2client/_pycrypto_crypt.py
Normal file
124
src/oauth2client/_pycrypto_crypt.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""pyCrypto Crypto-related routines for oauth2client."""
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Util.asn1 import DerSequence
|
||||
|
||||
from oauth2client import _helpers
|
||||
|
||||
|
||||
class PyCryptoVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
|
||||
with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify. If string, will be
|
||||
encoded to bytes as utf-8.
|
||||
signature: string or bytes, The signature on the message.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the
|
||||
public key that this object was constructed with.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._pubkey).verify(
|
||||
SHA256.new(message), signature)
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
||||
is expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
"""
|
||||
if is_x509_cert:
|
||||
key_pem = _helpers._to_bytes(key_pem)
|
||||
pemLines = key_pem.replace(b' ', b'').split()
|
||||
certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
|
||||
certSeq = DerSequence()
|
||||
certSeq.decode(certDer)
|
||||
tbsSeq = DerSequence()
|
||||
tbsSeq.decode(certSeq[0])
|
||||
pubkey = RSA.importKey(tbsSeq[6])
|
||||
else:
|
||||
pubkey = RSA.importKey(key_pem)
|
||||
return PyCryptoVerifier(pubkey)
|
||||
|
||||
|
||||
class PyCryptoSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: string, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
message = _helpers._to_bytes(message, encoding='utf-8')
|
||||
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password='notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM
|
||||
files.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if the key isn't in PEM format.
|
||||
"""
|
||||
parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
|
||||
if parsed_pem_key:
|
||||
pkey = RSA.importKey(parsed_pem_key)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'No key in PEM format was detected. This implementation '
|
||||
'can only use the PyCrypto library for keys in PEM '
|
||||
'format.')
|
||||
return PyCryptoSigner(pkey)
|
||||
2133
src/oauth2client/client.py
Normal file
2133
src/oauth2client/client.py
Normal file
File diff suppressed because it is too large
Load Diff
174
src/oauth2client/clientsecrets.py
Normal file
174
src/oauth2client/clientsecrets.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for reading OAuth 2.0 client secret files.
|
||||
|
||||
A client_secrets.json file contains all the information needed to interact with
|
||||
an OAuth 2.0 protected service.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
# Properties that make a client_secrets.json file valid.
|
||||
TYPE_WEB = 'web'
|
||||
TYPE_INSTALLED = 'installed'
|
||||
|
||||
VALID_CLIENT = {
|
||||
TYPE_WEB: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
TYPE_INSTALLED: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class InvalidClientSecretsError(Error):
|
||||
"""Format of ClientSecrets file is invalid."""
|
||||
|
||||
|
||||
def _validate_clientsecrets(clientsecrets_dict):
|
||||
"""Validate parsed client secrets from a file.
|
||||
|
||||
Args:
|
||||
clientsecrets_dict: dict, a dictionary holding the client secrets.
|
||||
|
||||
Returns:
|
||||
tuple, a string of the client type and the information parsed
|
||||
from the file.
|
||||
"""
|
||||
_INVALID_FILE_FORMAT_MSG = (
|
||||
'Invalid file format. See '
|
||||
'https://developers.google.com/api-client-library/'
|
||||
'python/guide/aaa_client_secrets')
|
||||
|
||||
if clientsecrets_dict is None:
|
||||
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
|
||||
try:
|
||||
(client_type, client_info), = clientsecrets_dict.items()
|
||||
except (ValueError, AttributeError):
|
||||
raise InvalidClientSecretsError(
|
||||
_INVALID_FILE_FORMAT_MSG + ' '
|
||||
'Expected a JSON object with a single property for a "web" or '
|
||||
'"installed" application')
|
||||
|
||||
if client_type not in VALID_CLIENT:
|
||||
raise InvalidClientSecretsError(
|
||||
'Unknown client type: {0}.'.format(client_type))
|
||||
|
||||
for prop_name in VALID_CLIENT[client_type]['required']:
|
||||
if prop_name not in client_info:
|
||||
raise InvalidClientSecretsError(
|
||||
'Missing property "{0}" in a client type of "{1}".'.format(
|
||||
prop_name, client_type))
|
||||
for prop_name in VALID_CLIENT[client_type]['string']:
|
||||
if client_info[prop_name].startswith('[['):
|
||||
raise InvalidClientSecretsError(
|
||||
'Property "{0}" is not configured.'.format(prop_name))
|
||||
return client_type, client_info
|
||||
|
||||
|
||||
def load(fp):
|
||||
obj = json.load(fp)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loads(s):
|
||||
obj = json.loads(s)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def _loadfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fp:
|
||||
obj = json.load(fp)
|
||||
except IOError as exc:
|
||||
raise InvalidClientSecretsError('Error opening file', exc.filename,
|
||||
exc.strerror, exc.errno)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loadfile(filename, cache=None):
|
||||
"""Loading of client_secrets JSON file, optionally backed by a cache.
|
||||
|
||||
Typical cache storage would be App Engine memcache service,
|
||||
but you can pass in any other cache client that implements
|
||||
these methods:
|
||||
|
||||
* ``get(key, namespace=ns)``
|
||||
* ``set(key, value, namespace=ns)``
|
||||
|
||||
Usage::
|
||||
|
||||
# without caching
|
||||
client_type, client_info = loadfile('secrets.json')
|
||||
# using App Engine memcache service
|
||||
from google.appengine.api import memcache
|
||||
client_type, client_info = loadfile('secrets.json', cache=memcache)
|
||||
|
||||
Args:
|
||||
filename: string, Path to a client_secrets.json file on a filesystem.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. If not specified, the file is always being loaded from
|
||||
a filesystem.
|
||||
|
||||
Raises:
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
I/O failure. Can happen only on cache miss.
|
||||
|
||||
Returns:
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
JSON contents is validated only during first load. Cache hits are not
|
||||
validated.
|
||||
"""
|
||||
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
|
||||
|
||||
if not cache:
|
||||
return _loadfile(filename)
|
||||
|
||||
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
|
||||
if obj is None:
|
||||
client_type, client_info = _loadfile(filename)
|
||||
obj = {client_type: client_info}
|
||||
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
|
||||
|
||||
return next(six.iteritems(obj))
|
||||
6
src/oauth2client/contrib/__init__.py
Normal file
6
src/oauth2client/contrib/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Contributed modules.
|
||||
|
||||
Contrib contains modules that are not considered part of the core oauth2client
|
||||
library but provide additional functionality. These modules are intended to
|
||||
make it easier to use oauth2client.
|
||||
"""
|
||||
163
src/oauth2client/contrib/_appengine_ndb.py
Normal file
163
src/oauth2client/contrib/_appengine_ndb.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google App Engine utilities helper.
|
||||
|
||||
Classes that directly require App Engine's ndb library. Provided
|
||||
as a separate module in case of failure to import ndb while
|
||||
other App Engine libraries are present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from google.appengine.ext import ndb
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
NDB_KEY = ndb.Key
|
||||
"""Key constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
NDB_MODEL = ndb.Model
|
||||
"""Model constant used by :mod:`oauth2client.contrib.appengine`."""
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be
|
||||
used interchangeably. This simply provides an NDB model for interacting
|
||||
with the same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used
|
||||
for the site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models.
|
||||
Since PickleProperty inherits from BlobProperty, the underlying
|
||||
representation of the data in the datastore will be the same as in the
|
||||
DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a flow '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB
|
||||
models. Since CredentialsProperty stores data as a blob and this
|
||||
inherits from BlobProperty, the data in the datastore will be the same
|
||||
as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials
|
||||
and subclasses.
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
_LOGGER.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise TypeError(
|
||||
'Property {0} must be convertible to a credentials '
|
||||
'instance; received: {1}.'.format(self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value
|
||||
is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the
|
||||
desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if
|
||||
the value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a
|
||||
property which can serialize and deserialize Credentials correctly, it
|
||||
can be used interchangeably with a CredentialsModel to access, insert
|
||||
and delete the same entities. This simply provides an NDB model for
|
||||
interacting with the same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
||||
81
src/oauth2client/contrib/_fcntl_opener.py
Normal file
81
src/oauth2client/contrib/_fcntl_opener.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import errno
|
||||
import fcntl
|
||||
import time
|
||||
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
|
||||
class _FcntlOpener(locked_file._Opener):
|
||||
"""Open, lock, and unlock a file using fcntl.lockf."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError: if the file is a symbolic
|
||||
link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise locked_file.AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
start_time = time.time()
|
||||
|
||||
locked_file.validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and
|
||||
# don't lock.
|
||||
if e.errno in (errno.EPERM, errno.EACCES):
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
|
||||
self._locked = True
|
||||
return
|
||||
except IOError as e:
|
||||
# If not retrying, then just pass on the error.
|
||||
if timeout == 0:
|
||||
raise
|
||||
if e.errno != errno.EACCES:
|
||||
raise
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
locked_file.logger.warn('Could not lock %s in %s seconds',
|
||||
self._filename, timeout)
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the fcntl.lockf primitive."""
|
||||
if self._locked:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
123
src/oauth2client/contrib/_metadata.py
Normal file
123
src/oauth2client/contrib/_metadata.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Provides helper methods for talking to the Compute Engine metadata server.
|
||||
|
||||
See https://cloud.google.com/compute/docs/metadata
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import httplib2
|
||||
from six.moves import http_client
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
|
||||
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
|
||||
|
||||
|
||||
def get(http_request, path, root=METADATA_ROOT, recursive=None):
|
||||
"""Fetch a resource from the metadata server.
|
||||
|
||||
Args:
|
||||
path: A string indicating the resource to retrieve. For example,
|
||||
'instance/service-accounts/defualt'
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadataserver.
|
||||
root: A string indicating the full path to the metadata server root.
|
||||
recursive: A boolean indicating whether to do a recursive query of
|
||||
metadata. See
|
||||
https://cloud.google.com/compute/docs/metadata#aggcontents
|
||||
|
||||
Returns:
|
||||
A dictionary if the metadata server returns JSON, otherwise a string.
|
||||
|
||||
Raises:
|
||||
httplib2.Httplib2Error if an error corrured while retrieving metadata.
|
||||
"""
|
||||
url = urlparse.urljoin(root, path)
|
||||
url = util._add_query_parameter(url, 'recursive', recursive)
|
||||
|
||||
response, content = http_request(
|
||||
url,
|
||||
headers=METADATA_HEADERS
|
||||
)
|
||||
|
||||
if response.status == http_client.OK:
|
||||
decoded = _helpers._from_bytes(content)
|
||||
if response['content-type'] == 'application/json':
|
||||
return json.loads(decoded)
|
||||
else:
|
||||
return decoded
|
||||
else:
|
||||
raise httplib2.HttpLib2Error(
|
||||
'Failed to retrieve {0} from the Google Compute Engine'
|
||||
'metadata service. Response:\n{1}'.format(url, response))
|
||||
|
||||
|
||||
def get_service_account_info(http_request, service_account='default'):
|
||||
"""Get information about a service account from the metadata server.
|
||||
|
||||
Args:
|
||||
service_account: An email specifying the service account for which to
|
||||
look up information. Default will be information for the "default"
|
||||
service account of the current compute engine instance.
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadata server.
|
||||
Returns:
|
||||
A dictionary with information about the specified service account,
|
||||
for example:
|
||||
|
||||
{
|
||||
'email': '...',
|
||||
'scopes': ['scope', ...],
|
||||
'aliases': ['default', '...']
|
||||
}
|
||||
"""
|
||||
return get(
|
||||
http_request,
|
||||
'instance/service-accounts/{0}/'.format(service_account),
|
||||
recursive=True)
|
||||
|
||||
|
||||
def get_token(http_request, service_account='default'):
|
||||
"""Fetch an oauth token for the
|
||||
|
||||
Args:
|
||||
service_account: An email specifying the service account this token
|
||||
should represent. Default will be a token for the "default" service
|
||||
account of the current compute engine instance.
|
||||
http_request: A callable that matches the method
|
||||
signature of httplib2.Http.request. Used to make the request to the
|
||||
metadataserver.
|
||||
|
||||
Returns:
|
||||
A tuple of (access token, token expiration), where access token is the
|
||||
access token as a string and token expiration is a datetime object
|
||||
that indicates when the access token will expire.
|
||||
"""
|
||||
token_json = get(
|
||||
http_request,
|
||||
'instance/service-accounts/{0}/token'.format(service_account))
|
||||
token_expiry = client._UTCNOW() + datetime.timedelta(
|
||||
seconds=token_json['expires_in'])
|
||||
return token_json['access_token'], token_expiry
|
||||
106
src/oauth2client/contrib/_win32_opener.py
Normal file
106
src/oauth2client/contrib/_win32_opener.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import errno
|
||||
import time
|
||||
|
||||
import pywintypes
|
||||
import win32con
|
||||
import win32file
|
||||
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
|
||||
class _Win32Opener(locked_file._Opener):
|
||||
"""Open, lock, and unlock a file using windows primitives."""
|
||||
|
||||
# Error #33:
|
||||
# 'The process cannot access the file because another process'
|
||||
FILE_IN_USE_ERROR = 33
|
||||
|
||||
# Error #158:
|
||||
# 'The segment is already unlocked.'
|
||||
FILE_ALREADY_UNLOCKED_ERROR = 158
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError: if the file is a symbolic
|
||||
link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise locked_file.AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
start_time = time.time()
|
||||
|
||||
locked_file.validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode
|
||||
# and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.LockFileEx(
|
||||
hfile,
|
||||
(win32con.LOCKFILE_FAIL_IMMEDIATELY |
|
||||
win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
|
||||
pywintypes.OVERLAPPED())
|
||||
self._locked = True
|
||||
return
|
||||
except pywintypes.error as e:
|
||||
if timeout == 0:
|
||||
raise
|
||||
|
||||
# If the error is not that the file is already
|
||||
# in use, raise.
|
||||
if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
|
||||
raise
|
||||
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
locked_file.logger.warn('Could not lock %s in %s seconds',
|
||||
self._filename, timeout)
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the win32 primitive."""
|
||||
if self._locked:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.UnlockFileEx(hfile, 0, -0x10000,
|
||||
pywintypes.OVERLAPPED())
|
||||
except pywintypes.error as e:
|
||||
if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
|
||||
raise
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
913
src/oauth2client/contrib/appengine.py
Normal file
913
src/oauth2client/contrib/appengine.py
Normal file
@@ -0,0 +1,913 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google App Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
|
||||
from google.appengine.api import app_identity
|
||||
from google.appengine.api import memcache
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext.webapp.util import login_required
|
||||
import httplib2
|
||||
import webapp2 as webapp
|
||||
|
||||
import oauth2client
|
||||
from oauth2client import client
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import xsrfutil
|
||||
|
||||
# This is a temporary fix for a Google internal issue.
|
||||
try:
|
||||
from oauth2client.contrib import _appengine_ndb
|
||||
except ImportError: # pragma: NO COVER
|
||||
_appengine_ndb = None
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
|
||||
|
||||
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
|
||||
|
||||
if _appengine_ndb is None: # pragma: NO COVER
|
||||
CredentialsNDBModel = None
|
||||
CredentialsNDBProperty = None
|
||||
FlowNDBProperty = None
|
||||
_NDB_KEY = None
|
||||
_NDB_MODEL = None
|
||||
SiteXsrfSecretKeyNDB = None
|
||||
else:
|
||||
CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
|
||||
CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
|
||||
FlowNDBProperty = _appengine_ndb.FlowNDBProperty
|
||||
_NDB_KEY = _appengine_ndb.NDB_KEY
|
||||
_NDB_MODEL = _appengine_ndb.NDB_MODEL
|
||||
SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
|
||||
|
||||
|
||||
def _safe_html(s):
|
||||
"""Escape text to make it safe to display.
|
||||
|
||||
Args:
|
||||
s: string, The text to escape.
|
||||
|
||||
Returns:
|
||||
The escaped text as a string.
|
||||
"""
|
||||
return cgi.escape(s, quote=1).replace("'", ''')
|
||||
|
||||
|
||||
class SiteXsrfSecretKey(db.Model):
|
||||
"""Storage for the sites XSRF secret key.
|
||||
|
||||
There will only be one instance stored of this model, the one used for the
|
||||
site.
|
||||
"""
|
||||
secret = db.StringProperty()
|
||||
|
||||
|
||||
def _generate_new_xsrf_secret_key():
|
||||
"""Returns a random XSRF secret key."""
|
||||
return os.urandom(16).encode("hex")
|
||||
|
||||
|
||||
def xsrf_secret_key():
|
||||
"""Return the secret key for use for XSRF protection.
|
||||
|
||||
If the Site entity does not have a secret key, this method will also create
|
||||
one and persist it.
|
||||
|
||||
Returns:
|
||||
The secret key.
|
||||
"""
|
||||
secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
if not secret:
|
||||
# Load the one and only instance of SiteXsrfSecretKey.
|
||||
model = SiteXsrfSecretKey.get_or_insert(key_name='site')
|
||||
if not model.secret:
|
||||
model.secret = _generate_new_xsrf_secret_key()
|
||||
model.put()
|
||||
secret = model.secret
|
||||
memcache.add(XSRF_MEMCACHE_ID, secret,
|
||||
namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
|
||||
return str(secret)
|
||||
|
||||
|
||||
class AppAssertionCredentials(client.AssertionCredentials):
|
||||
"""Credentials object for App Engine Assertion Grants
|
||||
|
||||
This object will allow an App Engine application to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the App Engine application itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
**kwargs: optional keyword args, including:
|
||||
service_account_id: service account id of the application. If None
|
||||
or unspecified, the default service account for
|
||||
the app is used.
|
||||
"""
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self._kwargs = kwargs
|
||||
self.service_account_id = kwargs.get('service_account_id', None)
|
||||
self._service_account_email = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(json_data)
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Since the underlying App Engine app_identity implementation does its
|
||||
own caching we can skip all the storage hoops and just to a refresh
|
||||
using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make the
|
||||
refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
scopes = self.scope.split()
|
||||
(token, _) = app_identity.get_access_token(
|
||||
scopes, service_account_id=self.service_account_id)
|
||||
except app_identity.Error as e:
|
||||
raise client.AccessTokenRefreshError(str(e))
|
||||
self.access_token = token
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError('Cannot serialize credentials '
|
||||
'for Google App Engine.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return app_identity.sign_blob(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the Google App Engine
|
||||
service account.
|
||||
"""
|
||||
if self._service_account_email is None:
|
||||
self._service_account_email = (
|
||||
app_identity.get_service_account_name())
|
||||
return self._service_account_email
|
||||
|
||||
|
||||
class FlowProperty(db.Property):
|
||||
"""App Engine datastore Property for Flow.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Flow
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
flow = super(FlowProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
return db.Blob(pickle.dumps(flow))
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return pickle.loads(value)
|
||||
|
||||
def validate(self, value):
|
||||
if value is not None and not isinstance(value, client.Flow):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a FlowThreeLegged instance ({1})'.format(self.name, value))
|
||||
return super(FlowProperty, self).validate(value)
|
||||
|
||||
def empty(self, value):
|
||||
return not value
|
||||
|
||||
|
||||
class CredentialsProperty(db.Property):
|
||||
"""App Engine datastore Property for Credentials.
|
||||
|
||||
Utility property that allows easy storage and retrieval of
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = client.Credentials
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
logger.info("get: Got type " + str(type(model_instance)))
|
||||
cred = super(CredentialsProperty, self).get_value_for_datastore(
|
||||
model_instance)
|
||||
if cred is None:
|
||||
cred = ''
|
||||
else:
|
||||
cred = cred.to_json()
|
||||
return db.Blob(cred)
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
logger.info("make: Got type " + str(type(value)))
|
||||
if value is None:
|
||||
return None
|
||||
if len(value) == 0:
|
||||
return None
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
def validate(self, value):
|
||||
value = super(CredentialsProperty, self).validate(value)
|
||||
logger.info("validate: Got type " + str(type(value)))
|
||||
if value is not None and not isinstance(value, client.Credentials):
|
||||
raise db.BadValueError(
|
||||
'Property {0} must be convertible '
|
||||
'to a Credentials instance ({1})'.format(self.name, value))
|
||||
return value
|
||||
|
||||
|
||||
class StorageByKeyName(client.Storage):
|
||||
"""Store and retrieve a credential to and from the App Engine datastore.
|
||||
|
||||
This Storage helper presumes the Credentials have been stored as a
|
||||
CredentialsProperty or CredentialsNDBProperty on a datastore model class,
|
||||
and that entities are stored by key_name.
|
||||
"""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, model, key_name, property_name, cache=None, user=None):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: db.Model or ndb.Model, model class
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
property_name: string, name of the property that is a
|
||||
CredentialsProperty or CredentialsNDBProperty.
|
||||
cache: memcache, a write-through cache to put in front of the
|
||||
datastore. If the model you are using is an NDB model, using
|
||||
a cache will be redundant since the model uses an instance
|
||||
cache and memcache for you.
|
||||
user: users.User object, optional. Can be used to grab user ID as a
|
||||
key_name if no key name is specified.
|
||||
"""
|
||||
super(StorageByKeyName, self).__init__()
|
||||
|
||||
if key_name is None:
|
||||
if user is None:
|
||||
raise ValueError('StorageByKeyName called with no '
|
||||
'key name or user.')
|
||||
key_name = user.user_id()
|
||||
|
||||
self._model = model
|
||||
self._key_name = key_name
|
||||
self._property_name = property_name
|
||||
self._cache = cache
|
||||
|
||||
def _is_ndb(self):
|
||||
"""Determine whether the model of the instance is an NDB model.
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether or not the model is an NDB or DB model.
|
||||
"""
|
||||
# issubclass will fail if one of the arguments is not a class, only
|
||||
# need worry about new-style classes since ndb and db models are
|
||||
# new-style
|
||||
if isinstance(self._model, type):
|
||||
if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
|
||||
return True
|
||||
elif issubclass(self._model, db.Model):
|
||||
return False
|
||||
|
||||
raise TypeError(
|
||||
'Model class not an NDB or DB model: {0}.'.format(self._model))
|
||||
|
||||
def _get_entity(self):
|
||||
"""Retrieve entity from datastore.
|
||||
|
||||
Uses a different model method for db or ndb models.
|
||||
|
||||
Returns:
|
||||
Instance of the model corresponding to the current storage object
|
||||
and stored using the key name of the storage object.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
return self._model.get_by_id(self._key_name)
|
||||
else:
|
||||
return self._model.get_by_key_name(self._key_name)
|
||||
|
||||
def _delete_entity(self):
|
||||
"""Delete entity from datastore.
|
||||
|
||||
Attempts to delete using the key_name stored on the object, whether or
|
||||
not the given key is in the datastore.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
_NDB_KEY(self._model, self._key_name).delete()
|
||||
else:
|
||||
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
|
||||
db.delete(entity_key)
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
if self._cache:
|
||||
json = self._cache.get(self._key_name)
|
||||
if json:
|
||||
credentials = client.Credentials.new_from_json(json)
|
||||
if credentials is None:
|
||||
entity = self._get_entity()
|
||||
if entity is not None:
|
||||
credentials = getattr(entity, self._property_name)
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
if credentials and hasattr(credentials, 'set_store'):
|
||||
credentials.set_store(self)
|
||||
return credentials
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity = self._model.get_or_insert(self._key_name)
|
||||
setattr(entity, self._property_name, credentials)
|
||||
entity.put()
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_delete(self):
|
||||
"""Delete Credential from datastore."""
|
||||
|
||||
if self._cache:
|
||||
self._cache.delete(self._key_name)
|
||||
|
||||
self._delete_entity()
|
||||
|
||||
|
||||
class CredentialsModel(db.Model):
|
||||
"""Storage for OAuth 2.0 Credentials
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsProperty()
|
||||
|
||||
|
||||
def _build_state_value(request_handler, user):
|
||||
"""Composes the value for the 'state' parameter.
|
||||
|
||||
Packs the current request URI and an XSRF token into an opaque string that
|
||||
can be passed to the authentication server via the 'state' parameter.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, The request.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The state value as a string.
|
||||
"""
|
||||
uri = request_handler.request.url
|
||||
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
|
||||
action_id=str(uri))
|
||||
return uri + ':' + token
|
||||
|
||||
|
||||
def _parse_state_value(state, user):
|
||||
"""Parse the value of the 'state' parameter.
|
||||
|
||||
Parses the value and validates the XSRF token in the state parameter.
|
||||
|
||||
Args:
|
||||
state: string, The value of the state parameter.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The redirect URI, or None if XSRF token is not valid.
|
||||
"""
|
||||
uri, token = state.rsplit(':', 1)
|
||||
if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
|
||||
action_id=uri):
|
||||
return uri
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OAuth2Decorator(object):
|
||||
"""Utility for making OAuth 2.0 easier.
|
||||
|
||||
Instantiate and then use with oauth_required or oauth_aware
|
||||
as decorators on webapp.RequestHandler methods.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2Decorator(
|
||||
client_id='837...ent.com',
|
||||
client_secret='Qh...wwI',
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
def set_credentials(self, credentials):
|
||||
self._tls.credentials = credentials
|
||||
|
||||
def get_credentials(self):
|
||||
"""A thread local Credentials object.
|
||||
|
||||
Returns:
|
||||
A client.Credentials object, or None if credentials hasn't been set
|
||||
in this thread yet, which may happen when calling has_credentials
|
||||
inside oauth_aware.
|
||||
"""
|
||||
return getattr(self._tls, 'credentials', None)
|
||||
|
||||
credentials = property(get_credentials, set_credentials)
|
||||
|
||||
def set_flow(self, flow):
|
||||
self._tls.flow = flow
|
||||
|
||||
def get_flow(self):
|
||||
"""A thread local Flow object.
|
||||
|
||||
Returns:
|
||||
A credentials.Flow object, or None if the flow hasn't been set in
|
||||
this thread yet, which happens in _create_flow() since Flows are
|
||||
created lazily.
|
||||
"""
|
||||
return getattr(self._tls, 'flow', None)
|
||||
|
||||
flow = property(get_flow, set_flow)
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, client_id, client_secret, scope,
|
||||
auth_uri=oauth2client.GOOGLE_AUTH_URI,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
user_agent=None,
|
||||
message=None,
|
||||
callback_path='/oauth2callback',
|
||||
token_response_param=None,
|
||||
_storage_class=StorageByKeyName,
|
||||
_credentials_class=CredentialsModel,
|
||||
_credentials_property_name='credentials',
|
||||
**kwargs):
|
||||
"""Constructor for OAuth2Decorator
|
||||
|
||||
Args:
|
||||
client_id: string, client identifier.
|
||||
client_secret: string client secret.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
auth_uri: string, URI for authorization endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider
|
||||
can be used.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
user_agent: string, User agent of your application, default to
|
||||
None.
|
||||
message: Message to display if there are problems with the
|
||||
OAuth 2.0 configuration. The message may contain HTML and
|
||||
will be presented on the web interface for any method that
|
||||
uses the decorator.
|
||||
callback_path: string, The absolute path to use as the callback
|
||||
URI. Note that this must match up with the URI given
|
||||
when registering the application in the APIs
|
||||
Console.
|
||||
token_response_param: string. If provided, the full JSON response
|
||||
to the access token request will be encoded
|
||||
and included in this query parameter in the
|
||||
callback URI. This is useful with providers
|
||||
(e.g. wordpress.com) that include extra
|
||||
fields that the client may want.
|
||||
_storage_class: "Protected" keyword argument not typically provided
|
||||
to this constructor. A storage class to aid in
|
||||
storing a Credentials object for a user in the
|
||||
datastore. Defaults to StorageByKeyName.
|
||||
_credentials_class: "Protected" keyword argument not typically
|
||||
provided to this constructor. A db or ndb Model
|
||||
class to hold credentials. Defaults to
|
||||
CredentialsModel.
|
||||
_credentials_property_name: "Protected" keyword argument not
|
||||
typically provided to this constructor.
|
||||
A string indicating the name of the
|
||||
field on the _credentials_class where a
|
||||
Credentials object will be stored.
|
||||
Defaults to 'credentials'.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
self._tls = threading.local()
|
||||
self.flow = None
|
||||
self.credentials = None
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scope = util.scopes_to_string(scope)
|
||||
self._auth_uri = auth_uri
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
self._message = message
|
||||
self._in_error = False
|
||||
self._callback_path = callback_path
|
||||
self._token_response_param = token_response_param
|
||||
self._storage_class = _storage_class
|
||||
self._credentials_class = _credentials_class
|
||||
self._credentials_property_name = _credentials_property_name
|
||||
|
||||
def _display_error_message(self, request_handler):
|
||||
request_handler.response.out.write('<html><body>')
|
||||
request_handler.response.out.write(_safe_html(self._message))
|
||||
request_handler.response.out.write('</body></html>')
|
||||
|
||||
def oauth_required(self, method):
|
||||
"""Decorator that starts the OAuth 2.0 dance.
|
||||
|
||||
Starts the OAuth dance for the logged in user if they haven't already
|
||||
granted access for this application.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def check_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
# Store the request URI in 'state' so we can use it later
|
||||
self.flow.params['state'] = _build_state_value(
|
||||
request_handler, user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
|
||||
if not self.has_credentials():
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
except client.AccessTokenRefreshError:
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
|
||||
return check_oauth
|
||||
|
||||
def _create_flow(self, request_handler):
|
||||
"""Create the Flow object.
|
||||
|
||||
The Flow is calculated lazily since we don't know where this app is
|
||||
running until it receives a request, at which point redirect_uri can be
|
||||
calculated and then the Flow object can be constructed.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, the request handler.
|
||||
"""
|
||||
if self.flow is None:
|
||||
redirect_uri = request_handler.request.relative_url(
|
||||
self._callback_path) # Usually /oauth2callback
|
||||
self.flow = client.OAuth2WebServerFlow(
|
||||
self._client_id, self._client_secret, self._scope,
|
||||
redirect_uri=redirect_uri, user_agent=self._user_agent,
|
||||
auth_uri=self._auth_uri, token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri, **self._kwargs)
|
||||
|
||||
def oauth_aware(self, method):
|
||||
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
|
||||
|
||||
Does all the setup for the OAuth dance, but doesn't initiate it.
|
||||
This decorator is useful if you want to create a page that knows
|
||||
whether or not the user has granted access to this application.
|
||||
From within a method decorated with @oauth_aware the has_credentials()
|
||||
and authorize_url() methods can be called.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def setup_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a
|
||||
# POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
self.flow.params['state'] = _build_state_value(request_handler,
|
||||
user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
return setup_oauth
|
||||
|
||||
def has_credentials(self):
|
||||
"""True if for the logged in user there are valid access Credentials.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
return self.credentials is not None and not self.credentials.invalid
|
||||
|
||||
def authorize_url(self):
|
||||
"""Returns the URL to start the OAuth dance.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
url = self.flow.step1_get_authorize_url()
|
||||
return str(url)
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Must only be called from within an @oauth_required decorated method, or
|
||||
from within an @oauth_aware decorated method where has_credentials()
|
||||
returns True.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
"""
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
||||
|
||||
@property
|
||||
def callback_path(self):
|
||||
"""The absolute path where the callback will occur.
|
||||
|
||||
Note this is the absolute path, not the absolute URI, that will be
|
||||
calculated by the decorator at runtime. See callback_handler() for how
|
||||
this should be used.
|
||||
|
||||
Returns:
|
||||
The callback path as a string.
|
||||
"""
|
||||
return self._callback_path
|
||||
|
||||
def callback_handler(self):
|
||||
"""RequestHandler for the OAuth 2.0 redirect callback.
|
||||
|
||||
Usage::
|
||||
|
||||
app = webapp.WSGIApplication([
|
||||
('/index', MyIndexHandler),
|
||||
...,
|
||||
(decorator.callback_path, decorator.callback_handler())
|
||||
])
|
||||
|
||||
Returns:
|
||||
A webapp.RequestHandler that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
decorator = self
|
||||
|
||||
class OAuth2Handler(webapp.RequestHandler):
|
||||
"""Handler for the redirect_uri of the OAuth 2.0 dance."""
|
||||
|
||||
@login_required
|
||||
def get(self):
|
||||
error = self.request.get('error')
|
||||
if error:
|
||||
errormsg = self.request.get('error_description', error)
|
||||
self.response.out.write(
|
||||
'The authorization request failed: {0}'.format(
|
||||
_safe_html(errormsg)))
|
||||
else:
|
||||
user = users.get_current_user()
|
||||
decorator._create_flow(self)
|
||||
credentials = decorator.flow.step2_exchange(
|
||||
self.request.params)
|
||||
decorator._storage_class(
|
||||
decorator._credentials_class, None,
|
||||
decorator._credentials_property_name,
|
||||
user=user).put(credentials)
|
||||
redirect_uri = _parse_state_value(
|
||||
str(self.request.get('state')), user)
|
||||
if redirect_uri is None:
|
||||
self.response.out.write(
|
||||
'The authorization request failed')
|
||||
return
|
||||
|
||||
if (decorator._token_response_param and
|
||||
credentials.token_response):
|
||||
resp_json = json.dumps(credentials.token_response)
|
||||
redirect_uri = util._add_query_parameter(
|
||||
redirect_uri, decorator._token_response_param,
|
||||
resp_json)
|
||||
|
||||
self.redirect(redirect_uri)
|
||||
|
||||
return OAuth2Handler
|
||||
|
||||
def callback_application(self):
|
||||
"""WSGI application for handling the OAuth 2.0 redirect callback.
|
||||
|
||||
If you need finer grained control use `callback_handler` which returns
|
||||
just the webapp.RequestHandler.
|
||||
|
||||
Returns:
|
||||
A webapp.WSGIApplication that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
return webapp.WSGIApplication([
|
||||
(self.callback_path, self.callback_handler())
|
||||
])
|
||||
|
||||
|
||||
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
|
||||
"""An OAuth2Decorator that builds from a clientsecrets file.
|
||||
|
||||
Uses a clientsecrets file as the source for all the information when
|
||||
constructing an OAuth2Decorator.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2DecoratorFromClientSecrets(
|
||||
os.path.join(os.path.dirname(__file__), 'client_secrets.json')
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be
|
||||
# used in API calls
|
||||
|
||||
"""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or iterable of strings, scope(s) of the credentials
|
||||
being requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface
|
||||
for any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and
|
||||
set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename,
|
||||
cache=cache)
|
||||
if client_type not in (clientsecrets.TYPE_WEB,
|
||||
clientsecrets.TYPE_INSTALLED):
|
||||
raise clientsecrets.InvalidClientSecretsError(
|
||||
"OAuth2Decorator doesn't support this OAuth 2.0 flow.")
|
||||
|
||||
constructor_kwargs = dict(kwargs)
|
||||
constructor_kwargs.update({
|
||||
'auth_uri': client_info['auth_uri'],
|
||||
'token_uri': client_info['token_uri'],
|
||||
'message': message,
|
||||
})
|
||||
revoke_uri = client_info.get('revoke_uri')
|
||||
if revoke_uri is not None:
|
||||
constructor_kwargs['revoke_uri'] = revoke_uri
|
||||
super(OAuth2DecoratorFromClientSecrets, self).__init__(
|
||||
client_info['client_id'], client_info['client_secret'],
|
||||
scope, **constructor_kwargs)
|
||||
if message is not None:
|
||||
self._message = message
|
||||
else:
|
||||
self._message = 'Please configure your application for OAuth 2.0.'
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def oauth2decorator_from_clientsecrets(filename, scope,
|
||||
message=None, cache=None):
|
||||
"""Creates an OAuth2Decorator populated from a clientsecrets file.
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or list of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may
|
||||
contain HTML and will be presented on the web interface for
|
||||
any method that uses the decorator.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
|
||||
Returns: An OAuth2Decorator
|
||||
"""
|
||||
return OAuth2DecoratorFromClientSecrets(filename, scope,
|
||||
message=message, cache=cache)
|
||||
146
src/oauth2client/contrib/devshell.py
Normal file
146
src/oauth2client/contrib/devshell.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
|
||||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Errors for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(Error):
|
||||
"""Errors for communication with the Developer Shell server."""
|
||||
|
||||
|
||||
class NoDevshellServer(Error):
|
||||
"""Error when no Developer Shell server can be contacted."""
|
||||
|
||||
# The request for credential information to the Developer Shell client socket
|
||||
# is always an empty PBLite-formatted JSON object, so just define it as a
|
||||
# constant.
|
||||
CREDENTIAL_INFO_REQUEST_JSON = '[]'
|
||||
|
||||
|
||||
class CredentialInfoResponse(object):
|
||||
"""Credential information response from Developer Shell server.
|
||||
|
||||
The credential information response from Developer Shell socket is a
|
||||
PBLite-formatted JSON array with fields encoded by their index in the
|
||||
array:
|
||||
|
||||
* Index 0 - user email
|
||||
* Index 1 - default project ID. None if the project context is not known.
|
||||
* Index 2 - OAuth2 access token. None if there is no valid auth context.
|
||||
* Index 3 - Seconds until the access token expires. None if not present.
|
||||
"""
|
||||
|
||||
def __init__(self, json_string):
|
||||
"""Initialize the response data from JSON PBLite array."""
|
||||
pbl = json.loads(json_string)
|
||||
if not isinstance(pbl, list):
|
||||
raise ValueError('Not a list: ' + str(pbl))
|
||||
pbl_len = len(pbl)
|
||||
self.user_email = pbl[0] if pbl_len > 0 else None
|
||||
self.project_id = pbl[1] if pbl_len > 1 else None
|
||||
self.access_token = pbl[2] if pbl_len > 2 else None
|
||||
self.expires_in = pbl[3] if pbl_len > 3 else None
|
||||
|
||||
|
||||
def _SendRecv():
|
||||
"""Communicate with the Developer Shell server socket."""
|
||||
|
||||
port = int(os.getenv(DEVSHELL_ENV, 0))
|
||||
if port == 0:
|
||||
raise NoDevshellServer()
|
||||
|
||||
sock = socket.socket()
|
||||
sock.connect(('localhost', port))
|
||||
|
||||
data = CREDENTIAL_INFO_REQUEST_JSON
|
||||
msg = '{0}\n{1}'.format(len(data), data)
|
||||
sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
|
||||
|
||||
header = sock.recv(6).decode()
|
||||
if '\n' not in header:
|
||||
raise CommunicationError('saw no newline in the first 6 bytes')
|
||||
len_str, json_str = header.split('\n', 1)
|
||||
to_read = int(len_str) - len(json_str)
|
||||
if to_read > 0:
|
||||
json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
|
||||
|
||||
return CredentialInfoResponse(json_str)
|
||||
|
||||
|
||||
class DevshellCredentials(client.GoogleCredentials):
|
||||
"""Credentials object for Google Developer Shell environment.
|
||||
|
||||
This object will allow a Google Developer Shell session to identify its
|
||||
user to Google and other OAuth 2.0 servers that can verify assertions. It
|
||||
can be used for the purpose of accessing data stored under the user
|
||||
account.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, user_agent=None):
|
||||
super(DevshellCredentials, self).__init__(
|
||||
None, # access_token, initialized below
|
||||
None, # client_id
|
||||
None, # client_secret
|
||||
None, # refresh_token
|
||||
None, # token_expiry
|
||||
None, # token_uri
|
||||
user_agent)
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http_request):
|
||||
self.devshell_response = _SendRecv()
|
||||
self.access_token = self.devshell_response.access_token
|
||||
expires_in = self.devshell_response.expires_in
|
||||
if expires_in is not None:
|
||||
delta = datetime.timedelta(seconds=expires_in)
|
||||
self.token_expiry = client._UTCNOW() + delta
|
||||
else:
|
||||
self.token_expiry = None
|
||||
|
||||
@property
|
||||
def user_email(self):
|
||||
return self.devshell_response.user_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.devshell_response.project_id
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot load Developer Shell credentials from JSON.')
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize Developer Shell credentials.')
|
||||
65
src/oauth2client/contrib/dictionary_storage.py
Normal file
65
src/oauth2client/contrib/dictionary_storage.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Dictionary storage for OAuth2 Credentials."""
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class DictionaryStorage(client.Storage):
|
||||
"""Store and retrieve credentials to and from a dictionary-like object.
|
||||
|
||||
Args:
|
||||
dictionary: A dictionary or dictionary-like object.
|
||||
key: A string or other hashable. The credentials will be stored in
|
||||
``dictionary[key]``.
|
||||
lock: An optional threading.Lock-like object. The lock will be
|
||||
acquired before anything is written or read from the
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, dictionary, key, lock=None):
|
||||
"""Construct a DictionaryStorage instance."""
|
||||
super(DictionaryStorage, self).__init__(lock=lock)
|
||||
self._dictionary = dictionary
|
||||
self._key = key
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve the credentials from the dictionary, if they exist.
|
||||
|
||||
Returns: A :class:`oauth2client.client.OAuth2Credentials` instance.
|
||||
"""
|
||||
serialized = self._dictionary.get(self._key)
|
||||
|
||||
if serialized is None:
|
||||
return None
|
||||
|
||||
credentials = client.OAuth2Credentials.from_json(serialized)
|
||||
credentials.set_store(self)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Save the credentials to the dictionary.
|
||||
|
||||
Args:
|
||||
credentials: A :class:`oauth2client.client.OAuth2Credentials`
|
||||
instance.
|
||||
"""
|
||||
serialized = credentials.to_json()
|
||||
self._dictionary[self._key] = serialized
|
||||
|
||||
def locked_delete(self):
|
||||
"""Remove the credentials from the dictionary, if they exist."""
|
||||
self._dictionary.pop(self._key, None)
|
||||
477
src/oauth2client/contrib/django_util/__init__.py
Normal file
477
src/oauth2client/contrib/django_util/__init__.py
Normal file
@@ -0,0 +1,477 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for the Django web framework.
|
||||
|
||||
Provides Django views and helpers the make using the OAuth2 web server
|
||||
flow easier. It includes an ``oauth_required`` decorator to automatically
|
||||
ensure that user credentials are available, and an ``oauth_enabled`` decorator
|
||||
to check if the user has authorized, and helper shortcuts to create the
|
||||
authorization URL otherwise.
|
||||
|
||||
There are two basic use cases supported. The first is using Google OAuth as the
|
||||
primary form of authentication, which is the simpler approach recommended
|
||||
for applications without their own user system.
|
||||
|
||||
The second use case is adding Google OAuth credentials to an
|
||||
existing Django model containing a Django user field. Most of the
|
||||
configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in
|
||||
settings.py. See "Adding Credentials To An Existing Django User System" for
|
||||
usage differences.
|
||||
|
||||
Only Django versions 1.8+ are supported.
|
||||
|
||||
Configuration
|
||||
===============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`.
|
||||
|
||||
Add the helper to your INSTALLED_APPS:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: installed_apps
|
||||
|
||||
INSTALLED_APPS = (
|
||||
# other apps
|
||||
"django.contrib.sessions.middleware"
|
||||
"oauth2client.contrib.django_util"
|
||||
)
|
||||
|
||||
This helper also requires the Django Session Middleware, so
|
||||
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
|
||||
|
||||
Add the client secrets created earlier to the settings. You can either
|
||||
specify the path to the credentials file in JSON format
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_file
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json
|
||||
|
||||
Or, directly configure the client Id and client secret.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: secrets_config
|
||||
|
||||
GOOGLE_OAUTH2_CLIENT_ID=client-id-field
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field
|
||||
|
||||
By default, the default scopes for the required decorator only contains the
|
||||
``email`` scopes. You can change that default in the settings.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: scopes
|
||||
|
||||
GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',)
|
||||
|
||||
By default, the decorators will add an `oauth` object to the Django request
|
||||
object, and include all of its state and helpers inside that object. If the
|
||||
`oauth` name conflicts with another usage, it can be changed
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: request_prefix
|
||||
|
||||
# changes request.oauth to request.google_oauth
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth'
|
||||
|
||||
Add the oauth2 routes to your application's urls.py urlpatterns.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: urls.py
|
||||
:name: urls
|
||||
|
||||
from oauth2client.contrib.django_util.site import urls as oauth2_urls
|
||||
|
||||
urlpatterns += [url(r'^oauth2/', include(oauth2_urls))]
|
||||
|
||||
To require OAuth2 credentials for a view, use the `oauth2_required` decorator.
|
||||
This creates a credentials object with an id_token, and allows you to create
|
||||
an `http` object to build service clients with. These are all attached to the
|
||||
request.oauth
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.oauth.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(calendarId='primary').execute()['items']
|
||||
return HttpResponse("email: {0} , calendar: {1}".format(
|
||||
email,str(events)))
|
||||
return HttpResponse(
|
||||
"email: {0} , calendar: {1}".format(email, str(events)))
|
||||
|
||||
To make OAuth2 optional and provide an authorization link in your own views.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled2
|
||||
|
||||
from oauth2client.contrib.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email']))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
If a view needs a scope not included in the default scopes specified in
|
||||
the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth)
|
||||
and specify additional scopes in the decorator arguments.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_additional_scopes
|
||||
|
||||
@oauth_enabled(scopes=['https://www.googleapis.com/auth/drive'])
|
||||
def drive_required(request):
|
||||
if request.oauth.has_credentials():
|
||||
service = build(serviceName='drive', version='v2',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.files().list().execute()['items']
|
||||
return HttpResponse(str(events))
|
||||
else:
|
||||
return HttpResponse(
|
||||
'Here is an OAuth Authorize link: <a href="{0}">Authorize'
|
||||
'</a>'.format(request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
To provide a callback on authorization being completed, use the
|
||||
oauth2_authorized signal:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: signals
|
||||
|
||||
from oauth2client.contrib.django_util.signals import oauth2_authorized
|
||||
|
||||
def test_callback(sender, request, credentials, **kwargs):
|
||||
print("Authorization Signal Received {0}".format(
|
||||
credentials.id_token['email']))
|
||||
|
||||
oauth2_authorized.connect(test_callback)
|
||||
|
||||
Adding Credentials To An Existing Django User System
|
||||
=====================================================
|
||||
|
||||
As an alternative to storing the credentials in the session, the helper
|
||||
can be configured to store the fields on a Django model. This might be useful
|
||||
if you need to use the credentials outside the context of a user request. It
|
||||
also prevents the need for a logged in user to repeat the OAuth flow when
|
||||
starting a new session.
|
||||
|
||||
To use, change ``settings.py``
|
||||
|
||||
.. code-block:: python
|
||||
:caption: settings.py
|
||||
:name: storage_model_config
|
||||
|
||||
GOOGLE_OAUTH2_STORAGE_MODEL = {
|
||||
'model': 'path.to.model.MyModel',
|
||||
'user_property': 'user_id',
|
||||
'credentials_property': 'credential'
|
||||
}
|
||||
|
||||
Where ``path.to.model`` class is the fully qualified name of a
|
||||
``django.db.model`` class containing a ``django.contrib.auth.models.User``
|
||||
field with the name specified by `user_property` and a
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name
|
||||
specified by `credentials_property`. For the sample configuration given,
|
||||
our model would look like
|
||||
|
||||
.. code-block:: python
|
||||
:caption: models.py
|
||||
:name: storage_model_model
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from oauth2client.contrib.django_util.models import CredentialsField
|
||||
|
||||
class MyModel(models.Model):
|
||||
# ... other fields here ...
|
||||
user = models.OneToOneField(User)
|
||||
credential = CredentialsField()
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
import django.conf
|
||||
from django.core import exceptions
|
||||
from django.core import urlresolvers
|
||||
import httplib2
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client.contrib import dictionary_storage
|
||||
from oauth2client.contrib.django_util import storage
|
||||
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',)
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth'
|
||||
|
||||
|
||||
def _load_client_secrets(filename):
|
||||
"""Loads client secrets from the given filename.
|
||||
|
||||
Args:
|
||||
filename: The name of the file containing the JSON secret key.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item containing the client id, and the second
|
||||
item containing a client secret.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {} is not supported, only the WEB flow '
|
||||
'type is supported.'.format(client_type))
|
||||
return client_info['client_id'], client_info['client_secret']
|
||||
|
||||
|
||||
def _get_oauth2_client_id_and_secret(settings_instance):
|
||||
"""Initializes client id and client secret based on the settings.
|
||||
|
||||
Args:
|
||||
settings_instance: An instance of ``django.conf.settings``.
|
||||
|
||||
Returns:
|
||||
A 2-tuple, the first item is the client id and the second
|
||||
item is the client secret.
|
||||
"""
|
||||
secret_json = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None)
|
||||
if secret_json is not None:
|
||||
return _load_client_secrets(secret_json)
|
||||
else:
|
||||
client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID",
|
||||
None)
|
||||
client_secret = getattr(settings_instance,
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET", None)
|
||||
if client_id is not None and client_secret is not None:
|
||||
return client_id, client_secret
|
||||
else:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or "
|
||||
"both GOOGLE_OAUTH2_CLIENT_ID and "
|
||||
"GOOGLE_OAUTH2_CLIENT_SECRET in settings.py")
|
||||
|
||||
|
||||
def _get_storage_model():
|
||||
"""This configures whether the credentials will be stored in the session
|
||||
or the Django ORM based on the settings. By default, the credentials
|
||||
will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL`
|
||||
is found in the settings. Usually, the ORM storage is used to integrate
|
||||
credentials into an existing Django user system.
|
||||
|
||||
Returns:
|
||||
A tuple containing three strings, or None. If
|
||||
``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple
|
||||
will contain the fully qualifed path of the `django.db.model`,
|
||||
the name of the ``django.contrib.auth.models.User`` field on the
|
||||
model, and the name of the
|
||||
:class:`oauth2client.contrib.django_util.models.CredentialsField`
|
||||
field on the model. If Django ORM storage is not configured,
|
||||
this function returns None.
|
||||
"""
|
||||
storage_model_settings = getattr(django.conf.settings,
|
||||
'GOOGLE_OAUTH2_STORAGE_MODEL', None)
|
||||
if storage_model_settings is not None:
|
||||
return (storage_model_settings['model'],
|
||||
storage_model_settings['user_property'],
|
||||
storage_model_settings['credentials_property'])
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
class OAuth2Settings(object):
|
||||
"""Initializes Django OAuth2 Helper Settings
|
||||
|
||||
This class loads the OAuth2 Settings from the Django settings, and then
|
||||
provides those settings as attributes to the rest of the views and
|
||||
decorators in the module.
|
||||
|
||||
Attributes:
|
||||
scopes: A list of OAuth2 scopes that the decorators and views will use
|
||||
as defaults.
|
||||
request_prefix: The name of the attribute that the decorators use to
|
||||
attach the UserOAuth2 object to the Django request object.
|
||||
client_id: The OAuth2 Client ID.
|
||||
client_secret: The OAuth2 Client Secret.
|
||||
"""
|
||||
|
||||
def __init__(self, settings_instance):
|
||||
self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES',
|
||||
GOOGLE_OAUTH2_DEFAULT_SCOPES)
|
||||
self.request_prefix = getattr(settings_instance,
|
||||
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
|
||||
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
|
||||
self.client_id, self.client_secret = \
|
||||
_get_oauth2_client_id_and_secret(settings_instance)
|
||||
|
||||
if ('django.contrib.sessions.middleware.SessionMiddleware'
|
||||
not in settings_instance.MIDDLEWARE_CLASSES):
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'The Google OAuth2 Helper requires session middleware to '
|
||||
'be installed. Edit your MIDDLEWARE_CLASSES setting'
|
||||
' to include \'django.contrib.sessions.middleware.'
|
||||
'SessionMiddleware\'.')
|
||||
(self.storage_model, self.storage_model_user_property,
|
||||
self.storage_model_credentials_property) = _get_storage_model()
|
||||
|
||||
|
||||
oauth2_settings = OAuth2Settings(django.conf.settings)
|
||||
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
|
||||
|
||||
def get_storage(request):
|
||||
""" Gets a Credentials storage object provided by the Django OAuth2 Helper
|
||||
object.
|
||||
|
||||
Args:
|
||||
request: Reference to the current request object.
|
||||
|
||||
Returns:
|
||||
An :class:`oauth2.client.Storage` object.
|
||||
"""
|
||||
storage_model = oauth2_settings.storage_model
|
||||
user_property = oauth2_settings.storage_model_user_property
|
||||
credentials_property = oauth2_settings.storage_model_credentials_property
|
||||
|
||||
if storage_model:
|
||||
module_name, class_name = storage_model.rsplit('.', 1)
|
||||
module = importlib.import_module(module_name)
|
||||
storage_model_class = getattr(module, class_name)
|
||||
return storage.DjangoORMStorage(storage_model_class,
|
||||
user_property,
|
||||
request.user,
|
||||
credentials_property)
|
||||
else:
|
||||
# use session
|
||||
return dictionary_storage.DictionaryStorage(
|
||||
request.session, key=_CREDENTIALS_KEY)
|
||||
|
||||
|
||||
def _redirect_with_params(url_name, *args, **kwargs):
|
||||
"""Helper method to create a redirect response with URL params.
|
||||
|
||||
This builds a redirect string that converts kwargs into a
|
||||
query string.
|
||||
|
||||
Args:
|
||||
url_name: The name of the url to redirect to.
|
||||
kwargs: the query string param and their values to build.
|
||||
|
||||
Returns:
|
||||
A properly formatted redirect string.
|
||||
"""
|
||||
url = urlresolvers.reverse(url_name, args=args)
|
||||
params = parse.urlencode(kwargs, True)
|
||||
return "{0}?{1}".format(url, params)
|
||||
|
||||
|
||||
def _credentials_from_request(request):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
# ORM storage requires a logged in user
|
||||
if (oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
return get_storage(request).get()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Class to create oauth2 objects on Django request objects containing
|
||||
credentials and helper methods.
|
||||
"""
|
||||
|
||||
def __init__(self, request, scopes=None, return_url=None):
|
||||
"""Initialize the Oauth2 Object.
|
||||
|
||||
Args:
|
||||
request: Django request object.
|
||||
scopes: Scopes desired for this OAuth2 flow.
|
||||
return_url: The url to return to after the OAuth flow is complete,
|
||||
defaults to the request's current URL path.
|
||||
"""
|
||||
self.request = request
|
||||
self.return_url = return_url or request.get_full_path()
|
||||
if scopes:
|
||||
self._scopes = set(oauth2_settings.scopes) | set(scopes)
|
||||
else:
|
||||
self._scopes = set(oauth2_settings.scopes)
|
||||
|
||||
def get_authorize_redirect(self):
|
||||
"""Creates a URl to start the OAuth2 authorization flow."""
|
||||
get_params = {
|
||||
'return_url': self.return_url,
|
||||
'scopes': self._get_scopes()
|
||||
}
|
||||
|
||||
return _redirect_with_params('google_oauth:authorize', **get_params)
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user
|
||||
and required scopes."""
|
||||
credentials = _credentials_from_request(self.request)
|
||||
return (credentials and not credentials.invalid and
|
||||
credentials.has_scopes(self._get_scopes()))
|
||||
|
||||
def _get_scopes(self):
|
||||
"""Returns the scopes associated with this object, kept up to
|
||||
date for incremental auth."""
|
||||
if _credentials_from_request(self.request):
|
||||
return (self._scopes |
|
||||
_credentials_from_request(self.request).scopes)
|
||||
else:
|
||||
return self._scopes
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""Returns the scopes associated with this OAuth2 object."""
|
||||
# make sure previously requested custom scopes are maintained
|
||||
# in future authorizations
|
||||
return self._get_scopes()
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""Gets the authorized credentials for this flow, if they exist."""
|
||||
return _credentials_from_request(self.request)
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
"""Helper method to create an HTTP client authorized with OAuth2
|
||||
credentials."""
|
||||
if self.has_credentials():
|
||||
return self.credentials.authorize(httplib2.Http())
|
||||
return None
|
||||
32
src/oauth2client/contrib/django_util/apps.py
Normal file
32
src/oauth2client/contrib/django_util/apps.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Application Config For Django OAuth2 Helper.
|
||||
|
||||
Django 1.7+ provides an
|
||||
[applications](https://docs.djangoproject.com/en/1.8/ref/applications/)
|
||||
API so that Django projects can introspect on installed applications using a
|
||||
stable API. This module exists to follow that convention.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Django 1.7+ only supports Python 2.7+
|
||||
if sys.hexversion >= 0x02070000: # pragma: NO COVER
|
||||
from django.apps import AppConfig
|
||||
|
||||
class GoogleOAuth2HelperConfig(AppConfig):
|
||||
""" App Config for Django Helper"""
|
||||
name = 'oauth2client.django_util'
|
||||
verbose_name = "Google OAuth2 Django Helper"
|
||||
145
src/oauth2client/contrib/django_util/decorators.py
Normal file
145
src/oauth2client/contrib/django_util/decorators.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Decorators for Django OAuth2 Flow.
|
||||
|
||||
Contains two decorators, ``oauth_required`` and ``oauth_enabled``.
|
||||
|
||||
``oauth_required`` will ensure that a user has an oauth object containing
|
||||
credentials associated with the request, and if not, redirect to the
|
||||
authorization flow.
|
||||
|
||||
``oauth_enabled`` will attach the oauth2 object containing credentials if it
|
||||
exists. If it doesn't, the view will still render, but helper methods will be
|
||||
attached to start the oauth2 flow.
|
||||
"""
|
||||
|
||||
from django import shortcuts
|
||||
import django.conf
|
||||
from six import wraps
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client.contrib import django_util
|
||||
|
||||
|
||||
def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to require OAuth2 credentials for a view.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_required_2
|
||||
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_required
|
||||
|
||||
@oauth_required
|
||||
def requires_default_scopes(request):
|
||||
email = request.credentials.id_token['email']
|
||||
service = build(serviceName='calendar', version='v3',
|
||||
http=request.oauth.http,
|
||||
developerKey=API_KEY)
|
||||
events = service.events().list(
|
||||
calendarId='primary').execute()['items']
|
||||
return HttpResponse(
|
||||
"email: {0}, calendar: {1}".format(email, str(events)))
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate, must have the Django
|
||||
request object as the first argument.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
An OAuth2 Authorize view if credentials are not found or if the
|
||||
credentials are missing the required scopes. Otherwise,
|
||||
the decorated view.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(request, *args, **kwargs):
|
||||
if not (django_util.oauth2_settings.storage_model is None or
|
||||
request.user.is_authenticated()):
|
||||
redirect_str = '{0}?next={1}'.format(
|
||||
django.conf.settings.LOGIN_URL,
|
||||
parse.quote(request.path))
|
||||
return shortcuts.redirect(redirect_str)
|
||||
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
if not user_oauth.has_credentials():
|
||||
return shortcuts.redirect(user_oauth.get_authorize_redirect())
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
|
||||
def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs):
|
||||
""" Decorator to enable OAuth Credentials if authorized, and setup
|
||||
the oauth object on the request object to provide helper functions
|
||||
to start the flow otherwise.
|
||||
|
||||
.. code-block:: python
|
||||
:caption: views.py
|
||||
:name: views_enabled3
|
||||
|
||||
from oauth2client.django_util.decorators import oauth_enabled
|
||||
|
||||
@oauth_enabled
|
||||
def optional_oauth2(request):
|
||||
if request.oauth.has_credentials():
|
||||
# this could be passed into a view
|
||||
# request.oauth.http is also initialized
|
||||
return HttpResponse("User email: {0}".format(
|
||||
request.oauth.credentials.id_token['email'])
|
||||
else:
|
||||
return HttpResponse('Here is an OAuth Authorize link:
|
||||
<a href="{0}">Authorize</a>'.format(
|
||||
request.oauth.get_authorize_redirect()))
|
||||
|
||||
|
||||
Args:
|
||||
decorated_function: View function to decorate.
|
||||
scopes: Scopes to require, will default.
|
||||
decorator_kwargs: Can include ``return_url`` to specify the URL to
|
||||
return to after OAuth2 authorization is complete.
|
||||
|
||||
Returns:
|
||||
The decorated view function.
|
||||
"""
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def enabled_wrapper(request, *args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url',
|
||||
request.get_full_path())
|
||||
user_oauth = django_util.UserOAuth2(request, scopes, return_url)
|
||||
setattr(request, django_util.oauth2_settings.request_prefix,
|
||||
user_oauth)
|
||||
return wrapped_function(request, *args, **kwargs)
|
||||
|
||||
return enabled_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
75
src/oauth2client/contrib/django_util/models.py
Normal file
75
src/oauth2client/contrib/django_util/models.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains classes used for the Django ORM storage."""
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
from django.db import models
|
||||
from django.utils import encoding
|
||||
|
||||
import oauth2client
|
||||
|
||||
|
||||
class CredentialsField(models.Field):
|
||||
"""Django ORM field for storing OAuth2 Credentials."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
kwargs['null'] = True
|
||||
super(CredentialsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'BinaryField'
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
"""Overrides ``models.Field`` method. This converts the value
|
||||
returned from the database to an instance of this class.
|
||||
"""
|
||||
return self.to_python(value)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
bytes (from serialization etc) to an instance of this class"""
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, oauth2client.client.Credentials):
|
||||
return value
|
||||
else:
|
||||
return pickle.loads(base64.b64decode(encoding.smart_bytes(value)))
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""Overrides ``models.Field`` method. This is used to convert
|
||||
the value from an instances of this class to bytes that can be
|
||||
inserted into the database.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return encoding.smart_text(base64.b64encode(pickle.dumps(value)))
|
||||
|
||||
def value_to_string(self, obj):
|
||||
"""Convert the field value from the provided model to a string.
|
||||
|
||||
Used during model serialization.
|
||||
|
||||
Args:
|
||||
obj: db.Model, model object
|
||||
|
||||
Returns:
|
||||
string, the serialized field value
|
||||
"""
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
||||
28
src/oauth2client/contrib/django_util/signals.py
Normal file
28
src/oauth2client/contrib/django_util/signals.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Signals for Google OAuth2 Helper.
|
||||
|
||||
This module contains signals for Google OAuth2 Helper. Currently it only
|
||||
contains one, which fires when an OAuth2 authorization flow has completed.
|
||||
"""
|
||||
|
||||
import django.dispatch
|
||||
|
||||
"""Signal that fires when OAuth2 Flow has completed.
|
||||
It passes the Django request object and the OAuth2 credentials object to the
|
||||
receiver.
|
||||
"""
|
||||
oauth2_authorized = django.dispatch.Signal(
|
||||
providing_args=["request", "credentials"])
|
||||
26
src/oauth2client/contrib/django_util/site.py
Normal file
26
src/oauth2client/contrib/django_util/site.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains Django URL patterns used for OAuth2 flow."""
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from oauth2client.contrib.django_util import views
|
||||
|
||||
urlpatterns = [
|
||||
urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
|
||||
urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
|
||||
]
|
||||
|
||||
urls = (urlpatterns, "google_oauth", "google_oauth")
|
||||
81
src/oauth2client/contrib/django_util/storage.py
Normal file
81
src/oauth2client/contrib/django_util/storage.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains a storage module that stores credentials using the Django ORM."""
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class DjangoORMStorage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from the Django datastore.
|
||||
|
||||
This Storage helper presumes the Credentials
|
||||
have been stored as a CredentialsField
|
||||
on a db model class.
|
||||
"""
|
||||
|
||||
def __init__(self, model_class, key_name, key_value, property_name):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: string, fully qualified name of db.Model model class.
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
key_value: string, key value for the entity that has the
|
||||
credentials.
|
||||
property_name: string, name of the property that is an
|
||||
CredentialsProperty.
|
||||
"""
|
||||
super(DjangoORMStorage, self).__init__()
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve stored credential from the Django ORM.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials retrieved from the Django ORM, associated
|
||||
with the ``model``, ``key_value``->``key_name`` pair used to query
|
||||
for the model, and ``property_name`` identifying the
|
||||
``CredentialsProperty`` field, all of which are defined in the
|
||||
constructor for this Storage object.
|
||||
|
||||
"""
|
||||
query = {self.key_name: self.key_value}
|
||||
entities = self.model_class.objects.filter(**query)
|
||||
if len(entities) > 0:
|
||||
credential = getattr(entities[0], self.property_name)
|
||||
if getattr(credential, 'set_store', None) is not None:
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
else:
|
||||
return None
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the Django datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity, _ = self.model_class.objects.get_or_create(
|
||||
**{self.key_name: self.key_value})
|
||||
|
||||
setattr(entity, self.property_name, credentials)
|
||||
entity.save()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials from the datastore."""
|
||||
query = {self.key_name: self.key_value}
|
||||
self.model_class.objects.filter(**query).delete()
|
||||
190
src/oauth2client/contrib/django_util/views.py
Normal file
190
src/oauth2client/contrib/django_util/views.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains the views used by the OAuth2 flows.
|
||||
|
||||
Their are two views used by the OAuth2 flow, the authorize and the callback
|
||||
view. The authorize view kicks off the three-legged OAuth flow, and the
|
||||
callback view validates the flow and if successful stores the credentials
|
||||
in the configured storage."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
from django import http
|
||||
from django import shortcuts
|
||||
from django.conf import settings
|
||||
from django.core import urlresolvers
|
||||
from django.shortcuts import redirect
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client.contrib import django_util
|
||||
from oauth2client.contrib.django_util import get_storage
|
||||
from oauth2client.contrib.django_util import signals
|
||||
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
|
||||
|
||||
def _make_flow(request, scopes, return_url=None):
|
||||
"""Creates a Web Server Flow
|
||||
|
||||
Args:
|
||||
request: A Django request object.
|
||||
scopes: the request oauth2 scopes.
|
||||
return_url: The URL to return to after the flow is complete. Defaults
|
||||
to the path of the current request.
|
||||
|
||||
Returns:
|
||||
An OAuth2 flow object that has been stored in the session.
|
||||
"""
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
request.session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
flow = client.OAuth2WebServerFlow(
|
||||
client_id=django_util.oauth2_settings.client_id,
|
||||
client_secret=django_util.oauth2_settings.client_secret,
|
||||
scope=scopes,
|
||||
state=state,
|
||||
redirect_uri=request.build_absolute_uri(
|
||||
urlresolvers.reverse("google_oauth:callback")))
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
request.session[flow_key] = pickle.dumps(flow)
|
||||
return flow
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token, request):
|
||||
""" Looks up the flow in session to recover information about requested
|
||||
scopes.
|
||||
|
||||
Args:
|
||||
csrf_token: The token passed in the callback request that should
|
||||
match the one previously generated and stored in the request on the
|
||||
initial authorization view.
|
||||
|
||||
Returns:
|
||||
The OAuth2 Flow object associated with this flow based on the
|
||||
CSRF token.
|
||||
"""
|
||||
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
|
||||
return None if flow_pickle is None else pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
def oauth2_callback(request):
|
||||
""" View that handles the user's return from OAuth2 provider.
|
||||
|
||||
This view verifies the CSRF state and OAuth authorization code, and on
|
||||
success stores the credentials obtained in the storage provider,
|
||||
and redirects to the return_url specified in the authorize view and
|
||||
stored in the session.
|
||||
|
||||
Args:
|
||||
request: Django request.
|
||||
|
||||
Returns:
|
||||
A redirect response back to the return_url.
|
||||
"""
|
||||
if 'error' in request.GET:
|
||||
reason = request.GET.get(
|
||||
'error_description', request.GET.get('error', ''))
|
||||
return http.HttpResponseBadRequest(
|
||||
'Authorization failed {0}'.format(reason))
|
||||
|
||||
try:
|
||||
encoded_state = request.GET['state']
|
||||
code = request.GET['code']
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest(
|
||||
'Request missing state or authorization code')
|
||||
|
||||
try:
|
||||
server_csrf = request.session[_CSRF_KEY]
|
||||
except KeyError:
|
||||
return http.HttpResponseBadRequest(
|
||||
'No existing session for this flow.')
|
||||
|
||||
try:
|
||||
state = json.loads(encoded_state)
|
||||
client_csrf = state['csrf_token']
|
||||
return_url = state['return_url']
|
||||
except (ValueError, KeyError):
|
||||
return http.HttpResponseBadRequest('Invalid state parameter.')
|
||||
|
||||
if client_csrf != server_csrf:
|
||||
return http.HttpResponseBadRequest('Invalid CSRF token.')
|
||||
|
||||
flow = _get_flow_for_token(client_csrf, request)
|
||||
|
||||
if not flow:
|
||||
return http.HttpResponseBadRequest('Missing Oauth2 flow.')
|
||||
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except client.FlowExchangeError as exchange_error:
|
||||
return http.HttpResponseBadRequest(
|
||||
'An error has occurred: {0}'.format(exchange_error))
|
||||
|
||||
get_storage(request).put(credentials)
|
||||
|
||||
signals.oauth2_authorized.send(sender=signals.oauth2_authorized,
|
||||
request=request, credentials=credentials)
|
||||
|
||||
return shortcuts.redirect(return_url)
|
||||
|
||||
|
||||
def oauth2_authorize(request):
|
||||
""" View to start the OAuth2 Authorization flow.
|
||||
|
||||
This view starts the OAuth2 authorization flow. If scopes is passed in
|
||||
as a GET URL parameter, it will authorize those scopes, otherwise the
|
||||
default scopes specified in settings. The return_url can also be
|
||||
specified as a GET parameter, otherwise the referer header will be
|
||||
checked, and if that isn't found it will return to the root path.
|
||||
|
||||
Args:
|
||||
request: The Django request object.
|
||||
|
||||
Returns:
|
||||
A redirect to Google OAuth2 Authorization.
|
||||
"""
|
||||
return_url = request.GET.get('return_url', None)
|
||||
|
||||
# Model storage (but not session storage) requires a logged in user
|
||||
if django_util.oauth2_settings.storage_model:
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('{0}?next={1}'.format(
|
||||
settings.LOGIN_URL, parse.quote(request.get_full_path())))
|
||||
# This checks for the case where we ended up here because of a logged
|
||||
# out user but we had credentials for it in the first place
|
||||
elif get_storage(request).get() is not None:
|
||||
return redirect(return_url)
|
||||
|
||||
scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
|
||||
|
||||
if not return_url:
|
||||
return_url = request.META.get('HTTP_REFERER', '/')
|
||||
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
|
||||
auth_url = flow.step1_get_authorize_url()
|
||||
return shortcuts.redirect(auth_url)
|
||||
556
src/oauth2client/contrib/flask_util.py
Normal file
556
src/oauth2client/contrib/flask_util.py
Normal file
@@ -0,0 +1,556 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for the Flask web framework
|
||||
|
||||
Provides a Flask extension that makes using OAuth2 web server flow easier.
|
||||
The extension includes views that handle the entire auth flow and a
|
||||
``@required`` decorator to automatically ensure that user credentials are
|
||||
available.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure, you'll need a set of OAuth2 web application credentials from the
|
||||
`Google Developer's Console <https://console.developers.google.com/project/_/\
|
||||
apiui/credential>`__.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from oauth2client.contrib.flask_util import UserOAuth2
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SECRET_KEY'] = 'your-secret-key'
|
||||
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json'
|
||||
|
||||
# or, specify the client id and secret separately
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id'
|
||||
app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret'
|
||||
|
||||
oauth2 = UserOAuth2(app)
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Once configured, you can use the :meth:`UserOAuth2.required` decorator to
|
||||
ensure that credentials are available within a view.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3,7,10
|
||||
|
||||
# Note that app.route should be the outermost decorator.
|
||||
@app.route('/needs_credentials')
|
||||
@oauth2.required
|
||||
def example():
|
||||
# http is authorized with the user's credentials and can be used
|
||||
# to make http calls.
|
||||
http = oauth2.http()
|
||||
|
||||
# Or, you can access the credentials directly
|
||||
credentials = oauth2.credentials
|
||||
|
||||
If you want credentials to be optional for a view, you can leave the decorator
|
||||
off and use :meth:`UserOAuth2.has_credentials` to check.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
@app.route('/optional')
|
||||
def optional():
|
||||
if oauth2.has_credentials():
|
||||
return 'Credentials found!'
|
||||
else:
|
||||
return 'No credentials!'
|
||||
|
||||
|
||||
When credentials are available, you can use :attr:`UserOAuth2.email` and
|
||||
:attr:`UserOAuth2.user_id` to access information from the `ID Token
|
||||
<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, if
|
||||
available.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4
|
||||
|
||||
@app.route('/info')
|
||||
@oauth2.required
|
||||
def info():
|
||||
return "Hello, {} ({})".format(oauth2.email, oauth2.user_id)
|
||||
|
||||
|
||||
URLs & Trigging Authorization
|
||||
=============================
|
||||
|
||||
The extension will add two new routes to your application:
|
||||
|
||||
* ``"oauth2.authorize"`` -> ``/oauth2authorize``
|
||||
* ``"oauth2.callback"`` -> ``/oauth2callback``
|
||||
|
||||
When configuring your OAuth2 credentials on the Google Developer's Console, be
|
||||
sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized
|
||||
callback url.
|
||||
|
||||
Typically you don't not need to use these routes directly, just be sure to
|
||||
decorate any views that require credentials with ``@oauth2.required``. If
|
||||
needed, you can trigger authorization at any time by redirecting the user
|
||||
to the URL returned by :meth:`UserOAuth2.authorize_url`.
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
@app.route('/login')
|
||||
def login():
|
||||
return oauth2.authorize_url("/")
|
||||
|
||||
|
||||
Incremental Auth
|
||||
================
|
||||
|
||||
This extension also supports `Incremental Auth <https://developers.google.com\
|
||||
/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. To enable it,
|
||||
configure the extension with ``include_granted_scopes``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
oauth2 = UserOAuth2(app, include_granted_scopes=True)
|
||||
|
||||
Then specify any additional scopes needed on the decorator, for example:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 2,7
|
||||
|
||||
@app.route('/drive')
|
||||
@oauth2.required(scopes=["https://www.googleapis.com/auth/drive"])
|
||||
def requires_drive():
|
||||
...
|
||||
|
||||
@app.route('/calendar')
|
||||
@oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"])
|
||||
def requires_calendar():
|
||||
...
|
||||
|
||||
The decorator will ensure that the the user has authorized all specified scopes
|
||||
before allowing them to access the view, and will also ensure that credentials
|
||||
do not lose any previously authorized scopes.
|
||||
|
||||
|
||||
Storage
|
||||
=======
|
||||
|
||||
By default, the extension uses a Flask session-based storage solution. This
|
||||
means that credentials are only available for the duration of a session. It
|
||||
also means that with Flask's default configuration, the credentials will be
|
||||
visible in the session cookie. It's highly recommended to use database-backed
|
||||
session and to use https whenever handling user credentials.
|
||||
|
||||
If you need the credentials to be available longer than a user session or
|
||||
available outside of a request context, you will need to implement your own
|
||||
:class:`oauth2client.Storage`.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from flask import Blueprint
|
||||
from flask import _app_ctx_stack
|
||||
from flask import current_app
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
except ImportError: # pragma: NO COVER
|
||||
raise ImportError('The flask utilities require flask 0.9 or newer.')
|
||||
|
||||
import httplib2
|
||||
import six.moves.http_client as httplib
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client.contrib import dictionary_storage
|
||||
|
||||
|
||||
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
|
||||
|
||||
_DEFAULT_SCOPES = ('email',)
|
||||
_CREDENTIALS_KEY = 'google_oauth2_credentials'
|
||||
_FLOW_KEY = 'google_oauth2_flow_{0}'
|
||||
_CSRF_KEY = 'google_oauth2_csrf_token'
|
||||
|
||||
|
||||
def _get_flow_for_token(csrf_token):
|
||||
"""Retrieves the flow instance associated with a given CSRF token from
|
||||
the Flask session."""
|
||||
flow_pickle = session.pop(
|
||||
_FLOW_KEY.format(csrf_token), None)
|
||||
|
||||
if flow_pickle is None:
|
||||
return None
|
||||
else:
|
||||
return pickle.loads(flow_pickle)
|
||||
|
||||
|
||||
class UserOAuth2(object):
|
||||
"""Flask extension for making OAuth 2.0 easier.
|
||||
|
||||
Configuration values:
|
||||
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json
|
||||
file, obtained from the credentials screen in the Google Developers
|
||||
console.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This
|
||||
is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not
|
||||
specified.
|
||||
* ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client
|
||||
secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE``
|
||||
is not specified.
|
||||
|
||||
If app is specified, all arguments will be passed along to init_app.
|
||||
|
||||
If no app is specified, then you should call init_app in your application
|
||||
factory to finish initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, app=None, *args, **kwargs):
|
||||
self.app = app
|
||||
if app is not None:
|
||||
self.init_app(app, *args, **kwargs)
|
||||
|
||||
def init_app(self, app, scopes=None, client_secrets_file=None,
|
||||
client_id=None, client_secret=None, authorize_callback=None,
|
||||
storage=None, **kwargs):
|
||||
"""Initialize this extension for the given app.
|
||||
|
||||
Arguments:
|
||||
app: A Flask application.
|
||||
scopes: Optional list of scopes to authorize.
|
||||
client_secrets_file: Path to a file containing client secrets. You
|
||||
can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config
|
||||
value.
|
||||
client_id: If not specifying a client secrets file, specify the
|
||||
OAuth2 client id. You can also specify the
|
||||
GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a
|
||||
client secret.
|
||||
client_secret: The OAuth2 client secret. You can also specify the
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET config value.
|
||||
authorize_callback: A function that is executed after successful
|
||||
user authorization.
|
||||
storage: A oauth2client.client.Storage subclass for storing the
|
||||
credentials. By default, this is a Flask session based storage.
|
||||
kwargs: Any additional args are passed along to the Flow
|
||||
constructor.
|
||||
"""
|
||||
self.app = app
|
||||
self.authorize_callback = authorize_callback
|
||||
self.flow_kwargs = kwargs
|
||||
|
||||
if storage is None:
|
||||
storage = dictionary_storage.DictionaryStorage(
|
||||
session, key=_CREDENTIALS_KEY)
|
||||
self.storage = storage
|
||||
|
||||
if scopes is None:
|
||||
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
|
||||
self.scopes = scopes
|
||||
|
||||
self._load_config(client_secrets_file, client_id, client_secret)
|
||||
|
||||
app.register_blueprint(self._create_blueprint())
|
||||
|
||||
def _load_config(self, client_secrets_file, client_id, client_secret):
|
||||
"""Loads oauth2 configuration in order of priority.
|
||||
|
||||
Priority:
|
||||
1. Config passed to the constructor or init_app.
|
||||
2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app
|
||||
config.
|
||||
3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and
|
||||
GOOGLE_OAUTH2_CLIENT_SECRET app config.
|
||||
|
||||
Raises:
|
||||
ValueError if no config could be found.
|
||||
"""
|
||||
if client_id and client_secret:
|
||||
self.client_id, self.client_secret = client_id, client_secret
|
||||
return
|
||||
|
||||
if client_secrets_file:
|
||||
self._load_client_secrets(client_secrets_file)
|
||||
return
|
||||
|
||||
if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config:
|
||||
self._load_client_secrets(
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'])
|
||||
return
|
||||
|
||||
try:
|
||||
self.client_id, self.client_secret = (
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_ID'],
|
||||
self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET'])
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'OAuth2 configuration could not be found. Either specify the '
|
||||
'client_secrets_file or client_id and client_secret or set '
|
||||
'the app configuration variables '
|
||||
'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
|
||||
'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
|
||||
|
||||
def _load_client_secrets(self, filename):
|
||||
"""Loads client secrets from the given filename."""
|
||||
client_type, client_info = clientsecrets.loadfile(filename)
|
||||
if client_type != clientsecrets.TYPE_WEB:
|
||||
raise ValueError(
|
||||
'The flow specified in {0} is not supported.'.format(
|
||||
client_type))
|
||||
|
||||
self.client_id = client_info['client_id']
|
||||
self.client_secret = client_info['client_secret']
|
||||
|
||||
def _make_flow(self, return_url=None, **kwargs):
|
||||
"""Creates a Web Server Flow"""
|
||||
# Generate a CSRF token to prevent malicious requests.
|
||||
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
||||
|
||||
session[_CSRF_KEY] = csrf_token
|
||||
|
||||
state = json.dumps({
|
||||
'csrf_token': csrf_token,
|
||||
'return_url': return_url
|
||||
})
|
||||
|
||||
kw = self.flow_kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
|
||||
extra_scopes = kw.pop('scopes', [])
|
||||
scopes = set(self.scopes).union(set(extra_scopes))
|
||||
|
||||
flow = client.OAuth2WebServerFlow(
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
scope=scopes,
|
||||
state=state,
|
||||
redirect_uri=url_for('oauth2.callback', _external=True),
|
||||
**kw)
|
||||
|
||||
flow_key = _FLOW_KEY.format(csrf_token)
|
||||
session[flow_key] = pickle.dumps(flow)
|
||||
|
||||
return flow
|
||||
|
||||
def _create_blueprint(self):
|
||||
bp = Blueprint('oauth2', __name__)
|
||||
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
|
||||
bp.add_url_rule('/oauth2callback', 'callback', self.callback_view)
|
||||
|
||||
return bp
|
||||
|
||||
def authorize_view(self):
|
||||
"""Flask view that starts the authorization flow.
|
||||
|
||||
Starts flow by redirecting the user to the OAuth2 provider.
|
||||
"""
|
||||
args = request.args.to_dict()
|
||||
|
||||
# Scopes will be passed as mutliple args, and to_dict() will only
|
||||
# return one. So, we use getlist() to get all of the scopes.
|
||||
args['scopes'] = request.args.getlist('scopes')
|
||||
|
||||
return_url = args.pop('return_url', None)
|
||||
if return_url is None:
|
||||
return_url = request.referrer or '/'
|
||||
|
||||
flow = self._make_flow(return_url=return_url, **args)
|
||||
auth_url = flow.step1_get_authorize_url()
|
||||
|
||||
return redirect(auth_url)
|
||||
|
||||
def callback_view(self):
|
||||
"""Flask view that handles the user's return from OAuth2 provider.
|
||||
|
||||
On return, exchanges the authorization code for credentials and stores
|
||||
the credentials.
|
||||
"""
|
||||
if 'error' in request.args:
|
||||
reason = request.args.get(
|
||||
'error_description', request.args.get('error', ''))
|
||||
return ('Authorization failed: {0}'.format(reason),
|
||||
httplib.BAD_REQUEST)
|
||||
|
||||
try:
|
||||
encoded_state = request.args['state']
|
||||
server_csrf = session[_CSRF_KEY]
|
||||
code = request.args['code']
|
||||
except KeyError:
|
||||
return 'Invalid request', httplib.BAD_REQUEST
|
||||
|
||||
try:
|
||||
state = json.loads(encoded_state)
|
||||
client_csrf = state['csrf_token']
|
||||
return_url = state['return_url']
|
||||
except (ValueError, KeyError):
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
if client_csrf != server_csrf:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
flow = _get_flow_for_token(server_csrf)
|
||||
|
||||
if flow is None:
|
||||
return 'Invalid request state', httplib.BAD_REQUEST
|
||||
|
||||
# Exchange the auth code for credentials.
|
||||
try:
|
||||
credentials = flow.step2_exchange(code)
|
||||
except client.FlowExchangeError as exchange_error:
|
||||
current_app.logger.exception(exchange_error)
|
||||
content = 'An error occurred: {0}'.format(exchange_error)
|
||||
return content, httplib.BAD_REQUEST
|
||||
|
||||
# Save the credentials to the storage.
|
||||
self.storage.put(credentials)
|
||||
|
||||
if self.authorize_callback:
|
||||
self.authorize_callback(credentials)
|
||||
|
||||
return redirect(return_url)
|
||||
|
||||
@property
|
||||
def credentials(self):
|
||||
"""The credentials for the current user or None if unavailable."""
|
||||
ctx = _app_ctx_stack.top
|
||||
|
||||
if not hasattr(ctx, _CREDENTIALS_KEY):
|
||||
ctx.google_oauth2_credentials = self.storage.get()
|
||||
|
||||
return ctx.google_oauth2_credentials
|
||||
|
||||
def has_credentials(self):
|
||||
"""Returns True if there are valid credentials for the current user."""
|
||||
if not self.credentials:
|
||||
return False
|
||||
# Is the access token expired? If so, do we have an refresh token?
|
||||
elif (self.credentials.access_token_expired and
|
||||
not self.credentials.refresh_token):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Returns the user's email address or None if there are no credentials.
|
||||
|
||||
The email address is provided by the current credentials' id_token.
|
||||
This should not be used as unique identifier as the user can change
|
||||
their email. If you need a unique identifier, use user_id.
|
||||
"""
|
||||
if not self.credentials:
|
||||
return None
|
||||
try:
|
||||
return self.credentials.id_token['email']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
"""Returns the a unique identifier for the user
|
||||
|
||||
Returns None if there are no credentials.
|
||||
|
||||
The id is provided by the current credentials' id_token.
|
||||
"""
|
||||
if not self.credentials:
|
||||
return None
|
||||
try:
|
||||
return self.credentials.id_token['sub']
|
||||
except KeyError:
|
||||
current_app.logger.error(
|
||||
'Invalid id_token {0}'.format(self.credentials.id_token))
|
||||
|
||||
def authorize_url(self, return_url, **kwargs):
|
||||
"""Creates a URL that can be used to start the authorization flow.
|
||||
|
||||
When the user is directed to the URL, the authorization flow will
|
||||
begin. Once complete, the user will be redirected to the specified
|
||||
return URL.
|
||||
|
||||
Any kwargs are passed into the flow constructor.
|
||||
"""
|
||||
return url_for('oauth2.authorize', return_url=return_url, **kwargs)
|
||||
|
||||
def required(self, decorated_function=None, scopes=None,
|
||||
**decorator_kwargs):
|
||||
"""Decorator to require OAuth2 credentials for a view.
|
||||
|
||||
If credentials are not available for the current user, then they will
|
||||
be redirected to the authorization flow. Once complete, the user will
|
||||
be redirected back to the original page.
|
||||
"""
|
||||
|
||||
def curry_wrapper(wrapped_function):
|
||||
@wraps(wrapped_function)
|
||||
def required_wrapper(*args, **kwargs):
|
||||
return_url = decorator_kwargs.pop('return_url', request.url)
|
||||
|
||||
requested_scopes = set(self.scopes)
|
||||
if scopes is not None:
|
||||
requested_scopes |= set(scopes)
|
||||
if self.has_credentials():
|
||||
requested_scopes |= self.credentials.scopes
|
||||
|
||||
requested_scopes = list(requested_scopes)
|
||||
|
||||
# Does the user have credentials and does the credentials have
|
||||
# all of the needed scopes?
|
||||
if (self.has_credentials() and
|
||||
self.credentials.has_scopes(requested_scopes)):
|
||||
return wrapped_function(*args, **kwargs)
|
||||
# Otherwise, redirect to authorization
|
||||
else:
|
||||
auth_url = self.authorize_url(
|
||||
return_url,
|
||||
scopes=requested_scopes,
|
||||
**decorator_kwargs)
|
||||
|
||||
return redirect(auth_url)
|
||||
|
||||
return required_wrapper
|
||||
|
||||
if decorated_function:
|
||||
return curry_wrapper(decorated_function)
|
||||
else:
|
||||
return curry_wrapper
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Can only be called if there are valid credentials for the user, such
|
||||
as inside of a view that is decorated with @required.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
|
||||
Raises:
|
||||
ValueError if no credentials are available.
|
||||
"""
|
||||
if not self.credentials:
|
||||
raise ValueError('No credentials available.')
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
||||
162
src/oauth2client/contrib/gce.py
Normal file
162
src/oauth2client/contrib/gce.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google Compute Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import httplib2
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client.contrib import _metadata
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SCOPES_WARNING = """\
|
||||
You have requested explicit scopes to be used with a GCE service account.
|
||||
Using this argument will have no effect on the actual scopes for tokens
|
||||
requested. These scopes are set at VM instance creation time and
|
||||
can't be overridden in the request.
|
||||
"""
|
||||
|
||||
|
||||
class AppAssertionCredentials(client.AssertionCredentials):
|
||||
"""Credentials object for Compute Engine Assertion Grants
|
||||
|
||||
This object will allow a Compute Engine instance to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under an account assigned to
|
||||
the Compute Engine instance itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
Note that :attr:`service_account_email` and :attr:`scopes`
|
||||
will both return None until the credentials have been refreshed.
|
||||
To check whether credentials have previously been refreshed use
|
||||
:attr:`invalid`.
|
||||
"""
|
||||
|
||||
def __init__(self, email=None, *args, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
email: an email that specifies the service account to use.
|
||||
Only necessary if using custom service accounts
|
||||
(see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount).
|
||||
"""
|
||||
if 'scopes' in kwargs:
|
||||
warnings.warn(_SCOPES_WARNING)
|
||||
kwargs['scopes'] = None
|
||||
|
||||
# Assertion type is no longer used, but still in the
|
||||
# parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None, *args, **kwargs)
|
||||
|
||||
self.service_account_email = email
|
||||
self.scopes = None
|
||||
self.invalid = True
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def retrieve_scopes(self, http):
|
||||
"""Retrieves the canonical list of scopes for this access token.
|
||||
|
||||
Overrides client.Credentials.retrieve_scopes. Fetches scopes info
|
||||
from the metadata server.
|
||||
|
||||
Args:
|
||||
http: httplib2.Http, an http object to be used to make the refresh
|
||||
request.
|
||||
|
||||
Returns:
|
||||
A set of strings containing the canonical list of scopes.
|
||||
"""
|
||||
self._retrieve_info(http.request)
|
||||
return self.scopes
|
||||
|
||||
def _retrieve_info(self, http_request):
|
||||
"""Validates invalid service accounts by retrieving service account info.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make the
|
||||
request to the metadata server
|
||||
"""
|
||||
if self.invalid:
|
||||
info = _metadata.get_service_account_info(
|
||||
http_request,
|
||||
service_account=self.service_account_email or 'default')
|
||||
self.invalid = False
|
||||
self.service_account_email = info['email']
|
||||
self.scopes = info['scopes']
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Skip all the storage hoops and just refresh using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method
|
||||
signature of httplib2.Http.request, used to make
|
||||
the refresh request.
|
||||
|
||||
Raises:
|
||||
HttpAccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
self._retrieve_info(http_request)
|
||||
self.access_token, self.token_expiry = _metadata.get_token(
|
||||
http_request, service_account=self.service_account_email)
|
||||
except httplib2.HttpLib2Error as e:
|
||||
raise client.HttpAccessTokenRefreshError(str(e))
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return False
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
This method is provided to support a common interface, but
|
||||
the actual key used for a Google Compute Engine service account
|
||||
is not available, so it can't be used to sign content.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Raises:
|
||||
NotImplementedError, always.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Compute Engine service accounts cannot sign blobs')
|
||||
98
src/oauth2client/contrib/keyring_storage.py
Normal file
98
src/oauth2client/contrib/keyring_storage.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A keyring based Storage.
|
||||
|
||||
A Storage for Credentials that uses the keyring module.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import keyring
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from the keyring.
|
||||
|
||||
To use this module you must have the keyring module installed. See
|
||||
<http://pypi.python.org/pypi/keyring/>. This is an optional module and is
|
||||
not installed with oauth2client by default because it does not work on all
|
||||
the platforms that oauth2client supports, such as Google App Engine.
|
||||
|
||||
The keyring module <http://pypi.python.org/pypi/keyring/> is a
|
||||
cross-platform library for access the keyring capabilities of the local
|
||||
system. The user will be prompted for their keyring password when this
|
||||
module is used, and the manner in which the user is prompted will vary per
|
||||
platform.
|
||||
|
||||
Usage::
|
||||
|
||||
from oauth2client import keyring_storage
|
||||
|
||||
s = keyring_storage.Storage('name_of_application', 'user1')
|
||||
credentials = s.get()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, service_name, user_name):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
service_name: string, The name of the service under which the
|
||||
credentials are stored.
|
||||
user_name: string, The name of the user to store credentials for.
|
||||
"""
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._service_name = service_name
|
||||
self._user_name = user_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
content = keyring.get_password(self._service_name, self._user_name)
|
||||
|
||||
if content is not None:
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name,
|
||||
credentials.to_json())
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name, '')
|
||||
234
src/oauth2client/contrib/locked_file.py
Normal file
234
src/oauth2client/contrib/locked_file.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Locked file interface that should work on Unix and Windows pythons.
|
||||
|
||||
This module first tries to use fcntl locking to ensure serialized access
|
||||
to a file, then falls back on a lock file if that is unavialable.
|
||||
|
||||
Usage::
|
||||
|
||||
f = LockedFile('filename', 'r+b', 'rb')
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
print('Acquired filename with r+b mode')
|
||||
f.file_handle().write('locked data')
|
||||
else:
|
||||
print('Acquired filename with rb mode')
|
||||
f.unlock_and_close()
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
__author__ = 'cache@google.com (David T McWherter)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CredentialsFileSymbolicLinkError(Exception):
|
||||
"""Credentials files must not be symbolic links."""
|
||||
|
||||
|
||||
class AlreadyLockedException(Exception):
|
||||
"""Trying to lock a file that has already been locked by the LockedFile."""
|
||||
pass
|
||||
|
||||
|
||||
def validate_file(filename):
|
||||
if os.path.islink(filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: {0} is a symbolic link.'.format(filename))
|
||||
|
||||
|
||||
class _Opener(object):
|
||||
"""Base class for different locking primitives."""
|
||||
|
||||
def __init__(self, filename, mode, fallback_mode):
|
||||
"""Create an Opener.
|
||||
|
||||
Args:
|
||||
filename: string, The pathname of the file.
|
||||
mode: string, The preferred mode to access the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
"""
|
||||
self._locked = False
|
||||
self._filename = filename
|
||||
self._mode = mode
|
||||
self._fallback_mode = fallback_mode
|
||||
self._fh = None
|
||||
self._lock_fd = None
|
||||
|
||||
def is_locked(self):
|
||||
"""Was the file locked."""
|
||||
return self._locked
|
||||
|
||||
def file_handle(self):
|
||||
"""The file handle to the file. Valid only after opened."""
|
||||
return self._fh
|
||||
|
||||
def filename(self):
|
||||
"""The filename that is being locked."""
|
||||
return self._filename
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
"""
|
||||
pass
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close the file."""
|
||||
pass
|
||||
|
||||
|
||||
class _PosixOpener(_Opener):
|
||||
"""Lock files using Posix advisory lock files."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Tries to create a .lock file next to the file we're trying to open.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
self._locked = False
|
||||
|
||||
validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
self._lock_fd = os.open(lock_filename,
|
||||
os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||
self._locked = True
|
||||
break
|
||||
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
if (time.time() - start_time) >= timeout:
|
||||
logger.warn('Could not acquire lock %s in %s seconds',
|
||||
lock_filename, timeout)
|
||||
# Close the file and open in fallback_mode.
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock a file by removing the .lock file, and close the handle."""
|
||||
if self._locked:
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
os.close(self._lock_fd)
|
||||
os.unlink(lock_filename)
|
||||
self._locked = False
|
||||
self._lock_fd = None
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
|
||||
def _posix_lockfile(self, filename):
|
||||
"""The name of the lock file to use for posix locking."""
|
||||
return '{0}.lock'.format(filename)
|
||||
|
||||
|
||||
class LockedFile(object):
|
||||
"""Represent a file that has exclusive access."""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
|
||||
"""Construct a LockedFile.
|
||||
|
||||
Args:
|
||||
filename: string, The path of the file to open.
|
||||
mode: string, The mode to try to open the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
use_native_locking: bool, Whether or not fcntl/win32 locking is
|
||||
used.
|
||||
"""
|
||||
opener = None
|
||||
if not opener and use_native_locking:
|
||||
try:
|
||||
from oauth2client.contrib._win32_opener import _Win32Opener
|
||||
opener = _Win32Opener(filename, mode, fallback_mode)
|
||||
except ImportError:
|
||||
try:
|
||||
from oauth2client.contrib._fcntl_opener import _FcntlOpener
|
||||
opener = _FcntlOpener(filename, mode, fallback_mode)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not opener:
|
||||
opener = _PosixOpener(filename, mode, fallback_mode)
|
||||
|
||||
self._opener = opener
|
||||
|
||||
def filename(self):
|
||||
"""Return the filename we were constructed with."""
|
||||
return self._opener._filename
|
||||
|
||||
def file_handle(self):
|
||||
"""Return the file_handle to the opened file."""
|
||||
return self._opener.file_handle()
|
||||
|
||||
def is_locked(self):
|
||||
"""Return whether we successfully locked the file."""
|
||||
return self._opener.is_locked()
|
||||
|
||||
def open_and_lock(self, timeout=0, delay=0.05):
|
||||
"""Open the file, trying to lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, The number of seconds to try to acquire the lock.
|
||||
delay: float, The number of seconds to wait between retry attempts.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
"""
|
||||
self._opener.open_and_lock(timeout, delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close a file."""
|
||||
self._opener.unlock_and_close()
|
||||
355
src/oauth2client/contrib/multiprocess_file_storage.py
Normal file
355
src/oauth2client/contrib/multiprocess_file_storage.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Multiprocess file credential storage.
|
||||
|
||||
This module provides file-based storage that supports multiple credentials and
|
||||
cross-thread and process access.
|
||||
|
||||
This module supersedes the functionality previously found in `multistore_file`.
|
||||
|
||||
This module provides :class:`MultiprocessFileStorage` which:
|
||||
* Is tied to a single credential via a user-specified key. This key can be
|
||||
used to distinguish between multiple users, client ids, and/or scopes.
|
||||
* Can be safely accessed and refreshed across threads and processes.
|
||||
|
||||
Process & thread safety guarantees the following behavior:
|
||||
* If one thread or process refreshes a credential, subsequent refreshes
|
||||
from other processes will re-fetch the credentials from the file instead
|
||||
of performing an http request.
|
||||
* If two processes or threads attempt to refresh concurrently, only one
|
||||
will be able to acquire the lock and refresh, with the deadlock caveat
|
||||
below.
|
||||
* The interprocess lock will not deadlock, instead, the if a process can
|
||||
not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE``
|
||||
it will allow refreshing the credential but will not write the updated
|
||||
credential to disk, This logic happens during every lock cycle - if the
|
||||
credentials are refreshed again it will retry locking and writing as
|
||||
normal.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Before using the storage, you need to decide how you want to key the
|
||||
credentials. A few common strategies include:
|
||||
|
||||
* If you're storing credentials for multiple users in a single file, use
|
||||
a unique identifier for each user as the key.
|
||||
* If you're storing credentials for multiple client IDs in a single file,
|
||||
use the client ID as the key.
|
||||
* If you're storing multiple credentials for one user, use the scopes as
|
||||
the key.
|
||||
* If you have a complicated setup, use a compound key. For example, you
|
||||
can use a combination of the client ID and scopes as the key.
|
||||
|
||||
Create an instance of :class:`MultiprocessFileStorage` for each credential you
|
||||
want to store, for example::
|
||||
|
||||
filename = 'credentials'
|
||||
key = '{}-{}'.format(client_id, user_id)
|
||||
storage = MultiprocessFileStorage(filename, key)
|
||||
|
||||
To store the credentials::
|
||||
|
||||
storage.put(credentials)
|
||||
|
||||
If you're going to continue to use the credentials after storing them, be sure
|
||||
to call :func:`set_store`::
|
||||
|
||||
credentials.set_store(storage)
|
||||
|
||||
To retrieve the credentials::
|
||||
|
||||
storage.get(credentials)
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
import fasteners
|
||||
from six import iteritems
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
#: The maximum amount of time, in seconds, to wait when acquire the
|
||||
#: interprocess lock before falling back to read-only mode.
|
||||
INTERPROCESS_LOCK_DEADLINE = 1
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_backends = {}
|
||||
_backends_lock = threading.Lock()
|
||||
|
||||
|
||||
def _create_file_if_needed(filename):
|
||||
"""Creates the an empty file if it does not already exist.
|
||||
|
||||
Returns:
|
||||
True if the file was created, False otherwise.
|
||||
"""
|
||||
if os.path.exists(filename):
|
||||
return False
|
||||
else:
|
||||
# Equivalent to "touch".
|
||||
open(filename, 'a+b').close()
|
||||
logger.info('Credential file {0} created'.format(filename))
|
||||
return True
|
||||
|
||||
|
||||
def _load_credentials_file(credentials_file):
|
||||
"""Load credentials from the given file handle.
|
||||
|
||||
The file is expected to be in this format:
|
||||
|
||||
{
|
||||
"file_version": 2,
|
||||
"credentials": {
|
||||
"key": "base64 encoded json representation of credentials."
|
||||
}
|
||||
}
|
||||
|
||||
This function will warn and return empty credentials instead of raising
|
||||
exceptions.
|
||||
|
||||
Args:
|
||||
credentials_file: An open file handle.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping user-defined keys to an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
try:
|
||||
credentials_file.seek(0)
|
||||
data = json.load(credentials_file)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
'Credentials file could not be loaded, will ignore and '
|
||||
'overwrite.')
|
||||
return {}
|
||||
|
||||
if data.get('file_version') != 2:
|
||||
logger.warning(
|
||||
'Credentials file is not version 2, will ignore and '
|
||||
'overwrite.')
|
||||
return {}
|
||||
|
||||
credentials = {}
|
||||
|
||||
for key, encoded_credential in iteritems(data.get('credentials', {})):
|
||||
try:
|
||||
credential_json = base64.b64decode(encoded_credential)
|
||||
credential = client.Credentials.new_from_json(credential_json)
|
||||
credentials[key] = credential
|
||||
except:
|
||||
logger.warning(
|
||||
'Invalid credential {0} in file, ignoring.'.format(key))
|
||||
|
||||
return credentials
|
||||
|
||||
|
||||
def _write_credentials_file(credentials_file, credentials):
|
||||
"""Writes credentials to a file.
|
||||
|
||||
Refer to :func:`_load_credentials_file` for the format.
|
||||
|
||||
Args:
|
||||
credentials_file: An open file handle, must be read/write.
|
||||
credentials: A dictionary mapping user-defined keys to an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
data = {'file_version': 2, 'credentials': {}}
|
||||
|
||||
for key, credential in iteritems(credentials):
|
||||
credential_json = credential.to_json()
|
||||
encoded_credential = _helpers._from_bytes(base64.b64encode(
|
||||
_helpers._to_bytes(credential_json)))
|
||||
data['credentials'][key] = encoded_credential
|
||||
|
||||
credentials_file.seek(0)
|
||||
json.dump(data, credentials_file)
|
||||
credentials_file.truncate()
|
||||
|
||||
|
||||
class _MultiprocessStorageBackend(object):
|
||||
"""Thread-local backend for multiprocess storage.
|
||||
|
||||
Each process has only one instance of this backend per file. All threads
|
||||
share a single instance of this backend. This ensures that all threads
|
||||
use the same thread lock and process lock when accessing the file.
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
self._file = None
|
||||
self._filename = filename
|
||||
self._process_lock = fasteners.InterProcessLock(
|
||||
'{0}.lock'.format(filename))
|
||||
self._thread_lock = threading.Lock()
|
||||
self._read_only = False
|
||||
self._credentials = {}
|
||||
|
||||
def _load_credentials(self):
|
||||
"""(Re-)loads the credentials from the file."""
|
||||
if not self._file:
|
||||
return
|
||||
|
||||
loaded_credentials = _load_credentials_file(self._file)
|
||||
self._credentials.update(loaded_credentials)
|
||||
|
||||
logger.debug('Read credential file')
|
||||
|
||||
def _write_credentials(self):
|
||||
if self._read_only:
|
||||
logger.debug('In read-only mode, not writing credentials.')
|
||||
return
|
||||
|
||||
_write_credentials_file(self._file, self._credentials)
|
||||
logger.debug('Wrote credential file {0}.'.format(self._filename))
|
||||
|
||||
def acquire_lock(self):
|
||||
self._thread_lock.acquire()
|
||||
locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE)
|
||||
|
||||
if locked:
|
||||
_create_file_if_needed(self._filename)
|
||||
self._file = open(self._filename, 'r+')
|
||||
self._read_only = False
|
||||
|
||||
else:
|
||||
logger.warn(
|
||||
'Failed to obtain interprocess lock for credentials. '
|
||||
'If a credential is being refreshed, other processes may '
|
||||
'not see the updated access token and refresh as well.')
|
||||
if os.path.exists(self._filename):
|
||||
self._file = open(self._filename, 'r')
|
||||
else:
|
||||
self._file = None
|
||||
self._read_only = True
|
||||
|
||||
self._load_credentials()
|
||||
|
||||
def release_lock(self):
|
||||
if self._file is not None:
|
||||
self._file.close()
|
||||
self._file = None
|
||||
|
||||
if not self._read_only:
|
||||
self._process_lock.release()
|
||||
|
||||
self._thread_lock.release()
|
||||
|
||||
def _refresh_predicate(self, credentials):
|
||||
if credentials is None:
|
||||
return True
|
||||
elif credentials.invalid:
|
||||
return True
|
||||
elif credentials.access_token_expired:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def locked_get(self, key):
|
||||
# Check if the credential is already in memory.
|
||||
credentials = self._credentials.get(key, None)
|
||||
|
||||
# Use the refresh predicate to determine if the entire store should be
|
||||
# reloaded. This basically checks if the credentials are invalid
|
||||
# or expired. This covers the situation where another process has
|
||||
# refreshed the credentials and this process doesn't know about it yet.
|
||||
# In that case, this process won't needlessly refresh the credentials.
|
||||
if self._refresh_predicate(credentials):
|
||||
self._load_credentials()
|
||||
credentials = self._credentials.get(key, None)
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, key, credentials):
|
||||
self._load_credentials()
|
||||
self._credentials[key] = credentials
|
||||
self._write_credentials()
|
||||
|
||||
def locked_delete(self, key):
|
||||
self._load_credentials()
|
||||
self._credentials.pop(key, None)
|
||||
self._write_credentials()
|
||||
|
||||
|
||||
def _get_backend(filename):
|
||||
"""A helper method to get or create a backend with thread locking.
|
||||
|
||||
This ensures that only one backend is used per-file per-process, so that
|
||||
thread and process locks are appropriately shared.
|
||||
|
||||
Args:
|
||||
filename: The full path to the credential storage file.
|
||||
|
||||
Returns:
|
||||
An instance of :class:`_MultiprocessStorageBackend`.
|
||||
"""
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
with _backends_lock:
|
||||
if filename not in _backends:
|
||||
_backends[filename] = _MultiprocessStorageBackend(filename)
|
||||
return _backends[filename]
|
||||
|
||||
|
||||
class MultiprocessFileStorage(client.Storage):
|
||||
"""Multiprocess file credential storage.
|
||||
|
||||
Args:
|
||||
filename: The path to the file where credentials will be stored.
|
||||
key: An arbitrary string used to uniquely identify this set of
|
||||
credentials. For example, you may use the user's ID as the key or
|
||||
a combination of the client ID and user ID.
|
||||
"""
|
||||
def __init__(self, filename, key):
|
||||
self._key = key
|
||||
self._backend = _get_backend(filename)
|
||||
|
||||
def acquire_lock(self):
|
||||
self._backend.acquire_lock()
|
||||
|
||||
def release_lock(self):
|
||||
self._backend.release_lock()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieves the current credentials from the store.
|
||||
|
||||
Returns:
|
||||
An instance of :class:`oauth2client.client.Credentials` or `None`.
|
||||
"""
|
||||
credential = self._backend.locked_get(self._key)
|
||||
|
||||
if credential is not None:
|
||||
credential.set_store(self)
|
||||
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Writes the given credentials to the store.
|
||||
|
||||
Args:
|
||||
credentials: an instance of
|
||||
:class:`oauth2client.client.Credentials`.
|
||||
"""
|
||||
return self._backend.locked_put(self._key, credentials)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Deletes the current credentials from the store."""
|
||||
return self._backend.locked_delete(self._key)
|
||||
505
src/oauth2client/contrib/multistore_file.py
Normal file
505
src/oauth2client/contrib/multistore_file.py
Normal file
@@ -0,0 +1,505 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Multi-credential file store with lock support.
|
||||
|
||||
This module implements a JSON credential store where multiple
|
||||
credentials can be stored in one file. That file supports locking
|
||||
both in a single process and across processes.
|
||||
|
||||
The credential themselves are keyed off of:
|
||||
|
||||
* client_id
|
||||
* user_agent
|
||||
* scope
|
||||
|
||||
The format of the stored data is like so::
|
||||
|
||||
{
|
||||
'file_version': 1,
|
||||
'data': [
|
||||
{
|
||||
'key': {
|
||||
'clientId': '<client id>',
|
||||
'userAgent': '<user agent>',
|
||||
'scope': '<scope>'
|
||||
},
|
||||
'credential': {
|
||||
# JSON serialized Credentials.
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
__author__ = 'jbeda@google.com (Joe Beda)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.warning(
|
||||
'The oauth2client.contrib.multistore_file module has been deprecated and '
|
||||
'will be removed in the next release of oauth2client. Please migrate to '
|
||||
'multiprocess_file_storage.')
|
||||
|
||||
# A dict from 'filename'->_MultiStore instances
|
||||
_multistores = {}
|
||||
_multistores_lock = threading.Lock()
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class NewerCredentialStoreError(Error):
|
||||
"""The credential store is a newer version than supported."""
|
||||
|
||||
|
||||
def _dict_to_tuple_key(dictionary):
|
||||
"""Converts a dictionary to a tuple that can be used as an immutable key.
|
||||
|
||||
The resulting key is always sorted so that logically equivalent
|
||||
dictionaries always produce an identical tuple for a key.
|
||||
|
||||
Args:
|
||||
dictionary: the dictionary to use as the key.
|
||||
|
||||
Returns:
|
||||
A tuple representing the dictionary in it's naturally sorted ordering.
|
||||
"""
|
||||
return tuple(sorted(dictionary.items()))
|
||||
|
||||
|
||||
@util.positional(4)
|
||||
def get_credential_storage(filename, client_id, user_agent, scope,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
client_id: The client_id for the credential
|
||||
user_agent: The user agent for the credential
|
||||
scope: string or iterable of strings, Scope(s) being requested
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Recreate the legacy key with these specific parameters
|
||||
key = {'clientId': client_id, 'userAgent': user_agent,
|
||||
'scope': util.scopes_to_string(scope)}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_string_key(filename, key_string,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a single string as a key.
|
||||
|
||||
Allows you to provide a string as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_string: A string to use as the key for storing this credential.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Create a key dictionary that can be used
|
||||
key_dict = {'key': key_string}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key_dict, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_key(filename, key_dict,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a dictionary as a key.
|
||||
|
||||
Allows you to provide a dictionary as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_dict: A dictionary to use as the key for storing this credential.
|
||||
There is no ordering of the keys in the dictionary. Logically
|
||||
equivalent dictionaries will produce equivalent storage keys.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
key = _dict_to_tuple_key(key_dict)
|
||||
return multistore._get_storage(key)
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def get_all_credential_keys(filename, warn_on_readonly=True):
|
||||
"""Gets all the registered credential keys in the given Multistore.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A list of the credential keys present in the file. They are returned
|
||||
as dictionaries that can be passed into
|
||||
get_credential_storage_custom_key to get the actual credentials.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
multistore._lock()
|
||||
try:
|
||||
return multistore._get_all_credential_keys()
|
||||
finally:
|
||||
multistore._unlock()
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def _get_multistore(filename, warn_on_readonly=True):
|
||||
"""A helper method to initialize the multistore with proper locking.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A multistore object
|
||||
"""
|
||||
filename = os.path.expanduser(filename)
|
||||
_multistores_lock.acquire()
|
||||
try:
|
||||
multistore = _multistores.setdefault(
|
||||
filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
|
||||
finally:
|
||||
_multistores_lock.release()
|
||||
return multistore
|
||||
|
||||
|
||||
class _MultiStore(object):
|
||||
"""A file backed store for multiple credentials."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, filename, warn_on_readonly=True):
|
||||
"""Initialize the class.
|
||||
|
||||
This will create the file if necessary.
|
||||
"""
|
||||
self._file = locked_file.LockedFile(filename, 'r+', 'r')
|
||||
self._thread_lock = threading.Lock()
|
||||
self._read_only = False
|
||||
self._warn_on_readonly = warn_on_readonly
|
||||
|
||||
self._create_file_if_needed()
|
||||
|
||||
# Cache of deserialized store. This is only valid after the
|
||||
# _MultiStore is locked or _refresh_data_cache is called. This is
|
||||
# of the form of:
|
||||
#
|
||||
# ((key, value), (key, value)...) -> OAuth2Credential
|
||||
#
|
||||
# If this is None, then the store hasn't been read yet.
|
||||
self._data = None
|
||||
|
||||
class _Storage(client.Storage):
|
||||
"""A Storage object that can read/write a single credential."""
|
||||
|
||||
def __init__(self, multistore, key):
|
||||
self._multistore = multistore
|
||||
self._key = key
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
self._multistore._lock()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._multistore._unlock()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credential = self._multistore._get_credential(self._key)
|
||||
if credential:
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._update_credential(self._key, credentials)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._delete_credential(self._key)
|
||||
|
||||
def _create_file_if_needed(self):
|
||||
"""Create an empty file if necessary.
|
||||
|
||||
This method will not initialize the file. Instead it implements a
|
||||
simple version of "touch" to ensure the file has been created.
|
||||
"""
|
||||
if not os.path.exists(self._file.filename()):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._file.filename(), 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def _lock(self):
|
||||
"""Lock the entire multistore."""
|
||||
self._thread_lock.acquire()
|
||||
try:
|
||||
self._file.open_and_lock()
|
||||
except (IOError, OSError) as e:
|
||||
if e.errno == errno.ENOSYS:
|
||||
logger.warn('File system does not support locking the '
|
||||
'credentials file.')
|
||||
elif e.errno == errno.ENOLCK:
|
||||
logger.warn('File system is out of resources for writing the '
|
||||
'credentials file (is your disk full?).')
|
||||
elif e.errno == errno.EDEADLK:
|
||||
logger.warn('Lock contention on multistore file, opening '
|
||||
'in read-only mode.')
|
||||
elif e.errno == errno.EACCES:
|
||||
logger.warn('Cannot access credentials file.')
|
||||
else:
|
||||
raise
|
||||
if not self._file.is_locked():
|
||||
self._read_only = True
|
||||
if self._warn_on_readonly:
|
||||
logger.warn('The credentials file (%s) is not writable. '
|
||||
'Opening in read-only mode. Any refreshed '
|
||||
'credentials will only be '
|
||||
'valid for this run.', self._file.filename())
|
||||
|
||||
if os.path.getsize(self._file.filename()) == 0:
|
||||
logger.debug('Initializing empty multistore file')
|
||||
# The multistore is empty so write out an empty file.
|
||||
self._data = {}
|
||||
self._write()
|
||||
elif not self._read_only or self._data is None:
|
||||
# Only refresh the data if we are read/write or we haven't
|
||||
# cached the data yet. If we are readonly, we assume is isn't
|
||||
# changing out from under us and that we only have to read it
|
||||
# once. This prevents us from whacking any new access keys that
|
||||
# we have cached in memory but were unable to write out.
|
||||
self._refresh_data_cache()
|
||||
|
||||
def _unlock(self):
|
||||
"""Release the lock on the multistore."""
|
||||
self._file.unlock_and_close()
|
||||
self._thread_lock.release()
|
||||
|
||||
def _locked_json_read(self):
|
||||
"""Get the raw content of the multistore file.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Returns:
|
||||
The contents of the multistore decoded as JSON.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
self._file.file_handle().seek(0)
|
||||
return json.load(self._file.file_handle())
|
||||
|
||||
def _locked_json_write(self, data):
|
||||
"""Write a JSON serializable data structure to the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Args:
|
||||
data: The data to be serialized and written.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
if self._read_only:
|
||||
return
|
||||
self._file.file_handle().seek(0)
|
||||
json.dump(data, self._file.file_handle(),
|
||||
sort_keys=True, indent=2, separators=(',', ': '))
|
||||
self._file.file_handle().truncate()
|
||||
|
||||
def _refresh_data_cache(self):
|
||||
"""Refresh the contents of the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Raises:
|
||||
NewerCredentialStoreError: Raised when a newer client has written
|
||||
the store.
|
||||
"""
|
||||
self._data = {}
|
||||
try:
|
||||
raw_data = self._locked_json_read()
|
||||
except Exception:
|
||||
logger.warn('Credential data store could not be loaded. '
|
||||
'Will ignore and overwrite.')
|
||||
return
|
||||
|
||||
version = 0
|
||||
try:
|
||||
version = raw_data['file_version']
|
||||
except Exception:
|
||||
logger.warn('Missing version for credential data store. It may be '
|
||||
'corrupt or an old version. Overwriting.')
|
||||
if version > 1:
|
||||
raise NewerCredentialStoreError(
|
||||
'Credential file has file_version of {0}. '
|
||||
'Only file_version of 1 is supported.'.format(version))
|
||||
|
||||
credentials = []
|
||||
try:
|
||||
credentials = raw_data['data']
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
for cred_entry in credentials:
|
||||
try:
|
||||
key, credential = self._decode_credential_from_json(cred_entry)
|
||||
self._data[key] = credential
|
||||
except:
|
||||
# If something goes wrong loading a credential, just ignore it
|
||||
logger.info('Error decoding credential, skipping',
|
||||
exc_info=True)
|
||||
|
||||
def _decode_credential_from_json(self, cred_entry):
|
||||
"""Load a credential from our JSON serialization.
|
||||
|
||||
Args:
|
||||
cred_entry: A dict entry from the data member of our format
|
||||
|
||||
Returns:
|
||||
(key, cred) where the key is the key tuple and the cred is the
|
||||
OAuth2Credential object.
|
||||
"""
|
||||
raw_key = cred_entry['key']
|
||||
key = _dict_to_tuple_key(raw_key)
|
||||
credential = None
|
||||
credential = client.Credentials.new_from_json(
|
||||
json.dumps(cred_entry['credential']))
|
||||
return (key, credential)
|
||||
|
||||
def _write(self):
|
||||
"""Write the cached data back out.
|
||||
|
||||
The multistore must be locked.
|
||||
"""
|
||||
raw_data = {'file_version': 1}
|
||||
raw_creds = []
|
||||
raw_data['data'] = raw_creds
|
||||
for (cred_key, cred) in self._data.items():
|
||||
raw_key = dict(cred_key)
|
||||
raw_cred = json.loads(cred.to_json())
|
||||
raw_creds.append({'key': raw_key, 'credential': raw_cred})
|
||||
self._locked_json_write(raw_data)
|
||||
|
||||
def _get_all_credential_keys(self):
|
||||
"""Gets all the registered credential keys in the multistore.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries corresponding to all the keys currently
|
||||
registered
|
||||
"""
|
||||
return [dict(key) for key in self._data.keys()]
|
||||
|
||||
def _get_credential(self, key):
|
||||
"""Get a credential from the multistore.
|
||||
|
||||
The multistore must be locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
The credential specified or None if not present
|
||||
"""
|
||||
return self._data.get(key, None)
|
||||
|
||||
def _update_credential(self, key, cred):
|
||||
"""Update a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
cred: The OAuth2Credential to update/set
|
||||
"""
|
||||
self._data[key] = cred
|
||||
self._write()
|
||||
|
||||
def _delete_credential(self, key):
|
||||
"""Delete a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
"""
|
||||
try:
|
||||
del self._data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self._write()
|
||||
|
||||
def _get_storage(self, key):
|
||||
"""Get a Storage object to get/set a credential.
|
||||
|
||||
This Storage is a 'view' into the multistore.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
A Storage object that can be used to get/set this cred
|
||||
"""
|
||||
return self._Storage(self, key)
|
||||
173
src/oauth2client/contrib/sqlalchemy.py
Normal file
173
src/oauth2client/contrib/sqlalchemy.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 utilities for SQLAlchemy.
|
||||
|
||||
Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
In order to use this storage, you'll need to create table
|
||||
with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column.
|
||||
It's recommended to either put this column on some sort of user info
|
||||
table or put the column in a table with a belongs-to relationship to
|
||||
a user info table.
|
||||
|
||||
Here's an example of a simple table with a :class:`CredentialsType`
|
||||
column that's related to a user table by the `user_id` key.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from oauth2client.contrib.sqlalchemy import CredentialsType
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Credentials(Base):
|
||||
__tablename__ = 'credentials'
|
||||
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
credentials = Column(CredentialsType)
|
||||
|
||||
|
||||
class User(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
# bunch of other columns
|
||||
credentials = relationship('Credentials')
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
With tables ready, you are now able to store credentials in database.
|
||||
We will reuse tables defined above.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from oauth2client.client import OAuth2Credentials
|
||||
from oauth2client.contrib.sql_alchemy import Storage
|
||||
|
||||
session = Session()
|
||||
user = session.query(User).first()
|
||||
storage = Storage(
|
||||
session=session,
|
||||
model_class=Credentials,
|
||||
# This is the key column used to identify
|
||||
# the row that stores the credentials.
|
||||
key_name='user_id',
|
||||
key_value=user.id,
|
||||
property_name='credentials',
|
||||
)
|
||||
|
||||
# Store
|
||||
credentials = OAuth2Credentials(...)
|
||||
storage.put(credentials)
|
||||
|
||||
# Retrieve
|
||||
credentials = storage.get()
|
||||
|
||||
# Delete
|
||||
storage.delete()
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sqlalchemy.types
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
class CredentialsType(sqlalchemy.types.PickleType):
|
||||
"""Type representing credentials.
|
||||
|
||||
Alias for :class:`sqlalchemy.types.PickleType`.
|
||||
"""
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from SQLAlchemy.
|
||||
This helper presumes the Credentials
|
||||
have been stored as a Credentials column
|
||||
on a db model class.
|
||||
"""
|
||||
|
||||
def __init__(self, session, model_class, key_name,
|
||||
key_value, property_name):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
session: An instance of :class:`sqlalchemy.orm.Session`.
|
||||
model_class: SQLAlchemy declarative mapping.
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
key_value: key value for the entity that has the credentials
|
||||
property_name: A string indicating which property on the
|
||||
``model_class`` to store the credentials.
|
||||
This property must be a
|
||||
:class:`CredentialsType` column.
|
||||
"""
|
||||
super(Storage, self).__init__()
|
||||
|
||||
self.session = session
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve stored credential.
|
||||
|
||||
Returns:
|
||||
A :class:`oauth2client.Credentials` instance or `None`.
|
||||
"""
|
||||
filters = {self.key_name: self.key_value}
|
||||
query = self.session.query(self.model_class).filter_by(**filters)
|
||||
entity = query.first()
|
||||
|
||||
if entity:
|
||||
credential = getattr(entity, self.property_name)
|
||||
if credential and hasattr(credential, 'set_store'):
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
else:
|
||||
return None
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a credentials to the SQLAlchemy datastore.
|
||||
|
||||
Args:
|
||||
credentials: :class:`oauth2client.Credentials`
|
||||
"""
|
||||
filters = {self.key_name: self.key_value}
|
||||
query = self.session.query(self.model_class).filter_by(**filters)
|
||||
entity = query.first()
|
||||
|
||||
if not entity:
|
||||
entity = self.model_class(**filters)
|
||||
|
||||
setattr(entity, self.property_name, credentials)
|
||||
self.session.add(entity)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete credentials from the SQLAlchemy datastore."""
|
||||
filters = {self.key_name: self.key_value}
|
||||
self.session.query(self.model_class).filter_by(**filters).delete()
|
||||
106
src/oauth2client/contrib/xsrfutil.py
Normal file
106
src/oauth2client/contrib/xsrfutil.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# Copyright 2014 the Melange authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper methods for creating & verifying XSRF tokens."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hmac
|
||||
import time
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import util
|
||||
|
||||
__authors__ = [
|
||||
'"Doug Coker" <dcoker@google.com>',
|
||||
'"Joe Gregorio" <jcgregorio@google.com>',
|
||||
]
|
||||
|
||||
# Delimiter character
|
||||
DELIMITER = b':'
|
||||
|
||||
# 1 hour in seconds
|
||||
DEFAULT_TIMEOUT_SECS = 60 * 60
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def generate_token(key, user_id, action_id='', when=None):
|
||||
"""Generates a URL-safe token for the given user, action, time tuple.
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
when: the time in seconds since the epoch at which the user was
|
||||
authorized for this action. If not set the current time is used.
|
||||
|
||||
Returns:
|
||||
A string XSRF protection token.
|
||||
"""
|
||||
digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8'))
|
||||
digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8'))
|
||||
digester.update(DELIMITER)
|
||||
digester.update(_helpers._to_bytes(action_id, encoding='utf-8'))
|
||||
digester.update(DELIMITER)
|
||||
when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8')
|
||||
digester.update(when)
|
||||
digest = digester.digest()
|
||||
|
||||
token = base64.urlsafe_b64encode(digest + DELIMITER + when)
|
||||
return token
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def validate_token(key, token, user_id, action_id="", current_time=None):
|
||||
"""Validates that the given token authorizes the user for the action.
|
||||
|
||||
Tokens are invalid if the time of issue is too old or if the token
|
||||
does not match what generateToken outputs (i.e. the token was forged).
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
token: a string of the token generated by generateToken.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
|
||||
Returns:
|
||||
A boolean - True if the user is authorized for the action, False
|
||||
otherwise.
|
||||
"""
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(token)
|
||||
token_time = int(decoded.split(DELIMITER)[-1])
|
||||
except (TypeError, ValueError, binascii.Error):
|
||||
return False
|
||||
if current_time is None:
|
||||
current_time = time.time()
|
||||
# If the token is too old it's not valid.
|
||||
if current_time - token_time > DEFAULT_TIMEOUT_SECS:
|
||||
return False
|
||||
|
||||
# The given token should match the generated one with the same time.
|
||||
expected_token = generate_token(key, user_id, action_id=action_id,
|
||||
when=token_time)
|
||||
if len(token) != len(expected_token):
|
||||
return False
|
||||
|
||||
# Perform constant time comparison to avoid timing attacks
|
||||
different = 0
|
||||
for x, y in zip(bytearray(token), bytearray(expected_token)):
|
||||
different |= x ^ y
|
||||
return not different
|
||||
250
src/oauth2client/crypt.py
Normal file
250
src/oauth2client/crypt.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Crypto-related routines for oauth2client."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import _pure_python_crypt
|
||||
|
||||
|
||||
RsaSigner = _pure_python_crypt.RsaSigner
|
||||
RsaVerifier = _pure_python_crypt.RsaVerifier
|
||||
|
||||
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
|
||||
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
|
||||
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AppIdentityError(Exception):
|
||||
"""Error to indicate crypto failure."""
|
||||
|
||||
|
||||
def _bad_pkcs12_key_as_pem(*args, **kwargs):
|
||||
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
|
||||
|
||||
|
||||
try:
|
||||
from oauth2client import _openssl_crypt
|
||||
OpenSSLSigner = _openssl_crypt.OpenSSLSigner
|
||||
OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
|
||||
pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
|
||||
except ImportError: # pragma: NO COVER
|
||||
OpenSSLVerifier = None
|
||||
OpenSSLSigner = None
|
||||
pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
|
||||
|
||||
try:
|
||||
from oauth2client import _pycrypto_crypt
|
||||
PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
|
||||
PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
|
||||
except ImportError: # pragma: NO COVER
|
||||
PyCryptoVerifier = None
|
||||
PyCryptoSigner = None
|
||||
|
||||
|
||||
if OpenSSLSigner:
|
||||
Signer = OpenSSLSigner
|
||||
Verifier = OpenSSLVerifier
|
||||
elif PyCryptoSigner: # pragma: NO COVER
|
||||
Signer = PyCryptoSigner
|
||||
Verifier = PyCryptoVerifier
|
||||
else: # pragma: NO COVER
|
||||
Signer = RsaSigner
|
||||
Verifier = RsaVerifier
|
||||
|
||||
|
||||
def make_signed_jwt(signer, payload, key_id=None):
|
||||
"""Make a signed JWT.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
signer: crypt.Signer, Cryptographic signer.
|
||||
payload: dict, Dictionary of data to convert to JSON and then sign.
|
||||
key_id: string, (Optional) Key ID header.
|
||||
|
||||
Returns:
|
||||
string, The JWT for the payload.
|
||||
"""
|
||||
header = {'typ': 'JWT', 'alg': 'RS256'}
|
||||
if key_id is not None:
|
||||
header['kid'] = key_id
|
||||
|
||||
segments = [
|
||||
_helpers._urlsafe_b64encode(_helpers._json_encode(header)),
|
||||
_helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
|
||||
]
|
||||
signing_input = b'.'.join(segments)
|
||||
|
||||
signature = signer.sign(signing_input)
|
||||
segments.append(_helpers._urlsafe_b64encode(signature))
|
||||
|
||||
logger.debug(str(segments))
|
||||
|
||||
return b'.'.join(segments)
|
||||
|
||||
|
||||
def _verify_signature(message, signature, certs):
|
||||
"""Verifies signed content using a list of certificates.
|
||||
|
||||
Args:
|
||||
message: string or bytes, The message to verify.
|
||||
signature: string or bytes, The signature on the message.
|
||||
certs: iterable, certificates in PEM format.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If none of the certificates can verify the message
|
||||
against the signature.
|
||||
"""
|
||||
for pem in certs:
|
||||
verifier = Verifier.from_string(pem, is_x509_cert=True)
|
||||
if verifier.verify(message, signature):
|
||||
return
|
||||
|
||||
# If we have not returned, no certificate confirms the signature.
|
||||
raise AppIdentityError('Invalid token signature')
|
||||
|
||||
|
||||
def _check_audience(payload_dict, audience):
|
||||
"""Checks audience field from a JWT payload.
|
||||
|
||||
Does nothing if the passed in ``audience`` is null.
|
||||
|
||||
Args:
|
||||
payload_dict: dict, A dictionary containing a JWT payload.
|
||||
audience: string or NoneType, an audience to check for in
|
||||
the JWT payload.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If there is no ``'aud'`` field in the payload
|
||||
dictionary but there is an ``audience`` to check.
|
||||
AppIdentityError: If the ``'aud'`` field in the payload dictionary
|
||||
does not match the ``audience``.
|
||||
"""
|
||||
if audience is None:
|
||||
return
|
||||
|
||||
audience_in_payload = payload_dict.get('aud')
|
||||
if audience_in_payload is None:
|
||||
raise AppIdentityError(
|
||||
'No aud field in token: {0}'.format(payload_dict))
|
||||
if audience_in_payload != audience:
|
||||
raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
|
||||
audience_in_payload, audience, payload_dict))
|
||||
|
||||
|
||||
def _verify_time_range(payload_dict):
|
||||
"""Verifies the issued at and expiration from a JWT payload.
|
||||
|
||||
Makes sure the current time (in UTC) falls between the issued at and
|
||||
expiration for the JWT (with some skew allowed for via
|
||||
``CLOCK_SKEW_SECS``).
|
||||
|
||||
Args:
|
||||
payload_dict: dict, A dictionary containing a JWT payload.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: If there is no ``'iat'`` field in the payload
|
||||
dictionary.
|
||||
AppIdentityError: If there is no ``'exp'`` field in the payload
|
||||
dictionary.
|
||||
AppIdentityError: If the JWT expiration is too far in the future (i.e.
|
||||
if the expiration would imply a token lifetime
|
||||
longer than what is allowed.)
|
||||
AppIdentityError: If the token appears to have been issued in the
|
||||
future (up to clock skew).
|
||||
AppIdentityError: If the token appears to have expired in the past
|
||||
(up to clock skew).
|
||||
"""
|
||||
# Get the current time to use throughout.
|
||||
now = int(time.time())
|
||||
|
||||
# Make sure issued at and expiration are in the payload.
|
||||
issued_at = payload_dict.get('iat')
|
||||
if issued_at is None:
|
||||
raise AppIdentityError(
|
||||
'No iat field in token: {0}'.format(payload_dict))
|
||||
expiration = payload_dict.get('exp')
|
||||
if expiration is None:
|
||||
raise AppIdentityError(
|
||||
'No exp field in token: {0}'.format(payload_dict))
|
||||
|
||||
# Make sure the expiration gives an acceptable token lifetime.
|
||||
if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
|
||||
raise AppIdentityError(
|
||||
'exp field too far in future: {0}'.format(payload_dict))
|
||||
|
||||
# Make sure (up to clock skew) that the token wasn't issued in the future.
|
||||
earliest = issued_at - CLOCK_SKEW_SECS
|
||||
if now < earliest:
|
||||
raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
|
||||
now, earliest, payload_dict))
|
||||
# Make sure (up to clock skew) that the token isn't already expired.
|
||||
latest = expiration + CLOCK_SKEW_SECS
|
||||
if now > latest:
|
||||
raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
|
||||
now, latest, payload_dict))
|
||||
|
||||
|
||||
def verify_signed_jwt_with_certs(jwt, certs, audience=None):
|
||||
"""Verify a JWT against public certs.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
jwt: string, A JWT.
|
||||
certs: dict, Dictionary where values of public keys in PEM format.
|
||||
audience: string, The audience, 'aud', that this JWT should contain. If
|
||||
None then the JWT's 'aud' parameter is not verified.
|
||||
|
||||
Returns:
|
||||
dict, The deserialized JSON payload in the JWT.
|
||||
|
||||
Raises:
|
||||
AppIdentityError: if any checks are failed.
|
||||
"""
|
||||
jwt = _helpers._to_bytes(jwt)
|
||||
|
||||
if jwt.count(b'.') != 2:
|
||||
raise AppIdentityError(
|
||||
'Wrong number of segments in token: {0}'.format(jwt))
|
||||
|
||||
header, payload, signature = jwt.split(b'.')
|
||||
message_to_sign = header + b'.' + payload
|
||||
signature = _helpers._urlsafe_b64decode(signature)
|
||||
|
||||
# Parse token.
|
||||
payload_bytes = _helpers._urlsafe_b64decode(payload)
|
||||
try:
|
||||
payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
|
||||
except:
|
||||
raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
|
||||
|
||||
# Verify that the signature matches the message.
|
||||
_verify_signature(message_to_sign, signature, certs.values())
|
||||
|
||||
# Verify the issued at and created times in the payload.
|
||||
_verify_time_range(payload_dict)
|
||||
|
||||
# Check audience.
|
||||
_check_audience(payload_dict, audience)
|
||||
|
||||
return payload_dict
|
||||
106
src/oauth2client/file.py
Normal file
106
src/oauth2client/file.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for OAuth.
|
||||
|
||||
Utilities for making it easier to work with OAuth 2.0
|
||||
credentials.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
|
||||
class CredentialsFileSymbolicLinkError(Exception):
|
||||
"""Credentials files must not be symbolic links."""
|
||||
|
||||
|
||||
class Storage(client.Storage):
|
||||
"""Store and retrieve a single credential to and from a file."""
|
||||
|
||||
def __init__(self, filename):
|
||||
super(Storage, self).__init__(lock=threading.Lock())
|
||||
self._filename = filename
|
||||
|
||||
def _validate_file(self):
|
||||
if os.path.islink(self._filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: {0} is a symbolic link.'.format(self._filename))
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
credentials = None
|
||||
self._validate_file()
|
||||
try:
|
||||
f = open(self._filename, 'rb')
|
||||
content = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
return credentials
|
||||
|
||||
try:
|
||||
credentials = client.Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
def _create_file_if_needed(self):
|
||||
"""Create an empty file if necessary.
|
||||
|
||||
This method will not initialize the file. Instead it implements a
|
||||
simple version of "touch" to ensure the file has been created.
|
||||
"""
|
||||
if not os.path.exists(self._filename):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._filename, 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
self._create_file_if_needed()
|
||||
self._validate_file()
|
||||
f = open(self._filename, 'w')
|
||||
f.write(credentials.to_json())
|
||||
f.close()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
os.unlink(self._filename)
|
||||
673
src/oauth2client/service_account.py
Normal file
673
src/oauth2client/service_account.py
Normal file
@@ -0,0 +1,673 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""oauth2client Service account credentials class."""
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
|
||||
import oauth2client
|
||||
from oauth2client import _helpers
|
||||
from oauth2client import client
|
||||
from oauth2client import crypt
|
||||
from oauth2client import transport
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
_PASSWORD_DEFAULT = 'notasecret'
|
||||
_PKCS12_KEY = '_private_key_pkcs12'
|
||||
_PKCS12_ERROR = r"""
|
||||
This library only implements PKCS#12 support via the pyOpenSSL library.
|
||||
Either install pyOpenSSL, or please convert the .p12 file
|
||||
to .pem format:
|
||||
$ cat key.p12 | \
|
||||
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
||||
> openssl rsa > key.pem
|
||||
"""
|
||||
|
||||
|
||||
class ServiceAccountCredentials(client.AssertionCredentials):
|
||||
"""Service Account credential for OAuth 2.0 signed JWT grants.
|
||||
|
||||
Supports
|
||||
|
||||
* JSON keyfile (typically contains a PKCS8 key stored as
|
||||
PEM text)
|
||||
* ``.p12`` key (stores PKCS12 key and certificate)
|
||||
|
||||
Makes an assertion to server using a signed JWT assertion in exchange
|
||||
for an access token.
|
||||
|
||||
This credential does not require a flow to instantiate because it
|
||||
represents a two legged flow, and therefore has all of the required
|
||||
information to generate and refresh its own access tokens.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
signer: ``crypt.Signer``, A signer which can be used to sign content.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring
|
||||
an access token.
|
||||
private_key_id: string, (Optional) Private key identifier. Typically
|
||||
only used with a JSON keyfile. Can be sent in the
|
||||
header of a JWT token assertion.
|
||||
client_id: string, (Optional) Client ID for the project that owns the
|
||||
service account.
|
||||
user_agent: string, (Optional) User agent to use when sending
|
||||
request.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
kwargs: dict, Extra key-value pairs (both strings) to send in the
|
||||
payload body when making an assertion.
|
||||
"""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600
|
||||
"""Max lifetime of the token (one hour, in seconds)."""
|
||||
|
||||
NON_SERIALIZED_MEMBERS = (
|
||||
frozenset(['_signer']) |
|
||||
client.AssertionCredentials.NON_SERIALIZED_MEMBERS)
|
||||
"""Members that aren't serialized when object is converted to JSON."""
|
||||
|
||||
# Can be over-ridden by factory constructors. Used for
|
||||
# serialization/deserialization purposes.
|
||||
_private_key_pkcs8_pem = None
|
||||
_private_key_pkcs12 = None
|
||||
_private_key_password = None
|
||||
|
||||
def __init__(self,
|
||||
service_account_email,
|
||||
signer,
|
||||
scopes='',
|
||||
private_key_id=None,
|
||||
client_id=None,
|
||||
user_agent=None,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
**kwargs):
|
||||
|
||||
super(ServiceAccountCredentials, self).__init__(
|
||||
None, user_agent=user_agent, token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
self._service_account_email = service_account_email
|
||||
self._signer = signer
|
||||
self._scopes = util.scopes_to_string(scopes)
|
||||
self._private_key_id = private_key_id
|
||||
self.client_id = client_id
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
|
||||
def _to_json(self, strip, to_serialize=None):
|
||||
"""Utility function that creates JSON repr. of a credentials object.
|
||||
|
||||
Over-ride is needed since PKCS#12 keys will not in general be JSON
|
||||
serializable.
|
||||
|
||||
Args:
|
||||
strip: array, An array of names of members to exclude from the
|
||||
JSON.
|
||||
to_serialize: dict, (Optional) The properties for this object
|
||||
that will be serialized. This allows callers to
|
||||
modify before serializing.
|
||||
|
||||
Returns:
|
||||
string, a JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
if to_serialize is None:
|
||||
to_serialize = copy.copy(self.__dict__)
|
||||
pkcs12_val = to_serialize.get(_PKCS12_KEY)
|
||||
if pkcs12_val is not None:
|
||||
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
|
||||
return super(ServiceAccountCredentials, self)._to_json(
|
||||
strip, to_serialize=to_serialize)
|
||||
|
||||
@classmethod
|
||||
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
|
||||
token_uri=None, revoke_uri=None):
|
||||
"""Helper for factory constructors from JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile contents.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
creds_type = keyfile_dict.get('type')
|
||||
if creds_type != client.SERVICE_ACCOUNT:
|
||||
raise ValueError('Unexpected credentials type', creds_type,
|
||||
'Expected', client.SERVICE_ACCOUNT)
|
||||
|
||||
service_account_email = keyfile_dict['client_email']
|
||||
private_key_pkcs8_pem = keyfile_dict['private_key']
|
||||
private_key_id = keyfile_dict['private_key_id']
|
||||
client_id = keyfile_dict['client_id']
|
||||
if not token_uri:
|
||||
token_uri = keyfile_dict.get('token_uri',
|
||||
oauth2client.GOOGLE_TOKEN_URI)
|
||||
if not revoke_uri:
|
||||
revoke_uri = keyfile_dict.get('revoke_uri',
|
||||
oauth2client.GOOGLE_REVOKE_URI)
|
||||
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes,
|
||||
private_key_id=private_key_id,
|
||||
client_id=client_id, token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_name(cls, filename, scopes='',
|
||||
token_uri=None, revoke_uri=None):
|
||||
|
||||
"""Factory constructor from JSON keyfile by name.
|
||||
|
||||
Args:
|
||||
filename: string, The location of the keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in the key file, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in the key file, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
with open(filename, 'r') as file_obj:
|
||||
client_credentials = json.load(file_obj)
|
||||
return cls._from_parsed_json_keyfile(client_credentials, scopes,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
|
||||
token_uri=None, revoke_uri=None):
|
||||
"""Factory constructor from parsed JSON keyfile.
|
||||
|
||||
Args:
|
||||
keyfile_dict: dict-like object, The parsed dictionary-like object
|
||||
containing the contents of the JSON keyfile.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for OAuth 2.0 provider token endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint.
|
||||
If unset and not present in keyfile_dict, defaults
|
||||
to Google's endpoints.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
|
||||
KeyError, if one of the expected keys is not present in
|
||||
the keyfile.
|
||||
"""
|
||||
return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def _from_p12_keyfile_contents(cls, service_account_email,
|
||||
private_key_pkcs12,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
if private_key_password is None:
|
||||
private_key_password = _PASSWORD_DEFAULT
|
||||
if crypt.Signer is not crypt.OpenSSLSigner:
|
||||
raise NotImplementedError(_PKCS12_ERROR)
|
||||
signer = crypt.Signer.from_string(private_key_pkcs12,
|
||||
private_key_password)
|
||||
credentials = cls(service_account_email, signer, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
credentials._private_key_pkcs12 = private_key_pkcs12
|
||||
credentials._private_key_password = private_key_password
|
||||
return credentials
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile(cls, service_account_email, filename,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
filename: string, The location of the PKCS#12 keyfile.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
with open(filename, 'rb') as file_obj:
|
||||
private_key_pkcs12 = file_obj.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
|
||||
@classmethod
|
||||
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
|
||||
private_key_password=None, scopes='',
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
"""Factory constructor from JSON keyfile.
|
||||
|
||||
Args:
|
||||
service_account_email: string, The email associated with the
|
||||
service account.
|
||||
file_buffer: stream, A buffer that implements ``read()``
|
||||
and contains the PKCS#12 key contents.
|
||||
private_key_password: string, (Optional) Password for PKCS#12
|
||||
private key. Defaults to ``notasecret``.
|
||||
scopes: List or string, (Optional) Scopes to use when acquiring an
|
||||
access token.
|
||||
token_uri: string, URI for token endpoint. For convenience defaults
|
||||
to Google's endpoints but any OAuth 2.0 provider can be
|
||||
used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0
|
||||
provider can be used.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a credentials object created from
|
||||
the keyfile.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if pyOpenSSL is not installed / not the
|
||||
active crypto library.
|
||||
"""
|
||||
private_key_pkcs12 = file_buffer.read()
|
||||
return cls._from_p12_keyfile_contents(
|
||||
service_account_email, private_key_pkcs12,
|
||||
private_key_password=private_key_password, scopes=scopes,
|
||||
token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
|
||||
def _generate_assertion(self):
|
||||
"""Generate the assertion that will be used in the request."""
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'aud': self.token_uri,
|
||||
'scope': self._scopes,
|
||||
'iat': now,
|
||||
'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self._service_account_email,
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
return crypt.make_signed_jwt(self._signer, payload,
|
||||
key_id=self._private_key_id)
|
||||
|
||||
def sign_blob(self, blob):
|
||||
"""Cryptographically sign a blob (of bytes).
|
||||
|
||||
Implements abstract method
|
||||
:meth:`oauth2client.client.AssertionCredentials.sign_blob`.
|
||||
|
||||
Args:
|
||||
blob: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
tuple, A pair of the private key ID used to sign the blob and
|
||||
the signed contents.
|
||||
"""
|
||||
return self._private_key_id, self._signer.sign(blob)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""Get the email for the current service account.
|
||||
|
||||
Returns:
|
||||
string, The email associated with the service account.
|
||||
"""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
# NOTE: This is only useful for JSON keyfile.
|
||||
return {
|
||||
'type': 'service_account',
|
||||
'client_email': self._service_account_email,
|
||||
'private_key_id': self._private_key_id,
|
||||
'private_key': self._private_key_pkcs8_pem,
|
||||
'client_id': self.client_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
"""Deserialize a JSON-serialized instance.
|
||||
|
||||
Inverse to :meth:`to_json`.
|
||||
|
||||
Args:
|
||||
json_data: dict or string, Serialized JSON (as a string or an
|
||||
already parsed dictionary) representing a credential.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials from the serialized data.
|
||||
"""
|
||||
if not isinstance(json_data, dict):
|
||||
json_data = json.loads(_helpers._from_bytes(json_data))
|
||||
|
||||
private_key_pkcs8_pem = None
|
||||
pkcs12_val = json_data.get(_PKCS12_KEY)
|
||||
password = None
|
||||
if pkcs12_val is None:
|
||||
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
|
||||
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
|
||||
else:
|
||||
# NOTE: This assumes that private_key_pkcs8_pem is not also
|
||||
# in the serialized data. This would be very incorrect
|
||||
# state.
|
||||
pkcs12_val = base64.b64decode(pkcs12_val)
|
||||
password = json_data['_private_key_password']
|
||||
signer = crypt.Signer.from_string(pkcs12_val, password)
|
||||
|
||||
credentials = cls(
|
||||
json_data['_service_account_email'],
|
||||
signer,
|
||||
scopes=json_data['_scopes'],
|
||||
private_key_id=json_data['_private_key_id'],
|
||||
client_id=json_data['client_id'],
|
||||
user_agent=json_data['_user_agent'],
|
||||
**json_data['_kwargs']
|
||||
)
|
||||
if private_key_pkcs8_pem is not None:
|
||||
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
|
||||
if pkcs12_val is not None:
|
||||
credentials._private_key_pkcs12 = pkcs12_val
|
||||
if password is not None:
|
||||
credentials._private_key_password = password
|
||||
credentials.invalid = json_data['invalid']
|
||||
credentials.access_token = json_data['access_token']
|
||||
credentials.token_uri = json_data['token_uri']
|
||||
credentials.revoke_uri = json_data['revoke_uri']
|
||||
token_expiry = json_data.get('token_expiry', None)
|
||||
if token_expiry is not None:
|
||||
credentials.token_expiry = datetime.datetime.strptime(
|
||||
token_expiry, client.EXPIRY_FORMAT)
|
||||
return credentials
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self._scopes
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**self._kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def create_with_claims(self, claims):
|
||||
"""Create credentials that specify additional claims.
|
||||
|
||||
Args:
|
||||
claims: dict, key-value pairs for claims.
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a copy of the current service account
|
||||
credentials with updated claims to use when obtaining access
|
||||
tokens.
|
||||
"""
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(claims)
|
||||
result = self.__class__(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=self._scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
**new_kwargs)
|
||||
result.token_uri = self.token_uri
|
||||
result.revoke_uri = self.revoke_uri
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def create_delegated(self, sub):
|
||||
"""Create credentials that act as domain-wide delegation of authority.
|
||||
|
||||
Use the ``sub`` parameter as the subject to delegate on behalf of
|
||||
that user.
|
||||
|
||||
For example::
|
||||
|
||||
>>> account_sub = 'foo@email.com'
|
||||
>>> delegate_creds = creds.create_delegated(account_sub)
|
||||
|
||||
Args:
|
||||
sub: string, An email address that this service account will
|
||||
act on behalf of (via domain-wide delegation).
|
||||
|
||||
Returns:
|
||||
ServiceAccountCredentials, a copy of the current service account
|
||||
updated to act on behalf of ``sub``.
|
||||
"""
|
||||
return self.create_with_claims({'sub': sub})
|
||||
|
||||
|
||||
def _datetime_to_secs(utc_time):
|
||||
# TODO(issue 298): use time_delta.total_seconds()
|
||||
# time_delta.total_seconds() not supported in Python 2.6
|
||||
epoch = datetime.datetime(1970, 1, 1)
|
||||
time_delta = utc_time - epoch
|
||||
return time_delta.days * 86400 + time_delta.seconds
|
||||
|
||||
|
||||
class _JWTAccessCredentials(ServiceAccountCredentials):
|
||||
"""Self signed JWT credentials.
|
||||
|
||||
Makes an assertion to server using a self signed JWT from service account
|
||||
credentials. These credentials do NOT use OAuth 2.0 and instead
|
||||
authenticate directly.
|
||||
"""
|
||||
_MAX_TOKEN_LIFETIME_SECS = 3600
|
||||
"""Max lifetime of the token (one hour, in seconds)."""
|
||||
|
||||
def __init__(self,
|
||||
service_account_email,
|
||||
signer,
|
||||
scopes=None,
|
||||
private_key_id=None,
|
||||
client_id=None,
|
||||
user_agent=None,
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
||||
additional_claims=None):
|
||||
if additional_claims is None:
|
||||
additional_claims = {}
|
||||
super(_JWTAccessCredentials, self).__init__(
|
||||
service_account_email,
|
||||
signer,
|
||||
private_key_id=private_key_id,
|
||||
client_id=client_id,
|
||||
user_agent=user_agent,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri,
|
||||
**additional_claims)
|
||||
|
||||
def authorize(self, http):
|
||||
"""Authorize an httplib2.Http instance with a JWT assertion.
|
||||
|
||||
Unless specified, the 'aud' of the assertion will be the base
|
||||
uri of the request.
|
||||
|
||||
Args:
|
||||
http: An instance of ``httplib2.Http`` or something that acts
|
||||
like it.
|
||||
Returns:
|
||||
A modified instance of http that was passed in.
|
||||
Example::
|
||||
h = httplib2.Http()
|
||||
h = credentials.authorize(h)
|
||||
"""
|
||||
transport.wrap_http_for_jwt_access(self, http)
|
||||
return http
|
||||
|
||||
def get_access_token(self, http=None, additional_claims=None):
|
||||
"""Create a signed jwt.
|
||||
|
||||
Args:
|
||||
http: unused
|
||||
additional_claims: dict, additional claims to add to
|
||||
the payload of the JWT.
|
||||
Returns:
|
||||
An AccessTokenInfo with the signed jwt
|
||||
"""
|
||||
if additional_claims is None:
|
||||
if self.access_token is None or self.access_token_expired:
|
||||
self.refresh(None)
|
||||
return client.AccessTokenInfo(
|
||||
access_token=self.access_token, expires_in=self._expires_in())
|
||||
else:
|
||||
# Create a 1 time token
|
||||
token, unused_expiry = self._create_token(additional_claims)
|
||||
return client.AccessTokenInfo(
|
||||
access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS)
|
||||
|
||||
def revoke(self, http):
|
||||
"""Cannot revoke JWTAccessCredentials tokens."""
|
||||
pass
|
||||
|
||||
def create_scoped_required(self):
|
||||
# JWTAccessCredentials are unscoped by definition
|
||||
return True
|
||||
|
||||
def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
||||
# Returns an OAuth2 credentials with the given scope
|
||||
result = ServiceAccountCredentials(self._service_account_email,
|
||||
self._signer,
|
||||
scopes=scopes,
|
||||
private_key_id=self._private_key_id,
|
||||
client_id=self.client_id,
|
||||
user_agent=self._user_agent,
|
||||
token_uri=token_uri,
|
||||
revoke_uri=revoke_uri,
|
||||
**self._kwargs)
|
||||
if self._private_key_pkcs8_pem is not None:
|
||||
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
|
||||
if self._private_key_pkcs12 is not None:
|
||||
result._private_key_pkcs12 = self._private_key_pkcs12
|
||||
if self._private_key_password is not None:
|
||||
result._private_key_password = self._private_key_password
|
||||
return result
|
||||
|
||||
def refresh(self, http):
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http_request):
|
||||
self.access_token, self.token_expiry = self._create_token()
|
||||
|
||||
def _create_token(self, additional_claims=None):
|
||||
now = client._UTCNOW()
|
||||
lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
payload = {
|
||||
'iat': _datetime_to_secs(now),
|
||||
'exp': _datetime_to_secs(expiry),
|
||||
'iss': self._service_account_email,
|
||||
'sub': self._service_account_email
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
if additional_claims is not None:
|
||||
payload.update(additional_claims)
|
||||
jwt = crypt.make_signed_jwt(self._signer, payload,
|
||||
key_id=self._private_key_id)
|
||||
return jwt.decode('ascii'), expiry
|
||||
266
src/oauth2client/tools.py
Normal file
266
src/oauth2client/tools.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Command-line tools for authenticating via OAuth 2.0
|
||||
|
||||
Do the OAuth 2.0 Web Server dance for a command line application. Stores the
|
||||
generated credentials in a common file that is used by other example apps in
|
||||
the same directory.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from six.moves import BaseHTTPServer
|
||||
from six.moves import http_client
|
||||
from six.moves import input
|
||||
from six.moves import urllib
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
__all__ = ['argparser', 'run_flow', 'message_if_missing']
|
||||
|
||||
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
|
||||
|
||||
To make this sample run you will need to populate the client_secrets.json file
|
||||
found at:
|
||||
|
||||
{file_path}
|
||||
|
||||
with information from the APIs Console <https://code.google.com/apis/console>.
|
||||
|
||||
"""
|
||||
|
||||
_FAILED_START_MESSAGE = """
|
||||
Failed to start a local webserver listening on either port 8080
|
||||
or port 8090. Please check your firewall settings and locally
|
||||
running programs that may be blocking or using those ports.
|
||||
|
||||
Falling back to --noauth_local_webserver and continuing with
|
||||
authorization.
|
||||
"""
|
||||
|
||||
_BROWSER_OPENED_MESSAGE = """
|
||||
Your browser has been opened to visit:
|
||||
|
||||
{address}
|
||||
|
||||
If your browser is on a different machine then exit and re-run this
|
||||
application with the command-line parameter
|
||||
|
||||
--noauth_local_webserver
|
||||
"""
|
||||
|
||||
_GO_TO_LINK_MESSAGE = """
|
||||
Go to the following link in your browser:
|
||||
|
||||
{address}
|
||||
"""
|
||||
|
||||
|
||||
def _CreateArgumentParser():
|
||||
try:
|
||||
import argparse
|
||||
except ImportError: # pragma: NO COVER
|
||||
return None
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--auth_host_name', default='localhost',
|
||||
help='Hostname when running a local web server.')
|
||||
parser.add_argument('--noauth_local_webserver', action='store_true',
|
||||
default=False, help='Do not run a local web server.')
|
||||
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
|
||||
nargs='*', help='Port web server should listen on.')
|
||||
parser.add_argument(
|
||||
'--logging_level', default='ERROR',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
help='Set the logging level of detail.')
|
||||
return parser
|
||||
|
||||
# argparser is an ArgumentParser that contains command-line options expected
|
||||
# by tools.run(). Pass it in as part of the 'parents' argument to your own
|
||||
# ArgumentParser.
|
||||
argparser = _CreateArgumentParser()
|
||||
|
||||
|
||||
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
||||
"""A server to handle OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into query_params and then stops serving.
|
||||
"""
|
||||
query_params = {}
|
||||
|
||||
|
||||
class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""A handler for OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into the servers query_params and then stops serving.
|
||||
"""
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle a GET request.
|
||||
|
||||
Parses the query parameters and prints a message
|
||||
if the flow has completed. Note that we can't detect
|
||||
if an error occurred.
|
||||
"""
|
||||
self.send_response(http_client.OK)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
query = self.path.split('?', 1)[-1]
|
||||
query = dict(urllib.parse.parse_qsl(query))
|
||||
self.server.query_params = query
|
||||
self.wfile.write(
|
||||
b"<html><head><title>Authentication Status</title></head>")
|
||||
self.wfile.write(
|
||||
b"<body><p>The authentication flow has completed.</p>")
|
||||
self.wfile.write(b"</body></html>")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Do not log messages to stdout while running as cmd. line program."""
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def run_flow(flow, storage, flags=None, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
through all the steps to obtain credentials. It takes a ``Flow``
|
||||
argument and attempts to open an authorization server page in the
|
||||
user's default web browser. The server asks the user to grant your
|
||||
application access to the user's data. If the user grants access,
|
||||
the ``run()`` function returns new credentials. The new credentials
|
||||
are also stored in the ``storage`` argument, which updates the file
|
||||
associated with the ``Storage`` object.
|
||||
|
||||
It presumes it is run from a command-line application and supports the
|
||||
following flags:
|
||||
|
||||
``--auth_host_name`` (string, default: ``localhost``)
|
||||
Host name to use when running a local web server to handle
|
||||
redirects during OAuth authorization.
|
||||
|
||||
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
|
||||
Port to use when running a local web server to handle redirects
|
||||
during OAuth authorization. Repeat this option to specify a list
|
||||
of values.
|
||||
|
||||
``--[no]auth_local_webserver`` (boolean, default: ``True``)
|
||||
Run a local web server to handle redirects during OAuth
|
||||
authorization.
|
||||
|
||||
The tools module defines an ``ArgumentParser`` the already contains the
|
||||
flag definitions that ``run()`` requires. You can pass that
|
||||
``ArgumentParser`` to your ``ArgumentParser`` constructor::
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=[tools.argparser])
|
||||
flags = parser.parse_args(argv)
|
||||
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
flags: ``argparse.Namespace``, (Optional) The command-line flags. This
|
||||
is the object returned from calling ``parse_args()`` on
|
||||
``argparse.ArgumentParser`` as described above. Defaults
|
||||
to ``argparser.parse_args()``.
|
||||
http: An instance of ``httplib2.Http.request`` or something that
|
||||
acts like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
if flags is None:
|
||||
flags = argparser.parse_args()
|
||||
logging.getLogger().setLevel(getattr(logging, flags.logging_level))
|
||||
if not flags.noauth_local_webserver:
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in flags.auth_host_port:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((flags.auth_host_name, port),
|
||||
ClientRedirectHandler)
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
flags.noauth_local_webserver = not success
|
||||
if not success:
|
||||
print(_FAILED_START_MESSAGE)
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
oauth_callback = 'http://{host}:{port}/'.format(
|
||||
host=flags.auth_host_name, port=port_number)
|
||||
else:
|
||||
oauth_callback = client.OOB_CALLBACK_URN
|
||||
flow.redirect_uri = oauth_callback
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
|
||||
if flags.short_url:
|
||||
try:
|
||||
from googleapiclient.discovery import build
|
||||
service = build('urlshortener', 'v1', http=http)
|
||||
url_result = service.url().insert(body={'longUrl': authorize_url},
|
||||
key=u'AIzaSyBlmgbii8QfJSYmC9VTMOfqrAt5Vj5wtzE').execute()
|
||||
authorize_url = url_result['id']
|
||||
except:
|
||||
pass
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
import webbrowser
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url))
|
||||
else:
|
||||
print(_GO_TO_LINK_MESSAGE.format(address=authorize_url))
|
||||
|
||||
code = None
|
||||
if not flags.noauth_local_webserver:
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
sys.exit('Authentication request was rejected.')
|
||||
if 'code' in httpd.query_params:
|
||||
code = httpd.query_params['code']
|
||||
else:
|
||||
print('Failed to find "code" in the query parameters '
|
||||
'of the redirect.')
|
||||
sys.exit('Try running with --noauth_local_webserver.')
|
||||
else:
|
||||
code = input('Enter verification code: ').strip()
|
||||
|
||||
try:
|
||||
credential = flow.step2_exchange(code, http=http)
|
||||
except client.FlowExchangeError as e:
|
||||
sys.exit('Authentication has failed: {0}'.format(e))
|
||||
|
||||
storage.put(credential)
|
||||
credential.set_store(storage)
|
||||
print('Authentication successful.')
|
||||
|
||||
return credential
|
||||
|
||||
|
||||
def message_if_missing(filename):
|
||||
"""Helpful message to display if the CLIENT_SECRETS file is missing."""
|
||||
return _CLIENT_SECRETS_MESSAGE.format(file_path=filename)
|
||||
245
src/oauth2client/transport.py
Normal file
245
src/oauth2client/transport.py
Normal file
@@ -0,0 +1,245 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import httplib2
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
from oauth2client._helpers import _to_bytes
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Properties present in file-like streams / buffers.
|
||||
_STREAM_PROPERTIES = ('read', 'seek', 'tell')
|
||||
|
||||
# Google Data client libraries may need to set this to [401, 403].
|
||||
REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
|
||||
|
||||
|
||||
class MemoryCache(object):
|
||||
"""httplib2 Cache implementation which only caches locally."""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
def get(self, key):
|
||||
return self.cache.get(key)
|
||||
|
||||
def set(self, key, value):
|
||||
self.cache[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
self.cache.pop(key, None)
|
||||
|
||||
|
||||
def get_cached_http():
|
||||
"""Return an HTTP object which caches results returned.
|
||||
|
||||
This is intended to be used in methods like
|
||||
oauth2client.client.verify_id_token(), which calls to the same URI
|
||||
to retrieve certs.
|
||||
|
||||
Returns:
|
||||
httplib2.Http, an HTTP object with a MemoryCache
|
||||
"""
|
||||
return _CACHED_HTTP
|
||||
|
||||
|
||||
def get_http_object():
|
||||
"""Return a new HTTP object.
|
||||
|
||||
Returns:
|
||||
httplib2.Http, an HTTP object.
|
||||
"""
|
||||
return httplib2.Http()
|
||||
|
||||
|
||||
def _initialize_headers(headers):
|
||||
"""Creates a copy of the headers.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers to copy.
|
||||
|
||||
Returns:
|
||||
dict, the copied headers or a new dictionary if the headers
|
||||
were None.
|
||||
"""
|
||||
return {} if headers is None else dict(headers)
|
||||
|
||||
|
||||
def _apply_user_agent(headers, user_agent):
|
||||
"""Adds a user-agent to the headers.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers to add / modify user
|
||||
agent within.
|
||||
user_agent: str, the user agent to add.
|
||||
|
||||
Returns:
|
||||
dict, the original headers passed in, but modified if the
|
||||
user agent is not None.
|
||||
"""
|
||||
if user_agent is not None:
|
||||
if 'user-agent' in headers:
|
||||
headers['user-agent'] = (user_agent + ' ' + headers['user-agent'])
|
||||
else:
|
||||
headers['user-agent'] = user_agent
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def clean_headers(headers):
|
||||
"""Forces header keys and values to be strings, i.e not unicode.
|
||||
|
||||
The httplib module just concats the header keys and values in a way that
|
||||
may make the message header a unicode string, which, if it then tries to
|
||||
contatenate to a binary request body may result in a unicode decode error.
|
||||
|
||||
Args:
|
||||
headers: dict, A dictionary of headers.
|
||||
|
||||
Returns:
|
||||
The same dictionary but with all the keys converted to strings.
|
||||
"""
|
||||
clean = {}
|
||||
try:
|
||||
for k, v in six.iteritems(headers):
|
||||
if not isinstance(k, six.binary_type):
|
||||
k = str(k)
|
||||
if not isinstance(v, six.binary_type):
|
||||
v = str(v)
|
||||
clean[_to_bytes(k)] = _to_bytes(v)
|
||||
except UnicodeEncodeError:
|
||||
from oauth2client.client import NonAsciiHeaderError
|
||||
raise NonAsciiHeaderError(k, ': ', v)
|
||||
return clean
|
||||
|
||||
|
||||
def wrap_http_for_auth(credentials, http):
|
||||
"""Prepares an HTTP object's request method for auth.
|
||||
|
||||
Wraps HTTP requests with logic to catch auth failures (typically
|
||||
identified via a 401 status code). In the event of failure, tries
|
||||
to refresh the token used and then retry the original request.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials used to identify
|
||||
the authenticated user.
|
||||
http: httplib2.Http, an http object to be used to make
|
||||
auth requests.
|
||||
"""
|
||||
orig_request_method = http.request
|
||||
|
||||
# The closure that will replace 'httplib2.Http.request'.
|
||||
def new_request(uri, method='GET', body=None, headers=None,
|
||||
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
||||
connection_type=None):
|
||||
if not credentials.access_token:
|
||||
_LOGGER.info('Attempting refresh to obtain '
|
||||
'initial access_token')
|
||||
credentials._refresh(orig_request_method)
|
||||
|
||||
# Clone and modify the request headers to add the appropriate
|
||||
# Authorization header.
|
||||
headers = _initialize_headers(headers)
|
||||
credentials.apply(headers)
|
||||
_apply_user_agent(headers, credentials.user_agent)
|
||||
|
||||
body_stream_position = None
|
||||
# Check if the body is a file-like stream.
|
||||
if all(getattr(body, stream_prop, None) for stream_prop in
|
||||
_STREAM_PROPERTIES):
|
||||
body_stream_position = body.tell()
|
||||
|
||||
resp, content = orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
# A stored token may expire between the time it is retrieved and
|
||||
# the time the request is made, so we may need to try twice.
|
||||
max_refresh_attempts = 2
|
||||
for refresh_attempt in range(max_refresh_attempts):
|
||||
if resp.status not in REFRESH_STATUS_CODES:
|
||||
break
|
||||
_LOGGER.info('Refreshing due to a %s (attempt %s/%s)',
|
||||
resp.status, refresh_attempt + 1,
|
||||
max_refresh_attempts)
|
||||
credentials._refresh(orig_request_method)
|
||||
credentials.apply(headers)
|
||||
if body_stream_position is not None:
|
||||
body.seek(body_stream_position)
|
||||
|
||||
resp, content = orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
return resp, content
|
||||
|
||||
# Replace the request method with our own closure.
|
||||
http.request = new_request
|
||||
|
||||
# Set credentials as a property of the request method.
|
||||
setattr(http.request, 'credentials', credentials)
|
||||
|
||||
|
||||
def wrap_http_for_jwt_access(credentials, http):
|
||||
"""Prepares an HTTP object's request method for JWT access.
|
||||
|
||||
Wraps HTTP requests with logic to catch auth failures (typically
|
||||
identified via a 401 status code). In the event of failure, tries
|
||||
to refresh the token used and then retry the original request.
|
||||
|
||||
Args:
|
||||
credentials: _JWTAccessCredentials, the credentials used to identify
|
||||
a service account that uses JWT access tokens.
|
||||
http: httplib2.Http, an http object to be used to make
|
||||
auth requests.
|
||||
"""
|
||||
orig_request_method = http.request
|
||||
wrap_http_for_auth(credentials, http)
|
||||
# The new value of ``http.request`` set by ``wrap_http_for_auth``.
|
||||
authenticated_request_method = http.request
|
||||
|
||||
# The closure that will replace 'httplib2.Http.request'.
|
||||
def new_request(uri, method='GET', body=None, headers=None,
|
||||
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
||||
connection_type=None):
|
||||
if 'aud' in credentials._kwargs:
|
||||
# Preemptively refresh token, this is not done for OAuth2
|
||||
if (credentials.access_token is None or
|
||||
credentials.access_token_expired):
|
||||
credentials.refresh(None)
|
||||
return authenticated_request_method(uri, method, body,
|
||||
headers, redirections,
|
||||
connection_type)
|
||||
else:
|
||||
# If we don't have an 'aud' (audience) claim,
|
||||
# create a 1-time token with the uri root as the audience
|
||||
headers = _initialize_headers(headers)
|
||||
_apply_user_agent(headers, credentials.user_agent)
|
||||
uri_root = uri.split('?', 1)[0]
|
||||
token, unused_expiry = credentials._create_token({'aud': uri_root})
|
||||
|
||||
headers['Authorization'] = 'Bearer ' + token
|
||||
return orig_request_method(uri, method, body,
|
||||
clean_headers(headers),
|
||||
redirections, connection_type)
|
||||
|
||||
# Replace the request method with our own closure.
|
||||
http.request = new_request
|
||||
|
||||
|
||||
_CACHED_HTTP = httplib2.Http(MemoryCache())
|
||||
206
src/oauth2client/util.py
Normal file
206
src/oauth2client/util.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Common utility library."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
__author__ = [
|
||||
'rafek@google.com (Rafe Kaplan)',
|
||||
'guido@google.com (Guido van Rossum)',
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
'positional',
|
||||
'POSITIONAL_WARNING',
|
||||
'POSITIONAL_EXCEPTION',
|
||||
'POSITIONAL_IGNORE',
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = 'WARNING'
|
||||
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
||||
POSITIONAL_IGNORE = 'IGNORE'
|
||||
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
||||
POSITIONAL_IGNORE])
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments my be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
|
||||
fn(10, 'kw1', 'kw2') # Raises exception.
|
||||
fn(10, kwonly1='kw1') # Ok.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
To define a function like above, do::
|
||||
|
||||
@positional(1)
|
||||
def fn(pos1, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
If no default value is provided to a keyword argument, it becomes a
|
||||
required keyword argument::
|
||||
|
||||
@positional(0)
|
||||
def fn(required_kw):
|
||||
...
|
||||
|
||||
This must be called with the keyword parameter::
|
||||
|
||||
fn() # Raises exception.
|
||||
fn(10) # Raises exception.
|
||||
fn(required_kw=10) # Ok.
|
||||
|
||||
When defining instance or class methods always remember to account for
|
||||
``self`` and ``cls``::
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
@positional(2)
|
||||
def my_method(self, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@positional(2)
|
||||
def my_method(cls, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``util.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after the this index must be
|
||||
keyword only.
|
||||
|
||||
Returns:
|
||||
A decorator that prevents using arguments after max_positional_args
|
||||
from being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError: if a key-word only argument is provided as a positional
|
||||
parameter, but only if
|
||||
util.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
"""
|
||||
|
||||
def positional_decorator(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ''
|
||||
if max_positional_args != 1:
|
||||
plural_s = 's'
|
||||
message = ('{function}() takes at most {args_max} positional '
|
||||
'argument{plural} ({args_given} given)'.format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s))
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
return wrapped(*args, **kwargs)
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, six.integer_types):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def scopes_to_string(scopes):
|
||||
"""Converts scope value to a string.
|
||||
|
||||
If scopes is a string then it is simply passed through. If scopes is an
|
||||
iterable then a string is returned that is all the individual scopes
|
||||
concatenated with spaces.
|
||||
|
||||
Args:
|
||||
scopes: string or iterable of strings, the scopes.
|
||||
|
||||
Returns:
|
||||
The scopes formatted as a single string.
|
||||
"""
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes
|
||||
else:
|
||||
return ' '.join(scopes)
|
||||
|
||||
|
||||
def string_to_scopes(scopes):
|
||||
"""Converts stringifed scope value to a list.
|
||||
|
||||
If scopes is a list then it is simply passed through. If scopes is an
|
||||
string then a list of each individual scope is returned.
|
||||
|
||||
Args:
|
||||
scopes: a string or iterable of strings, the scopes.
|
||||
|
||||
Returns:
|
||||
The scopes in a list.
|
||||
"""
|
||||
if not scopes:
|
||||
return []
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes.split(' ')
|
||||
else:
|
||||
return scopes
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
Replaces the current value if it already exists in the URL.
|
||||
|
||||
Args:
|
||||
url: string, url to add the query parameter to.
|
||||
name: string, query parameter name.
|
||||
value: string, query parameter value.
|
||||
|
||||
Returns:
|
||||
Updated query parameter. Does not update the url if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
parsed = list(urllib.parse.urlparse(url))
|
||||
q = dict(urllib.parse.parse_qsl(parsed[4]))
|
||||
q[name] = value
|
||||
parsed[4] = urllib.parse.urlencode(q)
|
||||
return urllib.parse.urlunparse(parsed)
|
||||
3
src/passlib/__init__.py
Normal file
3
src/passlib/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""passlib - suite of password hashing & generation routines"""
|
||||
|
||||
__version__ = '1.6.5'
|
||||
1
src/passlib/_setup/__init__.py
Normal file
1
src/passlib/_setup/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""passlib.setup - helpers used by passlib's setup.py script"""
|
||||
87
src/passlib/_setup/docdist.py
Normal file
87
src/passlib/_setup/docdist.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""custom command to build doc.zip file"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import os
|
||||
from distutils import dir_util
|
||||
from distutils.cmd import Command
|
||||
from distutils.errors import *
|
||||
from distutils.spawn import spawn
|
||||
# local
|
||||
__all__ = [
|
||||
"docdist"
|
||||
]
|
||||
#=============================================================================
|
||||
# command
|
||||
#=============================================================================
|
||||
class docdist(Command):
|
||||
|
||||
description = "create zip file containing standalone html docs"
|
||||
|
||||
user_options = [
|
||||
('build-dir=', None, 'Build directory'),
|
||||
('dist-dir=', 'd',
|
||||
"directory to put the source distribution archive(s) in "
|
||||
"[default: dist]"),
|
||||
('format=', 'f',
|
||||
"archive format to create (tar, ztar, gztar, zip)"),
|
||||
('sign', 's', 'sign files using gpg'),
|
||||
('identity=', 'i', 'GPG identity used to sign files'),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_dir = None
|
||||
self.dist_dir = None
|
||||
self.format = None
|
||||
self.keep_temp = False
|
||||
self.sign = False
|
||||
self.identity = None
|
||||
|
||||
def finalize_options(self):
|
||||
if self.identity and not self.sign:
|
||||
raise DistutilsOptionError(
|
||||
"Must use --sign for --identity to have meaning"
|
||||
)
|
||||
if self.build_dir is None:
|
||||
cmd = self.get_finalized_command('build')
|
||||
self.build_dir = os.path.join(cmd.build_base, 'docdist')
|
||||
if not self.dist_dir:
|
||||
self.dist_dir = "dist"
|
||||
if not self.format:
|
||||
self.format = "zip"
|
||||
|
||||
def run(self):
|
||||
# call build sphinx to build docs
|
||||
self.run_command("build_sphinx")
|
||||
cmd = self.get_finalized_command("build_sphinx")
|
||||
source_dir = cmd.builder_target_dir
|
||||
|
||||
# copy to directory with appropriate name
|
||||
dist = self.distribution
|
||||
arc_name = "%s-docs-%s" % (dist.get_name(), dist.get_version())
|
||||
tmp_dir = os.path.join(self.build_dir, arc_name)
|
||||
if os.path.exists(tmp_dir):
|
||||
dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
|
||||
self.copy_tree(source_dir, tmp_dir, preserve_symlinks=True)
|
||||
|
||||
# make archive from dir
|
||||
arc_base = os.path.join(self.dist_dir, arc_name)
|
||||
self.arc_filename = self.make_archive(arc_base, self.format,
|
||||
self.build_dir)
|
||||
|
||||
# Sign if requested
|
||||
if self.sign:
|
||||
gpg_args = ["gpg", "--detach-sign", "-a", self.arc_filename]
|
||||
if self.identity:
|
||||
gpg_args[2:2] = ["--local-user", self.identity]
|
||||
spawn(gpg_args,
|
||||
dry_run=self.dry_run)
|
||||
|
||||
# cleanup
|
||||
if not self.keep_temp:
|
||||
dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
57
src/passlib/_setup/stamp.py
Normal file
57
src/passlib/_setup/stamp.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""update version string during build"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from distutils.dist import Distribution
|
||||
# pkg
|
||||
# local
|
||||
__all__ = [
|
||||
"stamp_source",
|
||||
"stamp_distutils_output",
|
||||
]
|
||||
#=============================================================================
|
||||
# helpers
|
||||
#=============================================================================
|
||||
def get_command_class(opts, name):
|
||||
return opts['cmdclass'].get(name) or Distribution().get_command_class(name)
|
||||
|
||||
def stamp_source(base_dir, version, dry_run=False):
|
||||
"""update version string in passlib dist"""
|
||||
path = os.path.join(base_dir, "passlib", "__init__.py")
|
||||
with open(path) as fh:
|
||||
input = fh.read()
|
||||
output, count = re.subn('(?m)^__version__\s*=.*$',
|
||||
'__version__ = ' + repr(version),
|
||||
input)
|
||||
assert count == 1, "failed to replace version string"
|
||||
if not dry_run:
|
||||
os.unlink(path) # sdist likes to use hardlinks
|
||||
with open(path, "w") as fh:
|
||||
fh.write(output)
|
||||
|
||||
def stamp_distutils_output(opts, version):
|
||||
|
||||
# subclass buildpy to update version string in source
|
||||
_build_py = get_command_class(opts, "build_py")
|
||||
class build_py(_build_py):
|
||||
def build_packages(self):
|
||||
_build_py.build_packages(self)
|
||||
stamp_source(self.build_lib, version, self.dry_run)
|
||||
opts['cmdclass']['build_py'] = build_py
|
||||
|
||||
# subclass sdist to do same thing
|
||||
_sdist = get_command_class(opts, "sdist")
|
||||
class sdist(_sdist):
|
||||
def make_release_tree(self, base_dir, files):
|
||||
_sdist.make_release_tree(self, base_dir, files)
|
||||
stamp_source(base_dir, version, self.dry_run)
|
||||
opts['cmdclass']['sdist'] = sdist
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
1069
src/passlib/apache.py
Normal file
1069
src/passlib/apache.py
Normal file
File diff suppressed because it is too large
Load Diff
192
src/passlib/apps.py
Normal file
192
src/passlib/apps.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""passlib.apps"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from itertools import chain
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.context import LazyCryptContext
|
||||
from passlib.utils import sys_bits
|
||||
# local
|
||||
__all__ = [
|
||||
'custom_app_context',
|
||||
'django_context',
|
||||
'ldap_context', 'ldap_nocrypt_context',
|
||||
'mysql_context', 'mysql4_context', 'mysql3_context',
|
||||
'phpass_context',
|
||||
'phpbb3_context',
|
||||
'postgres_context',
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# master containing all identifiable hashes
|
||||
#=============================================================================
|
||||
def _load_master_config():
|
||||
from passlib.registry import list_crypt_handlers
|
||||
|
||||
# get master list
|
||||
schemes = list_crypt_handlers()
|
||||
|
||||
# exclude the ones we know have ambiguous or greedy identify() methods.
|
||||
excluded = [
|
||||
# frequently confused for eachother
|
||||
'bigcrypt',
|
||||
'crypt16',
|
||||
|
||||
# no good identifiers
|
||||
'cisco_pix',
|
||||
'cisco_type7',
|
||||
'htdigest',
|
||||
'mysql323',
|
||||
'oracle10',
|
||||
|
||||
# all have same size
|
||||
'lmhash',
|
||||
'msdcc',
|
||||
'msdcc2',
|
||||
'nthash',
|
||||
|
||||
# plaintext handlers
|
||||
'plaintext',
|
||||
'ldap_plaintext',
|
||||
|
||||
# disabled handlers
|
||||
'django_disabled',
|
||||
'unix_disabled',
|
||||
'unix_fallback',
|
||||
]
|
||||
for name in excluded:
|
||||
schemes.remove(name)
|
||||
|
||||
# return config
|
||||
return dict(schemes=schemes, default="sha256_crypt")
|
||||
master_context = LazyCryptContext(onload=_load_master_config)
|
||||
|
||||
#=============================================================================
|
||||
# for quickly bootstrapping new custom applications
|
||||
#=============================================================================
|
||||
custom_app_context = LazyCryptContext(
|
||||
# choose some reasonbly strong schemes
|
||||
schemes=["sha512_crypt", "sha256_crypt"],
|
||||
|
||||
# set some useful global options
|
||||
default="sha256_crypt" if sys_bits < 64 else "sha512_crypt",
|
||||
all__vary_rounds = 0.1,
|
||||
|
||||
# set a good starting point for rounds selection
|
||||
sha512_crypt__min_rounds = 535000,
|
||||
sha256_crypt__min_rounds = 535000,
|
||||
|
||||
# if the admin user category is selected, make a much stronger hash,
|
||||
admin__sha512_crypt__min_rounds = 1024000,
|
||||
admin__sha256_crypt__min_rounds = 1024000,
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# django
|
||||
#=============================================================================
|
||||
_django10_schemes = [
|
||||
"django_salted_sha1", "django_salted_md5", "django_des_crypt",
|
||||
"hex_md5", "django_disabled",
|
||||
]
|
||||
|
||||
django10_context = LazyCryptContext(
|
||||
schemes=_django10_schemes,
|
||||
default="django_salted_sha1",
|
||||
deprecated=["hex_md5"],
|
||||
)
|
||||
|
||||
_django14_schemes = ["django_pbkdf2_sha256", "django_pbkdf2_sha1",
|
||||
"django_bcrypt"] + _django10_schemes
|
||||
django14_context = LazyCryptContext(
|
||||
schemes=_django14_schemes,
|
||||
deprecated=_django10_schemes,
|
||||
)
|
||||
|
||||
_django16_schemes = _django14_schemes[:]
|
||||
_django16_schemes.insert(1, "django_bcrypt_sha256")
|
||||
django16_context = LazyCryptContext(
|
||||
schemes=_django16_schemes,
|
||||
deprecated=_django10_schemes,
|
||||
)
|
||||
|
||||
# this will always point to latest version
|
||||
django_context = django16_context
|
||||
|
||||
#=============================================================================
|
||||
# ldap
|
||||
#=============================================================================
|
||||
std_ldap_schemes = ["ldap_salted_sha1", "ldap_salted_md5",
|
||||
"ldap_sha1", "ldap_md5",
|
||||
"ldap_plaintext" ]
|
||||
|
||||
# create context with all std ldap schemes EXCEPT crypt
|
||||
ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes)
|
||||
|
||||
# create context with all possible std ldap + ldap crypt schemes
|
||||
def _iter_ldap_crypt_schemes():
|
||||
from passlib.utils import unix_crypt_schemes
|
||||
return ('ldap_' + name for name in unix_crypt_schemes)
|
||||
|
||||
def _iter_ldap_schemes():
|
||||
"""helper which iterates over supported std ldap schemes"""
|
||||
return chain(std_ldap_schemes, _iter_ldap_crypt_schemes())
|
||||
ldap_context = LazyCryptContext(_iter_ldap_schemes())
|
||||
|
||||
### create context with all std ldap schemes + crypt schemes for localhost
|
||||
##def _iter_host_ldap_schemes():
|
||||
## "helper which iterates over supported std ldap schemes"
|
||||
## from passlib.handlers.ldap_digests import get_host_ldap_crypt_schemes
|
||||
## return chain(std_ldap_schemes, get_host_ldap_crypt_schemes())
|
||||
##ldap_host_context = LazyCryptContext(_iter_host_ldap_schemes())
|
||||
|
||||
#=============================================================================
|
||||
# mysql
|
||||
#=============================================================================
|
||||
mysql3_context = LazyCryptContext(["mysql323"])
|
||||
mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323")
|
||||
mysql_context = mysql4_context # tracks latest mysql version supported
|
||||
|
||||
#=============================================================================
|
||||
# postgres
|
||||
#=============================================================================
|
||||
postgres_context = LazyCryptContext(["postgres_md5"])
|
||||
|
||||
#=============================================================================
|
||||
# phpass & variants
|
||||
#=============================================================================
|
||||
def _create_phpass_policy(**kwds):
|
||||
"""helper to choose default alg based on bcrypt availability"""
|
||||
kwds['default'] = 'bcrypt' if hash.bcrypt.has_backend() else 'phpass'
|
||||
return kwds
|
||||
|
||||
phpass_context = LazyCryptContext(
|
||||
schemes=["bcrypt", "phpass", "bsdi_crypt"],
|
||||
onload=_create_phpass_policy,
|
||||
)
|
||||
|
||||
phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H")
|
||||
|
||||
# TODO: support the drupal phpass variants (see phpass homepage)
|
||||
|
||||
#=============================================================================
|
||||
# roundup
|
||||
#=============================================================================
|
||||
|
||||
_std_roundup_schemes = [ "ldap_hex_sha1", "ldap_hex_md5", "ldap_des_crypt", "roundup_plaintext" ]
|
||||
roundup10_context = LazyCryptContext(_std_roundup_schemes)
|
||||
|
||||
# NOTE: 'roundup15' really applies to roundup 1.4.17+
|
||||
roundup_context = roundup15_context = LazyCryptContext(
|
||||
schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ],
|
||||
deprecated=_std_roundup_schemes,
|
||||
default = "ldap_pbkdf2_sha1",
|
||||
ldap_pbkdf2_sha1__default_rounds = 10000,
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
2719
src/passlib/context.py
Normal file
2719
src/passlib/context.py
Normal file
File diff suppressed because it is too large
Load Diff
193
src/passlib/exc.py
Normal file
193
src/passlib/exc.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""passlib.exc -- exceptions & warnings raised by passlib"""
|
||||
#=============================================================================
|
||||
# exceptions
|
||||
#=============================================================================
|
||||
class MissingBackendError(RuntimeError):
|
||||
"""Error raised if multi-backend handler has no available backends;
|
||||
or if specifically requested backend is not available.
|
||||
|
||||
:exc:`!MissingBackendError` derives
|
||||
from :exc:`RuntimeError`, since it usually indicates
|
||||
lack of an external library or OS feature.
|
||||
This is primarily raised by handlers which depend on
|
||||
external libraries (which is currently just
|
||||
:class:`~passlib.hash.bcrypt`).
|
||||
"""
|
||||
|
||||
class PasswordSizeError(ValueError):
|
||||
"""Error raised if a password exceeds the maximum size allowed
|
||||
by Passlib (4096 characters).
|
||||
|
||||
Many password hash algorithms take proportionately larger amounts of time and/or
|
||||
memory depending on the size of the password provided. This could present
|
||||
a potential denial of service (DOS) situation if a maliciously large
|
||||
password is provided to an application. Because of this, Passlib enforces
|
||||
a maximum size limit, but one which should be *much* larger
|
||||
than any legitimate password. :exc:`!PasswordSizeError` derives
|
||||
from :exc:`!ValueError`.
|
||||
|
||||
.. note::
|
||||
Applications wishing to use a different limit should set the
|
||||
``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before
|
||||
Passlib is loaded. The value can be any large positive integer.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
def __init__(self):
|
||||
ValueError.__init__(self, "password exceeds maximum allowed size")
|
||||
|
||||
# this also prevents a glibc crypt segfault issue, detailed here ...
|
||||
# http://www.openwall.com/lists/oss-security/2011/11/15/1
|
||||
|
||||
|
||||
class PasslibSecurityError(RuntimeError):
|
||||
"""
|
||||
Error raised if critical security issue is detected
|
||||
(e.g. an attempt is made to use a vulnerable version of a bcrypt backend).
|
||||
|
||||
.. versionadded:: 1.6.3
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# warnings
|
||||
#=============================================================================
|
||||
class PasslibWarning(UserWarning):
|
||||
"""base class for Passlib's user warnings,
|
||||
derives from the builtin :exc:`UserWarning`.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
class PasslibConfigWarning(PasslibWarning):
|
||||
"""Warning issued when non-fatal issue is found related to the configuration
|
||||
of a :class:`~passlib.context.CryptContext` instance.
|
||||
|
||||
This occurs primarily in one of two cases:
|
||||
|
||||
* The CryptContext contains rounds limits which exceed the hard limits
|
||||
imposed by the underlying algorithm.
|
||||
* An explicit rounds value was provided which exceeds the limits
|
||||
imposed by the CryptContext.
|
||||
|
||||
In both of these cases, the code will perform correctly & securely;
|
||||
but the warning is issued as a sign the configuration may need updating.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
class PasslibHashWarning(PasslibWarning):
|
||||
"""Warning issued when non-fatal issue is found with parameters
|
||||
or hash string passed to a passlib hash class.
|
||||
|
||||
This occurs primarily in one of two cases:
|
||||
|
||||
* A rounds value or other setting was explicitly provided which
|
||||
exceeded the handler's limits (and has been clamped
|
||||
by the :ref:`relaxed<relaxed-keyword>` flag).
|
||||
|
||||
* A malformed hash string was encountered which (while parsable)
|
||||
should be re-encoded.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
class PasslibRuntimeWarning(PasslibWarning):
|
||||
"""Warning issued when something unexpected happens during runtime.
|
||||
|
||||
The fact that it's a warning instead of an error means Passlib
|
||||
was able to correct for the issue, but that it's anomalous enough
|
||||
that the developers would love to hear under what conditions it occurred.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
class PasslibSecurityWarning(PasslibWarning):
|
||||
"""Special warning issued when Passlib encounters something
|
||||
that might affect security.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# error constructors
|
||||
#
|
||||
# note: these functions are used by the hashes in Passlib to raise common
|
||||
# error messages. They are currently just functions which return ValueError,
|
||||
# rather than subclasses of ValueError, since the specificity isn't needed
|
||||
# yet; and who wants to import a bunch of error classes when catching
|
||||
# ValueError will do?
|
||||
#=============================================================================
|
||||
|
||||
def _get_name(handler):
|
||||
return handler.name if handler else "<unnamed>"
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# generic helpers
|
||||
#------------------------------------------------------------------------
|
||||
def type_name(value):
|
||||
"""return pretty-printed string containing name of value's type"""
|
||||
cls = value.__class__
|
||||
if cls.__module__ and cls.__module__ not in ["__builtin__", "builtins"]:
|
||||
return "%s.%s" % (cls.__module__, cls.__name__)
|
||||
elif value is None:
|
||||
return 'None'
|
||||
else:
|
||||
return cls.__name__
|
||||
|
||||
def ExpectedTypeError(value, expected, param):
|
||||
"""error message when param was supposed to be one type, but found another"""
|
||||
# NOTE: value is never displayed, since it may sometimes be a password.
|
||||
name = type_name(value)
|
||||
return TypeError("%s must be %s, not %s" % (param, expected, name))
|
||||
|
||||
def ExpectedStringError(value, param):
|
||||
"""error message when param was supposed to be unicode or bytes"""
|
||||
return ExpectedTypeError(value, "unicode or bytes", param)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# encrypt/verify parameter errors
|
||||
#------------------------------------------------------------------------
|
||||
def MissingDigestError(handler=None):
|
||||
"""raised when verify() method gets passed config string instead of hash"""
|
||||
name = _get_name(handler)
|
||||
return ValueError("expected %s hash, got %s config string instead" %
|
||||
(name, name))
|
||||
|
||||
def NullPasswordError(handler=None):
|
||||
"""raised by OS crypt() supporting hashes, which forbid NULLs in password"""
|
||||
name = _get_name(handler)
|
||||
return ValueError("%s does not allow NULL bytes in password" % name)
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# errors when parsing hashes
|
||||
#------------------------------------------------------------------------
|
||||
def InvalidHashError(handler=None):
|
||||
"""error raised if unrecognized hash provided to handler"""
|
||||
return ValueError("not a valid %s hash" % _get_name(handler))
|
||||
|
||||
def MalformedHashError(handler=None, reason=None):
|
||||
"""error raised if recognized-but-malformed hash provided to handler"""
|
||||
text = "malformed %s hash" % _get_name(handler)
|
||||
if reason:
|
||||
text = "%s (%s)" % (text, reason)
|
||||
return ValueError(text)
|
||||
|
||||
def ZeroPaddedRoundsError(handler=None):
|
||||
"""error raised if hash was recognized but contained zero-padded rounds field"""
|
||||
return MalformedHashError(handler, "zero-padded rounds")
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# settings / hash component errors
|
||||
#------------------------------------------------------------------------
|
||||
def ChecksumSizeError(handler, raw=False):
|
||||
"""error raised if hash was recognized, but checksum was wrong size"""
|
||||
# TODO: if handler.use_defaults is set, this came from app-provided value,
|
||||
# not from parsing a hash string, might want different error msg.
|
||||
checksum_size = handler.checksum_size
|
||||
unit = "bytes" if raw else "chars"
|
||||
reason = "checksum must be exactly %d %s" % (checksum_size, unit)
|
||||
return MalformedHashError(handler, reason)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
1
src/passlib/ext/__init__.py
Normal file
1
src/passlib/ext/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
6
src/passlib/ext/django/__init__.py
Normal file
6
src/passlib/ext/django/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""passlib.ext.django.models -- monkeypatch django hashing framework
|
||||
|
||||
this plugin monkeypatches django's hashing framework
|
||||
so that it uses a passlib context object, allowing handling of arbitrary
|
||||
hashes in Django databases.
|
||||
"""
|
||||
328
src/passlib/ext/django/models.py
Normal file
328
src/passlib/ext/django/models.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""passlib.ext.django.models -- monkeypatch django hashing framework"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
from django import VERSION
|
||||
from django.conf import settings
|
||||
# pkg
|
||||
from passlib.context import CryptContext
|
||||
from passlib.exc import ExpectedTypeError
|
||||
from passlib.ext.django.utils import _PatchManager, hasher_to_passlib_name, \
|
||||
get_passlib_hasher, get_preset_config
|
||||
from passlib.utils.compat import callable, unicode, bytes
|
||||
# local
|
||||
__all__ = ["password_context"]
|
||||
|
||||
#=============================================================================
|
||||
# global attrs
|
||||
#=============================================================================
|
||||
|
||||
# the context object which this patches contrib.auth to use for password hashing.
|
||||
# configuration controlled by ``settings.PASSLIB_CONFIG``.
|
||||
password_context = CryptContext()
|
||||
|
||||
# function mapping User objects -> passlib user category.
|
||||
# may be overridden via ``settings.PASSLIB_GET_CATEGORY``.
|
||||
def _get_category(user):
|
||||
"""default get_category() implementation"""
|
||||
if user.is_superuser:
|
||||
return "superuser"
|
||||
elif user.is_staff:
|
||||
return "staff"
|
||||
else:
|
||||
return None
|
||||
|
||||
# object used to track state of patches applied to django.
|
||||
_manager = _PatchManager(log=logging.getLogger(__name__ + "._manager"))
|
||||
|
||||
# patch status
|
||||
_patched = False
|
||||
|
||||
#=============================================================================
|
||||
# applying & removing the patches
|
||||
#=============================================================================
|
||||
def _apply_patch():
|
||||
"""monkeypatch django's password handling to use ``passlib_context``,
|
||||
assumes the caller will configure the object.
|
||||
"""
|
||||
#
|
||||
# setup constants
|
||||
#
|
||||
log.debug("preparing to monkeypatch 'django.contrib.auth' ...")
|
||||
global _patched
|
||||
assert not _patched, "monkeypatching already applied"
|
||||
HASHERS_PATH = "django.contrib.auth.hashers"
|
||||
MODELS_PATH = "django.contrib.auth.models"
|
||||
USER_PATH = MODELS_PATH + ":User"
|
||||
FORMS_PATH = "django.contrib.auth.forms"
|
||||
|
||||
#
|
||||
# import UNUSABLE_PASSWORD and is_password_usable() helpers
|
||||
# (providing stubs for older django versions)
|
||||
#
|
||||
if VERSION < (1,4):
|
||||
has_hashers = False
|
||||
if VERSION < (1,0):
|
||||
UNUSABLE_PASSWORD = "!"
|
||||
else:
|
||||
from django.contrib.auth.models import UNUSABLE_PASSWORD
|
||||
|
||||
def is_password_usable(encoded):
|
||||
return (encoded is not None and encoded != UNUSABLE_PASSWORD)
|
||||
|
||||
def is_valid_secret(secret):
|
||||
return secret is not None
|
||||
|
||||
elif VERSION < (1,6):
|
||||
has_hashers = True
|
||||
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \
|
||||
is_password_usable
|
||||
|
||||
# NOTE: 1.4 - 1.5 - empty passwords no longer valid.
|
||||
def is_valid_secret(secret):
|
||||
return bool(secret)
|
||||
|
||||
else:
|
||||
has_hashers = True
|
||||
from django.contrib.auth.hashers import is_password_usable
|
||||
|
||||
# 1.6 - empty passwords valid again
|
||||
def is_valid_secret(secret):
|
||||
return secret is not None
|
||||
|
||||
if VERSION < (1,6):
|
||||
def make_unusable_password():
|
||||
return UNUSABLE_PASSWORD
|
||||
else:
|
||||
from django.contrib.auth.hashers import make_password as _make_password
|
||||
def make_unusable_password():
|
||||
return _make_password(None)
|
||||
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes
|
||||
has_unsalted_sha1 = (VERSION >= (1,4,6))
|
||||
|
||||
#
|
||||
# backport ``User.set_unusable_password()`` for Django 0.9
|
||||
# (simplifies rest of the code)
|
||||
#
|
||||
if not hasattr(_manager.getorig(USER_PATH), "set_unusable_password"):
|
||||
assert VERSION < (1,0)
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def set_unusable_password(user):
|
||||
user.password = make_unusable_password()
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def has_usable_password(user):
|
||||
return is_password_usable(user.password)
|
||||
|
||||
#
|
||||
# patch ``User.set_password() & ``User.check_password()`` to use
|
||||
# context & get_category (would just leave these as wrappers for hashers
|
||||
# module under django 1.4, but then we couldn't pass User object into
|
||||
# get_category very easily)
|
||||
#
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def set_password(user, password):
|
||||
"""passlib replacement for User.set_password()"""
|
||||
if is_valid_secret(password):
|
||||
# NOTE: pulls _get_category from module globals
|
||||
cat = _get_category(user)
|
||||
user.password = password_context.encrypt(password, category=cat)
|
||||
else:
|
||||
user.set_unusable_password()
|
||||
|
||||
@_manager.monkeypatch(USER_PATH)
|
||||
def check_password(user, password):
|
||||
"""passlib replacement for User.check_password()"""
|
||||
hash = user.password
|
||||
if not is_valid_secret(password) or not is_password_usable(hash):
|
||||
return False
|
||||
if not hash and VERSION < (1,4):
|
||||
return False
|
||||
# NOTE: pulls _get_category from module globals
|
||||
cat = _get_category(user)
|
||||
ok, new_hash = password_context.verify_and_update(password, hash,
|
||||
category=cat)
|
||||
if ok and new_hash is not None:
|
||||
# migrate to new hash if needed.
|
||||
user.password = new_hash
|
||||
user.save()
|
||||
return ok
|
||||
|
||||
#
|
||||
# override check_password() with our own implementation
|
||||
#
|
||||
@_manager.monkeypatch(HASHERS_PATH, enable=has_hashers)
|
||||
@_manager.monkeypatch(MODELS_PATH)
|
||||
def check_password(password, encoded, setter=None, preferred="default"):
|
||||
"""passlib replacement for check_password()"""
|
||||
# XXX: this currently ignores "preferred" keyword, since its purpose
|
||||
# was for hash migration, and that's handled by the context.
|
||||
if not is_valid_secret(password) or not is_password_usable(encoded):
|
||||
return False
|
||||
ok = password_context.verify(password, encoded)
|
||||
if ok and setter and password_context.needs_update(encoded):
|
||||
setter(password)
|
||||
return ok
|
||||
|
||||
#
|
||||
# patch the other functions defined in the ``hashers`` module, as well
|
||||
# as any other known locations where they're imported within ``contrib.auth``
|
||||
#
|
||||
if has_hashers:
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(MODELS_PATH)
|
||||
def make_password(password, salt=None, hasher="default"):
|
||||
"""passlib replacement for make_password()"""
|
||||
if not is_valid_secret(password):
|
||||
return make_unusable_password()
|
||||
if hasher == "default":
|
||||
scheme = None
|
||||
else:
|
||||
scheme = hasher_to_passlib_name(hasher)
|
||||
kwds = dict(scheme=scheme)
|
||||
handler = password_context.handler(scheme)
|
||||
if "salt" in handler.setting_kwds:
|
||||
if hasher.startswith("unsalted_"):
|
||||
# Django 1.4.6+ uses a separate 'unsalted_sha1' hasher for "sha1$$digest",
|
||||
# but passlib just reuses it's "sha1" handler ("sha1$salt$digest"). To make
|
||||
# this work, have to explicitly tell the sha1 handler to use an empty salt.
|
||||
kwds['salt'] = ''
|
||||
elif salt:
|
||||
# Django make_password() autogenerates a salt if salt is bool False (None / ''),
|
||||
# so we only pass the keyword on if there's actually a fixed salt.
|
||||
kwds['salt'] = salt
|
||||
return password_context.encrypt(password, **kwds)
|
||||
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(FORMS_PATH)
|
||||
def get_hasher(algorithm="default"):
|
||||
"""passlib replacement for get_hasher()"""
|
||||
if algorithm == "default":
|
||||
scheme = None
|
||||
else:
|
||||
scheme = hasher_to_passlib_name(algorithm)
|
||||
# NOTE: resolving scheme -> handler instead of
|
||||
# passing scheme into get_passlib_hasher(),
|
||||
# in case context contains custom handler
|
||||
# shadowing name of a builtin handler.
|
||||
handler = password_context.handler(scheme)
|
||||
return get_passlib_hasher(handler, algorithm=algorithm)
|
||||
|
||||
# identify_hasher() was added in django 1.5,
|
||||
# patching it anyways for 1.4, so passlib's version is always available.
|
||||
@_manager.monkeypatch(HASHERS_PATH)
|
||||
@_manager.monkeypatch(FORMS_PATH)
|
||||
def identify_hasher(encoded):
|
||||
"""passlib helper to identify hasher from encoded password"""
|
||||
handler = password_context.identify(encoded, resolve=True,
|
||||
required=True)
|
||||
algorithm = None
|
||||
if (has_unsalted_sha1 and handler.name == "django_salted_sha1" and
|
||||
encoded.startswith("sha1$$")):
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
|
||||
# but passlib just reuses the "sha1$salt$digest" handler.
|
||||
# we want to resolve to correct django hasher.
|
||||
algorithm = "unsalted_sha1"
|
||||
return get_passlib_hasher(handler, algorithm=algorithm)
|
||||
|
||||
_patched = True
|
||||
log.debug("... finished monkeypatching django")
|
||||
|
||||
def _remove_patch():
|
||||
"""undo the django monkeypatching done by this module.
|
||||
offered as a last resort if it's ever needed.
|
||||
|
||||
.. warning::
|
||||
This may cause problems if any other Django modules have imported
|
||||
their own copies of the patched functions, though the patched
|
||||
code has been designed to throw an error as soon as possible in
|
||||
this case.
|
||||
"""
|
||||
global _patched
|
||||
if _patched:
|
||||
log.debug("removing django monkeypatching...")
|
||||
_manager.unpatch_all(unpatch_conflicts=True)
|
||||
password_context.load({})
|
||||
_patched = False
|
||||
log.debug("...finished removing django monkeypatching")
|
||||
return True
|
||||
if _manager: # pragma: no cover -- sanity check
|
||||
log.warning("reverting partial monkeypatching of django...")
|
||||
_manager.unpatch_all()
|
||||
password_context.load({})
|
||||
log.debug("...finished removing django monkeypatching")
|
||||
return True
|
||||
log.debug("django not monkeypatched")
|
||||
return False
|
||||
|
||||
#=============================================================================
|
||||
# main code
|
||||
#=============================================================================
|
||||
def _load():
|
||||
global _get_category
|
||||
|
||||
# TODO: would like to add support for inheriting config from a preset
|
||||
# (or from existing hasher state) and letting PASSLIB_CONFIG
|
||||
# be an update, not a replacement.
|
||||
|
||||
# TODO: wrap and import any custom hashers as passlib handlers,
|
||||
# so they could be used in the passlib config.
|
||||
|
||||
# load config from settings
|
||||
_UNSET = object()
|
||||
config = getattr(settings, "PASSLIB_CONFIG", _UNSET)
|
||||
if config is _UNSET:
|
||||
# XXX: should probably deprecate this alias
|
||||
config = getattr(settings, "PASSLIB_CONTEXT", _UNSET)
|
||||
if config is _UNSET:
|
||||
config = "passlib-default"
|
||||
if config is None:
|
||||
warn("setting PASSLIB_CONFIG=None is deprecated, "
|
||||
"and support will be removed in Passlib 1.8, "
|
||||
"use PASSLIB_CONFIG='disabled' instead.",
|
||||
DeprecationWarning)
|
||||
config = "disabled"
|
||||
elif not isinstance(config, (unicode, bytes, dict)):
|
||||
raise ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG")
|
||||
|
||||
# load custom category func (if any)
|
||||
get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None)
|
||||
if get_category and not callable(get_category):
|
||||
raise ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY")
|
||||
|
||||
# check if we've been disabled
|
||||
if config == "disabled":
|
||||
if _patched: # pragma: no cover -- sanity check
|
||||
log.error("didn't expect monkeypatching would be applied!")
|
||||
_remove_patch()
|
||||
return
|
||||
|
||||
# resolve any preset aliases
|
||||
if isinstance(config, str) and '\n' not in config:
|
||||
config = get_preset_config(config)
|
||||
|
||||
# setup context
|
||||
_apply_patch()
|
||||
password_context.load(config)
|
||||
if get_category:
|
||||
# NOTE: _get_category is module global which is read by
|
||||
# monkeypatched functions constructed by _apply_patch()
|
||||
_get_category = get_category
|
||||
log.debug("passlib.ext.django loaded")
|
||||
|
||||
# wrap load function so we can undo any patching if something goes wrong
|
||||
try:
|
||||
_load()
|
||||
except:
|
||||
_remove_patch()
|
||||
raise
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
507
src/passlib/ext/django/utils.py
Normal file
507
src/passlib/ext/django/utils.py
Normal file
@@ -0,0 +1,507 @@
|
||||
"""passlib.ext.django.utils - helper functions used by this plugin"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from weakref import WeakKeyDictionary
|
||||
from warnings import warn
|
||||
# site
|
||||
try:
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
log.debug("found django %r installation", DJANGO_VERSION)
|
||||
except ImportError:
|
||||
log.debug("django installation not found")
|
||||
DJANGO_VERSION = ()
|
||||
# pkg
|
||||
from passlib.context import CryptContext
|
||||
from passlib.exc import PasslibRuntimeWarning
|
||||
from passlib.registry import get_crypt_handler, list_crypt_handlers
|
||||
from passlib.utils import classproperty
|
||||
from passlib.utils.compat import bytes, get_method_function, iteritems
|
||||
# local
|
||||
__all__ = [
|
||||
"get_preset_config",
|
||||
"get_passlib_hasher",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# default policies
|
||||
#=============================================================================
|
||||
|
||||
# map preset names -> passlib.app attrs
|
||||
_preset_map = {
|
||||
"django-1.0": "django10_context",
|
||||
"django-1.4": "django14_context",
|
||||
"django-1.6": "django16_context",
|
||||
"django-latest": "django_context",
|
||||
}
|
||||
|
||||
def get_preset_config(name):
|
||||
"""Returns configuration string for one of the preset strings
|
||||
supported by the ``PASSLIB_CONFIG`` setting.
|
||||
Currently supported presets:
|
||||
|
||||
* ``"passlib-default"`` - default config used by this release of passlib.
|
||||
* ``"django-default"`` - config matching currently installed django version.
|
||||
* ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.6"``).
|
||||
* ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs
|
||||
* ``"django-1.4"`` - config used by stock Django 1.4 installs
|
||||
* ``"django-1.6"`` - config used by stock Django 1.6 installs
|
||||
"""
|
||||
# TODO: add preset which includes HASHERS + PREFERRED_HASHERS,
|
||||
# after having imported any custom hashers. e.g. "django-current"
|
||||
if name == "django-default":
|
||||
if not DJANGO_VERSION:
|
||||
raise ValueError("can't resolve django-default preset, "
|
||||
"django not installed")
|
||||
if DJANGO_VERSION < (1,4):
|
||||
name = "django-1.0"
|
||||
elif DJANGO_VERSION < (1,6):
|
||||
name = "django-1.4"
|
||||
else:
|
||||
name = "django-1.6"
|
||||
if name == "passlib-default":
|
||||
return PASSLIB_DEFAULT
|
||||
try:
|
||||
attr = _preset_map[name]
|
||||
except KeyError:
|
||||
raise ValueError("unknown preset config name: %r" % name)
|
||||
import passlib.apps
|
||||
return getattr(passlib.apps, attr).to_string()
|
||||
|
||||
# default context used by passlib 1.6
|
||||
PASSLIB_DEFAULT = """
|
||||
[passlib]
|
||||
|
||||
; list of schemes supported by configuration
|
||||
; currently all django 1.6, 1.4, and 1.0 hashes,
|
||||
; and three common modular crypt format hashes.
|
||||
schemes =
|
||||
django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_bcrypt_sha256,
|
||||
django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5,
|
||||
sha512_crypt, bcrypt, phpass
|
||||
|
||||
; default scheme to use for new hashes
|
||||
default = django_pbkdf2_sha256
|
||||
|
||||
; hashes using these schemes will automatically be re-hashed
|
||||
; when the user logs in (currently all django 1.0 hashes)
|
||||
deprecated =
|
||||
django_pbkdf2_sha1, django_salted_sha1, django_salted_md5,
|
||||
django_des_crypt, hex_md5
|
||||
|
||||
; sets some common options, including minimum rounds for two primary hashes.
|
||||
; if a hash has less than this number of rounds, it will be re-hashed.
|
||||
all__vary_rounds = 0.05
|
||||
sha512_crypt__min_rounds = 80000
|
||||
django_pbkdf2_sha256__min_rounds = 10000
|
||||
|
||||
; set somewhat stronger iteration counts for ``User.is_staff``
|
||||
staff__sha512_crypt__default_rounds = 100000
|
||||
staff__django_pbkdf2_sha256__default_rounds = 12500
|
||||
|
||||
; and even stronger ones for ``User.is_superuser``
|
||||
superuser__sha512_crypt__default_rounds = 120000
|
||||
superuser__django_pbkdf2_sha256__default_rounds = 15000
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# translating passlib names <-> hasher names
|
||||
#=============================================================================
|
||||
|
||||
# prefix used to shoehorn passlib's handler names into django hasher namespace;
|
||||
# allows get_hasher() to be meaningfully called even if passlib handler
|
||||
# is the one being used.
|
||||
PASSLIB_HASHER_PREFIX = "passlib_"
|
||||
|
||||
# prefix all the django-specific hash formats are stored under w/in passlib;
|
||||
# all of these hashes should expose their hasher name via ``.django_name``.
|
||||
DJANGO_PASSLIB_PREFIX = "django_"
|
||||
|
||||
# non-django-specific hashes which also expose ``.django_name``.
|
||||
_other_django_hashes = ["hex_md5"]
|
||||
|
||||
def passlib_to_hasher_name(passlib_name):
|
||||
"""convert passlib handler name -> hasher name"""
|
||||
handler = get_crypt_handler(passlib_name)
|
||||
if hasattr(handler, "django_name"):
|
||||
return handler.django_name
|
||||
return PASSLIB_HASHER_PREFIX + passlib_name
|
||||
|
||||
def hasher_to_passlib_name(hasher_name):
|
||||
"""convert hasher name -> passlib handler name"""
|
||||
if hasher_name.startswith(PASSLIB_HASHER_PREFIX):
|
||||
return hasher_name[len(PASSLIB_HASHER_PREFIX):]
|
||||
if hasher_name == "unsalted_sha1":
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
|
||||
# but passlib just reuses the "sha1$salt$digest" handler.
|
||||
hasher_name = "sha1"
|
||||
for name in list_crypt_handlers():
|
||||
if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes:
|
||||
handler = get_crypt_handler(name)
|
||||
if getattr(handler, "django_name", None) == hasher_name:
|
||||
return name
|
||||
# XXX: this should only happen for custom hashers that have been registered.
|
||||
# _HasherHandler (below) is work in progress that would fix this.
|
||||
raise ValueError("can't translate hasher name to passlib name: %r" %
|
||||
hasher_name)
|
||||
|
||||
#=============================================================================
|
||||
# wrapping passlib handlers as django hashers
|
||||
#=============================================================================
|
||||
_GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--"
|
||||
|
||||
class _HasherWrapper(object):
|
||||
"""helper for wrapping passlib handlers in Hasher-compatible class."""
|
||||
|
||||
# filled in by subclass, drives the other methods.
|
||||
passlib_handler = None
|
||||
iterations = None
|
||||
|
||||
@classproperty
|
||||
def algorithm(cls):
|
||||
assert not hasattr(cls.passlib_handler, "django_name")
|
||||
return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name
|
||||
|
||||
def salt(self):
|
||||
# NOTE: passlib's handler.encrypt() should generate new salt each time,
|
||||
# so this just returns a special constant which tells
|
||||
# encode() (below) not to pass a salt keyword along.
|
||||
return _GEN_SALT_SIGNAL
|
||||
|
||||
def verify(self, password, encoded):
|
||||
return self.passlib_handler.verify(password, encoded)
|
||||
|
||||
def encode(self, password, salt=None, iterations=None):
|
||||
kwds = {}
|
||||
if salt is not None and salt != _GEN_SALT_SIGNAL:
|
||||
kwds['salt'] = salt
|
||||
if iterations is not None:
|
||||
kwds['rounds'] = iterations
|
||||
elif self.iterations is not None:
|
||||
kwds['rounds'] = self.iterations
|
||||
return self.passlib_handler.encrypt(password, **kwds)
|
||||
|
||||
_translate_kwds = dict(checksum="hash", rounds="iterations")
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
from django.contrib.auth.hashers import mask_hash
|
||||
from django.utils.translation import ugettext_noop as _
|
||||
from django.utils.datastructures import SortedDict
|
||||
handler = self.passlib_handler
|
||||
items = [
|
||||
# since this is user-facing, we're reporting passlib's name,
|
||||
# without the distracting PASSLIB_HASHER_PREFIX prepended.
|
||||
(_('algorithm'), handler.name),
|
||||
]
|
||||
if hasattr(handler, "parsehash"):
|
||||
kwds = handler.parsehash(encoded, sanitize=mask_hash)
|
||||
for key, value in iteritems(kwds):
|
||||
key = self._translate_kwds.get(key, key)
|
||||
items.append((_(key), value))
|
||||
return SortedDict(items)
|
||||
|
||||
# added in django 1.6
|
||||
def must_update(self, encoded):
|
||||
# TODO: would like to do something useful here,
|
||||
# but would require access to password context,
|
||||
# which would mean a serious recoding of this ext.
|
||||
return False
|
||||
|
||||
# cache of hasher wrappers generated by get_passlib_hasher()
|
||||
_hasher_cache = WeakKeyDictionary()
|
||||
|
||||
def get_passlib_hasher(handler, algorithm=None):
|
||||
"""create *Hasher*-compatible wrapper for specified passlib hash.
|
||||
|
||||
This takes in the name of a passlib hash (or the handler object itself),
|
||||
and returns a wrapper instance which should be compatible with
|
||||
Django 1.4's Hashers framework.
|
||||
|
||||
If the named hash corresponds to one of Django's builtin hashers,
|
||||
an instance of the real hasher class will be returned.
|
||||
|
||||
Note that the format of the handler won't be altered,
|
||||
so will probably not be compatible with Django's algorithm format,
|
||||
so the monkeypatch provided by this plugin must have been applied.
|
||||
|
||||
.. note::
|
||||
This function requires Django 1.4 or later.
|
||||
"""
|
||||
if DJANGO_VERSION < (1,4):
|
||||
raise RuntimeError("get_passlib_hasher() requires Django >= 1.4")
|
||||
if isinstance(handler, str):
|
||||
handler = get_crypt_handler(handler)
|
||||
if hasattr(handler, "django_name"):
|
||||
# return native hasher instance
|
||||
# XXX: should add this to _hasher_cache[]
|
||||
name = handler.django_name
|
||||
if name == "sha1" and algorithm == "unsalted_sha1":
|
||||
# django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes,
|
||||
# but passlib just reuses the "sha1$salt$digest" handler.
|
||||
# we want to resolve to correct django hasher.
|
||||
name = algorithm
|
||||
return _get_hasher(name)
|
||||
if handler.name == "django_disabled":
|
||||
raise ValueError("can't wrap unusable-password handler")
|
||||
try:
|
||||
return _hasher_cache[handler]
|
||||
except KeyError:
|
||||
name = "Passlib_%s_PasswordHasher" % handler.name.title()
|
||||
cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler))
|
||||
hasher = _hasher_cache[handler] = cls()
|
||||
return hasher
|
||||
|
||||
def _get_hasher(algorithm):
|
||||
"""wrapper to call django.contrib.auth.hashers:get_hasher()"""
|
||||
import sys
|
||||
module = sys.modules.get("passlib.ext.django.models")
|
||||
if module is None:
|
||||
# we haven't patched django, so just import directly
|
||||
from django.contrib.auth.hashers import get_hasher
|
||||
else:
|
||||
# we've patched django, so have to use patch manager to retrieve
|
||||
# original get_hasher() function...
|
||||
get_hasher = module._manager.getorig("django.contrib.auth.hashers:get_hasher")
|
||||
return get_hasher(algorithm)
|
||||
|
||||
#=============================================================================
|
||||
# adapting django hashers -> passlib handlers
|
||||
#=============================================================================
|
||||
# TODO: this code probably halfway works, mainly just needs
|
||||
# a routine to read HASHERS and PREFERRED_HASHER.
|
||||
|
||||
##from passlib.registry import register_crypt_handler
|
||||
##from passlib.utils import classproperty, to_native_str, to_unicode
|
||||
##from passlib.utils.compat import unicode
|
||||
##
|
||||
##
|
||||
##class _HasherHandler(object):
|
||||
## "helper for wrapping Hasher instances as passlib handlers"
|
||||
## # FIXME: this generic wrapper doesn't handle custom settings
|
||||
## # FIXME: genconfig / genhash not supported.
|
||||
##
|
||||
## def __init__(self, hasher):
|
||||
## self.django_hasher = hasher
|
||||
## if hasattr(hasher, "iterations"):
|
||||
## # assume encode() accepts an "iterations" parameter.
|
||||
## # fake min/max rounds
|
||||
## self.min_rounds = 1
|
||||
## self.max_rounds = 0xFFFFffff
|
||||
## self.default_rounds = self.django_hasher.iterations
|
||||
## self.setting_kwds += ("rounds",)
|
||||
##
|
||||
## # hasher instance - filled in by constructor
|
||||
## django_hasher = None
|
||||
##
|
||||
## setting_kwds = ("salt",)
|
||||
## context_kwds = ()
|
||||
##
|
||||
## @property
|
||||
## def name(self):
|
||||
## # XXX: need to make sure this wont' collide w/ builtin django hashes.
|
||||
## # maybe by renaming this to django compatible aliases?
|
||||
## return DJANGO_PASSLIB_PREFIX + self.django_name
|
||||
##
|
||||
## @property
|
||||
## def django_name(self):
|
||||
## # expose this so hasher_to_passlib_name() extracts original name
|
||||
## return self.django_hasher.algorithm
|
||||
##
|
||||
## @property
|
||||
## def ident(self):
|
||||
## # this should always be correct, as django relies on ident prefix.
|
||||
## return unicode(self.django_name + "$")
|
||||
##
|
||||
## @property
|
||||
## def identify(self, hash):
|
||||
## # this should always work, as django relies on ident prefix.
|
||||
## return to_unicode(hash, "latin-1", "hash").startswith(self.ident)
|
||||
##
|
||||
## @property
|
||||
## def genconfig(self):
|
||||
## # XXX: not sure how to support this.
|
||||
## return None
|
||||
##
|
||||
## @property
|
||||
## def genhash(self, secret, config):
|
||||
## if config is not None:
|
||||
## # XXX: not sure how to support this.
|
||||
## raise NotImplementedError("genhash() for hashers not implemented")
|
||||
## return self.encrypt(secret)
|
||||
##
|
||||
## @property
|
||||
## def encrypt(self, secret, salt=None, **kwds):
|
||||
## # NOTE: from how make_password() is coded, all hashers
|
||||
## # should have salt param. but only some will have
|
||||
## # 'iterations' parameter.
|
||||
## opts = {}
|
||||
## if 'rounds' in self.setting_kwds and 'rounds' in kwds:
|
||||
## opts['iterations'] = kwds.pop("rounds")
|
||||
## if kwds:
|
||||
## raise TypeError("unexpected keyword arguments: %r" % list(kwds))
|
||||
## if isinstance(secret, unicode):
|
||||
## secret = secret.encode("utf-8")
|
||||
## if salt is None:
|
||||
## salt = self.django_hasher.salt()
|
||||
## return to_native_str(self.django_hasher(secret, salt, **opts))
|
||||
##
|
||||
## @property
|
||||
## def verify(self, secret, hash):
|
||||
## hash = to_native_str(hash, "utf-8", "hash")
|
||||
## if isinstance(secret, unicode):
|
||||
## secret = secret.encode("utf-8")
|
||||
## return self.django_hasher.verify(secret, hash)
|
||||
##
|
||||
##def register_hasher(hasher):
|
||||
## handler = _HasherHandler(hasher)
|
||||
## register_crypt_handler(handler)
|
||||
## return handler
|
||||
|
||||
#=============================================================================
|
||||
# monkeypatch helpers
|
||||
#=============================================================================
|
||||
# private singleton indicating lack-of-value
|
||||
_UNSET = object()
|
||||
|
||||
class _PatchManager(object):
|
||||
"""helper to manage monkeypatches and run sanity checks"""
|
||||
|
||||
# NOTE: this could easily use a dict interface,
|
||||
# but keeping it distinct to make clear that it's not a dict,
|
||||
# since it has important side-effects.
|
||||
|
||||
#===================================================================
|
||||
# init and support
|
||||
#===================================================================
|
||||
def __init__(self, log=None):
|
||||
# map of key -> (original value, patched value)
|
||||
# original value may be _UNSET
|
||||
self.log = log or logging.getLogger(__name__ + "._PatchManager")
|
||||
self._state = {}
|
||||
|
||||
# bool value tests if any patches are currently applied.
|
||||
__bool__ = __nonzero__ = lambda self: bool(self._state)
|
||||
|
||||
def _import_path(self, path):
|
||||
"""retrieve obj and final attribute name from resource path"""
|
||||
name, attr = path.split(":")
|
||||
obj = __import__(name, fromlist=[attr], level=0)
|
||||
while '.' in attr:
|
||||
head, attr = attr.split(".", 1)
|
||||
obj = getattr(obj, head)
|
||||
return obj, attr
|
||||
|
||||
@staticmethod
|
||||
def _is_same_value(left, right):
|
||||
"""check if two values are the same (stripping method wrappers, etc)"""
|
||||
return get_method_function(left) == get_method_function(right)
|
||||
|
||||
#===================================================================
|
||||
# reading
|
||||
#===================================================================
|
||||
def _get_path(self, key, default=_UNSET):
|
||||
obj, attr = self._import_path(key)
|
||||
return getattr(obj, attr, default)
|
||||
|
||||
def get(self, path, default=None):
|
||||
"""return current value for path"""
|
||||
return self._get_path(path, default)
|
||||
|
||||
def getorig(self, path, default=None):
|
||||
"""return original (unpatched) value for path"""
|
||||
try:
|
||||
value, _= self._state[path]
|
||||
except KeyError:
|
||||
value = self._get_path(path)
|
||||
return default if value is _UNSET else value
|
||||
|
||||
def check_all(self, strict=False):
|
||||
"""run sanity check on all keys, issue warning if out of sync"""
|
||||
same = self._is_same_value
|
||||
for path, (orig, expected) in iteritems(self._state):
|
||||
if same(self._get_path(path), expected):
|
||||
continue
|
||||
msg = "another library has patched resource: %r" % path
|
||||
if strict:
|
||||
raise RuntimeError(msg)
|
||||
else:
|
||||
warn(msg, PasslibRuntimeWarning)
|
||||
|
||||
#===================================================================
|
||||
# patching
|
||||
#===================================================================
|
||||
def _set_path(self, path, value):
|
||||
obj, attr = self._import_path(path)
|
||||
if value is _UNSET:
|
||||
if hasattr(obj, attr):
|
||||
delattr(obj, attr)
|
||||
else:
|
||||
setattr(obj, attr, value)
|
||||
|
||||
def patch(self, path, value):
|
||||
"""monkeypatch object+attr at <path> to have <value>, stores original"""
|
||||
assert value != _UNSET
|
||||
current = self._get_path(path)
|
||||
try:
|
||||
orig, expected = self._state[path]
|
||||
except KeyError:
|
||||
self.log.debug("patching resource: %r", path)
|
||||
orig = current
|
||||
else:
|
||||
self.log.debug("modifying resource: %r", path)
|
||||
if not self._is_same_value(current, expected):
|
||||
warn("overridding resource another library has patched: %r"
|
||||
% path, PasslibRuntimeWarning)
|
||||
self._set_path(path, value)
|
||||
self._state[path] = (orig, value)
|
||||
|
||||
##def patch_many(self, **kwds):
|
||||
## "override specified resources with new values"
|
||||
## for path, value in iteritems(kwds):
|
||||
## self.patch(path, value)
|
||||
|
||||
def monkeypatch(self, parent, name=None, enable=True):
|
||||
"""function decorator which patches function of same name in <parent>"""
|
||||
def builder(func):
|
||||
if enable:
|
||||
sep = "." if ":" in parent else ":"
|
||||
path = parent + sep + (name or func.__name__)
|
||||
self.patch(path, func)
|
||||
return func
|
||||
return builder
|
||||
|
||||
#===================================================================
|
||||
# unpatching
|
||||
#===================================================================
|
||||
def unpatch(self, path, unpatch_conflicts=True):
|
||||
try:
|
||||
orig, expected = self._state[path]
|
||||
except KeyError:
|
||||
return
|
||||
current = self._get_path(path)
|
||||
self.log.debug("unpatching resource: %r", path)
|
||||
if not self._is_same_value(current, expected):
|
||||
if unpatch_conflicts:
|
||||
warn("reverting resource another library has patched: %r"
|
||||
% path, PasslibRuntimeWarning)
|
||||
else:
|
||||
warn("not reverting resource another library has patched: %r"
|
||||
% path, PasslibRuntimeWarning)
|
||||
del self._state[path]
|
||||
return
|
||||
self._set_path(path, orig)
|
||||
del self._state[path]
|
||||
|
||||
def unpatch_all(self, **kwds):
|
||||
for key in list(self._state):
|
||||
self.unpatch(key, **kwds)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
1
src/passlib/handlers/__init__.py
Normal file
1
src/passlib/handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""passlib.handlers -- holds implementations of all passlib's builtin hash formats"""
|
||||
607
src/passlib/handlers/bcrypt.py
Normal file
607
src/passlib/handlers/bcrypt.py
Normal file
@@ -0,0 +1,607 @@
|
||||
"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm.
|
||||
|
||||
TODO:
|
||||
|
||||
* support 2x and altered-2a hashes?
|
||||
http://www.openwall.com/lists/oss-security/2011/06/27/9
|
||||
|
||||
* deal with lack of PY3-compatibile c-ext implementation
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement, absolute_import
|
||||
# core
|
||||
from base64 import b64encode
|
||||
from hashlib import sha256
|
||||
import os
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
try:
|
||||
import bcrypt as _bcrypt
|
||||
except ImportError: # pragma: no cover
|
||||
_bcrypt = None
|
||||
try:
|
||||
import bcryptor as _bcryptor
|
||||
except ImportError: # pragma: no cover
|
||||
_bcryptor = None
|
||||
# pkg
|
||||
from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError
|
||||
from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \
|
||||
classproperty, rng, getrandstr, test_crypt, to_unicode
|
||||
from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii
|
||||
import passlib.utils.handlers as uh
|
||||
|
||||
# local
|
||||
__all__ = [
|
||||
"bcrypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# support funcs & constants
|
||||
#=============================================================================
|
||||
_builtin_bcrypt = None
|
||||
|
||||
def _load_builtin():
|
||||
global _builtin_bcrypt
|
||||
if _builtin_bcrypt is None:
|
||||
from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt
|
||||
|
||||
IDENT_2 = u("$2$")
|
||||
IDENT_2A = u("$2a$")
|
||||
IDENT_2X = u("$2x$")
|
||||
IDENT_2Y = u("$2y$")
|
||||
IDENT_2B = u("$2b$")
|
||||
_BNULL = b('\x00')
|
||||
|
||||
def _detect_pybcrypt():
|
||||
"""
|
||||
internal helper which tries to distinguish pybcrypt vs bcrypt.
|
||||
|
||||
:returns:
|
||||
True if cext-based py-bcrypt,
|
||||
False if ffi-based bcrypt,
|
||||
None if 'bcrypt' module not found.
|
||||
|
||||
.. versionchanged:: 1.6.3
|
||||
|
||||
Now assuming bcrypt installed, unless py-bcrypt explicitly detected.
|
||||
Previous releases assumed py-bcrypt by default.
|
||||
|
||||
Making this change since py-bcrypt is (apparently) unmaintained and static,
|
||||
whereas bcrypt is being actively maintained, and it's internal structure may shift.
|
||||
"""
|
||||
# NOTE: this is also used by the unittests.
|
||||
|
||||
# check for module.
|
||||
try:
|
||||
import bcrypt
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
# py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),
|
||||
# which bcrypt lacks (confirmed for v1.0 - 2.0)
|
||||
# "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.
|
||||
try:
|
||||
from bcrypt._bcrypt import __version__
|
||||
except ImportError:
|
||||
return False
|
||||
return True
|
||||
|
||||
#=============================================================================
|
||||
# handler
|
||||
#=============================================================================
|
||||
class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler):
|
||||
"""This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 12, must be between 4 and 31, inclusive.
|
||||
This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`
|
||||
-- increasing the rounds by +1 will double the amount of time taken.
|
||||
|
||||
:type ident: str
|
||||
:param ident:
|
||||
Specifies which version of the BCrypt algorithm will be used when creating a new hash.
|
||||
Typically this option is not needed, as the default (``"2a"``) is usually the correct choice.
|
||||
If specified, it must be one of the following:
|
||||
|
||||
* ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.
|
||||
* ``"2a"`` - some implementations suffered from a very rare security flaw.
|
||||
current default for compatibility purposes.
|
||||
* ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,
|
||||
identical to ``"2a"`` in all but name.
|
||||
* ``"2b"`` - latest revision of the official BCrypt algorithm (will be default in Passlib 1.7).
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This class now supports ``"2y"`` hashes, and recognizes
|
||||
(but does not support) the broken ``"2x"`` hashes.
|
||||
(see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`
|
||||
for details).
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
Added a pure-python backend.
|
||||
|
||||
.. versionchanged:: 1.6.3
|
||||
|
||||
Added support for ``"2b"`` variant.
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "bcrypt"
|
||||
setting_kwds = ("salt", "rounds", "ident")
|
||||
checksum_size = 31
|
||||
checksum_chars = bcrypt64.charmap
|
||||
|
||||
#--HasManyIdents--
|
||||
default_ident = IDENT_2A
|
||||
ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)
|
||||
ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y,
|
||||
u("2b"): IDENT_2B}
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 22
|
||||
salt_chars = bcrypt64.charmap
|
||||
# NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 12 # current passlib default
|
||||
min_rounds = 4 # minimum from bcrypt specification
|
||||
max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
|
||||
rounds_cost = "log2"
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
ident, tail = cls._parse_ident(hash)
|
||||
if ident == IDENT_2X:
|
||||
raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
|
||||
"currently supported")
|
||||
rounds_str, data = tail.split(u("$"))
|
||||
rounds = int(rounds_str)
|
||||
if rounds_str != u('%02d') % (rounds,):
|
||||
raise uh.exc.MalformedHashError(cls, "malformed cost field")
|
||||
salt, chk = data[:22], data[22:]
|
||||
return cls(
|
||||
rounds=rounds,
|
||||
salt=salt,
|
||||
checksum=chk or None,
|
||||
ident=ident,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt,
|
||||
self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
# NOTE: this should be kept separate from to_string()
|
||||
# so that bcrypt_sha256() can still use it, while overriding to_string()
|
||||
def _get_config(self, ident):
|
||||
"""internal helper to prepare config string for backends"""
|
||||
config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
|
||||
return uascii_to_str(config)
|
||||
|
||||
#===================================================================
|
||||
# specialized salt generation - fixes passlib issue 25
|
||||
#===================================================================
|
||||
|
||||
@classmethod
|
||||
def _bind_needs_update(cls, **settings):
|
||||
return cls._needs_update
|
||||
|
||||
@classmethod
|
||||
def _needs_update(cls, hash, secret):
|
||||
if isinstance(hash, bytes):
|
||||
hash = hash.decode("ascii")
|
||||
# check for incorrect padding bits (passlib issue 25)
|
||||
if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]:
|
||||
return True
|
||||
# TODO: try to detect incorrect $2x$ hashes using *secret*
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def normhash(cls, hash):
|
||||
"""helper to normalize hash, correcting any bcrypt padding bits"""
|
||||
if cls.identify(hash):
|
||||
return cls.from_string(hash).to_string()
|
||||
else:
|
||||
return hash
|
||||
|
||||
def _generate_salt(self, salt_size):
|
||||
# generate random salt as normal,
|
||||
# but repair last char so the padding bits always decode to zero.
|
||||
salt = super(bcrypt, self)._generate_salt(salt_size)
|
||||
return bcrypt64.repair_unused(salt)
|
||||
|
||||
def _norm_salt(self, salt, **kwds):
|
||||
salt = super(bcrypt, self)._norm_salt(salt, **kwds)
|
||||
assert salt is not None, "HasSalt didn't generate new salt!"
|
||||
changed, salt = bcrypt64.check_repair_unused(salt)
|
||||
if changed:
|
||||
# FIXME: if salt was provided by user, this message won't be
|
||||
# correct. not sure if we want to throw error, or use different warning.
|
||||
warn(
|
||||
"encountered a bcrypt salt with incorrectly set padding bits; "
|
||||
"you may want to use bcrypt.normhash() "
|
||||
"to fix this; see Passlib 1.5.3 changelog.",
|
||||
PasslibHashWarning)
|
||||
return salt
|
||||
|
||||
def _norm_checksum(self, checksum):
|
||||
checksum = super(bcrypt, self)._norm_checksum(checksum)
|
||||
if not checksum:
|
||||
return None
|
||||
changed, checksum = bcrypt64.check_repair_unused(checksum)
|
||||
if changed:
|
||||
warn(
|
||||
"encountered a bcrypt hash with incorrectly set padding bits; "
|
||||
"you may want to use bcrypt.normhash() "
|
||||
"to fix this; see Passlib 1.5.3 changelog.",
|
||||
PasslibHashWarning)
|
||||
return checksum
|
||||
|
||||
#===================================================================
|
||||
# primary interface
|
||||
#===================================================================
|
||||
backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")
|
||||
|
||||
# backend workaround detection
|
||||
_has_wraparound_bug = False
|
||||
_lacks_20_support = False
|
||||
_lacks_2y_support = False
|
||||
_lacks_2b_support = False
|
||||
|
||||
@classmethod
|
||||
def set_backend(cls, *a, **k):
|
||||
backend = super(bcrypt, cls).set_backend(*a, **k)
|
||||
cls._scan_backend(backend)
|
||||
return backend
|
||||
|
||||
@classmethod
|
||||
def _scan_backend(cls, backend):
|
||||
"""
|
||||
check for known bugs & feature support once backend is loaded
|
||||
"""
|
||||
# check for cryptblowfish 8bit bug (fixed in 2y/2b);
|
||||
# even though it's not known to be present in any of passlib's backends.
|
||||
# this is treated as FATAL, because it can easily result in seriously malformed hashes,
|
||||
# and we can't correct for it ourselves.
|
||||
# test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>
|
||||
# NOTE: reference hash taken from above url, and is the incorrectly generate 2x hash.
|
||||
if cls.verify(u("\xA3"),
|
||||
"$2a$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"):
|
||||
raise PasslibSecurityError(
|
||||
"passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
|
||||
"the crypt_blowfish 8-bit bug (CVE-2011-2483), "
|
||||
"and should be upgraded or replaced with another backend." % backend)
|
||||
|
||||
# check for bsd wraparound bug (fixed in 2b)
|
||||
# this is treated as a warning, because it's rare in the field,
|
||||
# and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.
|
||||
# test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>
|
||||
# NOTE: reference hash is of password "0"*72
|
||||
# NOTE: if in future we need to deliberately create hashes which have this bug,
|
||||
# can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'
|
||||
cls._has_wraparound_bug = False
|
||||
if cls.verify(("0123456789"*26)[:255],
|
||||
"$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"):
|
||||
warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
|
||||
"the bsd wraparound bug, "
|
||||
"and should be upgraded or replaced with another backend "
|
||||
"(this warning will be fatal under passlib 1.7)" % backend)
|
||||
cls._has_wraparound_bug = True
|
||||
|
||||
def _detect_lacks_variant(ident, refhash):
|
||||
"""helper to detect if backend *lacks* support for specified bcrypt variant"""
|
||||
assert refhash.startswith(ident)
|
||||
# NOTE: can't use cls.verify() directly or we have recursion error
|
||||
try:
|
||||
result = cls.verify("test", refhash)
|
||||
except (ValueError, _bcryptor.engine.SaltError if _bcryptor else ValueError):
|
||||
# backends without support will throw various errors about unrecognized version
|
||||
# pybcrypt, bcrypt -- raises ValueError
|
||||
# bcryptor -- raises bcryptor.engine.SaltError
|
||||
log.debug("%r backend lacks %r support", backend, ident)
|
||||
return True
|
||||
assert result, "%r backend %r check failed" % (backend, ident)
|
||||
return False
|
||||
|
||||
# check for native 2 support
|
||||
# NOTE: have to clear workaround first, so verify() doesn't enable it during detection.
|
||||
cls._lacks_20_support = False
|
||||
cls._lacks_20_support = _detect_lacks_variant("$2$", "$2$04$5BJqKfqMQvV7nS.yUguNcu"
|
||||
"RfMMOXK0xPWavM7pOzjEi5ze5T1k8/S")
|
||||
|
||||
# TODO: check for 2x support
|
||||
|
||||
# check for native 2y support
|
||||
cls._lacks_2y_support = False
|
||||
cls._lacks_2y_support = _detect_lacks_variant("$2y$", "$2y$04$5BJqKfqMQvV7nS.yUguNcu"
|
||||
"eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK")
|
||||
|
||||
# check for native 2b support
|
||||
cls._lacks_2b_support = False
|
||||
cls._lacks_2b_support = _detect_lacks_variant("$2b$", "$2b$04$5BJqKfqMQvV7nS.yUguNcu"
|
||||
"eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK")
|
||||
|
||||
# sanity check
|
||||
assert cls._lacks_2b_support or not cls._has_wraparound_bug, \
|
||||
"sanity check failed: %r backend supports $2b$ but has wraparound bug" % backend
|
||||
|
||||
@classproperty
|
||||
def _has_backend_bcrypt(cls):
|
||||
return _bcrypt is not None and not _detect_pybcrypt()
|
||||
|
||||
@classproperty
|
||||
def _has_backend_pybcrypt(cls):
|
||||
return _bcrypt is not None and _detect_pybcrypt()
|
||||
|
||||
@classproperty
|
||||
def _has_backend_bcryptor(cls):
|
||||
return _bcryptor is not None
|
||||
|
||||
@classproperty
|
||||
def _has_backend_builtin(cls):
|
||||
if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]:
|
||||
return False
|
||||
# look at it cross-eyed, and it loads itself
|
||||
_load_builtin()
|
||||
return True
|
||||
|
||||
@classproperty
|
||||
def _has_backend_os_crypt(cls):
|
||||
# XXX: what to do if "2" isn't supported, but "2a" is?
|
||||
# "2" is *very* rare, and can fake it using "2a"+repeat_string
|
||||
h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
|
||||
h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
|
||||
return test_crypt("test",h1) and test_crypt("test", h2)
|
||||
|
||||
@classmethod
|
||||
def _no_backends_msg(cls):
|
||||
return "no bcrypt backends available -- recommend you install one (e.g. 'pip install bcrypt')"
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
"""common backend code"""
|
||||
|
||||
# make sure it's unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
# NOTE: especially important to forbid NULLs for bcrypt, since many
|
||||
# backends (bcryptor, bcrypt) happily accept them, and then
|
||||
# silently truncate the password at first NULL they encounter!
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(self)
|
||||
|
||||
# ensure backend is loaded before workaround detection
|
||||
self.get_backend()
|
||||
|
||||
# protect from wraparound bug by truncating secret before handing it to the backend.
|
||||
# bcrypt only uses first 72 bytes anyways.
|
||||
if self._has_wraparound_bug and len(secret) >= 255:
|
||||
secret = secret[:72]
|
||||
|
||||
# special case handling for variants (ordered most common first)
|
||||
ident = self.ident
|
||||
if ident == IDENT_2A:
|
||||
# fall through and use backend w/o hacks
|
||||
pass
|
||||
|
||||
elif ident == IDENT_2B:
|
||||
if self._lacks_2b_support:
|
||||
# handle $2b$ hash format even if backend is too old.
|
||||
# have it generate a 2A digest, then return it as a 2B hash.
|
||||
ident = IDENT_2A
|
||||
|
||||
elif ident == IDENT_2Y:
|
||||
if self._lacks_2y_support:
|
||||
# handle $2y$ hash format (not supported by BSDs, being phased out on others)
|
||||
# have it generate a 2A digest, then return it as a 2Y hash.
|
||||
ident = IDENT_2A
|
||||
|
||||
elif ident == IDENT_2:
|
||||
if self._lacks_20_support:
|
||||
# handle legacy $2$ format (not supported by most backends except BSD os_crypt)
|
||||
# we can fake $2$ behavior using the $2a$ algorithm
|
||||
# by repeating the password until it's at least 72 chars in length.
|
||||
if secret:
|
||||
secret = repeat_string(secret, 72)
|
||||
ident = IDENT_2A
|
||||
|
||||
elif ident == IDENT_2X:
|
||||
|
||||
# NOTE: shouldn't get here.
|
||||
# XXX: could check if backend does actually offer 'support'
|
||||
raise RuntimeError("$2x$ hashes not currently supported by passlib")
|
||||
|
||||
else:
|
||||
raise AssertionError("unexpected ident value: %r" % ident)
|
||||
|
||||
# invoke backend
|
||||
config = self._get_config(ident)
|
||||
return self._calc_checksum_backend(secret, config)
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret, config):
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash:
|
||||
assert hash.startswith(config) and len(hash) == len(config)+31
|
||||
return hash[-31:]
|
||||
else:
|
||||
# NOTE: Have to raise this error because python3's crypt.crypt() only accepts unicode.
|
||||
# This means it can't handle any passwords that aren't either unicode
|
||||
# or utf-8 encoded bytes. However, hashing a password with an alternate
|
||||
# encoding should be a pretty rare edge case; if user needs it, they can just
|
||||
# install bcrypt backend.
|
||||
# XXX: is this the right error type to raise?
|
||||
# maybe have safe_crypt() not swallow UnicodeDecodeError, and have handlers
|
||||
# like sha256_crypt trap it if they have alternate method of handling them?
|
||||
raise uh.exc.MissingBackendError(
|
||||
"non-utf8 encoded passwords can't be handled by crypt.crypt() under python3, "
|
||||
"recommend running `pip install bcrypt`.",
|
||||
)
|
||||
|
||||
def _calc_checksum_bcrypt(self, secret, config):
|
||||
# bcrypt behavior:
|
||||
# hash must be ascii bytes
|
||||
# secret must be bytes
|
||||
# returns bytes
|
||||
if isinstance(config, unicode):
|
||||
config = config.encode("ascii")
|
||||
hash = _bcrypt.hashpw(secret, config)
|
||||
assert hash.startswith(config) and len(hash) == len(config)+31
|
||||
assert isinstance(hash, bytes)
|
||||
return hash[-31:].decode("ascii")
|
||||
|
||||
def _calc_checksum_pybcrypt(self, secret, config):
|
||||
# py-bcrypt behavior:
|
||||
# py2: unicode secret/hash encoded as ascii bytes before use,
|
||||
# bytes taken as-is; returns ascii bytes.
|
||||
# py3: unicode secret encoded as utf-8 bytes,
|
||||
# hash encoded as ascii bytes, returns ascii unicode.
|
||||
hash = _bcrypt.hashpw(secret, config)
|
||||
assert hash.startswith(config) and len(hash) == len(config)+31
|
||||
return str_to_uascii(hash[-31:])
|
||||
|
||||
def _calc_checksum_bcryptor(self, secret, config):
|
||||
# bcryptor behavior:
|
||||
# py2: unicode secret/hash encoded as ascii bytes before use,
|
||||
# bytes taken as-is; returns ascii bytes.
|
||||
# py3: not supported
|
||||
hash = _bcryptor.engine.Engine(False).hash_key(secret, config)
|
||||
assert hash.startswith(config) and len(hash) == len(config)+31
|
||||
return str_to_uascii(hash[-31:])
|
||||
|
||||
def _calc_checksum_builtin(self, secret, config):
|
||||
chk = _builtin_bcrypt(secret, config[1:config.index("$", 1)],
|
||||
self.salt.encode("ascii"), self.rounds)
|
||||
return chk.decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
_UDOLLAR = u("$")
|
||||
|
||||
class bcrypt_sha256(bcrypt):
|
||||
"""This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
|
||||
all the same optional keywords as the base :class:`bcrypt` hash.
|
||||
|
||||
.. versionadded:: 1.6.2
|
||||
"""
|
||||
name = "bcrypt_sha256"
|
||||
|
||||
# this is locked at 2a for now.
|
||||
ident_values = (IDENT_2A,)
|
||||
|
||||
# sample hash:
|
||||
# $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
|
||||
# $bcrypt-sha256$ -- prefix/identifier
|
||||
# 2a -- bcrypt variant
|
||||
# , -- field separator
|
||||
# 6 -- bcrypt work factor
|
||||
# $ -- section separator
|
||||
# /3OeRpbOf8/l6nPPRdZPp. -- salt
|
||||
# $ -- section separator
|
||||
# nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest
|
||||
|
||||
# XXX: we can't use .ident attr due to bcrypt code using it.
|
||||
# working around that via prefix.
|
||||
prefix = u('$bcrypt-sha256$')
|
||||
|
||||
_hash_re = re.compile(r"""
|
||||
^
|
||||
[$]bcrypt-sha256
|
||||
[$](?P<variant>[a-z0-9]+)
|
||||
,(?P<rounds>\d{1,2})
|
||||
[$](?P<salt>[^$]{22})
|
||||
([$](?P<digest>.{31}))?
|
||||
$
|
||||
""", re.X)
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
if not hash:
|
||||
return False
|
||||
return hash.startswith(cls.prefix)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
if not hash.startswith(cls.prefix):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
m = cls._hash_re.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
rounds = m.group("rounds")
|
||||
if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:
|
||||
raise uh.exc.ZeroPaddedRoundsError(cls)
|
||||
return cls(ident=m.group("variant"),
|
||||
rounds=int(rounds),
|
||||
salt=m.group("salt"),
|
||||
checksum=m.group("digest"),
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s,%d$%s") % (self.prefix, self.ident.strip(_UDOLLAR),
|
||||
self.rounds, self.salt)
|
||||
if self.checksum:
|
||||
hash = u("%s$%s") % (hash, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: can't use digest directly, since bcrypt stops at first NULL.
|
||||
# NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password
|
||||
# (XXX: citation needed), so we don't want key to be > 55 bytes.
|
||||
# thus, have to use base64 (44 bytes) rather than hex (64 bytes).
|
||||
# XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256
|
||||
# may switch to hex encoding, since it's simpler to implement elsewhere.
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
key = b64encode(sha256(secret).digest())
|
||||
|
||||
# hand result off to normal bcrypt algorithm
|
||||
return super(bcrypt_sha256, self)._calc_checksum(key)
|
||||
|
||||
# patch set_backend so it modifies bcrypt class, not this one...
|
||||
# else the bcrypt.set_backend() tests will call the wrong class.
|
||||
@classmethod
|
||||
def set_backend(cls, *args, **kwds):
|
||||
return bcrypt.set_backend(*args, **kwds)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
219
src/passlib/handlers/cisco.py
Normal file
219
src/passlib/handlers/cisco.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""passlib.handlers.cisco - Cisco password hashes"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import h64, right_pad_string, to_unicode
|
||||
from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \
|
||||
join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"cisco_pix",
|
||||
"cisco_type7",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# cisco pix firewall hash
|
||||
#=============================================================================
|
||||
class cisco_pix(uh.HasUserContext, uh.StaticHandler):
|
||||
"""This class implements the password hash used by Cisco PIX firewalls,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
It does a single round of hashing, and relies on the username
|
||||
as the salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods
|
||||
have the following extra keyword:
|
||||
|
||||
:type user: str
|
||||
:param user:
|
||||
String containing name of user account this password is associated with.
|
||||
|
||||
This is *required* in order to correctly hash passwords associated
|
||||
with a user account on the Cisco device, as it is used to salt
|
||||
the hash.
|
||||
|
||||
Conversely, this *must* be omitted or set to ``""`` in order to correctly
|
||||
hash passwords which don't have an associated user account
|
||||
(such as the "enable" password).
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "cisco_pix"
|
||||
checksum_size = 16
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
# XXX: no idea what unicode policy is, but all examples are
|
||||
# 7-bit ascii compatible, so using UTF-8
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
user = self.user
|
||||
if user:
|
||||
# not positive about this, but it looks like per-user
|
||||
# accounts use the first 4 chars of the username as the salt,
|
||||
# whereas global "enable" passwords don't have any salt at all.
|
||||
if isinstance(user, unicode):
|
||||
user = user.encode("utf-8")
|
||||
secret += user[:4]
|
||||
|
||||
# null-pad or truncate to 16 bytes
|
||||
secret = right_pad_string(secret, 16)
|
||||
|
||||
# md5 digest
|
||||
hash = md5(secret).digest()
|
||||
|
||||
# drop every 4th byte
|
||||
hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3)
|
||||
|
||||
# encode using Hash64
|
||||
return h64.encode_bytes(hash).decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# type 7
|
||||
#=============================================================================
|
||||
class cisco_type7(uh.GenericHandler):
|
||||
"""This class implements the Type 7 password encoding used by Cisco IOS,
|
||||
and follows the :ref:`password-hash-api`.
|
||||
It has a simple 4-5 bit salt, but is nonetheless a reversible encoding
|
||||
instead of a real hash.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genhash` methods
|
||||
have the following optional keywords:
|
||||
|
||||
:type salt: int
|
||||
:param salt:
|
||||
This may be an optional salt integer drawn from ``range(0,16)``.
|
||||
If omitted, one will be chosen at random.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` values that are out of range.
|
||||
|
||||
Note that while this class outputs digests in upper-case hexadecimal,
|
||||
it will accept lower-case as well.
|
||||
|
||||
This class also provides the following additional method:
|
||||
|
||||
.. automethod:: decode
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
name = "cisco_type7"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_chars = uh.UPPER_HEX_CHARS
|
||||
|
||||
# NOTE: encoding could handle max_salt_value=99, but since key is only 52
|
||||
# chars in size, not sure what appropriate behavior is for that edge case.
|
||||
min_salt_value = 0
|
||||
max_salt_value = 52
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def genconfig(cls):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def genhash(cls, secret, config):
|
||||
# special case to handle ``config=None`` in same style as StaticHandler
|
||||
if config is None:
|
||||
return cls.encrypt(secret)
|
||||
else:
|
||||
return super(cisco_type7, cls).genhash(secret, config)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
if len(hash) < 2:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt = int(hash[:2]) # may throw ValueError
|
||||
return cls(salt=salt, checksum=hash[2:].upper())
|
||||
|
||||
def __init__(self, salt=None, **kwds):
|
||||
super(cisco_type7, self).__init__(**kwds)
|
||||
self.salt = self._norm_salt(salt)
|
||||
|
||||
def _norm_salt(self, salt):
|
||||
"""the salt for this algorithm is an integer 0-52, not a string"""
|
||||
# XXX: not entirely sure that values >15 are valid, so for
|
||||
# compatibility we don't output those values, but we do accept them.
|
||||
if salt is None:
|
||||
if self.use_defaults:
|
||||
salt = self._generate_salt()
|
||||
else:
|
||||
raise TypeError("no salt specified")
|
||||
if not isinstance(salt, int):
|
||||
raise uh.exc.ExpectedTypeError(salt, "integer", "salt")
|
||||
if salt < 0 or salt > self.max_salt_value:
|
||||
msg = "salt/offset must be in 0..52 range"
|
||||
if self.relaxed:
|
||||
warn(msg, uh.PasslibHashWarning)
|
||||
salt = 0 if salt < 0 else self.max_salt_value
|
||||
else:
|
||||
raise ValueError(msg)
|
||||
return salt
|
||||
|
||||
def _generate_salt(self):
|
||||
return uh.rng.randint(0, 15)
|
||||
|
||||
def to_string(self):
|
||||
return "%02d%s" % (self.salt, uascii_to_str(self.checksum))
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# XXX: no idea what unicode policy is, but all examples are
|
||||
# 7-bit ascii compatible, so using UTF-8
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()
|
||||
|
||||
@classmethod
|
||||
def decode(cls, hash, encoding="utf-8"):
|
||||
"""decode hash, returning original password.
|
||||
|
||||
:arg hash: encoded password
|
||||
:param encoding: optional encoding to use (defaults to ``UTF-8``).
|
||||
:returns: password as unicode
|
||||
"""
|
||||
self = cls.from_string(hash)
|
||||
tmp = unhexlify(self.checksum.encode("ascii"))
|
||||
raw = self._cipher(tmp, self.salt)
|
||||
return raw.decode(encoding) if encoding else raw
|
||||
|
||||
# type7 uses a xor-based vingere variant, using the following secret key:
|
||||
_key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87")
|
||||
|
||||
@classmethod
|
||||
def _cipher(cls, data, salt):
|
||||
"""xor static key against data - encrypts & decrypts"""
|
||||
key = cls._key
|
||||
key_size = len(key)
|
||||
return join_byte_values(
|
||||
value ^ ord(key[(salt + idx) % key_size])
|
||||
for idx, value in enumerate(iter_byte_values(data))
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
517
src/passlib/handlers/des_crypt.py
Normal file
517
src/passlib/handlers/des_crypt.py
Normal file
@@ -0,0 +1,517 @@
|
||||
"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode
|
||||
from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode
|
||||
from passlib.utils.des import des_encrypt_int_block
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"des_crypt",
|
||||
"bsdi_crypt",
|
||||
"bigcrypt",
|
||||
"crypt16",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# pure-python backend for des_crypt family
|
||||
#=============================================================================
|
||||
_BNULL = b('\x00')
|
||||
|
||||
def _crypt_secret_to_key(secret):
|
||||
"""convert secret to 64-bit DES key.
|
||||
|
||||
this only uses the first 8 bytes of the secret,
|
||||
and discards the high 8th bit of each byte at that.
|
||||
a null parity bit is inserted after every 7th bit of the output.
|
||||
"""
|
||||
# NOTE: this would set the parity bits correctly,
|
||||
# but des_encrypt_int_block() would just ignore them...
|
||||
##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
|
||||
## for i, c in enumerate(secret[:8]))
|
||||
return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
|
||||
for i, c in enumerate(secret[:8]))
|
||||
|
||||
def _raw_des_crypt(secret, salt):
|
||||
"""pure-python backed for des_crypt"""
|
||||
assert len(salt) == 2
|
||||
|
||||
# NOTE: some OSes will accept non-HASH64 characters in the salt,
|
||||
# but what value they assign these characters varies wildy,
|
||||
# so just rejecting them outright.
|
||||
# NOTE: the same goes for single-character salts...
|
||||
# some OSes duplicate the char, some insert a '.' char,
|
||||
# and openbsd does something which creates an invalid hash.
|
||||
try:
|
||||
salt_value = h64.decode_int12(salt)
|
||||
except ValueError: # pragma: no cover - always caught by class
|
||||
raise ValueError("invalid chars in salt")
|
||||
|
||||
# gotta do something - no official policy since this predates unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
assert isinstance(secret, bytes)
|
||||
|
||||
# forbidding NULL char because underlying crypt() rejects them too.
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(des_crypt)
|
||||
|
||||
# convert first 8 bytes of secret string into an integer
|
||||
key_value = _crypt_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result = des_encrypt_int_block(key_value, 0, salt_value, 25)
|
||||
|
||||
# run h64 encode on result
|
||||
return h64big.encode_int64(result)
|
||||
|
||||
def _bsdi_secret_to_key(secret):
|
||||
"""covert secret to DES key used by bsdi_crypt"""
|
||||
key_value = _crypt_secret_to_key(secret)
|
||||
idx = 8
|
||||
end = len(secret)
|
||||
while idx < end:
|
||||
next = idx+8
|
||||
tmp_value = _crypt_secret_to_key(secret[idx:next])
|
||||
key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
|
||||
idx = next
|
||||
return key_value
|
||||
|
||||
def _raw_bsdi_crypt(secret, rounds, salt):
|
||||
"""pure-python backend for bsdi_crypt"""
|
||||
|
||||
# decode salt
|
||||
try:
|
||||
salt_value = h64.decode_int24(salt)
|
||||
except ValueError: # pragma: no cover - always caught by class
|
||||
raise ValueError("invalid salt")
|
||||
|
||||
# gotta do something - no official policy since this predates unicode
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
assert isinstance(secret, bytes)
|
||||
|
||||
# forbidding NULL char because underlying crypt() rejects them too.
|
||||
if _BNULL in secret:
|
||||
raise uh.exc.NullPasswordError(bsdi_crypt)
|
||||
|
||||
# convert secret string into an integer
|
||||
key_value = _bsdi_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
|
||||
|
||||
# run h64 encode on result
|
||||
return h64big.encode_int64(result)
|
||||
|
||||
#=============================================================================
|
||||
# handlers
|
||||
#=============================================================================
|
||||
class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "des_crypt"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
checksum_size = 11
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
# FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
|
||||
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>[./a-z0-9]{11})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
salt, chk = hash[:2], hash[2:]
|
||||
return cls(salt=salt, checksum=chk or None)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
_has_backend_builtin = True
|
||||
|
||||
@classproperty
|
||||
def _has_backend_os_crypt(cls):
|
||||
return test_crypt("test", 'abgOeLfPimXQo')
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
# NOTE: safe_crypt encodes unicode secret -> utf8
|
||||
# no official policy since des-crypt predates unicode
|
||||
hash = safe_crypt(secret, self.salt)
|
||||
if hash:
|
||||
assert hash.startswith(self.salt) and len(hash) == 13
|
||||
return hash[2:]
|
||||
else:
|
||||
return self._calc_checksum_builtin(secret)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 5001, must be between 1 and 16777215, inclusive.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
:meth:`encrypt` will now issue a warning if an even number of rounds is used
|
||||
(see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "bsdi_crypt"
|
||||
setting_kwds = ("salt", "rounds")
|
||||
checksum_size = 11
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 4
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasRounds--
|
||||
default_rounds = 5001
|
||||
min_rounds = 1
|
||||
max_rounds = 16777215 # (1<<24)-1
|
||||
rounds_cost = "linear"
|
||||
|
||||
# NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
|
||||
# but that seems to be an OS policy, not a algorithm limitation.
|
||||
|
||||
#===================================================================
|
||||
# parsing
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
_
|
||||
(?P<rounds>[./a-z0-9]{4})
|
||||
(?P<salt>[./a-z0-9]{4})
|
||||
(?P<chk>[./a-z0-9]{11})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
rounds, salt, chk = m.group("rounds", "salt", "chk")
|
||||
return cls(
|
||||
rounds=h64.decode_int24(rounds.encode("ascii")),
|
||||
salt=salt,
|
||||
checksum=chk,
|
||||
)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
|
||||
self.salt, self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# validation
|
||||
#===================================================================
|
||||
|
||||
# flag so CryptContext won't generate even rounds.
|
||||
_avoid_even_rounds = True
|
||||
|
||||
def _norm_rounds(self, rounds):
|
||||
rounds = super(bsdi_crypt, self)._norm_rounds(rounds)
|
||||
# issue warning if app provided an even rounds value
|
||||
if self.use_defaults and not rounds & 1:
|
||||
warn("bsdi_crypt rounds should be odd, "
|
||||
"as even rounds may reveal weak DES keys",
|
||||
uh.exc.PasslibSecurityWarning)
|
||||
return rounds
|
||||
|
||||
@classmethod
|
||||
def _bind_needs_update(cls, **settings):
|
||||
return cls._needs_update
|
||||
|
||||
@classmethod
|
||||
def _needs_update(cls, hash, secret):
|
||||
# mark bsdi_crypt hashes as deprecated if they have even rounds.
|
||||
assert cls.identify(hash)
|
||||
if isinstance(hash, unicode):
|
||||
hash = hash.encode("ascii")
|
||||
rounds = h64.decode_int24(hash[1:5])
|
||||
return not rounds & 1
|
||||
|
||||
#===================================================================
|
||||
# backends
|
||||
#===================================================================
|
||||
backends = ("os_crypt", "builtin")
|
||||
|
||||
_has_backend_builtin = True
|
||||
|
||||
@classproperty
|
||||
def _has_backend_os_crypt(cls):
|
||||
return test_crypt("test", '_/...lLDAxARksGCHin.')
|
||||
|
||||
def _calc_checksum_builtin(self, secret):
|
||||
return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
|
||||
|
||||
def _calc_checksum_os_crypt(self, secret):
|
||||
config = self.to_string()
|
||||
hash = safe_crypt(secret, config)
|
||||
if hash:
|
||||
assert hash.startswith(config[:9]) and len(hash) == 20
|
||||
return hash[-11:]
|
||||
else:
|
||||
return self._calc_checksum_builtin(secret)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class bigcrypt(uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "bigcrypt"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
# NOTE: checksum chars must be multiple of 11
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# internal helpers
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>([./a-z0-9]{11})+)?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt, chk = m.group("salt", "chk")
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _norm_checksum(self, value):
|
||||
value = super(bigcrypt, self)._norm_checksum(value)
|
||||
if value and len(value) % 11:
|
||||
raise uh.exc.InvalidHashError(self)
|
||||
return value
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
|
||||
idx = 8
|
||||
end = len(secret)
|
||||
while idx < end:
|
||||
next = idx + 8
|
||||
chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
|
||||
idx = next
|
||||
return chk.decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
class crypt16(uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "crypt16"
|
||||
setting_kwds = ("salt",)
|
||||
checksum_size = 22
|
||||
checksum_chars = uh.HASH64_CHARS
|
||||
|
||||
#--HasSalt--
|
||||
min_salt_size = max_salt_size = 2
|
||||
salt_chars = uh.HASH64_CHARS
|
||||
|
||||
#===================================================================
|
||||
# internal helpers
|
||||
#===================================================================
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
(?P<salt>[./a-z0-9]{2})
|
||||
(?P<chk>[./a-z0-9]{22})?
|
||||
$"""), re.X|re.I)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
salt, chk = m.group("salt", "chk")
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
hash = u("%s%s") % (self.salt, self.checksum or u(''))
|
||||
return uascii_to_str(hash)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
|
||||
# parse salt value
|
||||
try:
|
||||
salt_value = h64.decode_int12(self.salt.encode("ascii"))
|
||||
except ValueError: # pragma: no cover - caught by class
|
||||
raise ValueError("invalid chars in salt")
|
||||
|
||||
# convert first 8 byts of secret string into an integer,
|
||||
key1 = _crypt_secret_to_key(secret)
|
||||
|
||||
# run data through des using input of 0
|
||||
result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
|
||||
|
||||
# convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
|
||||
key2 = _crypt_secret_to_key(secret[8:16])
|
||||
|
||||
# run data through des using input of 0
|
||||
result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
|
||||
|
||||
# done
|
||||
chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
|
||||
return chk.decode("ascii")
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
144
src/passlib/handlers/digests.py
Normal file
144
src/passlib/handlers/digests.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""passlib.handlers.digests - plain hash digests
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import hashlib
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_native_str, to_bytes, render_bytes, consteq
|
||||
from passlib.utils.compat import bascii_to_str, bytes, unicode, str_to_uascii
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.utils.md4 import md4
|
||||
# local
|
||||
__all__ = [
|
||||
"create_hex_hash",
|
||||
"hex_md4",
|
||||
"hex_md5",
|
||||
"hex_sha1",
|
||||
"hex_sha256",
|
||||
"hex_sha512",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# helpers for hexadecimal hashes
|
||||
#=============================================================================
|
||||
class HexDigestHash(uh.StaticHandler):
|
||||
"""this provides a template for supporting passwords stored as plain hexadecimal hashes"""
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
_hash_func = None # hash function to use - filled in by create_hex_hash()
|
||||
checksum_size = None # filled in by create_hex_hash()
|
||||
checksum_chars = uh.HEX_CHARS
|
||||
|
||||
#===================================================================
|
||||
# methods
|
||||
#===================================================================
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
return hash.lower()
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(self._hash_func(secret).hexdigest())
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
def create_hex_hash(hash, digest_name, module=__name__):
|
||||
# NOTE: could set digest_name=hash.name for cpython, but not for some other platforms.
|
||||
h = hash()
|
||||
name = "hex_" + digest_name
|
||||
return type(name, (HexDigestHash,), dict(
|
||||
name=name,
|
||||
__module__=module, # so ABCMeta won't clobber it
|
||||
_hash_func=staticmethod(hash), # sometimes it's a function, sometimes not. so wrap it.
|
||||
checksum_size=h.digest_size*2,
|
||||
__doc__="""This class implements a plain hexadecimal %s hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports no optional or contextual keywords.
|
||||
""" % (digest_name,)
|
||||
))
|
||||
|
||||
#=============================================================================
|
||||
# predefined handlers
|
||||
#=============================================================================
|
||||
hex_md4 = create_hex_hash(md4, "md4")
|
||||
hex_md5 = create_hex_hash(hashlib.md5, "md5")
|
||||
hex_md5.django_name = "unsalted_md5"
|
||||
hex_sha1 = create_hex_hash(hashlib.sha1, "sha1")
|
||||
hex_sha256 = create_hex_hash(hashlib.sha256, "sha256")
|
||||
hex_sha512 = create_hex_hash(hashlib.sha512, "sha512")
|
||||
|
||||
#=============================================================================
|
||||
# htdigest
|
||||
#=============================================================================
|
||||
class htdigest(uh.PasswordHash):
|
||||
"""htdigest hash function.
|
||||
|
||||
.. todo::
|
||||
document this hash
|
||||
"""
|
||||
name = "htdigest"
|
||||
setting_kwds = ()
|
||||
context_kwds = ("user", "realm", "encoding")
|
||||
default_encoding = "utf-8"
|
||||
|
||||
@classmethod
|
||||
def encrypt(cls, secret, user, realm, encoding=None):
|
||||
# NOTE: this was deliberately written so that raw bytes are passed through
|
||||
# unchanged, the encoding kwd is only used to handle unicode values.
|
||||
if not encoding:
|
||||
encoding = cls.default_encoding
|
||||
uh.validate_secret(secret)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode(encoding)
|
||||
user = to_bytes(user, encoding, "user")
|
||||
realm = to_bytes(realm, encoding, "realm")
|
||||
data = render_bytes("%s:%s:%s", user, realm, secret)
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def _norm_hash(cls, hash):
|
||||
"""normalize hash to native string, and validate it"""
|
||||
hash = to_native_str(hash, param="hash")
|
||||
if len(hash) != 32:
|
||||
raise uh.exc.MalformedHashError(cls, "wrong size")
|
||||
for char in hash:
|
||||
if char not in uh.LC_HEX_CHARS:
|
||||
raise uh.exc.MalformedHashError(cls, "invalid chars in hash")
|
||||
return hash
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash, user, realm, encoding="utf-8"):
|
||||
hash = cls._norm_hash(hash)
|
||||
other = cls.encrypt(secret, user, realm, encoding)
|
||||
return consteq(hash, other)
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
try:
|
||||
cls._norm_hash(hash)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def genconfig(cls):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def genhash(cls, secret, config, user, realm, encoding="utf-8"):
|
||||
if config is not None:
|
||||
cls._norm_hash(config)
|
||||
return cls.encrypt(secret, user, realm, encoding)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
472
src/passlib/handlers/django.py
Normal file
472
src/passlib/handlers/django.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""passlib.handlers.django- Django password hash support"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode
|
||||
from binascii import hexlify
|
||||
from hashlib import md5, sha1, sha256
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.hash import bcrypt, pbkdf2_sha1, pbkdf2_sha256
|
||||
from passlib.utils import to_unicode, classproperty
|
||||
from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u
|
||||
from passlib.utils.pbkdf2 import pbkdf2
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"django_salted_sha1",
|
||||
"django_salted_md5",
|
||||
"django_bcrypt",
|
||||
"django_pbkdf2_sha1",
|
||||
"django_pbkdf2_sha256",
|
||||
"django_des_crypt",
|
||||
"django_disabled",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# lazy imports & constants
|
||||
#=============================================================================
|
||||
|
||||
# imported by django_des_crypt._calc_checksum()
|
||||
des_crypt = None
|
||||
|
||||
def _import_des_crypt():
|
||||
global des_crypt
|
||||
if des_crypt is None:
|
||||
from passlib.hash import des_crypt
|
||||
return des_crypt
|
||||
|
||||
# django 1.4's salt charset
|
||||
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
#=============================================================================
|
||||
# salted hashes
|
||||
#=============================================================================
|
||||
class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
|
||||
"""base class providing common code for django hashes"""
|
||||
# name, ident, checksum_size must be set by subclass.
|
||||
# ident must include "$" suffix.
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
|
||||
min_salt_size = 0
|
||||
# NOTE: django 1.0-1.3 would accept empty salt strings.
|
||||
# django 1.4 won't, but this appears to be regression
|
||||
# (https://code.djangoproject.com/ticket/18144)
|
||||
# so presumably it will be fixed in a later release.
|
||||
default_salt_size = 12
|
||||
max_salt_size = None
|
||||
salt_chars = SALT_CHARS
|
||||
|
||||
checksum_chars = uh.LOWER_HEX_CHARS
|
||||
|
||||
@classproperty
|
||||
def _stub_checksum(cls):
|
||||
return cls.checksum_chars[0] * cls.checksum_size
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
return uh.render_mc2(self.ident, self.salt,
|
||||
self.checksum or self._stub_checksum)
|
||||
|
||||
class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash):
|
||||
"""base class providing common code for django hashes w/ variable rounds"""
|
||||
setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",)
|
||||
|
||||
min_rounds = 1
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls)
|
||||
return cls(rounds=rounds, salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
return uh.render_mc3(self.ident, self.rounds, self.salt,
|
||||
self.checksum or self._stub_checksum)
|
||||
|
||||
class django_salted_sha1(DjangoSaltedHash):
|
||||
"""This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and uses a single round of SHA1.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class.
|
||||
|
||||
.. versionchanged: 1.6
|
||||
This class now generates 12-character salts instead of 5,
|
||||
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
|
||||
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
|
||||
generates these hashes; but hashes generated in this manner will still be
|
||||
correctly interpreted by earlier versions of Django.
|
||||
"""
|
||||
name = "django_salted_sha1"
|
||||
django_name = "sha1"
|
||||
ident = u("sha1$")
|
||||
checksum_size = 40
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest())
|
||||
|
||||
class django_salted_md5(DjangoSaltedHash):
|
||||
"""This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and uses a single round of MD5.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!MD5PasswordHasher` class.
|
||||
|
||||
.. versionchanged: 1.6
|
||||
This class now generates 12-character salts instead of 5,
|
||||
and generated salts uses the character range ``[0-9a-zA-Z]`` instead of
|
||||
the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4
|
||||
generates these hashes; but hashes generated in this manner will still be
|
||||
correctly interpreted by earlier versions of Django.
|
||||
"""
|
||||
name = "django_salted_md5"
|
||||
django_name = "md5"
|
||||
ident = u("md5$")
|
||||
checksum_size = 32
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest())
|
||||
|
||||
django_bcrypt = uh.PrefixWrapper("django_bcrypt", bcrypt,
|
||||
prefix=u('bcrypt$'), ident=u("bcrypt$"),
|
||||
# NOTE: this docstring is duplicated in the docs, since sphinx
|
||||
# seems to be having trouble reading it via autodata::
|
||||
doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This is identical to :class:`!bcrypt` itself, but with
|
||||
the Django-specific prefix ``"bcrypt$"`` prepended.
|
||||
|
||||
See :doc:`/lib/passlib.hash.bcrypt` for more details,
|
||||
the usage and behavior is identical.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!BCryptPasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
""")
|
||||
django_bcrypt.django_name = "bcrypt"
|
||||
|
||||
class django_bcrypt_sha256(bcrypt):
|
||||
"""This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
While the algorithm and format is somewhat different,
|
||||
the api and options for this hash are identical to :class:`!bcrypt` itself,
|
||||
see :doc:`/lib/passlib.hash.bcrypt` for more details.
|
||||
|
||||
.. versionadded:: 1.6.2
|
||||
"""
|
||||
name = "django_bcrypt_sha256"
|
||||
django_name = "bcrypt_sha256"
|
||||
_digest = sha256
|
||||
|
||||
# NOTE: django bcrypt ident locked at "$2a$", so omitting 'ident' support.
|
||||
setting_kwds = ("salt", "rounds")
|
||||
|
||||
# sample hash:
|
||||
# bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
|
||||
|
||||
# XXX: we can't use .ident attr due to bcrypt code using it.
|
||||
# working around that via django_prefix
|
||||
django_prefix = u('bcrypt_sha256$')
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
if not hash:
|
||||
return False
|
||||
return hash.startswith(cls.django_prefix)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
if not hash.startswith(cls.django_prefix):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
bhash = hash[len(cls.django_prefix):]
|
||||
if not bhash.startswith("$2"):
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
return super(django_bcrypt_sha256, cls).from_string(bhash)
|
||||
|
||||
def __init__(self, **kwds):
|
||||
if 'ident' in kwds and kwds.get("use_defaults"):
|
||||
raise TypeError("%s does not support the ident keyword" %
|
||||
self.__class__.__name__)
|
||||
return super(django_bcrypt_sha256, self).__init__(**kwds)
|
||||
|
||||
def to_string(self):
|
||||
bhash = super(django_bcrypt_sha256, self).to_string()
|
||||
return uascii_to_str(self.django_prefix) + bhash
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
secret = hexlify(self._digest(secret).digest())
|
||||
return super(django_bcrypt_sha256, self)._calc_checksum(secret)
|
||||
|
||||
# patch set_backend so it modifies bcrypt class, not this one...
|
||||
# else it would clobber our _calc_checksum() wrapper above.
|
||||
@classmethod
|
||||
def set_backend(cls, *args, **kwds):
|
||||
return bcrypt.set_backend(*args, **kwds)
|
||||
|
||||
class django_pbkdf2_sha256(DjangoVariableHash):
|
||||
"""This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 29000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!PBKDF2PasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
name = "django_pbkdf2_sha256"
|
||||
django_name = "pbkdf2_sha256"
|
||||
ident = u('pbkdf2_sha256$')
|
||||
min_salt_size = 1
|
||||
max_rounds = 0xffffffff # setting at 32-bit limit for now
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
checksum_size = 44 # 32 bytes -> base64
|
||||
default_rounds = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000
|
||||
_prf = "hmac-sha256"
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds,
|
||||
keylen=None, prf=self._prf)
|
||||
return b64encode(hash).rstrip().decode("ascii")
|
||||
|
||||
class django_pbkdf2_sha1(django_pbkdf2_sha256):
|
||||
"""This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, a 12 character one will be autogenerated (this is recommended).
|
||||
If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of characters to use when autogenerating new salts.
|
||||
Defaults to 12, but can be any positive value.
|
||||
|
||||
:type rounds: int
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 131000, but must be within ``range(1,1<<32)``.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
name = "django_pbkdf2_sha1"
|
||||
django_name = "pbkdf2_sha1"
|
||||
ident = u('pbkdf2_sha1$')
|
||||
checksum_size = 28 # 20 bytes -> base64
|
||||
default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000
|
||||
_prf = "hmac-sha1"
|
||||
|
||||
#=============================================================================
|
||||
# other
|
||||
#=============================================================================
|
||||
class django_des_crypt(uh.HasSalt, uh.GenericHandler):
|
||||
"""This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a fixed-length salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:type salt: str
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
|
||||
|
||||
This should be compatible with the hashes generated by
|
||||
Django 1.4's :class:`!CryptPasswordHasher` class.
|
||||
Note that Django only supports this hash on Unix systems
|
||||
(though :class:`!django_des_crypt` is available cross-platform
|
||||
under Passlib).
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This class will now accept hashes with empty salt strings,
|
||||
since Django 1.4 generates them this way.
|
||||
"""
|
||||
name = "django_des_crypt"
|
||||
django_name = "crypt"
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
ident = u("crypt$")
|
||||
checksum_chars = salt_chars = uh.HASH64_CHARS
|
||||
checksum_size = 11
|
||||
min_salt_size = default_salt_size = 2
|
||||
_stub_checksum = u('.')*11
|
||||
|
||||
# NOTE: regarding duplicate salt field:
|
||||
#
|
||||
# django 1.0 had a "crypt$<salt1>$<salt2><digest>" hash format,
|
||||
# used [a-z0-9] to generate a 5 char salt, stored it in salt1,
|
||||
# duplicated the first two chars of salt1 as salt2.
|
||||
# it would throw an error if salt1 was empty.
|
||||
#
|
||||
# django 1.4 started generating 2 char salt using the full alphabet,
|
||||
# left salt1 empty, and only paid attention to salt2.
|
||||
#
|
||||
# in order to be compatible with django 1.0, the hashes generated
|
||||
# by this function will always include salt1, unless the following
|
||||
# class-level field is disabled (mainly used for testing)
|
||||
use_duplicate_salt = True
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls)
|
||||
if chk:
|
||||
# chk should be full des_crypt hash
|
||||
if not salt:
|
||||
# django 1.4 always uses empty salt field,
|
||||
# so extract salt from des_crypt hash <chk>
|
||||
salt = chk[:2]
|
||||
elif salt[:2] != chk[:2]:
|
||||
# django 1.0 stored 5 chars in salt field, and duplicated
|
||||
# the first two chars in <chk>. we keep the full salt,
|
||||
# but make sure the first two chars match as sanity check.
|
||||
raise uh.exc.MalformedHashError(cls,
|
||||
"first two digits of salt and checksum must match")
|
||||
# in all cases, strip salt chars from <chk>
|
||||
chk = chk[2:]
|
||||
return cls(salt=salt, checksum=chk)
|
||||
|
||||
def to_string(self):
|
||||
salt = self.salt
|
||||
chk = salt[:2] + (self.checksum or self._stub_checksum)
|
||||
if self.use_duplicate_salt:
|
||||
# filling in salt field, so that we're compatible with django 1.0
|
||||
return uh.render_mc2(self.ident, salt, chk)
|
||||
else:
|
||||
# django 1.4+ style hash
|
||||
return uh.render_mc2(self.ident, "", chk)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
# NOTE: we lazily import des_crypt,
|
||||
# since most django deploys won't use django_des_crypt
|
||||
global des_crypt
|
||||
if des_crypt is None:
|
||||
_import_des_crypt()
|
||||
return des_crypt(salt=self.salt[:2])._calc_checksum(secret)
|
||||
|
||||
class django_disabled(uh.StaticHandler):
|
||||
"""This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class does not implement a hash, but instead
|
||||
claims the special hash string ``"!"`` which Django uses
|
||||
to indicate an account's password has been disabled.
|
||||
|
||||
* newly encrypted passwords will hash to ``"!"``.
|
||||
* it rejects all passwords.
|
||||
|
||||
.. note::
|
||||
|
||||
Django 1.6 prepends a randomly generate 40-char alphanumeric string
|
||||
to each unusuable password. This class recognizes such strings,
|
||||
but for backwards compatibility, still returns ``"!"``.
|
||||
|
||||
.. versionchanged:: 1.6.2 added Django 1.6 support
|
||||
"""
|
||||
name = "django_disabled"
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
return hash.startswith(u("!"))
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
return u("!")
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash):
|
||||
uh.validate_secret(secret)
|
||||
if not cls.identify(hash):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
return False
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
206
src/passlib/handlers/fshp.py
Normal file
206
src/passlib/handlers/fshp.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""passlib.handlers.fshp
|
||||
"""
|
||||
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode, b64decode
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils import to_unicode
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\
|
||||
unicode
|
||||
from passlib.utils.pbkdf2 import pbkdf1
|
||||
# local
|
||||
__all__ = [
|
||||
'fshp',
|
||||
]
|
||||
#=============================================================================
|
||||
# sha1-crypt
|
||||
#=============================================================================
|
||||
class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a variable-length salt, and a variable number of rounds.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
|
||||
|
||||
:param salt:
|
||||
Optional raw salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 16 bytes, but can be any non-negative value.
|
||||
|
||||
:param rounds:
|
||||
Optional number of rounds to use.
|
||||
Defaults to 480000, must be between 1 and 4294967295, inclusive.
|
||||
|
||||
:param variant:
|
||||
Optionally specifies variant of FSHP to use.
|
||||
|
||||
* ``0`` - uses SHA-1 digest (deprecated).
|
||||
* ``1`` - uses SHA-2/256 digest (default).
|
||||
* ``2`` - uses SHA-2/384 digest.
|
||||
* ``3`` - uses SHA-2/512 digest.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include ``rounds``
|
||||
that are too small or too large, and ``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
"""
|
||||
|
||||
#===================================================================
|
||||
# class attrs
|
||||
#===================================================================
|
||||
#--GenericHandler--
|
||||
name = "fshp"
|
||||
setting_kwds = ("salt", "salt_size", "rounds", "variant")
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
ident = u("{FSHP")
|
||||
# checksum_size is property() that depends on variant
|
||||
|
||||
#--HasRawSalt--
|
||||
default_salt_size = 16 # current passlib default, FSHP uses 8
|
||||
min_salt_size = 0
|
||||
max_salt_size = None
|
||||
|
||||
#--HasRounds--
|
||||
# FIXME: should probably use different default rounds
|
||||
# based on the variant. setting for default variant (sha256) for now.
|
||||
default_rounds = 480000 # current passlib default, FSHP uses 4096
|
||||
min_rounds = 1 # set by FSHP
|
||||
max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP
|
||||
rounds_cost = "linear"
|
||||
|
||||
#--variants--
|
||||
default_variant = 1
|
||||
_variant_info = {
|
||||
# variant: (hash name, digest size)
|
||||
0: ("sha1", 20),
|
||||
1: ("sha256", 32),
|
||||
2: ("sha384", 48),
|
||||
3: ("sha512", 64),
|
||||
}
|
||||
_variant_aliases = dict(
|
||||
[(unicode(k),k) for k in _variant_info] +
|
||||
[(v[0],k) for k,v in iteritems(_variant_info)]
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# instance attrs
|
||||
#===================================================================
|
||||
variant = None
|
||||
|
||||
#===================================================================
|
||||
# init
|
||||
#===================================================================
|
||||
def __init__(self, variant=None, **kwds):
|
||||
# NOTE: variant must be set first, since it controls checksum size, etc.
|
||||
self.use_defaults = kwds.get("use_defaults") # load this early
|
||||
self.variant = self._norm_variant(variant)
|
||||
super(fshp, self).__init__(**kwds)
|
||||
|
||||
def _norm_variant(self, variant):
|
||||
if variant is None:
|
||||
if not self.use_defaults:
|
||||
raise TypeError("no variant specified")
|
||||
variant = self.default_variant
|
||||
if isinstance(variant, bytes):
|
||||
variant = variant.decode("ascii")
|
||||
if isinstance(variant, unicode):
|
||||
try:
|
||||
variant = self._variant_aliases[variant]
|
||||
except KeyError:
|
||||
raise ValueError("invalid fshp variant")
|
||||
if not isinstance(variant, int):
|
||||
raise TypeError("fshp variant must be int or known alias")
|
||||
if variant not in self._variant_info:
|
||||
raise ValueError("invalid fshp variant")
|
||||
return variant
|
||||
|
||||
@property
|
||||
def checksum_alg(self):
|
||||
return self._variant_info[self.variant][0]
|
||||
|
||||
@property
|
||||
def checksum_size(self):
|
||||
return self._variant_info[self.variant][1]
|
||||
|
||||
#===================================================================
|
||||
# formatting
|
||||
#===================================================================
|
||||
|
||||
_hash_regex = re.compile(u(r"""
|
||||
^
|
||||
\{FSHP
|
||||
(\d+)\| # variant
|
||||
(\d+)\| # salt size
|
||||
(\d+)\} # rounds
|
||||
([a-zA-Z0-9+/]+={0,3}) # digest
|
||||
$"""), re.X)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
variant, salt_size, rounds, data = m.group(1,2,3,4)
|
||||
variant = int(variant)
|
||||
salt_size = int(salt_size)
|
||||
rounds = int(rounds)
|
||||
try:
|
||||
data = b64decode(data.encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
salt = data[:salt_size]
|
||||
chk = data[salt_size:]
|
||||
return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant)
|
||||
|
||||
@property
|
||||
def _stub_checksum(self):
|
||||
return b('\x00') * self.checksum_size
|
||||
|
||||
def to_string(self):
|
||||
chk = self.checksum or self._stub_checksum
|
||||
salt = self.salt
|
||||
data = bascii_to_str(b64encode(salt+chk))
|
||||
return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data)
|
||||
|
||||
#===================================================================
|
||||
# backend
|
||||
#===================================================================
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
# NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed.
|
||||
# this has only a minimal impact on security,
|
||||
# but it is worth noting this deviation.
|
||||
return pbkdf1(
|
||||
secret=self.salt,
|
||||
salt=secret,
|
||||
rounds=self.rounds,
|
||||
keylen=self.checksum_size,
|
||||
hash=self.checksum_alg,
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
270
src/passlib/handlers/ldap_digests.py
Normal file
270
src/passlib/handlers/ldap_digests.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""passlib.handlers.digests - plain hash digests
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
from base64 import b64encode, b64decode
|
||||
from hashlib import md5, sha1
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import re
|
||||
from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.handlers.misc import plaintext
|
||||
from passlib.utils import to_native_str, unix_crypt_schemes, \
|
||||
classproperty, to_unicode
|
||||
from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u
|
||||
import passlib.utils.handlers as uh
|
||||
# local
|
||||
__all__ = [
|
||||
"ldap_plaintext",
|
||||
"ldap_md5",
|
||||
"ldap_sha1",
|
||||
"ldap_salted_md5",
|
||||
"ldap_salted_sha1",
|
||||
|
||||
##"get_active_ldap_crypt_schemes",
|
||||
"ldap_des_crypt",
|
||||
"ldap_bsdi_crypt",
|
||||
"ldap_md5_crypt",
|
||||
"ldap_sha1_crypt"
|
||||
"ldap_bcrypt",
|
||||
"ldap_sha256_crypt",
|
||||
"ldap_sha512_crypt",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# ldap helpers
|
||||
#=============================================================================
|
||||
class _Base64DigestHelper(uh.StaticHandler):
|
||||
"""helper for ldap_md5 / ldap_sha1"""
|
||||
# XXX: could combine this with hex digests in digests.py
|
||||
|
||||
ident = None # required - prefix identifier
|
||||
_hash_func = None # required - hash function
|
||||
_hash_regex = None # required - regexp to recognize hash
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
|
||||
@classproperty
|
||||
def _hash_prefix(cls):
|
||||
"""tell StaticHandler to strip ident from checksum"""
|
||||
return cls.ident
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
chk = self._hash_func(secret).digest()
|
||||
return b64encode(chk).decode("ascii")
|
||||
|
||||
class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
|
||||
"""helper for ldap_salted_md5 / ldap_salted_sha1"""
|
||||
setting_kwds = ("salt", "salt_size")
|
||||
checksum_chars = uh.PADDED_BASE64_CHARS
|
||||
|
||||
ident = None # required - prefix identifier
|
||||
checksum_size = None # required
|
||||
_hash_func = None # required - hash function
|
||||
_hash_regex = None # required - regexp to recognize hash
|
||||
_stub_checksum = None # required - default checksum to plug in
|
||||
min_salt_size = max_salt_size = 4
|
||||
|
||||
# NOTE: openldap implementation uses 4 byte salt,
|
||||
# but it's been reported (issue 30) that some servers use larger salts.
|
||||
# the semi-related rfc3112 recommends support for up to 16 byte salts.
|
||||
min_salt_size = 4
|
||||
default_salt_size = 4
|
||||
max_salt_size = 16
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
hash = to_unicode(hash, "ascii", "hash")
|
||||
m = cls._hash_regex.match(hash)
|
||||
if not m:
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
try:
|
||||
data = b64decode(m.group("tmp").encode("ascii"))
|
||||
except TypeError:
|
||||
raise uh.exc.MalformedHashError(cls)
|
||||
cs = cls.checksum_size
|
||||
assert cs
|
||||
return cls(checksum=data[:cs], salt=data[cs:])
|
||||
|
||||
def to_string(self):
|
||||
data = (self.checksum or self._stub_checksum) + self.salt
|
||||
hash = self.ident + b64encode(data).decode("ascii")
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
return self._hash_func(secret + self.salt).digest()
|
||||
|
||||
#=============================================================================
|
||||
# implementations
|
||||
#=============================================================================
|
||||
class ldap_md5(_Base64DigestHelper):
|
||||
"""This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
|
||||
"""
|
||||
name = "ldap_md5"
|
||||
ident = u("{MD5}")
|
||||
_hash_func = md5
|
||||
_hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
|
||||
|
||||
class ldap_sha1(_Base64DigestHelper):
|
||||
"""This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords.
|
||||
"""
|
||||
name = "ldap_sha1"
|
||||
ident = u("{SHA}")
|
||||
_hash_func = sha1
|
||||
_hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
|
||||
|
||||
class ldap_salted_md5(_SaltedBase64DigestHelper):
|
||||
"""This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 4 bytes for compatibility with the LDAP spec,
|
||||
but some systems use larger salts, and Passlib supports
|
||||
any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This format now supports variable length salts, instead of a fix 4 bytes.
|
||||
"""
|
||||
name = "ldap_salted_md5"
|
||||
ident = u("{SMD5}")
|
||||
checksum_size = 16
|
||||
_hash_func = md5
|
||||
_hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$"))
|
||||
_stub_checksum = b('\x00') * 16
|
||||
|
||||
class ldap_salted_sha1(_SaltedBase64DigestHelper):
|
||||
"""This class stores passwords using LDAP's salted SHA1 format, and follows the :ref:`password-hash-api`.
|
||||
|
||||
It supports a 4-16 byte salt.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
|
||||
|
||||
:type salt: bytes
|
||||
:param salt:
|
||||
Optional salt string.
|
||||
If not specified, one will be autogenerated (this is recommended).
|
||||
If specified, it may be any 4-16 byte string.
|
||||
|
||||
:type salt_size: int
|
||||
:param salt_size:
|
||||
Optional number of bytes to use when autogenerating new salts.
|
||||
Defaults to 4 bytes for compatibility with the LDAP spec,
|
||||
but some systems use larger salts, and Passlib supports
|
||||
any value between 4-16.
|
||||
|
||||
:type relaxed: bool
|
||||
:param relaxed:
|
||||
By default, providing an invalid value for one of the other
|
||||
keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
|
||||
and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
|
||||
will be issued instead. Correctable errors include
|
||||
``salt`` strings that are too long.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
This format now supports variable length salts, instead of a fix 4 bytes.
|
||||
"""
|
||||
name = "ldap_salted_sha1"
|
||||
ident = u("{SSHA}")
|
||||
checksum_size = 20
|
||||
_hash_func = sha1
|
||||
_hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$"))
|
||||
_stub_checksum = b('\x00') * 20
|
||||
|
||||
class ldap_plaintext(plaintext):
|
||||
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
|
||||
|
||||
This class acts much like the generic :class:`!passlib.hash.plaintext` handler,
|
||||
except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix
|
||||
used by RFC2307 passwords.
|
||||
|
||||
The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
|
||||
following additional contextual keyword:
|
||||
|
||||
:type encoding: str
|
||||
:param encoding:
|
||||
This controls the character encoding to use (defaults to ``utf-8``).
|
||||
|
||||
This encoding will be used to encode :class:`!unicode` passwords
|
||||
under Python 2, and decode :class:`!bytes` hashes under Python 3.
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
The ``encoding`` keyword was added.
|
||||
"""
|
||||
# NOTE: this subclasses plaintext, since all it does differently
|
||||
# is override identify()
|
||||
|
||||
name = "ldap_plaintext"
|
||||
_2307_pat = re.compile(u(r"^\{\w+\}.*$"))
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
# NOTE: identifies all strings EXCEPT those with {XXX} prefix
|
||||
hash = uh.to_unicode_for_identify(hash)
|
||||
return bool(hash) and cls._2307_pat.match(hash) is None
|
||||
|
||||
#=============================================================================
|
||||
# {CRYPT} wrappers
|
||||
# the following are wrappers around the base crypt algorithms,
|
||||
# which add the ldap required {CRYPT} prefix
|
||||
#=============================================================================
|
||||
ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ]
|
||||
|
||||
def _init_ldap_crypt_handlers():
|
||||
# NOTE: I don't like to implicitly modify globals() like this,
|
||||
# but don't want to write out all these handlers out either :)
|
||||
g = globals()
|
||||
for wname in unix_crypt_schemes:
|
||||
name = 'ldap_' + wname
|
||||
g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True)
|
||||
del g
|
||||
_init_ldap_crypt_handlers()
|
||||
|
||||
##_lcn_host = None
|
||||
##def get_host_ldap_crypt_schemes():
|
||||
## global _lcn_host
|
||||
## if _lcn_host is None:
|
||||
## from passlib.hosts import host_context
|
||||
## schemes = host_context.schemes()
|
||||
## _lcn_host = [
|
||||
## "ldap_" + name
|
||||
## for name in unix_crypt_names
|
||||
## if name in schemes
|
||||
## ]
|
||||
## return _lcn_host
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user