mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 06:11:39 +00:00
Compare commits
807 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
127d354972 | ||
|
|
a099981b3c | ||
|
|
857230def3 | ||
|
|
693c23e562 | ||
|
|
5c6e31200e | ||
|
|
074318d797 | ||
|
|
838b377d4b | ||
|
|
f1061d7190 | ||
|
|
948e40f51a | ||
|
|
fb6746e694 | ||
|
|
e68632e840 | ||
|
|
049319c499 | ||
|
|
958c2ee356 | ||
|
|
6b569df751 | ||
|
|
7587a58dc3 | ||
|
|
7ea3930975 | ||
|
|
cccaf5fe54 | ||
|
|
932dbd74ee | ||
|
|
4cbb2300df | ||
|
|
ecb4efa352 | ||
|
|
03d7fe0407 | ||
|
|
3f6dff1543 | ||
|
|
9857869799 | ||
|
|
8879c5d8c7 | ||
|
|
7d1edd41c7 | ||
|
|
970542eb7f | ||
|
|
5270929b57 | ||
|
|
6543ead625 | ||
|
|
817d618d08 | ||
|
|
a0833393a8 | ||
|
|
811a2db58b | ||
|
|
8b9af97012 | ||
|
|
36b874d8a0 | ||
|
|
5680bc05b5 | ||
|
|
90b8751874 | ||
|
|
51712af78d | ||
|
|
f5681cb746 | ||
|
|
85d52a4d68 | ||
|
|
cec7f88d2b | ||
|
|
4043bcab61 | ||
|
|
1efa50aeab | ||
|
|
400fa08ebc | ||
|
|
eda1a1de24 | ||
|
|
31d8cd09dd | ||
|
|
91b3ddb489 | ||
|
|
2392d575c2 | ||
|
|
8b47748df9 | ||
|
|
7a9899f747 | ||
|
|
2e5e784f4c | ||
|
|
5b9d14e966 | ||
|
|
5fb0b1b5ff | ||
|
|
a08d95a742 | ||
|
|
64380cbd4d | ||
|
|
837d98deaf | ||
|
|
a63ac294db | ||
|
|
d20a0d8455 | ||
|
|
cd710e99c0 | ||
|
|
1cedbc9423 | ||
|
|
2ce5915e70 | ||
|
|
e602d3fef0 | ||
|
|
c6e1e5c1cf | ||
|
|
837bff58e7 | ||
|
|
01d50adce7 | ||
|
|
05cbe1c6f3 | ||
|
|
939bd8d9ab | ||
|
|
204a689848 | ||
|
|
a852c7e5ca | ||
|
|
b3ad45f2cc | ||
|
|
9f988bb464 | ||
|
|
ce0ad0a3ea | ||
|
|
c5daf892c6 | ||
|
|
bd484cbe41 | ||
|
|
cbfb0a7310 | ||
|
|
45027da057 | ||
|
|
fb53f2ed0e | ||
|
|
37e0e8942e | ||
|
|
ef86508bbb | ||
|
|
f6459e20f9 | ||
|
|
aeff5edaeb | ||
|
|
d274d05336 | ||
|
|
207eb0990c | ||
|
|
1cbe8297aa | ||
|
|
5b8fcebabd | ||
|
|
820f17ce74 | ||
|
|
753ecd7244 | ||
|
|
df3ea385ee | ||
|
|
eb041e9e65 | ||
|
|
96eb2496e4 | ||
|
|
cbe89c8c40 | ||
|
|
4d893c4da1 | ||
|
|
9dd0b135b9 | ||
|
|
5b0439ddb5 | ||
|
|
c3cb82a2de | ||
|
|
516f13bf48 | ||
|
|
c3bf18cf5a | ||
|
|
ece2d2943e | ||
|
|
610dbd4dcf | ||
|
|
fe3c043d61 | ||
|
|
83d8135722 | ||
|
|
6caf3f2252 | ||
|
|
b8331a3a4a | ||
|
|
c271349c30 | ||
|
|
329a6e0768 | ||
|
|
fae2dca9dc | ||
|
|
7112a42c96 | ||
|
|
d76618cbad | ||
|
|
d8db37786c | ||
|
|
79bc1065f3 | ||
|
|
b107afc13c | ||
|
|
fcfbf0a733 | ||
|
|
8460a7a87d | ||
|
|
2940dd71ab | ||
|
|
135ebaa251 | ||
|
|
f94b3eb383 | ||
|
|
a3db496f31 | ||
|
|
dd78c05d59 | ||
|
|
d9c4326a6b | ||
|
|
4ec90dbcfe | ||
|
|
b8c6800b37 | ||
|
|
a82a33996c | ||
|
|
9a27f19e2e | ||
|
|
c56c6e3e05 | ||
|
|
0c0fb37b33 | ||
|
|
e865d81dad | ||
|
|
cc86d67c26 | ||
|
|
3287a18cac | ||
|
|
135ea0f120 | ||
|
|
468928a2e6 | ||
|
|
57cafe78f8 | ||
|
|
8d27ef7a37 | ||
|
|
5acad994b6 | ||
|
|
6ebdf9ba4e | ||
|
|
f8067f11e1 | ||
|
|
4a0d23d652 | ||
|
|
42be930dfc | ||
|
|
47bc500f40 | ||
|
|
d536b6e43a | ||
|
|
5ef91076f9 | ||
|
|
5966e39406 | ||
|
|
044686b564 | ||
|
|
19018e4854 | ||
|
|
518e506df4 | ||
|
|
26ebf30da7 | ||
|
|
587fed282d | ||
|
|
98cabddcdc | ||
|
|
419bd01818 | ||
|
|
5a1ab0c168 | ||
|
|
74514b487d | ||
|
|
8e7bd453f4 | ||
|
|
348450b9d9 | ||
|
|
a50a481bb5 | ||
|
|
342e08e8fe | ||
|
|
65da6f39dc | ||
|
|
5774b99891 | ||
|
|
02ce970aea | ||
|
|
f8c24bf86a | ||
|
|
bbb486f7b2 | ||
|
|
bdbc7cf713 | ||
|
|
a44d4f0872 | ||
|
|
16636c34b5 | ||
|
|
ce2e379f30 | ||
|
|
4c4f5eef3e | ||
|
|
6a70a1412b | ||
|
|
a9025e2aba | ||
|
|
b1e26e3a48 | ||
|
|
7fc88f2641 | ||
|
|
68388a6011 | ||
|
|
481b060caf | ||
|
|
3cb8dae374 | ||
|
|
55999b5b65 | ||
|
|
1e126cd633 | ||
|
|
b6f7ff7038 | ||
|
|
8885bdd5f0 | ||
|
|
35b986e2be | ||
|
|
a80625d5f7 | ||
|
|
d2bb1a83a1 | ||
|
|
9f829c6990 | ||
|
|
6d52e06f66 | ||
|
|
1c8fcdbe6e | ||
|
|
b207814ffd | ||
|
|
f700e178a1 | ||
|
|
e75c7078ce | ||
|
|
74f7549b7f | ||
|
|
d9dc4ac68c | ||
|
|
134f170726 | ||
|
|
14fab0293e | ||
|
|
5318a7a9da | ||
|
|
46d655b328 | ||
|
|
099d0176d6 | ||
|
|
9aaee86120 | ||
|
|
e3d3927cfd | ||
|
|
c6d0f3da92 | ||
|
|
156fd4ee6f | ||
|
|
f192758f25 | ||
|
|
62f3507a32 | ||
|
|
25800a7883 | ||
|
|
15855bf6bf | ||
|
|
8dd683029b | ||
|
|
d10302ad9d | ||
|
|
06e74cf44f | ||
|
|
1b46b4b13b | ||
|
|
39d8c93444 | ||
|
|
6629f5578c | ||
|
|
5d4502e971 | ||
|
|
d070372117 | ||
|
|
330871cfbf | ||
|
|
d6dce1f0fe | ||
|
|
947c816591 | ||
|
|
c525d893d3 | ||
|
|
9ebdfc96a4 | ||
|
|
1ed0895803 | ||
|
|
eb4b8479f3 | ||
|
|
48fa6b755e | ||
|
|
4cef7c4c2d | ||
|
|
d072172ff5 | ||
|
|
6a90a76fb1 | ||
|
|
0ca083f5e9 | ||
|
|
f8e7ff86ab | ||
|
|
690832b7d7 | ||
|
|
78a42f29b1 | ||
|
|
d8be1dbb86 | ||
|
|
2995327a15 | ||
|
|
2cc48a0f25 | ||
|
|
511d436947 | ||
|
|
78a3a5f762 | ||
|
|
c98473d118 | ||
|
|
5b2e6591bc | ||
|
|
63ca5f8054 | ||
|
|
b1a2eb4de5 | ||
|
|
a1ca3523a3 | ||
|
|
ce5eb39f37 | ||
|
|
98438644c5 | ||
|
|
18d98a6384 | ||
|
|
ea49c9ef15 | ||
|
|
ddebd0c974 | ||
|
|
6e9c1dc08e | ||
|
|
7b433940bf | ||
|
|
dd2fffcfd5 | ||
|
|
679b5f144d | ||
|
|
f45601435c | ||
|
|
301cf2f1ba | ||
|
|
ba9a3a7980 | ||
|
|
80a002dadc | ||
|
|
206ba319af | ||
|
|
bdb217bed7 | ||
|
|
0805725a6f | ||
|
|
df177ac43f | ||
|
|
4ff76bab5c | ||
|
|
86c4c552db | ||
|
|
bc9582bcc1 | ||
|
|
f383c45e6b | ||
|
|
3bf5ffeb99 | ||
|
|
875879caed | ||
|
|
5f28bc82e0 | ||
|
|
0327f5c30f | ||
|
|
774c084708 | ||
|
|
325a06162e | ||
|
|
e3dbf56ef5 | ||
|
|
919f54d0d2 | ||
|
|
4563441a3a | ||
|
|
323c7da201 | ||
|
|
d667da4851 | ||
|
|
6be6b2ebde | ||
|
|
8f03a3aabb | ||
|
|
5f1489438c | ||
|
|
695843d7ec | ||
|
|
b9e4787b1c | ||
|
|
f2e0b436c6 | ||
|
|
e78dbeb056 | ||
|
|
cdbf740d38 | ||
|
|
bbbf7c5391 | ||
|
|
ad535b2e3f | ||
|
|
4f30ed6537 | ||
|
|
c6567d7830 | ||
|
|
a72ef287e3 | ||
|
|
86297a08bd | ||
|
|
0a81e51072 | ||
|
|
d33eb3b455 | ||
|
|
c6b1a163af | ||
|
|
8cca8f642c | ||
|
|
e8f49b8ecc | ||
|
|
4f1680810b | ||
|
|
7710578a3c | ||
|
|
02aae0d351 | ||
|
|
16512f3507 | ||
|
|
91976b2a2b | ||
|
|
fc44143587 | ||
|
|
70b160373d | ||
|
|
bafc648c9d | ||
|
|
e8e9e599f8 | ||
|
|
9051635bf5 | ||
|
|
dfb582ecc5 | ||
|
|
6ea1d6a237 | ||
|
|
a758a235dc | ||
|
|
5c20087f19 | ||
|
|
9b942ba05a | ||
|
|
96b6c89da9 | ||
|
|
3585d15353 | ||
|
|
d89d99da78 | ||
|
|
8a7468ef67 | ||
|
|
a76d4a79d1 | ||
|
|
882e75b7a3 | ||
|
|
222e00619e | ||
|
|
1f7edc5bb9 | ||
|
|
6a4712ec37 | ||
|
|
96376669ef | ||
|
|
a94cdbc633 | ||
|
|
3cd25f3c10 | ||
|
|
7281a813e0 | ||
|
|
3c1f141339 | ||
|
|
aa51d5be1d | ||
|
|
bb60488bf3 | ||
|
|
4fbabd9f35 | ||
|
|
f8750fe0b6 | ||
|
|
420fb1c393 | ||
|
|
7628b5a08d | ||
|
|
9038587f67 | ||
|
|
824dad6fab | ||
|
|
dbd2960841 | ||
|
|
b02fa6c358 | ||
|
|
bc1c51894c | ||
|
|
6136ece5dd | ||
|
|
bb204fa868 | ||
|
|
ce84ad8774 | ||
|
|
597a236e05 | ||
|
|
180606f721 | ||
|
|
dcfa718a9b | ||
|
|
6858f87926 | ||
|
|
743e808404 | ||
|
|
0ffb257f08 | ||
|
|
d670d5feab | ||
|
|
f34859f51b | ||
|
|
42ae43e81e | ||
|
|
41cad79a21 | ||
|
|
92c7525d0a | ||
|
|
86e496040c | ||
|
|
21c2ecfd1d | ||
|
|
720bd46683 | ||
|
|
b83967809d | ||
|
|
385d4e8ab2 | ||
|
|
96d52f47d1 | ||
|
|
e4353189dc | ||
|
|
dbe8dc67f2 | ||
|
|
4998c30d20 | ||
|
|
20e84b9c9a | ||
|
|
efdaa6a64e | ||
|
|
1a96622366 | ||
|
|
d5af189125 | ||
|
|
5ebaf8264a | ||
|
|
40bfde9697 | ||
|
|
a52805b35e | ||
|
|
0312258db7 | ||
|
|
2ddd9e2477 | ||
|
|
be44d2c601 | ||
|
|
ee44ba67cf | ||
|
|
c462216be7 | ||
|
|
e43ebd4d40 | ||
|
|
0ea0c416ec | ||
|
|
3f42eb86d2 | ||
|
|
b003e5065a | ||
|
|
cc25c40406 | ||
|
|
8e3c632a7c | ||
|
|
ab94208bb5 | ||
|
|
0562ed3eb9 | ||
|
|
3ceef052a3 | ||
|
|
d5b5251587 | ||
|
|
0ae73fa6b3 | ||
|
|
38b6fd213f | ||
|
|
39193ae92f | ||
|
|
c8c18497cc | ||
|
|
bdb0b3d6dc | ||
|
|
7312c8f396 | ||
|
|
831272a137 | ||
|
|
d50231b888 | ||
|
|
6160bb0953 | ||
|
|
1e013e6cd7 | ||
|
|
96955d9305 | ||
|
|
fdfa38a209 | ||
|
|
daa4b57af1 | ||
|
|
de3be6ba52 | ||
|
|
81c2e425ef | ||
|
|
4bf6b6fb96 | ||
|
|
4ed8497bd7 | ||
|
|
738280bbe5 | ||
|
|
ad9384aeac | ||
|
|
67f5416858 | ||
|
|
51567ff5c4 | ||
|
|
6f6a94c9b0 | ||
|
|
08bd3ecc91 | ||
|
|
6ebc0f4e81 | ||
|
|
bf39798263 | ||
|
|
92521acfa3 | ||
|
|
f8d43a19c1 | ||
|
|
842ddc2a26 | ||
|
|
bafed078e5 | ||
|
|
8999fb84de | ||
|
|
3b162924c5 | ||
|
|
242c61205d | ||
|
|
139727dd33 | ||
|
|
a52341e29e | ||
|
|
c2358f60fb | ||
|
|
c8ea108be3 | ||
|
|
8fdd0abc53 | ||
|
|
f396b2f476 | ||
|
|
6e9413eada | ||
|
|
30a5467b82 | ||
|
|
3ffa3ca5e5 | ||
|
|
f0351b8bec | ||
|
|
d8093fa1f0 | ||
|
|
2080f33fdb | ||
|
|
6a2925c546 | ||
|
|
26960c96d9 | ||
|
|
58a080fe6b | ||
|
|
3c3b7527a1 | ||
|
|
0d7028416a | ||
|
|
aa6dca4b4c | ||
|
|
3d7a7bd609 | ||
|
|
35854af1e9 | ||
|
|
4450652c32 | ||
|
|
d34e09f8d5 | ||
|
|
610ba5bf6a | ||
|
|
65debc6a27 | ||
|
|
61868d5fde | ||
|
|
998f4c6dff | ||
|
|
a2a8393775 | ||
|
|
d9ec9d3af9 | ||
|
|
7104b24633 | ||
|
|
9cd81c9148 | ||
|
|
56a60d1651 | ||
|
|
1cdf02bc31 | ||
|
|
5b469496d6 | ||
|
|
2a1d3a1ce1 | ||
|
|
9b89492d83 | ||
|
|
45b1eee8f3 | ||
|
|
e9dda2079a | ||
|
|
c279acbab8 | ||
|
|
082cd9b087 | ||
|
|
7d2e5d674a | ||
|
|
b2d95c545d | ||
|
|
127c3125ad | ||
|
|
63f1e0d504 | ||
|
|
1ad10b6954 | ||
|
|
820cf98087 | ||
|
|
15bd6beebd | ||
|
|
31871f192c | ||
|
|
7acfa4a2ac | ||
|
|
e212d68d60 | ||
|
|
6aeee89ee4 | ||
|
|
623572a652 | ||
|
|
f7c587c08b | ||
|
|
da1f346b1a | ||
|
|
056b56ed78 | ||
|
|
791581b850 | ||
|
|
08f426ba09 | ||
|
|
54371cffff | ||
|
|
c7946c0edf | ||
|
|
ae578c64a0 | ||
|
|
08bc1898cc | ||
|
|
dc626b1b3e | ||
|
|
7283e06750 | ||
|
|
c76368509e | ||
|
|
b54eb97f6b | ||
|
|
2d997fb046 | ||
|
|
e2cf769b20 | ||
|
|
281786b3b9 | ||
|
|
b06b8608d0 | ||
|
|
22dc39eb85 | ||
|
|
4a894958f0 | ||
|
|
38273a786a | ||
|
|
6ba0a5d942 | ||
|
|
2ad731f4e0 | ||
|
|
a491fb5471 | ||
|
|
33cfb940b4 | ||
|
|
46b79334e4 | ||
|
|
a7e841bcba | ||
|
|
f2400b35b0 | ||
|
|
31058336ce | ||
|
|
ac2cbef7f8 | ||
|
|
d7187ff998 | ||
|
|
2cc79f44ea | ||
|
|
067a67c14e | ||
|
|
054addfa9b | ||
|
|
6b7cf875de | ||
|
|
581e31499b | ||
|
|
a5883a8429 | ||
|
|
95c2d91a5b | ||
|
|
9f487d57fb | ||
|
|
e0d278e7ea | ||
|
|
36725c3574 | ||
|
|
8c911215b1 | ||
|
|
2b57a976c2 | ||
|
|
4817ce282a | ||
|
|
2da6666587 | ||
|
|
07f1bc050a | ||
|
|
9135e15b12 | ||
|
|
5c32a86257 | ||
|
|
d5bcac4d14 | ||
|
|
73bcadbbfe | ||
|
|
d3091d3dd8 | ||
|
|
f9ab78e393 | ||
|
|
7a94077906 | ||
|
|
6b438c3a46 | ||
|
|
5383ed22ea | ||
|
|
dbd09daa33 | ||
|
|
25ace13a3d | ||
|
|
8fc6112e32 | ||
|
|
ac2eb99d63 | ||
|
|
43707d8074 | ||
|
|
b0b3c18e99 | ||
|
|
349f2801c5 | ||
|
|
658e7beb2b | ||
|
|
e777eb6c99 | ||
|
|
8a6ce43ad3 | ||
|
|
72d182032b | ||
|
|
9b8189a3e4 | ||
|
|
14eb9ca3f8 | ||
|
|
cc2cba8c70 | ||
|
|
17660220fe | ||
|
|
d5538a79da | ||
|
|
fc5cd1c219 | ||
|
|
e10e63a87f | ||
|
|
93b05de15e | ||
|
|
a6a94060c6 | ||
|
|
a12ec0ffdc | ||
|
|
ae5d484309 | ||
|
|
2cdcc6f5ad | ||
|
|
3699e0199b | ||
|
|
5ec417d50c | ||
|
|
240edd6812 | ||
|
|
faa1b87926 | ||
|
|
08a9764465 | ||
|
|
e3a73ce7d1 | ||
|
|
d75a5e78a5 | ||
|
|
6a179215d6 | ||
|
|
c2dc2f0712 | ||
|
|
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 | ||
|
|
34b9029e90 | ||
|
|
4596accf5a | ||
|
|
0034704b3f | ||
|
|
a334645910 | ||
|
|
6519a5b007 | ||
|
|
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 | ||
|
|
a58e5e4276 |
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?):
|
||||
91
.travis.yml
Normal file
91
.travis.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
if: tag IS blank
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
language: python
|
||||
dist: precise
|
||||
python:
|
||||
- "2.7.15"
|
||||
env:
|
||||
- GAMOS=linux
|
||||
- PLATFORM=x86_64
|
||||
# - os: linux
|
||||
# language: python
|
||||
# dist: precise
|
||||
# python:
|
||||
# - "2.7.15"
|
||||
# env:
|
||||
# - GAMOS=linux
|
||||
# - PLATFORM=i686
|
||||
- os: osx
|
||||
language: generic
|
||||
osx_image: xcode10.1
|
||||
env:
|
||||
- GAMOS=macos
|
||||
- PLATFORM=x64
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
python@2
|
||||
- os: windows
|
||||
language: shell
|
||||
env:
|
||||
- GAMOS=windows
|
||||
PLATFORM=x64
|
||||
CINST_ARGS=""
|
||||
- os: windows
|
||||
language: shell
|
||||
env:
|
||||
- GAMOS=windows
|
||||
- CINST_ARGS=--forcex86
|
||||
- PLATFORM=x86
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then
|
||||
powershell Install-WindowsFeature Net-Framework-Core;
|
||||
cinst -y $CINST_ARGS python2;
|
||||
export PATH=$PATH:/c/Python27/scripts;
|
||||
cinst -y wixtoolset;
|
||||
fi
|
||||
- pip install pyinstaller
|
||||
|
||||
install:
|
||||
- cd src
|
||||
- pyinstaller --clean -F --distpath=gam $GAMOS-gam.spec
|
||||
- gam/gam version
|
||||
- export GAMVERSION=`gam/gam version simple`
|
||||
- cp LICENSE gam
|
||||
- cp whatsnew.txt gam
|
||||
- cp GamCommands.txt gam
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then
|
||||
cp gam-setup.bat gam;
|
||||
GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM.zip;
|
||||
/c/Program\ Files/7-Zip/7z.exe a -tzip $GAM_ARCHIVE gam -xr!.svn;
|
||||
mkdir gam-64;
|
||||
cp -rf gam/* gam-64/;
|
||||
/c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/candle.exe -arch $PLATFORM gam.wxs;
|
||||
/c/Program\ Files\ \(x86\)/Wix\ Toolset\ v3.11/bin/light.exe -ext /c/Program\ Files\ \(x86\)/WiX\ Toolset\ v3.11/bin/WixUIExtension.dll gam.wixobj -o gam-$GAMVERSION-$GAMOS-$PLATFORM.msi || true;
|
||||
rm *.wixpdb;
|
||||
else
|
||||
GAM_ARCHIVE=gam-$GAMVERSION-$GAMOS-$PLATFORM.tar.xz;
|
||||
tar cfJ $GAM_ARCHIVE gam/;
|
||||
fi
|
||||
|
||||
script:
|
||||
- gam/gam version
|
||||
|
||||
before_deploy:
|
||||
- export TRAVIS_TAG="preview"
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: bzambMcQwyv/o5c5GrKGCsZHgE5R85tg8sNFvPfpISz3+uosCjnBXas7wvCKzT75XUFi2ztfbYak6HdKf4sGnNHk0saEicB3slH+ghPyZbYzp76yvvduhFO2nWW3/F01tL+Yfqqt4/q8wFaWGjrC5km+6GLVyB4lWA/Uyu49qKnz02uSwyhBD/VFbO7DOQ65a1iWk9HngyMsu0Oi7HIbSjSLtxTHedNfOf3waW0NivTTxYXiYGX/MCu3GWhgIGj47a+H3A6FcQ/9QWvnKgnoixdgPBUz7kDb7ktsWwQsILPGStgH7iMuG49ZlXdEFmqwifBri2wvzmFEevBGZjHcupy1IGrNFRG+IUGKMotio+OkLHlLjuv7ZJtqCz/Vf5SNFgNyMSanx6jKEUJuYvndVg99IRXmYVwHFwPu5BAcJACpU6C0AfyGmmSqqwxCd46uXL62ynxNFpHuRfOqlDnmCTfZgjOciJSlDDpf+Xz9fF7+oCoeCi3mrcZVFjhd3tT6Oxw5HrsDtm0ZNld1cdLidaq8H6vOFgHMd0A9yNYZzTzXTvpmxzkXT4Zc7s+PYKN6z5fRZ+pJeckUjRXblvVEfs5HFSymavcOc5AkRwxpvOsTQMNmlnaJCBo5UNs0K/rVmRi5cFmaiwTcBCY0kTllOBJ4zWsfq8seiokWwNUNK2g=
|
||||
file_glob: true
|
||||
overwrite: true
|
||||
file: gam-$GAMVERSION-*
|
||||
skip_cleanup: true
|
||||
draft: true
|
||||
on:
|
||||
repo: jay0lee/GAM
|
||||
33
README.md
33
README.md
@@ -1,22 +1,21 @@
|
||||
GAM
|
||||
============================
|
||||
GAM is a command line tool for Google Apps Administrators to manage domain and user settings quickly and easily.
|
||||
|
||||
Downloads
|
||||
---------
|
||||
You can download the current GAM release from the [GitHub Releases] page.
|
||||
|
||||
Documentation
|
||||
------------------
|
||||
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily.
|
||||
# Quick Start
|
||||
## Linux / MacOS
|
||||
Open a terminal and run:
|
||||
```
|
||||
bash <(curl -s -S -L https://git.io/install-gam)
|
||||
```
|
||||
this will download GAM, install it and start setup.
|
||||
## Windows
|
||||
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
|
||||
# Documentation
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
Mailing List / Discussion group
|
||||
-------------------------------
|
||||
# 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.
|
||||
|
||||
Author
|
||||
------
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
|
||||
# IM Room
|
||||
[](https://gitter.im/jay0lee-GAM/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# Author
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>. Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
|
||||
3
src/.gitignore
vendored
3
src/.gitignore
vendored
@@ -67,3 +67,6 @@ gamcache/
|
||||
gam/
|
||||
gam-64/
|
||||
*.zip
|
||||
*.msi
|
||||
*.wixobj
|
||||
*.wixpdb
|
||||
|
||||
1062
src/GamCommands.txt
1062
src/GamCommands.txt
File diff suppressed because it is too large
Load Diff
@@ -1,642 +0,0 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "admin-settings:v2",
|
||||
"name": "admin-settings",
|
||||
"version": "v2",
|
||||
"revision": "20160616",
|
||||
"title": "Admin Settings API",
|
||||
"description": "Lets you access Google Apps Admin 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/admin-settings",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"basePath": "/a/feeds/domain/2.0/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/domain/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/domain/": {
|
||||
"description": "Manage domain settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"DefaultLanguage": {
|
||||
"id": "DefaultLanguage",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "default language value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OrganizationName": {
|
||||
"id": "OrganizationName",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "organization name value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MaximumNumberOfUsers": {
|
||||
"id": "MaximumNumberOfUsers",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "maximum number of users value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CurrentNumberOfUsers": {
|
||||
"id": "CurrentNumbersOfUsers",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "current number of users value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IsVerified": {
|
||||
"id": "IsVerified",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "boolean",
|
||||
"description": "current verification status value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Edition": {
|
||||
"id": "Edition",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain edition value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CustomerPIN": {
|
||||
"id": "CustomerPIN",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "customer pin value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreationTime": {
|
||||
"id": "CreationTime",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain creation time value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CountryCode": {
|
||||
"id": "CountryCode",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain country code value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AdminSecondaryEmail": {
|
||||
"id": "AdminSecondaryEmail",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "admin secondary email value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MXVerification": {
|
||||
"id": "MXVerification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain mx verification value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SSOGeneral": {
|
||||
"id": "SSOGeneral",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain sso general value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SSOSigningKey": {
|
||||
"id": "SSOSigningKey",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain sso signing key value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserEmailMigrationEnabled": {
|
||||
"id": "UserEmailMigrationEnabled",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "user email migration enabled value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"OutboundGateway": {
|
||||
"id": "OutboundGateway",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "domain outbound gateway value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"defaultLanguage": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.defaultLanguage.get",
|
||||
"path": "{domainName}/general/defaultLanguage",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "DefaultLanguage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizationName": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.organizationName.get",
|
||||
"path": "{domainName}/general/organizationName",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "OrganizationName"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"maximumNumberOfUsers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.maximumNumberOfUsers.get",
|
||||
"path": "{domainName}/general/maximumNumberOfUsers",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "MaximumNumberOfUsers"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"currentNumberOfUsers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.currentNumberOfUsers.get",
|
||||
"path": "{domainName}/general/currentNumberOfUsers",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "CurrentNumberOfUsers"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isVerified": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.isVerified.get",
|
||||
"path": "{domainName}/accountInformation/isVerified",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "IsVerified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edition": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.edition.get",
|
||||
"path": "{domainName}/accountInformation/edition",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Edition"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"customerPIN": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.customerPIN.get",
|
||||
"path": "{domainName}/accountInformation/customerPIN",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "CustomerPIN"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"creationTime": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.creationTime.get",
|
||||
"path": "{domainName}/accountInformation/creationTime",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "CreationTime"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"countryCode": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.countryCode.get",
|
||||
"path": "{domainName}/accountInformation/countryCode",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "CountryCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"adminSecondaryEmail": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.adminSecondaryEmail.get",
|
||||
"path": "{domainName}/accountInformation/adminSecondaryEmail",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "AdminSecondaryEmail"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mxVerification": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.mxVerification.get",
|
||||
"path": "{domainName}/verification/mx",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "MXVerification"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ssoGeneral": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.ssoGeneral.get",
|
||||
"path": "{domainName}/sso/general",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "SSOGeneral"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ssoSigningKey": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.ssoSigningKey.get",
|
||||
"path": "{domainName}/sso/signingkey",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "SSOSigningKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"userEmailMigrationEnabled": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.userEmailMigrationEnabled.get",
|
||||
"path": "{domainName}/email/migration",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "UserEmailMigrationEnabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outboundGateway": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "admin-settings.outboundGateway.get",
|
||||
"path": "{domainName}/email/gateway",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "OutboundGateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1484
src/atom/__init__.py
1484
src/atom/__init__.py
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# 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 is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
class BasicAuth(object):
|
||||
"""Sets the Authorization header as defined in RFC1945"""
|
||||
|
||||
def __init__(self, user_id, password):
|
||||
self.basic_cookie = base64.encodestring(
|
||||
'%s:%s' % (user_id, password)).strip()
|
||||
|
||||
def modify_request(self, http_request):
|
||||
http_request.headers['Authorization'] = 'Basic %s' % self.basic_cookie
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
|
||||
class NoAuth(object):
|
||||
|
||||
def modify_request(self, http_request):
|
||||
pass
|
||||
@@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""AtomPubClient provides CRUD ops. in line with the Atom Publishing Protocol.
|
||||
|
||||
"""
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import atom.http_core
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingHost(Error):
|
||||
pass
|
||||
|
||||
|
||||
class AtomPubClient(object):
|
||||
host = None
|
||||
auth_token = None
|
||||
ssl = False # Whether to force all requests over https
|
||||
xoauth_requestor_id = None
|
||||
|
||||
def __init__(self, http_client=None, host=None, auth_token=None, source=None,
|
||||
xoauth_requestor_id=None, **kwargs):
|
||||
"""Creates a new AtomPubClient instance.
|
||||
|
||||
Args:
|
||||
source: The name of your application.
|
||||
http_client: An object capable of performing HTTP requests through a
|
||||
request method. This object is used to perform the request
|
||||
when the AtomPubClient's request method is called. Used to
|
||||
allow HTTP requests to be directed to a mock server, or use
|
||||
an alternate library instead of the default of httplib to
|
||||
make HTTP requests.
|
||||
host: str The default host name to use if a host is not specified in the
|
||||
requested URI.
|
||||
auth_token: An object which sets the HTTP Authorization header when its
|
||||
modify_request method is called.
|
||||
"""
|
||||
self.http_client = http_client or atom.http_core.ProxiedHttpClient()
|
||||
if host is not None:
|
||||
self.host = host
|
||||
if auth_token is not None:
|
||||
self.auth_token = auth_token
|
||||
self.xoauth_requestor_id = xoauth_requestor_id
|
||||
self.source = source
|
||||
|
||||
def request(self, method=None, uri=None, auth_token=None,
|
||||
http_request=None, **kwargs):
|
||||
"""Performs an HTTP request to the server indicated.
|
||||
|
||||
Uses the http_client instance to make the request.
|
||||
|
||||
Args:
|
||||
method: The HTTP method as a string, usually one of 'GET', 'POST',
|
||||
'PUT', or 'DELETE'
|
||||
uri: The URI desired as a string or atom.http_core.Uri.
|
||||
http_request:
|
||||
auth_token: An authorization token object whose modify_request method
|
||||
sets the HTTP Authorization header.
|
||||
|
||||
Returns:
|
||||
The results of calling self.http_client.request. With the default
|
||||
http_client, this is an HTTP response object.
|
||||
"""
|
||||
# Modify the request based on the AtomPubClient settings and parameters
|
||||
# passed in to the request.
|
||||
http_request = self.modify_request(http_request)
|
||||
if isinstance(uri, (str, unicode)):
|
||||
uri = atom.http_core.Uri.parse_uri(uri)
|
||||
if uri is not None:
|
||||
uri.modify_request(http_request)
|
||||
if isinstance(method, (str, unicode)):
|
||||
http_request.method = method
|
||||
# Any unrecognized arguments are assumed to be capable of modifying the
|
||||
# HTTP request.
|
||||
for name, value in kwargs.iteritems():
|
||||
if value is not None:
|
||||
value.modify_request(http_request)
|
||||
# Default to an http request if the protocol scheme is not set.
|
||||
if http_request.uri.scheme is None:
|
||||
http_request.uri.scheme = 'http'
|
||||
# Override scheme. Force requests over https.
|
||||
if self.ssl:
|
||||
http_request.uri.scheme = 'https'
|
||||
if http_request.uri.path is None:
|
||||
http_request.uri.path = '/'
|
||||
# Add the Authorization header at the very end. The Authorization header
|
||||
# value may need to be calculated using information in the request.
|
||||
if auth_token:
|
||||
auth_token.modify_request(http_request)
|
||||
elif self.auth_token:
|
||||
self.auth_token.modify_request(http_request)
|
||||
# Check to make sure there is a host in the http_request.
|
||||
if http_request.uri.host is None:
|
||||
raise MissingHost('No host provided in request %s %s' % (
|
||||
http_request.method, str(http_request.uri)))
|
||||
# Perform the fully specified request using the http_client instance.
|
||||
# Sends the request to the server and returns the server's response.
|
||||
return self.http_client.request(http_request)
|
||||
|
||||
Request = request
|
||||
|
||||
def get(self, uri=None, auth_token=None, http_request=None, **kwargs):
|
||||
"""Performs a request using the GET method, returns an HTTP response."""
|
||||
return self.request(method='GET', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, **kwargs)
|
||||
|
||||
Get = get
|
||||
|
||||
def post(self, uri=None, data=None, auth_token=None, http_request=None,
|
||||
**kwargs):
|
||||
"""Sends data using the POST method, returns an HTTP response."""
|
||||
return self.request(method='POST', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, data=data, **kwargs)
|
||||
|
||||
Post = post
|
||||
|
||||
def put(self, uri=None, data=None, auth_token=None, http_request=None,
|
||||
**kwargs):
|
||||
"""Sends data using the PUT method, returns an HTTP response."""
|
||||
return self.request(method='PUT', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, data=data, **kwargs)
|
||||
|
||||
Put = put
|
||||
|
||||
def delete(self, uri=None, auth_token=None, http_request=None, **kwargs):
|
||||
"""Performs a request using the DELETE method, returns an HTTP response."""
|
||||
return self.request(method='DELETE', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, **kwargs)
|
||||
|
||||
Delete = delete
|
||||
|
||||
def modify_request(self, http_request):
|
||||
"""Changes the HTTP request before sending it to the server.
|
||||
|
||||
Sets the User-Agent HTTP header and fills in the HTTP host portion
|
||||
of the URL if one was not included in the request (for this it uses
|
||||
the self.host member if one is set). This method is called in
|
||||
self.request.
|
||||
|
||||
Args:
|
||||
http_request: An atom.http_core.HttpRequest() (optional) If one is
|
||||
not provided, a new HttpRequest is instantiated.
|
||||
|
||||
Returns:
|
||||
An atom.http_core.HttpRequest() with the User-Agent header set and
|
||||
if this client has a value in its host member, the host in the request
|
||||
URL is set.
|
||||
"""
|
||||
if http_request is None:
|
||||
http_request = atom.http_core.HttpRequest()
|
||||
|
||||
if self.host is not None and http_request.uri.host is None:
|
||||
http_request.uri.host = self.host
|
||||
|
||||
if self.xoauth_requestor_id is not None:
|
||||
http_request.uri.query['xoauth_requestor_id'] = self.xoauth_requestor_id
|
||||
|
||||
# Set the user agent header for logging purposes.
|
||||
if self.source:
|
||||
http_request.headers['User-Agent'] = '%s gdata-py/2.0.14' % self.source
|
||||
else:
|
||||
http_request.headers['User-Agent'] = 'gdata-py/2.0.14'
|
||||
|
||||
return http_request
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
|
||||
class CustomHeaders(object):
|
||||
"""Add custom headers to an http_request.
|
||||
|
||||
Usage:
|
||||
>>> custom_headers = atom.client.CustomHeaders(header1='value1',
|
||||
header2='value2')
|
||||
>>> client.get(uri, custom_headers=custom_headers)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Creates a CustomHeaders instance.
|
||||
|
||||
Initialize the headers dictionary with the arguments list.
|
||||
"""
|
||||
self.headers = kwargs
|
||||
|
||||
def modify_request(self, http_request):
|
||||
"""Changes the HTTP request before sending it to the server.
|
||||
|
||||
Adds the custom headers to the HTTP request.
|
||||
|
||||
Args:
|
||||
http_request: An atom.http_core.HttpRequest().
|
||||
|
||||
Returns:
|
||||
An atom.http_core.HttpRequest() with the added custom headers.
|
||||
"""
|
||||
|
||||
for name, value in self.headers.iteritems():
|
||||
if value is not None:
|
||||
http_request.headers[name] = value
|
||||
return http_request
|
||||
550
src/atom/core.py
550
src/atom/core.py
@@ -1,550 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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 is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import inspect
|
||||
try:
|
||||
from xml.etree import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
try:
|
||||
import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
try:
|
||||
from xml.etree import ElementTree
|
||||
except ImportError:
|
||||
from elementtree import ElementTree
|
||||
|
||||
|
||||
try:
|
||||
from xml.dom.minidom import parseString as xmlString
|
||||
except ImportError:
|
||||
xmlString = None
|
||||
|
||||
STRING_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
class XmlElement(object):
|
||||
"""Represents an element node in an XML document.
|
||||
|
||||
The text member is a UTF-8 encoded str or unicode.
|
||||
"""
|
||||
_qname = None
|
||||
_other_elements = None
|
||||
_other_attributes = None
|
||||
# The rule set contains mappings for XML qnames to child members and the
|
||||
# appropriate member classes.
|
||||
_rule_set = None
|
||||
_members = None
|
||||
text = None
|
||||
|
||||
def __init__(self, text=None, *args, **kwargs):
|
||||
if ('_members' not in self.__class__.__dict__
|
||||
or self.__class__._members is None):
|
||||
self.__class__._members = tuple(self.__class__._list_xml_members())
|
||||
for member_name, member_type in self.__class__._members:
|
||||
if member_name in kwargs:
|
||||
setattr(self, member_name, kwargs[member_name])
|
||||
else:
|
||||
if isinstance(member_type, list):
|
||||
setattr(self, member_name, [])
|
||||
else:
|
||||
setattr(self, member_name, None)
|
||||
self._other_elements = []
|
||||
self._other_attributes = {}
|
||||
if text is not None:
|
||||
self.text = text
|
||||
|
||||
def _list_xml_members(cls):
|
||||
"""Generator listing all members which are XML elements or attributes.
|
||||
|
||||
The following members would be considered XML members:
|
||||
foo = 'abc' - indicates an XML attribute with the qname abc
|
||||
foo = SomeElement - indicates an XML child element
|
||||
foo = [AnElement] - indicates a repeating XML child element, each instance
|
||||
will be stored in a list in this member
|
||||
foo = ('att1', '{http://example.com/namespace}att2') - indicates an XML
|
||||
attribute which has different parsing rules in different versions of
|
||||
the protocol. Version 1 of the XML parsing rules will look for an
|
||||
attribute with the qname 'att1' but verion 2 of the parsing rules will
|
||||
look for a namespaced attribute with the local name of 'att2' and an
|
||||
XML namespace of 'http://example.com/namespace'.
|
||||
"""
|
||||
members = []
|
||||
for pair in inspect.getmembers(cls):
|
||||
if not pair[0].startswith('_') and pair[0] != 'text':
|
||||
member_type = pair[1]
|
||||
if (isinstance(member_type, tuple) or isinstance(member_type, list)
|
||||
or isinstance(member_type, (str, unicode))
|
||||
or (inspect.isclass(member_type)
|
||||
and issubclass(member_type, XmlElement))):
|
||||
members.append(pair)
|
||||
return members
|
||||
|
||||
_list_xml_members = classmethod(_list_xml_members)
|
||||
|
||||
def _get_rules(cls, version):
|
||||
"""Initializes the _rule_set for the class which is used when parsing XML.
|
||||
|
||||
This method is used internally for parsing and generating XML for an
|
||||
XmlElement. It is not recommended that you call this method directly.
|
||||
|
||||
Returns:
|
||||
A tuple containing the XML parsing rules for the appropriate version.
|
||||
|
||||
The tuple looks like:
|
||||
(qname, {sub_element_qname: (member_name, member_class, repeating), ..},
|
||||
{attribute_qname: member_name})
|
||||
|
||||
To give a couple of concrete example, the atom.data.Control _get_rules
|
||||
with version of 2 will return:
|
||||
('{http://www.w3.org/2007/app}control',
|
||||
{'{http://www.w3.org/2007/app}draft': ('draft',
|
||||
<class 'atom.data.Draft'>,
|
||||
False)},
|
||||
{})
|
||||
Calling _get_rules with version 1 on gdata.data.FeedLink will produce:
|
||||
('{http://schemas.google.com/g/2005}feedLink',
|
||||
{'{http://www.w3.org/2005/Atom}feed': ('feed',
|
||||
<class 'gdata.data.GDFeed'>,
|
||||
False)},
|
||||
{'href': 'href', 'readOnly': 'read_only', 'countHint': 'count_hint',
|
||||
'rel': 'rel'})
|
||||
"""
|
||||
# Initialize the _rule_set to make sure there is a slot available to store
|
||||
# the parsing rules for this version of the XML schema.
|
||||
# Look for rule set in the class __dict__ proxy so that only the
|
||||
# _rule_set for this class will be found. By using the dict proxy
|
||||
# we avoid finding rule_sets defined in superclasses.
|
||||
# The four lines below provide support for any number of versions, but it
|
||||
# runs a bit slower then hard coding slots for two versions, so I'm using
|
||||
# the below two lines.
|
||||
#if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
|
||||
# cls._rule_set = []
|
||||
#while len(cls.__dict__['_rule_set']) < version:
|
||||
# cls._rule_set.append(None)
|
||||
# If there is no rule set cache in the class, provide slots for two XML
|
||||
# versions. If and when there is a version 3, this list will need to be
|
||||
# expanded.
|
||||
if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
|
||||
cls._rule_set = [None, None]
|
||||
# If a version higher than 2 is requested, fall back to version 2 because
|
||||
# 2 is currently the highest supported version.
|
||||
if version > 2:
|
||||
return cls._get_rules(2)
|
||||
# Check the dict proxy for the rule set to avoid finding any rule sets
|
||||
# which belong to the superclass. We only want rule sets for this class.
|
||||
if cls._rule_set[version-1] is None:
|
||||
# The rule set for each version consists of the qname for this element
|
||||
# ('{namespace}tag'), a dictionary (elements) for looking up the
|
||||
# corresponding class member when given a child element's qname, and a
|
||||
# dictionary (attributes) for looking up the corresponding class member
|
||||
# when given an XML attribute's qname.
|
||||
elements = {}
|
||||
attributes = {}
|
||||
if ('_members' not in cls.__dict__ or cls._members is None):
|
||||
cls._members = tuple(cls._list_xml_members())
|
||||
for member_name, target in cls._members:
|
||||
if isinstance(target, list):
|
||||
# This member points to a repeating element.
|
||||
elements[_get_qname(target[0], version)] = (member_name, target[0],
|
||||
True)
|
||||
elif isinstance(target, tuple):
|
||||
# This member points to a versioned XML attribute.
|
||||
if version <= len(target):
|
||||
attributes[target[version-1]] = member_name
|
||||
else:
|
||||
attributes[target[-1]] = member_name
|
||||
elif isinstance(target, (str, unicode)):
|
||||
# This member points to an XML attribute.
|
||||
attributes[target] = member_name
|
||||
elif issubclass(target, XmlElement):
|
||||
# This member points to a single occurance element.
|
||||
elements[_get_qname(target, version)] = (member_name, target, False)
|
||||
version_rules = (_get_qname(cls, version), elements, attributes)
|
||||
cls._rule_set[version-1] = version_rules
|
||||
return version_rules
|
||||
else:
|
||||
return cls._rule_set[version-1]
|
||||
|
||||
_get_rules = classmethod(_get_rules)
|
||||
|
||||
def get_elements(self, tag=None, namespace=None, version=1):
|
||||
"""Find all sub elements which match the tag and namespace.
|
||||
|
||||
To find all elements in this object, call get_elements with the tag and
|
||||
namespace both set to None (the default). This method searches through
|
||||
the object's members and the elements stored in _other_elements which
|
||||
did not match any of the XML parsing rules for this class.
|
||||
|
||||
Args:
|
||||
tag: str
|
||||
namespace: str
|
||||
version: int Specifies the version of the XML rules to be used when
|
||||
searching for matching elements.
|
||||
|
||||
Returns:
|
||||
A list of the matching XmlElements.
|
||||
"""
|
||||
matches = []
|
||||
ignored1, elements, ignored2 = self.__class__._get_rules(version)
|
||||
if elements:
|
||||
for qname, element_def in elements.iteritems():
|
||||
member = getattr(self, element_def[0])
|
||||
if member:
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
if element_def[2]:
|
||||
# If this is a repeating element, copy all instances into the
|
||||
# result list.
|
||||
matches.extend(member)
|
||||
else:
|
||||
matches.append(member)
|
||||
for element in self._other_elements:
|
||||
if _qname_matches(tag, namespace, element._qname):
|
||||
matches.append(element)
|
||||
return matches
|
||||
|
||||
GetElements = get_elements
|
||||
# FindExtensions and FindChildren are provided for backwards compatibility
|
||||
# to the atom.AtomBase class.
|
||||
# However, FindExtensions may return more results than the v1 atom.AtomBase
|
||||
# method does, because get_elements searches both the expected children
|
||||
# and the unexpected "other elements". The old AtomBase.FindExtensions
|
||||
# method searched only "other elements" AKA extension_elements.
|
||||
FindExtensions = get_elements
|
||||
FindChildren = get_elements
|
||||
|
||||
def get_attributes(self, tag=None, namespace=None, version=1):
|
||||
"""Find all attributes which match the tag and namespace.
|
||||
|
||||
To find all attributes in this object, call get_attributes with the tag
|
||||
and namespace both set to None (the default). This method searches
|
||||
through the object's members and the attributes stored in
|
||||
_other_attributes which did not fit any of the XML parsing rules for this
|
||||
class.
|
||||
|
||||
Args:
|
||||
tag: str
|
||||
namespace: str
|
||||
version: int Specifies the version of the XML rules to be used when
|
||||
searching for matching attributes.
|
||||
|
||||
Returns:
|
||||
A list of XmlAttribute objects for the matching attributes.
|
||||
"""
|
||||
matches = []
|
||||
ignored1, ignored2, attributes = self.__class__._get_rules(version)
|
||||
if attributes:
|
||||
for qname, attribute_def in attributes.iteritems():
|
||||
if isinstance(attribute_def, (list, tuple)):
|
||||
attribute_def = attribute_def[0]
|
||||
member = getattr(self, attribute_def)
|
||||
# TODO: ensure this hasn't broken existing behavior.
|
||||
#member = getattr(self, attribute_def[0])
|
||||
if member:
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
matches.append(XmlAttribute(qname, member))
|
||||
for qname, value in self._other_attributes.iteritems():
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
matches.append(XmlAttribute(qname, value))
|
||||
return matches
|
||||
|
||||
GetAttributes = get_attributes
|
||||
|
||||
def _harvest_tree(self, tree, version=1):
|
||||
"""Populates object members from the data in the tree Element."""
|
||||
qname, elements, attributes = self.__class__._get_rules(version)
|
||||
for element in tree:
|
||||
if elements and element.tag in elements:
|
||||
definition = elements[element.tag]
|
||||
# If this is a repeating element, make sure the member is set to a
|
||||
# list.
|
||||
if definition[2]:
|
||||
if getattr(self, definition[0]) is None:
|
||||
setattr(self, definition[0], [])
|
||||
getattr(self, definition[0]).append(_xml_element_from_tree(element,
|
||||
definition[1], version))
|
||||
else:
|
||||
setattr(self, definition[0], _xml_element_from_tree(element,
|
||||
definition[1], version))
|
||||
else:
|
||||
self._other_elements.append(_xml_element_from_tree(element, XmlElement,
|
||||
version))
|
||||
for attrib, value in tree.attrib.iteritems():
|
||||
if attributes and attrib in attributes:
|
||||
setattr(self, attributes[attrib], value)
|
||||
else:
|
||||
self._other_attributes[attrib] = value
|
||||
if tree.text:
|
||||
self.text = tree.text
|
||||
|
||||
def _to_tree(self, version=1, encoding=None):
|
||||
new_tree = ElementTree.Element(_get_qname(self, version))
|
||||
self._attach_members(new_tree, version, encoding)
|
||||
return new_tree
|
||||
|
||||
def _attach_members(self, tree, version=1, encoding=None):
|
||||
"""Convert members to XML elements/attributes and add them to the tree.
|
||||
|
||||
Args:
|
||||
tree: An ElementTree.Element which will be modified. The members of
|
||||
this object will be added as child elements or attributes
|
||||
according to the rules described in _expected_elements and
|
||||
_expected_attributes. The elements and attributes stored in
|
||||
other_attributes and other_elements are also added a children
|
||||
of this tree.
|
||||
version: int Ingnored in this method but used by VersionedElement.
|
||||
encoding: str (optional)
|
||||
"""
|
||||
qname, elements, attributes = self.__class__._get_rules(version)
|
||||
encoding = encoding or STRING_ENCODING
|
||||
# Add the expected elements and attributes to the tree.
|
||||
if elements:
|
||||
for tag, element_def in elements.iteritems():
|
||||
member = getattr(self, element_def[0])
|
||||
# If this is a repeating element and there are members in the list.
|
||||
if member and element_def[2]:
|
||||
for instance in member:
|
||||
instance._become_child(tree, version)
|
||||
elif member:
|
||||
member._become_child(tree, version)
|
||||
if attributes:
|
||||
for attribute_tag, member_name in attributes.iteritems():
|
||||
value = getattr(self, member_name)
|
||||
if value:
|
||||
tree.attrib[attribute_tag] = value
|
||||
# Add the unexpected (other) elements and attributes to the tree.
|
||||
for element in self._other_elements:
|
||||
element._become_child(tree, version)
|
||||
for key, value in self._other_attributes.iteritems():
|
||||
# I'm not sure if unicode can be used in the attribute name, so for now
|
||||
# we assume the encoding is correct for the attribute name.
|
||||
if not isinstance(value, unicode):
|
||||
value = value.decode(encoding)
|
||||
tree.attrib[key] = value
|
||||
if self.text:
|
||||
if isinstance(self.text, unicode):
|
||||
tree.text = self.text
|
||||
else:
|
||||
tree.text = self.text.decode(encoding)
|
||||
|
||||
def to_string(self, version=1, encoding=None, pretty_print=None):
|
||||
"""Converts this object to XML."""
|
||||
|
||||
tree_string = ElementTree.tostring(self._to_tree(version, encoding))
|
||||
|
||||
if pretty_print and xmlString is not None:
|
||||
return xmlString(tree_string).toprettyxml()
|
||||
|
||||
return tree_string
|
||||
|
||||
ToString = to_string
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
def _become_child(self, tree, version=1):
|
||||
"""Adds a child element to tree with the XML data in self."""
|
||||
new_child = ElementTree.Element('')
|
||||
tree.append(new_child)
|
||||
new_child.tag = _get_qname(self, version)
|
||||
self._attach_members(new_child, version)
|
||||
|
||||
def __get_extension_elements(self):
|
||||
return self._other_elements
|
||||
|
||||
def __set_extension_elements(self, elements):
|
||||
self._other_elements = elements
|
||||
|
||||
extension_elements = property(__get_extension_elements,
|
||||
__set_extension_elements,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
def __get_extension_attributes(self):
|
||||
return self._other_attributes
|
||||
|
||||
def __set_extension_attributes(self, attributes):
|
||||
self._other_attributes = attributes
|
||||
|
||||
extension_attributes = property(__get_extension_attributes,
|
||||
__set_extension_attributes,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
def _get_tag(self, version=1):
|
||||
qname = _get_qname(self, version)
|
||||
if qname:
|
||||
return qname[qname.find('}')+1:]
|
||||
return None
|
||||
|
||||
def _get_namespace(self, version=1):
|
||||
qname = _get_qname(self, version)
|
||||
if qname.startswith('{'):
|
||||
return qname[1:qname.find('}')]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _set_tag(self, tag):
|
||||
if isinstance(self._qname, tuple):
|
||||
self._qname = self._qname.copy()
|
||||
if self._qname[0].startswith('{'):
|
||||
self._qname[0] = '{%s}%s' % (self._get_namespace(1), tag)
|
||||
else:
|
||||
self._qname[0] = tag
|
||||
else:
|
||||
if self._qname is not None and self._qname.startswith('{'):
|
||||
self._qname = '{%s}%s' % (self._get_namespace(), tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
|
||||
def _set_namespace(self, namespace):
|
||||
tag = self._get_tag(1)
|
||||
if tag is None:
|
||||
tag = ''
|
||||
if isinstance(self._qname, tuple):
|
||||
self._qname = self._qname.copy()
|
||||
if namespace:
|
||||
self._qname[0] = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname[0] = tag
|
||||
else:
|
||||
if namespace:
|
||||
self._qname = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
|
||||
tag = property(_get_tag, _set_tag,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
namespace = property(_get_namespace, _set_namespace,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
# Provided for backwards compatibility to atom.ExtensionElement
|
||||
children = extension_elements
|
||||
attributes = extension_attributes
|
||||
|
||||
|
||||
def _get_qname(element, version):
|
||||
if isinstance(element._qname, tuple):
|
||||
if version <= len(element._qname):
|
||||
return element._qname[version-1]
|
||||
else:
|
||||
return element._qname[-1]
|
||||
else:
|
||||
return element._qname
|
||||
|
||||
|
||||
def _qname_matches(tag, namespace, qname):
|
||||
"""Logic determines if a QName matches the desired local tag and namespace.
|
||||
|
||||
This is used in XmlElement.get_elements and XmlElement.get_attributes to
|
||||
find matches in the element's members (among all expected-and-unexpected
|
||||
elements-and-attributes).
|
||||
|
||||
Args:
|
||||
expected_tag: string
|
||||
expected_namespace: string
|
||||
qname: string in the form '{xml_namespace}localtag' or 'tag' if there is
|
||||
no namespace.
|
||||
|
||||
Returns:
|
||||
boolean True if the member's tag and namespace fit the expected tag and
|
||||
namespace.
|
||||
"""
|
||||
# If there is no expected namespace or tag, then everything will match.
|
||||
if qname is None:
|
||||
member_tag = None
|
||||
member_namespace = None
|
||||
else:
|
||||
if qname.startswith('{'):
|
||||
member_namespace = qname[1:qname.index('}')]
|
||||
member_tag = qname[qname.index('}') + 1:]
|
||||
else:
|
||||
member_namespace = None
|
||||
member_tag = qname
|
||||
return ((tag is None and namespace is None)
|
||||
# If there is a tag, but no namespace, see if the local tag matches.
|
||||
or (namespace is None and member_tag == tag)
|
||||
# There was no tag, but there was a namespace so see if the namespaces
|
||||
# match.
|
||||
or (tag is None and member_namespace == namespace)
|
||||
# There was no tag, and the desired elements have no namespace, so check
|
||||
# to see that the member's namespace is None.
|
||||
or (tag is None and namespace == ''
|
||||
and member_namespace is None)
|
||||
# The tag and the namespace both match.
|
||||
or (tag == member_tag
|
||||
and namespace == member_namespace)
|
||||
# The tag matches, and the expected namespace is the empty namespace,
|
||||
# check to make sure the member's namespace is None.
|
||||
or (tag == member_tag and namespace == ''
|
||||
and member_namespace is None))
|
||||
|
||||
|
||||
def parse(xml_string, target_class=None, version=1, encoding=None):
|
||||
"""Parses the XML string according to the rules for the target_class.
|
||||
|
||||
Args:
|
||||
xml_string: str or unicode
|
||||
target_class: XmlElement or a subclass. If None is specified, the
|
||||
XmlElement class is used.
|
||||
version: int (optional) The version of the schema which should be used when
|
||||
converting the XML into an object. The default is 1.
|
||||
encoding: str (optional) The character encoding of the bytes in the
|
||||
xml_string. Default is 'UTF-8'.
|
||||
"""
|
||||
if target_class is None:
|
||||
target_class = XmlElement
|
||||
if isinstance(xml_string, unicode):
|
||||
if encoding is None:
|
||||
xml_string = xml_string.encode(STRING_ENCODING)
|
||||
else:
|
||||
xml_string = xml_string.encode(encoding)
|
||||
tree = ElementTree.fromstring(xml_string)
|
||||
return _xml_element_from_tree(tree, target_class, version)
|
||||
|
||||
|
||||
Parse = parse
|
||||
xml_element_from_string = parse
|
||||
XmlElementFromString = xml_element_from_string
|
||||
|
||||
|
||||
def _xml_element_from_tree(tree, target_class, version=1):
|
||||
if target_class._qname is None:
|
||||
instance = target_class()
|
||||
instance._qname = tree.tag
|
||||
instance._harvest_tree(tree, version)
|
||||
return instance
|
||||
# TODO handle the namespace-only case
|
||||
# Namespace only will be used with Google Spreadsheets rows and
|
||||
# Google Base item attributes.
|
||||
elif tree.tag == _get_qname(target_class, version):
|
||||
instance = target_class()
|
||||
instance._harvest_tree(tree, version)
|
||||
return instance
|
||||
return None
|
||||
|
||||
|
||||
class XmlAttribute(object):
|
||||
|
||||
def __init__(self, qname, value):
|
||||
self._qname = qname
|
||||
self.value = value
|
||||
|
||||
338
src/atom/data.py
338
src/atom/data.py
@@ -1,338 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# 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 is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import atom.core
|
||||
|
||||
|
||||
XML_TEMPLATE = '{http://www.w3.org/XML/1998/namespace}%s'
|
||||
ATOM_TEMPLATE = '{http://www.w3.org/2005/Atom}%s'
|
||||
APP_TEMPLATE_V1 = '{http://purl.org/atom/app#}%s'
|
||||
APP_TEMPLATE_V2 = '{http://www.w3.org/2007/app}%s'
|
||||
|
||||
|
||||
class Name(atom.core.XmlElement):
|
||||
"""The atom:name element."""
|
||||
_qname = ATOM_TEMPLATE % 'name'
|
||||
|
||||
|
||||
class Email(atom.core.XmlElement):
|
||||
"""The atom:email element."""
|
||||
_qname = ATOM_TEMPLATE % 'email'
|
||||
|
||||
|
||||
class Uri(atom.core.XmlElement):
|
||||
"""The atom:uri element."""
|
||||
_qname = ATOM_TEMPLATE % 'uri'
|
||||
|
||||
|
||||
class Person(atom.core.XmlElement):
|
||||
"""A foundation class which atom:author and atom:contributor extend.
|
||||
|
||||
A person contains information like name, email address, and web page URI for
|
||||
an author or contributor to an Atom feed.
|
||||
"""
|
||||
name = Name
|
||||
email = Email
|
||||
uri = Uri
|
||||
|
||||
|
||||
class Author(Person):
|
||||
"""The atom:author element.
|
||||
|
||||
An author is a required element in Feed unless each Entry contains an Author.
|
||||
"""
|
||||
_qname = ATOM_TEMPLATE % 'author'
|
||||
|
||||
|
||||
class Contributor(Person):
|
||||
"""The atom:contributor element."""
|
||||
_qname = ATOM_TEMPLATE % 'contributor'
|
||||
|
||||
|
||||
class Link(atom.core.XmlElement):
|
||||
"""The atom:link element."""
|
||||
_qname = ATOM_TEMPLATE % 'link'
|
||||
href = 'href'
|
||||
rel = 'rel'
|
||||
type = 'type'
|
||||
hreflang = 'hreflang'
|
||||
title = 'title'
|
||||
length = 'length'
|
||||
|
||||
|
||||
class Generator(atom.core.XmlElement):
|
||||
"""The atom:generator element."""
|
||||
_qname = ATOM_TEMPLATE % 'generator'
|
||||
uri = 'uri'
|
||||
version = 'version'
|
||||
|
||||
|
||||
class Text(atom.core.XmlElement):
|
||||
"""A foundation class from which atom:title, summary, etc. extend.
|
||||
|
||||
This class should never be instantiated.
|
||||
"""
|
||||
type = 'type'
|
||||
|
||||
|
||||
class Title(Text):
|
||||
"""The atom:title element."""
|
||||
_qname = ATOM_TEMPLATE % 'title'
|
||||
|
||||
|
||||
class Subtitle(Text):
|
||||
"""The atom:subtitle element."""
|
||||
_qname = ATOM_TEMPLATE % 'subtitle'
|
||||
|
||||
|
||||
class Rights(Text):
|
||||
"""The atom:rights element."""
|
||||
_qname = ATOM_TEMPLATE % 'rights'
|
||||
|
||||
|
||||
class Summary(Text):
|
||||
"""The atom:summary element."""
|
||||
_qname = ATOM_TEMPLATE % 'summary'
|
||||
|
||||
|
||||
class Content(Text):
|
||||
"""The atom:content element."""
|
||||
_qname = ATOM_TEMPLATE % 'content'
|
||||
src = 'src'
|
||||
|
||||
|
||||
class Category(atom.core.XmlElement):
|
||||
"""The atom:category element."""
|
||||
_qname = ATOM_TEMPLATE % 'category'
|
||||
term = 'term'
|
||||
scheme = 'scheme'
|
||||
label = 'label'
|
||||
|
||||
|
||||
class Id(atom.core.XmlElement):
|
||||
"""The atom:id element."""
|
||||
_qname = ATOM_TEMPLATE % 'id'
|
||||
|
||||
|
||||
class Icon(atom.core.XmlElement):
|
||||
"""The atom:icon element."""
|
||||
_qname = ATOM_TEMPLATE % 'icon'
|
||||
|
||||
|
||||
class Logo(atom.core.XmlElement):
|
||||
"""The atom:logo element."""
|
||||
_qname = ATOM_TEMPLATE % 'logo'
|
||||
|
||||
|
||||
class Draft(atom.core.XmlElement):
|
||||
"""The app:draft element which indicates if this entry should be public."""
|
||||
_qname = (APP_TEMPLATE_V1 % 'draft', APP_TEMPLATE_V2 % 'draft')
|
||||
|
||||
|
||||
class Control(atom.core.XmlElement):
|
||||
"""The app:control element indicating restrictions on publication.
|
||||
|
||||
The APP control element may contain a draft element indicating whether or
|
||||
not this entry should be publicly available.
|
||||
"""
|
||||
_qname = (APP_TEMPLATE_V1 % 'control', APP_TEMPLATE_V2 % 'control')
|
||||
draft = Draft
|
||||
|
||||
|
||||
class Date(atom.core.XmlElement):
|
||||
"""A parent class for atom:updated, published, etc."""
|
||||
|
||||
|
||||
class Updated(Date):
|
||||
"""The atom:updated element."""
|
||||
_qname = ATOM_TEMPLATE % 'updated'
|
||||
|
||||
|
||||
class Published(Date):
|
||||
"""The atom:published element."""
|
||||
_qname = ATOM_TEMPLATE % 'published'
|
||||
|
||||
|
||||
class LinkFinder(object):
|
||||
"""An "interface" providing methods to find link elements
|
||||
|
||||
Entry elements often contain multiple links which differ in the rel
|
||||
attribute or content type. Often, developers are interested in a specific
|
||||
type of link so this class provides methods to find specific classes of
|
||||
links.
|
||||
|
||||
This class is used as a mixin in Atom entries and feeds.
|
||||
"""
|
||||
|
||||
def find_url(self, rel):
|
||||
"""Returns the URL (as a string) in a link with the desired rel value."""
|
||||
for link in self.link:
|
||||
if link.rel == rel and link.href:
|
||||
return link.href
|
||||
return None
|
||||
|
||||
FindUrl = find_url
|
||||
|
||||
def get_link(self, rel):
|
||||
"""Returns a link object which has the desired rel value.
|
||||
|
||||
If you are interested in the URL instead of the link object,
|
||||
consider using find_url instead.
|
||||
"""
|
||||
for link in self.link:
|
||||
if link.rel == rel and link.href:
|
||||
return link
|
||||
return None
|
||||
|
||||
GetLink = get_link
|
||||
|
||||
def find_self_link(self):
|
||||
"""Find the first link with rel set to 'self'
|
||||
|
||||
Returns:
|
||||
A str containing the link's href or None if none of the links had rel
|
||||
equal to 'self'
|
||||
"""
|
||||
return self.find_url('self')
|
||||
|
||||
FindSelfLink = find_self_link
|
||||
|
||||
def get_self_link(self):
|
||||
return self.get_link('self')
|
||||
|
||||
GetSelfLink = get_self_link
|
||||
|
||||
def find_edit_link(self):
|
||||
return self.find_url('edit')
|
||||
|
||||
FindEditLink = find_edit_link
|
||||
|
||||
def get_edit_link(self):
|
||||
return self.get_link('edit')
|
||||
|
||||
GetEditLink = get_edit_link
|
||||
|
||||
def find_edit_media_link(self):
|
||||
link = self.find_url('edit-media')
|
||||
# Search for media-edit as well since Picasa API used media-edit instead.
|
||||
if link is None:
|
||||
return self.find_url('media-edit')
|
||||
return link
|
||||
|
||||
FindEditMediaLink = find_edit_media_link
|
||||
|
||||
def get_edit_media_link(self):
|
||||
link = self.get_link('edit-media')
|
||||
if link is None:
|
||||
return self.get_link('media-edit')
|
||||
return link
|
||||
|
||||
GetEditMediaLink = get_edit_media_link
|
||||
|
||||
def find_next_link(self):
|
||||
return self.find_url('next')
|
||||
|
||||
FindNextLink = find_next_link
|
||||
|
||||
def get_next_link(self):
|
||||
return self.get_link('next')
|
||||
|
||||
GetNextLink = get_next_link
|
||||
|
||||
def find_license_link(self):
|
||||
return self.find_url('license')
|
||||
|
||||
FindLicenseLink = find_license_link
|
||||
|
||||
def get_license_link(self):
|
||||
return self.get_link('license')
|
||||
|
||||
GetLicenseLink = get_license_link
|
||||
|
||||
def find_alternate_link(self):
|
||||
return self.find_url('alternate')
|
||||
|
||||
FindAlternateLink = find_alternate_link
|
||||
|
||||
def get_alternate_link(self):
|
||||
return self.get_link('alternate')
|
||||
|
||||
GetAlternateLink = get_alternate_link
|
||||
|
||||
|
||||
class FeedEntryParent(atom.core.XmlElement, LinkFinder):
|
||||
"""A super class for atom:feed and entry, contains shared attributes"""
|
||||
author = [Author]
|
||||
category = [Category]
|
||||
contributor = [Contributor]
|
||||
id = Id
|
||||
link = [Link]
|
||||
rights = Rights
|
||||
title = Title
|
||||
updated = Updated
|
||||
|
||||
def __init__(self, atom_id=None, text=None, *args, **kwargs):
|
||||
if atom_id is not None:
|
||||
self.id = atom_id
|
||||
atom.core.XmlElement.__init__(self, text=text, *args, **kwargs)
|
||||
|
||||
|
||||
class Source(FeedEntryParent):
|
||||
"""The atom:source element."""
|
||||
_qname = ATOM_TEMPLATE % 'source'
|
||||
generator = Generator
|
||||
icon = Icon
|
||||
logo = Logo
|
||||
subtitle = Subtitle
|
||||
|
||||
|
||||
class Entry(FeedEntryParent):
|
||||
"""The atom:entry element."""
|
||||
_qname = ATOM_TEMPLATE % 'entry'
|
||||
content = Content
|
||||
published = Published
|
||||
source = Source
|
||||
summary = Summary
|
||||
control = Control
|
||||
|
||||
|
||||
class Feed(Source):
|
||||
"""The atom:feed element which contains entries."""
|
||||
_qname = ATOM_TEMPLATE % 'feed'
|
||||
entry = [Entry]
|
||||
|
||||
|
||||
class ExtensionElement(atom.core.XmlElement):
|
||||
"""Provided for backwards compatibility to the v1 atom.ExtensionElement."""
|
||||
|
||||
def __init__(self, tag=None, namespace=None, attributes=None,
|
||||
children=None, text=None, *args, **kwargs):
|
||||
if namespace:
|
||||
self._qname = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
self.children = children or []
|
||||
self.attributes = attributes or {}
|
||||
self.text = text
|
||||
|
||||
_BecomeChildElement = atom.core.XmlElement._become_child
|
||||
364
src/atom/http.py
364
src/atom/http.py
@@ -1,364 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""HttpClients in this module use httplib to make HTTP requests.
|
||||
|
||||
This module make HTTP requests based on httplib, but there are environments
|
||||
in which an httplib based approach will not work (if running in Google App
|
||||
Engine for example). In those cases, higher level classes (like AtomService
|
||||
and GDataService) can swap out the HttpClient to transparently use a
|
||||
different mechanism for making HTTP requests.
|
||||
|
||||
HttpClient: Contains a request method which performs an HTTP call to the
|
||||
server.
|
||||
|
||||
ProxiedHttpClient: Contains a request method which connects to a proxy using
|
||||
settings stored in operating system environment variables then
|
||||
performs an HTTP call to the endpoint server.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import types
|
||||
import os
|
||||
import httplib
|
||||
import atom.url
|
||||
import atom.http_interface
|
||||
import socket
|
||||
import base64
|
||||
import atom.http_core
|
||||
ssl_imported = False
|
||||
ssl = None
|
||||
try:
|
||||
import ssl
|
||||
ssl_imported = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ProxyError(atom.http_interface.Error):
|
||||
pass
|
||||
|
||||
|
||||
class TestConfigurationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'application/atom+xml; charset=UTF-8'
|
||||
|
||||
|
||||
class HttpClient(atom.http_interface.GenericHttpClient):
|
||||
# Added to allow old v1 HttpClient objects to use the new
|
||||
# http_code.HttpClient. Used in unit tests to inject a mock client.
|
||||
v2_http_client = None
|
||||
|
||||
def __init__(self, headers=None):
|
||||
self.debug = False
|
||||
self.headers = headers or {}
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
|
||||
DELETE.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.http
|
||||
client = atom.http.HttpClient()
|
||||
http_response = client.request('GET', 'http://www.google.com/')
|
||||
|
||||
Args:
|
||||
operation: str The HTTP operation to be performed. This is usually one
|
||||
of 'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: filestream, list of parts, or other object which can be converted
|
||||
to a string. Should be set to None when performing a GET or DELETE.
|
||||
If data is a file-like object which can be read, this method will
|
||||
read a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be
|
||||
evaluated and sent.
|
||||
url: The full URL to which the request should be sent. Can be a string
|
||||
or atom.url.Url.
|
||||
headers: dict of strings. HTTP headers which should be sent
|
||||
in the request.
|
||||
"""
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
if isinstance(data, types.StringTypes):
|
||||
all_headers['Content-Length'] = str(len(data))
|
||||
else:
|
||||
raise atom.http_interface.ContentLengthRequired('Unable to calculate '
|
||||
'the length of the data parameter. Specify a value for '
|
||||
'Content-Length')
|
||||
|
||||
# Set the content type to the default value if none was set.
|
||||
if 'Content-Type' not in all_headers:
|
||||
all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE
|
||||
|
||||
if self.v2_http_client is not None:
|
||||
http_request = atom.http_core.HttpRequest(method=operation)
|
||||
atom.http_core.Uri.parse_uri(str(url)).modify_request(http_request)
|
||||
http_request.headers = all_headers
|
||||
if data:
|
||||
http_request._body_parts.append(data)
|
||||
return self.v2_http_client.request(http_request=http_request)
|
||||
|
||||
if not isinstance(url, atom.url.Url):
|
||||
if isinstance(url, types.StringTypes):
|
||||
url = atom.url.parse_url(url)
|
||||
else:
|
||||
raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
|
||||
'parameter because it was not a string or atom.url.Url')
|
||||
|
||||
connection = self._prepare_connection(url, all_headers)
|
||||
|
||||
if self.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
connection.putrequest(operation, self._get_access_url(url),
|
||||
skip_host=True)
|
||||
if url.port is not None:
|
||||
connection.putheader('Host', '%s:%s' % (url.host, url.port))
|
||||
else:
|
||||
connection.putheader('Host', url.host)
|
||||
|
||||
# Overcome a bug in Python 2.4 and 2.5
|
||||
# httplib.HTTPConnection.putrequest adding
|
||||
# HTTP request header 'Host: www.google.com:443' instead of
|
||||
# 'Host: www.google.com', and thus resulting the error message
|
||||
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
|
||||
if (url.protocol == 'https' and int(url.port or 443) == 443 and
|
||||
hasattr(connection, '_buffer') and
|
||||
isinstance(connection._buffer, list)):
|
||||
header_line = 'Host: %s:443' % url.host
|
||||
replacement_header_line = 'Host: %s' % url.host
|
||||
try:
|
||||
connection._buffer[connection._buffer.index(header_line)] = (
|
||||
replacement_header_line)
|
||||
except ValueError: # header_line missing from connection._buffer
|
||||
pass
|
||||
|
||||
# Send the HTTP headers.
|
||||
for header_name in all_headers:
|
||||
connection.putheader(header_name, all_headers[header_name])
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
for data_part in data:
|
||||
_send_data_part(data_part, connection)
|
||||
else:
|
||||
_send_data_part(data, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
def _prepare_connection(self, url, headers):
|
||||
if not isinstance(url, atom.url.Url):
|
||||
if isinstance(url, types.StringTypes):
|
||||
url = atom.url.parse_url(url)
|
||||
else:
|
||||
raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
|
||||
'parameter because it was not a string or atom.url.Url')
|
||||
if url.protocol == 'https':
|
||||
if not url.port:
|
||||
return httplib.HTTPSConnection(url.host)
|
||||
return httplib.HTTPSConnection(url.host, int(url.port))
|
||||
else:
|
||||
if not url.port:
|
||||
return httplib.HTTPConnection(url.host)
|
||||
return httplib.HTTPConnection(url.host, int(url.port))
|
||||
|
||||
def _get_access_url(self, url):
|
||||
return url.to_string()
|
||||
|
||||
|
||||
class ProxiedHttpClient(HttpClient):
|
||||
"""Performs an HTTP request through a proxy.
|
||||
|
||||
The proxy settings are obtained from enviroment variables. The URL of the
|
||||
proxy server is assumed to be stored in the environment variables
|
||||
'https_proxy' and 'http_proxy' respectively. If the proxy server requires
|
||||
a Basic Auth authorization header, the username and password are expected to
|
||||
be in the 'proxy-username' or 'proxy_username' variable and the
|
||||
'proxy-password' or 'proxy_password' variable, or in 'http_proxy' or
|
||||
'https_proxy' as "protocol://[username:password@]host:port".
|
||||
|
||||
After connecting to the proxy server, the request is completed as in
|
||||
HttpClient.request.
|
||||
"""
|
||||
def _prepare_connection(self, url, headers):
|
||||
proxy_settings = os.environ.get('%s_proxy' % url.protocol)
|
||||
if not proxy_settings:
|
||||
# The request was HTTP or HTTPS, but there was no appropriate proxy set.
|
||||
return HttpClient._prepare_connection(self, url, headers)
|
||||
else:
|
||||
#print '!!!!%s' % proxy_settings
|
||||
proxy_auth = _get_proxy_auth(proxy_settings)
|
||||
proxy_netloc = _get_proxy_net_location(proxy_settings)
|
||||
#print '!!!!%s' % proxy_auth
|
||||
#print '!!!!%s' % proxy_netloc
|
||||
if url.protocol == 'https':
|
||||
# Set any proxy auth headers
|
||||
if proxy_auth:
|
||||
proxy_auth = 'Proxy-authorization: %s' % proxy_auth
|
||||
|
||||
# Construct the proxy connect command.
|
||||
port = url.port
|
||||
if not port:
|
||||
port = '443'
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port)
|
||||
|
||||
# Set the user agent to send to the proxy
|
||||
if headers and 'User-Agent' in headers:
|
||||
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
|
||||
else:
|
||||
user_agent = 'User-Agent: python\r\n'
|
||||
|
||||
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
|
||||
|
||||
# Find the proxy host and port.
|
||||
proxy_url = atom.url.parse_url(proxy_netloc)
|
||||
if not proxy_url.port:
|
||||
proxy_url.port = '80'
|
||||
|
||||
# Connect to the proxy server, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
p_sock.connect((proxy_url.host, int(proxy_url.port)))
|
||||
p_sock.sendall(proxy_pieces)
|
||||
response = ''
|
||||
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
response += p_sock.recv(8192)
|
||||
|
||||
p_status = response.split()[1]
|
||||
if p_status != str(200):
|
||||
raise ProxyError('Error status=%s' % str(p_status))
|
||||
|
||||
# Trivial setup for ssl socket.
|
||||
sslobj = None
|
||||
if ssl_imported:
|
||||
sslobj = ssl.wrap_socket(p_sock, None, None)
|
||||
else:
|
||||
sock_ssl = socket.ssl(p_sock, None, None)
|
||||
sslobj = httplib.FakeSocket(p_sock, sock_ssl)
|
||||
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = httplib.HTTPConnection(proxy_url.host)
|
||||
connection.sock = sslobj
|
||||
return connection
|
||||
else:
|
||||
# If protocol was not https.
|
||||
# Find the proxy host and port.
|
||||
proxy_url = atom.url.parse_url(proxy_netloc)
|
||||
if not proxy_url.port:
|
||||
proxy_url.port = '80'
|
||||
|
||||
if proxy_auth:
|
||||
headers['Proxy-Authorization'] = proxy_auth.strip()
|
||||
|
||||
return httplib.HTTPConnection(proxy_url.host, int(proxy_url.port))
|
||||
|
||||
def _get_access_url(self, url):
|
||||
return url.to_string()
|
||||
|
||||
|
||||
def _get_proxy_auth(proxy_settings):
|
||||
"""Returns proxy authentication string for header.
|
||||
|
||||
Will check environment variables for proxy authentication info, starting with
|
||||
proxy(_/-)username and proxy(_/-)password before checking the given
|
||||
proxy_settings for a [protocol://]username:password@host[:port] string.
|
||||
|
||||
Args:
|
||||
proxy_settings: String from http_proxy or https_proxy environment variable.
|
||||
|
||||
Returns:
|
||||
Authentication string for proxy, or empty string if no proxy username was
|
||||
found.
|
||||
"""
|
||||
proxy_username = None
|
||||
proxy_password = None
|
||||
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
|
||||
if not proxy_username:
|
||||
if '@' in proxy_settings:
|
||||
protocol_and_proxy_auth = proxy_settings.split('@')[0].split(':')
|
||||
if len(protocol_and_proxy_auth) == 3:
|
||||
# 3 elements means we have [<protocol>, //<user>, <password>]
|
||||
proxy_username = protocol_and_proxy_auth[1].lstrip('/')
|
||||
proxy_password = protocol_and_proxy_auth[2]
|
||||
elif len(protocol_and_proxy_auth) == 2:
|
||||
# 2 elements means we have [<user>, <password>]
|
||||
proxy_username = protocol_and_proxy_auth[0]
|
||||
proxy_password = protocol_and_proxy_auth[1]
|
||||
if proxy_username:
|
||||
user_auth = base64.encodestring('%s:%s' % (proxy_username,
|
||||
proxy_password))
|
||||
return 'Basic %s\r\n' % (user_auth.strip())
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def _get_proxy_net_location(proxy_settings):
|
||||
"""Returns proxy host and port.
|
||||
|
||||
Args:
|
||||
proxy_settings: String from http_proxy or https_proxy environment variable.
|
||||
Must be in the form of protocol://[username:password@]host:port
|
||||
|
||||
Returns:
|
||||
String in the form of protocol://host:port
|
||||
"""
|
||||
if '@' in proxy_settings:
|
||||
protocol = proxy_settings.split(':')[0]
|
||||
netloc = proxy_settings.split('@')[1]
|
||||
return '%s://%s' % (protocol, netloc)
|
||||
else:
|
||||
return proxy_settings
|
||||
|
||||
|
||||
def _send_data_part(data, connection):
|
||||
if isinstance(data, types.StringTypes):
|
||||
connection.send(data)
|
||||
return
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == '': break
|
||||
connection.send(binarydata)
|
||||
return
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
connection.send(str(data))
|
||||
return
|
||||
@@ -1,597 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# 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 is used for version 2 of the Google Data APIs.
|
||||
# TODO: add proxy handling.
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import os
|
||||
import StringIO
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib
|
||||
ssl = None
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownSize(Error):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyError(Error):
|
||||
pass
|
||||
|
||||
|
||||
MIME_BOUNDARY = 'END_OF_PART'
|
||||
|
||||
|
||||
def get_headers(http_response):
|
||||
"""Retrieves all HTTP headers from an HTTP response from the server.
|
||||
|
||||
This method is provided for backwards compatibility for Python2.2 and 2.3.
|
||||
The httplib.HTTPResponse object in 2.2 and 2.3 does not have a getheaders
|
||||
method so this function will use getheaders if available, but if not it
|
||||
will retrieve a few using getheader.
|
||||
"""
|
||||
if hasattr(http_response, 'getheaders'):
|
||||
return http_response.getheaders()
|
||||
else:
|
||||
headers = []
|
||||
for header in (
|
||||
'location', 'content-type', 'content-length', 'age', 'allow',
|
||||
'cache-control', 'content-location', 'content-encoding', 'date',
|
||||
'etag', 'expires', 'last-modified', 'pragma', 'server',
|
||||
'set-cookie', 'transfer-encoding', 'vary', 'via', 'warning',
|
||||
'www-authenticate', 'gdata-version'):
|
||||
value = http_response.getheader(header, None)
|
||||
if value is not None:
|
||||
headers.append((header, value))
|
||||
return headers
|
||||
|
||||
|
||||
class HttpRequest(object):
|
||||
"""Contains all of the parameters for an HTTP 1.1 request.
|
||||
|
||||
The HTTP headers are represented by a dictionary, and it is the
|
||||
responsibility of the user to ensure that duplicate field names are combined
|
||||
into one header value according to the rules in section 4.2 of RFC 2616.
|
||||
"""
|
||||
method = None
|
||||
uri = None
|
||||
|
||||
def __init__(self, uri=None, method=None, headers=None):
|
||||
"""Construct an HTTP request.
|
||||
|
||||
Args:
|
||||
uri: The full path or partial path as a Uri object or a string.
|
||||
method: The HTTP method for the request, examples include 'GET', 'POST',
|
||||
etc.
|
||||
headers: dict of strings The HTTP headers to include in the request.
|
||||
"""
|
||||
self.headers = headers or {}
|
||||
self._body_parts = []
|
||||
if method is not None:
|
||||
self.method = method
|
||||
if isinstance(uri, (str, unicode)):
|
||||
uri = Uri.parse_uri(uri)
|
||||
self.uri = uri or Uri()
|
||||
|
||||
|
||||
def add_body_part(self, data, mime_type, size=None):
|
||||
"""Adds data to the HTTP request body.
|
||||
|
||||
If more than one part is added, this is assumed to be a mime-multipart
|
||||
request. This method is designed to create MIME 1.0 requests as specified
|
||||
in RFC 1341.
|
||||
|
||||
Args:
|
||||
data: str or a file-like object containing a part of the request body.
|
||||
mime_type: str The MIME type describing the data
|
||||
size: int Required if the data is a file like object. If the data is a
|
||||
string, the size is calculated so this parameter is ignored.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
size = len(data)
|
||||
if size is None:
|
||||
# TODO: support chunked transfer if some of the body is of unknown size.
|
||||
raise UnknownSize('Each part of the body must have a known size.')
|
||||
if 'Content-Length' in self.headers:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
else:
|
||||
content_length = 0
|
||||
# If this is the first part added to the body, then this is not a multipart
|
||||
# request.
|
||||
if len(self._body_parts) == 0:
|
||||
self.headers['Content-Type'] = mime_type
|
||||
content_length = size
|
||||
self._body_parts.append(data)
|
||||
elif len(self._body_parts) == 1:
|
||||
# This is the first member in a mime-multipart request, so change the
|
||||
# _body_parts list to indicate a multipart payload.
|
||||
self._body_parts.insert(0, 'Media multipart posting')
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
content_length += len(boundary_string) + size
|
||||
self._body_parts.insert(1, boundary_string)
|
||||
content_length += len('Media multipart posting')
|
||||
# Put the content type of the first part of the body into the multipart
|
||||
# payload.
|
||||
original_type_string = 'Content-Type: %s\r\n\r\n' % (
|
||||
self.headers['Content-Type'],)
|
||||
self._body_parts.insert(2, original_type_string)
|
||||
content_length += len(original_type_string)
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
self._body_parts.append(boundary_string)
|
||||
content_length += len(boundary_string)
|
||||
# Change the headers to indicate this is now a mime multipart request.
|
||||
self.headers['Content-Type'] = 'multipart/related; boundary="%s"' % (
|
||||
MIME_BOUNDARY,)
|
||||
self.headers['MIME-version'] = '1.0'
|
||||
# Include the mime type of this part.
|
||||
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
|
||||
self._body_parts.append(type_string)
|
||||
content_length += len(type_string)
|
||||
self._body_parts.append(data)
|
||||
ending_boundary_string = '\r\n--%s--' % (MIME_BOUNDARY,)
|
||||
self._body_parts.append(ending_boundary_string)
|
||||
content_length += len(ending_boundary_string)
|
||||
else:
|
||||
# This is a mime multipart request.
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
self._body_parts.insert(-1, boundary_string)
|
||||
content_length += len(boundary_string) + size
|
||||
# Include the mime type of this part.
|
||||
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
|
||||
self._body_parts.insert(-1, type_string)
|
||||
content_length += len(type_string)
|
||||
self._body_parts.insert(-1, data)
|
||||
self.headers['Content-Length'] = str(content_length)
|
||||
# I could add an "append_to_body_part" method as well.
|
||||
|
||||
AddBodyPart = add_body_part
|
||||
|
||||
def add_form_inputs(self, form_data,
|
||||
mime_type='application/x-www-form-urlencoded'):
|
||||
"""Form-encodes and adds data to the request body.
|
||||
|
||||
Args:
|
||||
form_data: dict or sequnce or two member tuples which contains the
|
||||
form keys and values.
|
||||
mime_type: str The MIME type of the form data being sent. Defaults
|
||||
to 'application/x-www-form-urlencoded'.
|
||||
"""
|
||||
body = urllib.urlencode(form_data)
|
||||
self.add_body_part(body, mime_type)
|
||||
|
||||
AddFormInputs = add_form_inputs
|
||||
|
||||
def _copy(self):
|
||||
"""Creates a deep copy of this request."""
|
||||
copied_uri = Uri(self.uri.scheme, self.uri.host, self.uri.port,
|
||||
self.uri.path, self.uri.query.copy())
|
||||
new_request = HttpRequest(uri=copied_uri, method=self.method,
|
||||
headers=self.headers.copy())
|
||||
new_request._body_parts = self._body_parts[:]
|
||||
return new_request
|
||||
|
||||
def _dump(self):
|
||||
"""Converts to a printable string for debugging purposes.
|
||||
|
||||
In order to preserve the request, it does not read from file-like objects
|
||||
in the body.
|
||||
"""
|
||||
output = 'HTTP Request\n method: %s\n url: %s\n headers:\n' % (
|
||||
self.method, str(self.uri))
|
||||
for header, value in self.headers.iteritems():
|
||||
output += ' %s: %s\n' % (header, value)
|
||||
output += ' body sections:\n'
|
||||
i = 0
|
||||
for part in self._body_parts:
|
||||
if isinstance(part, (str, unicode)):
|
||||
output += ' %s: %s\n' % (i, part)
|
||||
else:
|
||||
output += ' %s: <file like object>\n' % i
|
||||
i += 1
|
||||
return output
|
||||
|
||||
|
||||
def _apply_defaults(http_request):
|
||||
if http_request.uri.scheme is None:
|
||||
if http_request.uri.port == 443:
|
||||
http_request.uri.scheme = 'https'
|
||||
else:
|
||||
http_request.uri.scheme = 'http'
|
||||
|
||||
|
||||
class Uri(object):
|
||||
"""A URI as used in HTTP 1.1"""
|
||||
scheme = None
|
||||
host = None
|
||||
port = None
|
||||
path = None
|
||||
|
||||
def __init__(self, scheme=None, host=None, port=None, path=None, query=None):
|
||||
"""Constructor for a URI.
|
||||
|
||||
Args:
|
||||
scheme: str This is usually 'http' or 'https'.
|
||||
host: str The host name or IP address of the desired server.
|
||||
post: int The server's port number.
|
||||
path: str The path of the resource following the host. This begins with
|
||||
a /, example: '/calendar/feeds/default/allcalendars/full'
|
||||
query: dict of strings The URL query parameters. The keys and values are
|
||||
both escaped so this dict should contain the unescaped values.
|
||||
For example {'my key': 'val', 'second': '!!!'} will become
|
||||
'?my+key=val&second=%21%21%21' which is appended to the path.
|
||||
"""
|
||||
self.query = query or {}
|
||||
if scheme is not None:
|
||||
self.scheme = scheme
|
||||
if host is not None:
|
||||
self.host = host
|
||||
if port is not None:
|
||||
self.port = port
|
||||
if path:
|
||||
self.path = path
|
||||
|
||||
def _get_query_string(self):
|
||||
param_pairs = []
|
||||
for key, value in self.query.iteritems():
|
||||
param_pairs.append('='.join((urllib.quote_plus(key),
|
||||
urllib.quote_plus(str(value)))))
|
||||
return '&'.join(param_pairs)
|
||||
|
||||
def _get_relative_path(self):
|
||||
"""Returns the path with the query parameters escaped and appended."""
|
||||
param_string = self._get_query_string()
|
||||
if self.path is None:
|
||||
path = '/'
|
||||
else:
|
||||
path = self.path
|
||||
if param_string:
|
||||
return '?'.join([path, param_string])
|
||||
else:
|
||||
return path
|
||||
|
||||
def _to_string(self):
|
||||
if self.scheme is None and self.port == 443:
|
||||
scheme = 'https'
|
||||
elif self.scheme is None:
|
||||
scheme = 'http'
|
||||
else:
|
||||
scheme = self.scheme
|
||||
if self.path is None:
|
||||
path = '/'
|
||||
else:
|
||||
path = self.path
|
||||
if self.port is None:
|
||||
return '%s://%s%s' % (scheme, self.host, self._get_relative_path())
|
||||
else:
|
||||
return '%s://%s:%s%s' % (scheme, self.host, str(self.port),
|
||||
self._get_relative_path())
|
||||
|
||||
def __str__(self):
|
||||
return self._to_string()
|
||||
|
||||
def modify_request(self, http_request=None):
|
||||
"""Sets HTTP request components based on the URI."""
|
||||
if http_request is None:
|
||||
http_request = HttpRequest()
|
||||
if http_request.uri is None:
|
||||
http_request.uri = Uri()
|
||||
# Determine the correct scheme.
|
||||
if self.scheme:
|
||||
http_request.uri.scheme = self.scheme
|
||||
if self.port:
|
||||
http_request.uri.port = self.port
|
||||
if self.host:
|
||||
http_request.uri.host = self.host
|
||||
# Set the relative uri path
|
||||
if self.path:
|
||||
http_request.uri.path = self.path
|
||||
if self.query:
|
||||
http_request.uri.query = self.query.copy()
|
||||
return http_request
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
def parse_uri(uri_string):
|
||||
"""Creates a Uri object which corresponds to the URI string.
|
||||
|
||||
This method can accept partial URIs, but it will leave missing
|
||||
members of the Uri unset.
|
||||
"""
|
||||
parts = urlparse.urlparse(uri_string)
|
||||
uri = Uri()
|
||||
if parts[0]:
|
||||
uri.scheme = parts[0]
|
||||
if parts[1]:
|
||||
host_parts = parts[1].split(':')
|
||||
if host_parts[0]:
|
||||
uri.host = host_parts[0]
|
||||
if len(host_parts) > 1:
|
||||
uri.port = int(host_parts[1])
|
||||
if parts[2]:
|
||||
uri.path = parts[2]
|
||||
if parts[4]:
|
||||
param_pairs = parts[4].split('&')
|
||||
for pair in param_pairs:
|
||||
pair_parts = pair.split('=')
|
||||
if len(pair_parts) > 1:
|
||||
uri.query[urllib.unquote_plus(pair_parts[0])] = (
|
||||
urllib.unquote_plus(pair_parts[1]))
|
||||
elif len(pair_parts) == 1:
|
||||
uri.query[urllib.unquote_plus(pair_parts[0])] = None
|
||||
return uri
|
||||
|
||||
parse_uri = staticmethod(parse_uri)
|
||||
|
||||
ParseUri = parse_uri
|
||||
|
||||
|
||||
parse_uri = Uri.parse_uri
|
||||
|
||||
|
||||
ParseUri = Uri.parse_uri
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
status = None
|
||||
reason = None
|
||||
_body = None
|
||||
|
||||
def __init__(self, status=None, reason=None, headers=None, body=None):
|
||||
self._headers = headers or {}
|
||||
if status is not None:
|
||||
self.status = status
|
||||
if reason is not None:
|
||||
self.reason = reason
|
||||
if body is not None:
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body
|
||||
else:
|
||||
self._body = StringIO.StringIO(body)
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
if name in self._headers:
|
||||
return self._headers[name]
|
||||
else:
|
||||
return default
|
||||
|
||||
def getheaders(self):
|
||||
return self._headers
|
||||
|
||||
def read(self, amt=None):
|
||||
if self._body is None:
|
||||
return None
|
||||
if not amt:
|
||||
return self._body.read()
|
||||
else:
|
||||
return self._body.read(amt)
|
||||
|
||||
|
||||
def _dump_response(http_response):
|
||||
"""Converts to a string for printing debug messages.
|
||||
|
||||
Does not read the body since that may consume the content.
|
||||
"""
|
||||
output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % (
|
||||
http_response.status, http_response.reason)
|
||||
headers = get_headers(http_response)
|
||||
if isinstance(headers, dict):
|
||||
for header, value in headers.iteritems():
|
||||
output += ' %s: %s\n' % (header, value)
|
||||
else:
|
||||
for pair in headers:
|
||||
output += ' %s: %s\n' % (pair[0], pair[1])
|
||||
return output
|
||||
|
||||
|
||||
class HttpClient(object):
|
||||
"""Performs HTTP requests using httplib."""
|
||||
debug = None
|
||||
|
||||
def request(self, http_request):
|
||||
return self._http_request(http_request.method, http_request.uri,
|
||||
http_request.headers, http_request._body_parts)
|
||||
|
||||
Request = request
|
||||
|
||||
def _get_connection(self, uri, headers=None):
|
||||
"""Opens a socket connection to the server to set up an HTTP request.
|
||||
|
||||
Args:
|
||||
uri: The full URL for the request as a Uri object.
|
||||
headers: A dict of string pairs containing the HTTP headers for the
|
||||
request.
|
||||
"""
|
||||
connection = None
|
||||
if uri.scheme == 'https':
|
||||
if not uri.port:
|
||||
connection = httplib.HTTPSConnection(uri.host)
|
||||
else:
|
||||
connection = httplib.HTTPSConnection(uri.host, int(uri.port))
|
||||
else:
|
||||
if not uri.port:
|
||||
connection = httplib.HTTPConnection(uri.host)
|
||||
else:
|
||||
connection = httplib.HTTPConnection(uri.host, int(uri.port))
|
||||
return connection
|
||||
|
||||
def _http_request(self, method, uri, headers=None, body_parts=None):
|
||||
"""Makes an HTTP request using httplib.
|
||||
|
||||
Args:
|
||||
method: str example: 'GET', 'POST', 'PUT', 'DELETE', etc.
|
||||
uri: str or atom.http_core.Uri
|
||||
headers: dict of strings mapping to strings which will be sent as HTTP
|
||||
headers in the request.
|
||||
body_parts: list of strings, objects with a read method, or objects
|
||||
which can be converted to strings using str. Each of these
|
||||
will be sent in order as the body of the HTTP request.
|
||||
"""
|
||||
if isinstance(uri, (str, unicode)):
|
||||
uri = Uri.parse_uri(uri)
|
||||
|
||||
connection = self._get_connection(uri, headers=headers)
|
||||
|
||||
if self.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
if connection.host != uri.host:
|
||||
connection.putrequest(method, str(uri))
|
||||
else:
|
||||
connection.putrequest(method, uri._get_relative_path())
|
||||
|
||||
# Overcome a bug in Python 2.4 and 2.5
|
||||
# httplib.HTTPConnection.putrequest adding
|
||||
# HTTP request header 'Host: www.google.com:443' instead of
|
||||
# 'Host: www.google.com', and thus resulting the error message
|
||||
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
|
||||
if (uri.scheme == 'https' and int(uri.port or 443) == 443 and
|
||||
hasattr(connection, '_buffer') and
|
||||
isinstance(connection._buffer, list)):
|
||||
header_line = 'Host: %s:443' % uri.host
|
||||
replacement_header_line = 'Host: %s' % uri.host
|
||||
try:
|
||||
connection._buffer[connection._buffer.index(header_line)] = (
|
||||
replacement_header_line)
|
||||
except ValueError: # header_line missing from connection._buffer
|
||||
pass
|
||||
|
||||
# Send the HTTP headers.
|
||||
for header_name, value in headers.iteritems():
|
||||
connection.putheader(header_name, value)
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if body_parts and filter(lambda x: x != '', body_parts):
|
||||
for part in body_parts:
|
||||
_send_data_part(part, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
|
||||
def _send_data_part(data, connection):
|
||||
if isinstance(data, (str, unicode)):
|
||||
# I might want to just allow str, not unicode.
|
||||
connection.send(data)
|
||||
return
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == '': break
|
||||
connection.send(binarydata)
|
||||
return
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
connection.send(str(data))
|
||||
return
|
||||
|
||||
|
||||
class ProxiedHttpClient(HttpClient):
|
||||
|
||||
def _get_connection(self, uri, headers=None):
|
||||
# Check to see if there are proxy settings required for this request.
|
||||
proxy = None
|
||||
if uri.scheme == 'https':
|
||||
proxy = os.environ.get('https_proxy')
|
||||
elif uri.scheme == 'http':
|
||||
proxy = os.environ.get('http_proxy')
|
||||
if not proxy:
|
||||
return HttpClient._get_connection(self, uri, headers=headers)
|
||||
# Now we have the URL of the appropriate proxy server.
|
||||
# Get a username and password for the proxy if required.
|
||||
proxy_auth = _get_proxy_auth()
|
||||
if uri.scheme == 'https':
|
||||
import socket
|
||||
if proxy_auth:
|
||||
proxy_auth = 'Proxy-authorization: %s' % proxy_auth
|
||||
# Construct the proxy connect command.
|
||||
port = uri.port
|
||||
if not port:
|
||||
port = 443
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (uri.host, port)
|
||||
# Set the user agent to send to the proxy
|
||||
user_agent = ''
|
||||
if headers and 'User-Agent' in headers:
|
||||
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
|
||||
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
|
||||
# Find the proxy host and port.
|
||||
proxy_uri = Uri.parse_uri(proxy)
|
||||
if not proxy_uri.port:
|
||||
proxy_uri.port = '80'
|
||||
# Connect to the proxy server, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
p_sock.connect((proxy_uri.host, int(proxy_uri.port)))
|
||||
p_sock.sendall(proxy_pieces)
|
||||
response = ''
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
response += p_sock.recv(8192)
|
||||
p_status = response.split()[1]
|
||||
if p_status != str(200):
|
||||
raise ProxyError('Error status=%s' % str(p_status))
|
||||
# Trivial setup for ssl socket.
|
||||
sslobj = None
|
||||
if ssl is not None:
|
||||
sslobj = ssl.wrap_socket(p_sock, None, None)
|
||||
else:
|
||||
sock_ssl = socket.ssl(p_sock, None, Nonesock_)
|
||||
sslobj = httplib.FakeSocket(p_sock, sock_ssl)
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = httplib.HTTPConnection(proxy_uri.host)
|
||||
connection.sock = sslobj
|
||||
return connection
|
||||
elif uri.scheme == 'http':
|
||||
proxy_uri = Uri.parse_uri(proxy)
|
||||
if not proxy_uri.port:
|
||||
proxy_uri.port = '80'
|
||||
if proxy_auth:
|
||||
headers['Proxy-Authorization'] = proxy_auth.strip()
|
||||
return httplib.HTTPConnection(proxy_uri.host, int(proxy_uri.port))
|
||||
return None
|
||||
|
||||
|
||||
def _get_proxy_auth():
|
||||
import base64
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
user_auth = base64.b64encode('%s:%s' % (proxy_username,
|
||||
proxy_password))
|
||||
return 'Basic %s\r\n' % (user_auth.strip())
|
||||
else:
|
||||
return ''
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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 provides a common interface for all HTTP requests.
|
||||
|
||||
HttpResponse: Represents the server's response to an HTTP request. Provides
|
||||
an interface identical to httplib.HTTPResponse which is the response
|
||||
expected from higher level classes which use HttpClient.request.
|
||||
|
||||
GenericHttpClient: Provides an interface (superclass) for an object
|
||||
responsible for making HTTP requests. Subclasses of this object are
|
||||
used in AtomService and GDataService to make requests to the server. By
|
||||
changing the http_client member object, the AtomService is able to make
|
||||
HTTP requests using different logic (for example, when running on
|
||||
Google App Engine, the http_client makes requests using the App Engine
|
||||
urlfetch API).
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import StringIO
|
||||
|
||||
|
||||
USER_AGENT = '%s GData-Python 2.0.14+20110902+custom_mods'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnparsableUrlObject(Error):
|
||||
pass
|
||||
|
||||
|
||||
class ContentLengthRequired(Error):
|
||||
pass
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
"""Constructor for an HttpResponse object.
|
||||
|
||||
HttpResponse represents the server's response to an HTTP request from
|
||||
the client. The HttpClient.request method returns a httplib.HTTPResponse
|
||||
object and this HttpResponse class is designed to mirror the interface
|
||||
exposed by httplib.HTTPResponse.
|
||||
|
||||
Args:
|
||||
body: A file like object, with a read() method. The body could also
|
||||
be a string, and the constructor will wrap it so that
|
||||
HttpResponse.read(self) will return the full string.
|
||||
status: The HTTP status code as an int. Example: 200, 201, 404.
|
||||
reason: The HTTP status message which follows the code. Example:
|
||||
OK, Created, Not Found
|
||||
headers: A dictionary containing the HTTP headers in the server's
|
||||
response. A common header in the response is Content-Length.
|
||||
"""
|
||||
if body:
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body
|
||||
else:
|
||||
self._body = StringIO.StringIO(body)
|
||||
else:
|
||||
self._body = None
|
||||
if status is not None:
|
||||
self.status = int(status)
|
||||
else:
|
||||
self.status = None
|
||||
self.reason = reason
|
||||
self._headers = headers or {}
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
if name in self._headers:
|
||||
return self._headers[name]
|
||||
else:
|
||||
return default
|
||||
|
||||
def read(self, amt=None):
|
||||
if not amt:
|
||||
return self._body.read()
|
||||
else:
|
||||
return self._body.read(amt)
|
||||
|
||||
|
||||
class GenericHttpClient(object):
|
||||
debug = False
|
||||
|
||||
def __init__(self, http_client, headers=None):
|
||||
"""
|
||||
|
||||
Args:
|
||||
http_client: An object which provides a request method to make an HTTP
|
||||
request. The request method in GenericHttpClient performs a
|
||||
call-through to the contained HTTP client object.
|
||||
headers: A dictionary containing HTTP headers which should be included
|
||||
in every HTTP request. Common persistent headers include
|
||||
'User-Agent'.
|
||||
"""
|
||||
self.http_client = http_client
|
||||
self.headers = headers or {}
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
return self.http_client.request(operation, url, data=data,
|
||||
headers=all_headers)
|
||||
|
||||
def get(self, url, headers=None):
|
||||
return self.request('GET', url, headers=headers)
|
||||
|
||||
def post(self, url, data, headers=None):
|
||||
return self.request('POST', url, data=data, headers=headers)
|
||||
|
||||
def put(self, url, data, headers=None):
|
||||
return self.request('PUT', url, data=data, headers=headers)
|
||||
|
||||
def delete(self, url, headers=None):
|
||||
return self.request('DELETE', url, headers=headers)
|
||||
|
||||
|
||||
class GenericToken(object):
|
||||
"""Represents an Authorization token to be added to HTTP requests.
|
||||
|
||||
Some Authorization headers included calculated fields (digital
|
||||
signatures for example) which are based on the parameters of the HTTP
|
||||
request. Therefore the token is responsible for signing the request
|
||||
and adding the Authorization header.
|
||||
"""
|
||||
def perform_request(self, http_client, operation, url, data=None,
|
||||
headers=None):
|
||||
"""For the GenericToken, no Authorization token is set."""
|
||||
return http_client.request(operation, url, data=data, headers=headers)
|
||||
|
||||
def valid_for_scope(self, url):
|
||||
"""Tells the caller if the token authorizes access to the desired URL.
|
||||
|
||||
Since the generic token doesn't add an auth header, it is not valid for
|
||||
any scope.
|
||||
"""
|
||||
return False
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRecordingFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
"""Holds parameters of an HTTP request for matching against future requests.
|
||||
"""
|
||||
def __init__(self, operation, url, data=None, headers=None):
|
||||
self.operation = operation
|
||||
if isinstance(url, (str, unicode)):
|
||||
url = atom.url.parse_url(url)
|
||||
self.url = url
|
||||
self.data = data
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class MockResponse(atom.http_interface.HttpResponse):
|
||||
"""Simulates an httplib.HTTPResponse object."""
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
if body and hasattr(body, 'read'):
|
||||
self.body = body.read()
|
||||
else:
|
||||
self.body = body
|
||||
if status is not None:
|
||||
self.status = int(status)
|
||||
else:
|
||||
self.status = None
|
||||
self.reason = reason
|
||||
self._headers = headers or {}
|
||||
|
||||
def read(self):
|
||||
return self.body
|
||||
|
||||
|
||||
class MockHttpClient(atom.http_interface.GenericHttpClient):
|
||||
def __init__(self, headers=None, recordings=None, real_client=None):
|
||||
"""An HttpClient which responds to request with stored data.
|
||||
|
||||
The request-response pairs are stored as tuples in a member list named
|
||||
recordings.
|
||||
|
||||
The MockHttpClient can be switched from replay mode to record mode by
|
||||
setting the real_client member to an instance of an HttpClient which will
|
||||
make real HTTP requests and store the server's response in list of
|
||||
recordings.
|
||||
|
||||
Args:
|
||||
headers: dict containing HTTP headers which should be included in all
|
||||
HTTP requests.
|
||||
recordings: The initial recordings to be used for responses. This list
|
||||
contains tuples in the form: (MockRequest, MockResponse)
|
||||
real_client: An HttpClient which will make a real HTTP request. The
|
||||
response will be converted into a MockResponse and stored in
|
||||
recordings.
|
||||
"""
|
||||
self.recordings = recordings or []
|
||||
self.real_client = real_client
|
||||
self.headers = headers or {}
|
||||
|
||||
def add_response(self, response, operation, url, data=None, headers=None):
|
||||
"""Adds a request-response pair to the recordings list.
|
||||
|
||||
After the recording is added, future matching requests will receive the
|
||||
response.
|
||||
|
||||
Args:
|
||||
response: MockResponse
|
||||
operation: str
|
||||
url: str
|
||||
data: str, Currently the data is ignored when looking for matching
|
||||
requests.
|
||||
headers: dict of strings: Currently the headers are ignored when
|
||||
looking for matching requests.
|
||||
"""
|
||||
request = MockRequest(operation, url, data=data, headers=headers)
|
||||
self.recordings.append((request, response))
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Returns a matching MockResponse from the recordings.
|
||||
|
||||
If the real_client is set, the request will be passed along and the
|
||||
server's response will be added to the recordings and also returned.
|
||||
|
||||
If there is no match, a NoRecordingFound error will be raised.
|
||||
"""
|
||||
if self.real_client is None:
|
||||
if isinstance(url, (str, unicode)):
|
||||
url = atom.url.parse_url(url)
|
||||
for recording in self.recordings:
|
||||
if recording[0].operation == operation and recording[0].url == url:
|
||||
return recording[1]
|
||||
raise NoRecordingFound('No recodings found for %s %s' % (
|
||||
operation, url))
|
||||
else:
|
||||
# There is a real HTTP client, so make the request, and record the
|
||||
# response.
|
||||
response = self.real_client.request(operation, url, data=data,
|
||||
headers=headers)
|
||||
# TODO: copy the headers
|
||||
stored_response = MockResponse(body=response, status=response.status,
|
||||
reason=response.reason)
|
||||
self.add_response(stored_response, operation, url, data=data,
|
||||
headers=headers)
|
||||
return stored_response
|
||||
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# 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 is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
import StringIO
|
||||
import pickle
|
||||
import os.path
|
||||
import tempfile
|
||||
import atom.http_core
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRecordingFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class MockHttpClient(object):
|
||||
debug = None
|
||||
real_client = None
|
||||
last_request_was_live = False
|
||||
|
||||
# The following members are used to construct the session cache temp file
|
||||
# name.
|
||||
# These are combined to form the file name
|
||||
# /tmp/cache_prefix.cache_case_name.cache_test_name
|
||||
cache_name_prefix = 'gdata_live_test'
|
||||
cache_case_name = ''
|
||||
cache_test_name = ''
|
||||
|
||||
def __init__(self, recordings=None, real_client=None):
|
||||
self._recordings = recordings or []
|
||||
if real_client is not None:
|
||||
self.real_client = real_client
|
||||
|
||||
def add_response(self, http_request, status, reason, headers=None,
|
||||
body=None):
|
||||
response = MockHttpResponse(status, reason, headers, body)
|
||||
# TODO Scrub the request and the response.
|
||||
self._recordings.append((http_request._copy(), response))
|
||||
|
||||
AddResponse = add_response
|
||||
|
||||
def request(self, http_request):
|
||||
"""Provide a recorded response, or record a response for replay.
|
||||
|
||||
If the real_client is set, the request will be made using the
|
||||
real_client, and the response from the server will be recorded.
|
||||
If the real_client is None (the default), this method will examine
|
||||
the recordings and find the first which matches.
|
||||
"""
|
||||
request = http_request._copy()
|
||||
_scrub_request(request)
|
||||
if self.real_client is None:
|
||||
self.last_request_was_live = False
|
||||
for recording in self._recordings:
|
||||
if _match_request(recording[0], request):
|
||||
return recording[1]
|
||||
else:
|
||||
# Pass along the debug settings to the real client.
|
||||
self.real_client.debug = self.debug
|
||||
# Make an actual request since we can use the real HTTP client.
|
||||
self.last_request_was_live = True
|
||||
response = self.real_client.request(http_request)
|
||||
scrubbed_response = _scrub_response(response)
|
||||
self.add_response(request, scrubbed_response.status,
|
||||
scrubbed_response.reason,
|
||||
dict(atom.http_core.get_headers(scrubbed_response)),
|
||||
scrubbed_response.read())
|
||||
# Return the recording which we just added.
|
||||
return self._recordings[-1][1]
|
||||
raise NoRecordingFound('No recoding was found for request: %s %s' % (
|
||||
request.method, str(request.uri)))
|
||||
|
||||
Request = request
|
||||
|
||||
def _save_recordings(self, filename):
|
||||
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
|
||||
'wb')
|
||||
pickle.dump(self._recordings, recording_file)
|
||||
recording_file.close()
|
||||
|
||||
def _load_recordings(self, filename):
|
||||
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
|
||||
'rb')
|
||||
self._recordings = pickle.load(recording_file)
|
||||
recording_file.close()
|
||||
|
||||
def _delete_recordings(self, filename):
|
||||
full_path = os.path.join(tempfile.gettempdir(), filename)
|
||||
if os.path.exists(full_path):
|
||||
os.remove(full_path)
|
||||
|
||||
def _load_or_use_client(self, filename, http_client):
|
||||
if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
|
||||
self._load_recordings(filename)
|
||||
else:
|
||||
self.real_client = http_client
|
||||
|
||||
def use_cached_session(self, name=None, real_http_client=None):
|
||||
"""Attempts to load recordings from a previous live request.
|
||||
|
||||
If a temp file with the recordings exists, then it is used to fulfill
|
||||
requests. If the file does not exist, then a real client is used to
|
||||
actually make the desired HTTP requests. Requests and responses are
|
||||
recorded and will be written to the desired temprary cache file when
|
||||
close_session is called.
|
||||
|
||||
Args:
|
||||
name: str (optional) The file name of session file to be used. The file
|
||||
is loaded from the temporary directory of this machine. If no name
|
||||
is passed in, a default name will be constructed using the
|
||||
cache_name_prefix, cache_case_name, and cache_test_name of this
|
||||
object.
|
||||
real_http_client: atom.http_core.HttpClient the real client to be used
|
||||
if the cached recordings are not found. If the default
|
||||
value is used, this will be an
|
||||
atom.http_core.HttpClient.
|
||||
"""
|
||||
if real_http_client is None:
|
||||
real_http_client = atom.http_core.HttpClient()
|
||||
if name is None:
|
||||
self._recordings_cache_name = self.get_cache_file_name()
|
||||
else:
|
||||
self._recordings_cache_name = name
|
||||
self._load_or_use_client(self._recordings_cache_name, real_http_client)
|
||||
|
||||
def close_session(self):
|
||||
"""Saves recordings in the temporary file named in use_cached_session."""
|
||||
if self.real_client is not None:
|
||||
self._save_recordings(self._recordings_cache_name)
|
||||
|
||||
def delete_session(self, name=None):
|
||||
"""Removes recordings from a previous live request."""
|
||||
if name is None:
|
||||
self._delete_recordings(self._recordings_cache_name)
|
||||
else:
|
||||
self._delete_recordings(name)
|
||||
|
||||
def get_cache_file_name(self):
|
||||
return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
|
||||
self.cache_test_name)
|
||||
|
||||
def _dump(self):
|
||||
"""Provides debug information in a string."""
|
||||
output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % (
|
||||
self.real_client, self.get_cache_file_name())
|
||||
output += ' recordings:\n'
|
||||
i = 0
|
||||
for recording in self._recordings:
|
||||
output += ' recording %i is for: %s %s\n' % (
|
||||
i, recording[0].method, str(recording[0].uri))
|
||||
i += 1
|
||||
return output
|
||||
|
||||
|
||||
def _match_request(http_request, stored_request):
|
||||
"""Determines whether a request is similar enough to a stored request
|
||||
to cause the stored response to be returned."""
|
||||
# Check to see if the host names match.
|
||||
if (http_request.uri.host is not None
|
||||
and http_request.uri.host != stored_request.uri.host):
|
||||
return False
|
||||
# Check the request path in the URL (/feeds/private/full/x)
|
||||
elif http_request.uri.path != stored_request.uri.path:
|
||||
return False
|
||||
# Check the method used in the request (GET, POST, etc.)
|
||||
elif http_request.method != stored_request.method:
|
||||
return False
|
||||
# If there is a gsession ID in either request, make sure that it is matched
|
||||
# exactly.
|
||||
elif ('gsessionid' in http_request.uri.query
|
||||
or 'gsessionid' in stored_request.uri.query):
|
||||
if 'gsessionid' not in stored_request.uri.query:
|
||||
return False
|
||||
elif 'gsessionid' not in http_request.uri.query:
|
||||
return False
|
||||
elif (http_request.uri.query['gsessionid']
|
||||
!= stored_request.uri.query['gsessionid']):
|
||||
return False
|
||||
# Ignores differences in the query params (?start-index=5&max-results=20),
|
||||
# the body of the request, the port number, HTTP headers, just to name a
|
||||
# few.
|
||||
return True
|
||||
|
||||
|
||||
def _scrub_request(http_request):
|
||||
""" Removes email address and password from a client login request.
|
||||
|
||||
Since the mock server saves the request and response in plantext, sensitive
|
||||
information like the password should be removed before saving the
|
||||
recordings. At the moment only requests sent to a ClientLogin url are
|
||||
scrubbed.
|
||||
"""
|
||||
if (http_request and http_request.uri and http_request.uri.path and
|
||||
http_request.uri.path.endswith('ClientLogin')):
|
||||
# Remove the email and password from a ClientLogin request.
|
||||
http_request._body_parts = []
|
||||
http_request.add_form_inputs(
|
||||
{'form_data': 'client login request has been scrubbed'})
|
||||
else:
|
||||
# We can remove the body of the post from the recorded request, since
|
||||
# the request body is not used when finding a matching recording.
|
||||
http_request._body_parts = []
|
||||
return http_request
|
||||
|
||||
|
||||
def _scrub_response(http_response):
|
||||
return http_response
|
||||
|
||||
|
||||
class EchoHttpClient(object):
|
||||
"""Sends the request data back in the response.
|
||||
|
||||
Used to check the formatting of the request as it was sent. Always responds
|
||||
with a 200 OK, and some information from the HTTP request is returned in
|
||||
special Echo-X headers in the response. The following headers are added
|
||||
in the response:
|
||||
'Echo-Host': The host name and port number to which the HTTP connection is
|
||||
made. If no port was passed in, the header will contain
|
||||
host:None.
|
||||
'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
|
||||
'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
|
||||
'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
|
||||
"""
|
||||
|
||||
def request(self, http_request):
|
||||
return self._http_request(http_request.uri, http_request.method,
|
||||
http_request.headers, http_request._body_parts)
|
||||
|
||||
def _http_request(self, uri, method, headers=None, body_parts=None):
|
||||
body = StringIO.StringIO()
|
||||
response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
|
||||
if headers is None:
|
||||
response._headers = {}
|
||||
else:
|
||||
# Copy headers from the request to the response but convert values to
|
||||
# strings. Server response headers always come in as strings, so an int
|
||||
# should be converted to a corresponding string when echoing.
|
||||
for header, value in headers.iteritems():
|
||||
response._headers[header] = str(value)
|
||||
response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port))
|
||||
response._headers['Echo-Uri'] = uri._get_relative_path()
|
||||
response._headers['Echo-Scheme'] = uri.scheme
|
||||
response._headers['Echo-Method'] = method
|
||||
for part in body_parts:
|
||||
if isinstance(part, str):
|
||||
body.write(part)
|
||||
elif hasattr(part, 'read'):
|
||||
body.write(part.read())
|
||||
body.seek(0)
|
||||
return response
|
||||
|
||||
|
||||
class SettableHttpClient(object):
|
||||
"""An HTTP Client which responds with the data given in set_response."""
|
||||
|
||||
def __init__(self, status, reason, body, headers):
|
||||
"""Configures the response for the server.
|
||||
|
||||
See set_response for details on the arguments to the constructor.
|
||||
"""
|
||||
self.set_response(status, reason, body, headers)
|
||||
self.last_request = None
|
||||
|
||||
def set_response(self, status, reason, body, headers):
|
||||
"""Determines the response which will be sent for each request.
|
||||
|
||||
Args:
|
||||
status: An int for the HTTP status code, example: 200, 404, etc.
|
||||
reason: String for the HTTP reason, example: OK, NOT FOUND, etc.
|
||||
body: The body of the HTTP response as a string or a file-like
|
||||
object (something with a read method).
|
||||
headers: dict of strings containing the HTTP headers in the response.
|
||||
"""
|
||||
self.response = atom.http_core.HttpResponse(status=status, reason=reason,
|
||||
body=body)
|
||||
self.response._headers = headers.copy()
|
||||
|
||||
def request(self, http_request):
|
||||
self.last_request = http_request
|
||||
return self.response
|
||||
|
||||
|
||||
class MockHttpResponse(atom.http_core.HttpResponse):
|
||||
|
||||
def __init__(self, status=None, reason=None, headers=None, body=None):
|
||||
self._headers = headers or {}
|
||||
if status is not None:
|
||||
self.status = status
|
||||
if reason is not None:
|
||||
self.reason = reason
|
||||
if body is not None:
|
||||
# Instead of using a file-like object for the body, store as a string
|
||||
# so that reads can be repeated.
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body.read()
|
||||
else:
|
||||
self._body = body
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
@@ -1,243 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""MockService provides CRUD ops. for mocking calls to AtomPub services.
|
||||
|
||||
MockService: Exposes the publicly used methods of AtomService to provide
|
||||
a mock interface which can be used in unit tests.
|
||||
"""
|
||||
|
||||
import atom.service
|
||||
import pickle
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeffrey Scudder)'
|
||||
|
||||
|
||||
# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects.
|
||||
recordings = []
|
||||
# If set, the mock service HttpRequest are actually made through this object.
|
||||
real_request_handler = None
|
||||
|
||||
def ConcealValueWithSha(source):
|
||||
import sha
|
||||
return sha.new(source[:-5]).hexdigest()
|
||||
|
||||
def DumpRecordings(conceal_func=ConcealValueWithSha):
|
||||
if conceal_func:
|
||||
for recording_pair in recordings:
|
||||
recording_pair[0].ConcealSecrets(conceal_func)
|
||||
return pickle.dumps(recordings)
|
||||
|
||||
def LoadRecordings(recordings_file_or_string):
|
||||
if isinstance(recordings_file_or_string, str):
|
||||
atom.mock_service.recordings = pickle.loads(recordings_file_or_string)
|
||||
elif hasattr(recordings_file_or_string, 'read'):
|
||||
atom.mock_service.recordings = pickle.loads(
|
||||
recordings_file_or_string.read())
|
||||
|
||||
def HttpRequest(service, operation, data, uri, extra_headers=None,
|
||||
url_params=None, escape_params=True, content_type='application/atom+xml'):
|
||||
"""Simulates an HTTP call to the server, makes an actual HTTP request if
|
||||
real_request_handler is set.
|
||||
|
||||
This function operates in two different modes depending on if
|
||||
real_request_handler is set or not. If real_request_handler is not set,
|
||||
HttpRequest will look in this module's recordings list to find a response
|
||||
which matches the parameters in the function call. If real_request_handler
|
||||
is set, this function will call real_request_handler.HttpRequest, add the
|
||||
response to the recordings list, and respond with the actual response.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService object which contains some of the parameters
|
||||
needed to make the request. The following members are used to
|
||||
construct the HTTP call: server (str), additional_headers (dict),
|
||||
port (int), and ssl (bool).
|
||||
operation: str The HTTP operation to be performed. This is usually one of
|
||||
'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string.
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, this method will read
|
||||
a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be evaluated
|
||||
and sent.
|
||||
uri: The beginning of the URL to which the request should be sent.
|
||||
Examples: '/', '/base/feeds/snippets',
|
||||
'/m8/feeds/contacts/default/base'
|
||||
extra_headers: dict of strings. HTTP headers which should be sent
|
||||
in the request. These headers are in addition to those stored in
|
||||
service.additional_headers.
|
||||
url_params: dict of strings. Key value pairs to be added to the URL as
|
||||
URL parameters. For example {'foo':'bar', 'test':'param'} will
|
||||
become ?foo=bar&test=param.
|
||||
escape_params: bool default True. If true, the keys and values in
|
||||
url_params will be URL escaped when the form is constructed
|
||||
(Special characters converted to %XX form.)
|
||||
content_type: str The MIME type for the data being sent. Defaults to
|
||||
'application/atom+xml', this is only used if data is set.
|
||||
"""
|
||||
full_uri = atom.service.BuildUri(uri, url_params, escape_params)
|
||||
(server, port, ssl, uri) = atom.service.ProcessUrl(service, uri)
|
||||
current_request = MockRequest(operation, full_uri, host=server, ssl=ssl,
|
||||
data=data, extra_headers=extra_headers, url_params=url_params,
|
||||
escape_params=escape_params, content_type=content_type)
|
||||
# If the request handler is set, we should actually make the request using
|
||||
# the request handler and record the response to replay later.
|
||||
if real_request_handler:
|
||||
response = real_request_handler.HttpRequest(service, operation, data, uri,
|
||||
extra_headers=extra_headers, url_params=url_params,
|
||||
escape_params=escape_params, content_type=content_type)
|
||||
# TODO: need to copy the HTTP headers from the real response into the
|
||||
# recorded_response.
|
||||
recorded_response = MockHttpResponse(body=response.read(),
|
||||
status=response.status, reason=response.reason)
|
||||
# Insert a tuple which maps the request to the response object returned
|
||||
# when making an HTTP call using the real_request_handler.
|
||||
recordings.append((current_request, recorded_response))
|
||||
return recorded_response
|
||||
else:
|
||||
# Look through available recordings to see if one matches the current
|
||||
# request.
|
||||
for request_response_pair in recordings:
|
||||
if request_response_pair[0].IsMatch(current_request):
|
||||
return request_response_pair[1]
|
||||
return None
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
"""Represents a request made to an AtomPub server.
|
||||
|
||||
These objects are used to determine if a client request matches a recorded
|
||||
HTTP request to determine what the mock server's response will be.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, uri, host=None, ssl=False, port=None,
|
||||
data=None, extra_headers=None, url_params=None, escape_params=True,
|
||||
content_type='application/atom+xml'):
|
||||
"""Constructor for a MockRequest
|
||||
|
||||
Args:
|
||||
operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the
|
||||
HTTP operation requested on the resource.
|
||||
uri: str The URL describing the resource to be modified or feed to be
|
||||
retrieved. This should include the protocol (http/https) and the host
|
||||
(aka domain). For example, these are some valud full_uris:
|
||||
'http://example.com', 'https://www.google.com/accounts/ClientLogin'
|
||||
host: str (optional) The server name which will be placed at the
|
||||
beginning of the URL if the uri parameter does not begin with 'http'.
|
||||
Examples include 'example.com', 'www.google.com', 'www.blogger.com'.
|
||||
ssl: boolean (optional) If true, the request URL will begin with https
|
||||
instead of http.
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string. (optional)
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, the constructor
|
||||
will read the entire file into memory. If the data is a list of
|
||||
parts to be sent, each part will be evaluated and stored.
|
||||
extra_headers: dict (optional) HTTP headers included in the request.
|
||||
url_params: dict (optional) Key value pairs which should be added to
|
||||
the URL as URL parameters in the request. For example uri='/',
|
||||
url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'.
|
||||
escape_params: boolean (optional) Perform URL escaping on the keys and
|
||||
values specified in url_params. Defaults to True.
|
||||
content_type: str (optional) Provides the MIME type of the data being
|
||||
sent.
|
||||
"""
|
||||
self.operation = operation
|
||||
self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl)
|
||||
self.data = data
|
||||
self.extra_headers = extra_headers
|
||||
self.url_params = url_params or {}
|
||||
self.escape_params = escape_params
|
||||
self.content_type = content_type
|
||||
|
||||
def ConcealSecrets(self, conceal_func):
|
||||
"""Conceal secret data in this request."""
|
||||
if self.extra_headers.has_key('Authorization'):
|
||||
self.extra_headers['Authorization'] = conceal_func(
|
||||
self.extra_headers['Authorization'])
|
||||
|
||||
def IsMatch(self, other_request):
|
||||
"""Check to see if the other_request is equivalent to this request.
|
||||
|
||||
Used to determine if a recording matches an incoming request so that a
|
||||
recorded response should be sent to the client.
|
||||
|
||||
The matching is not exact, only the operation and URL are examined
|
||||
currently.
|
||||
|
||||
Args:
|
||||
other_request: MockRequest The request which we want to check this
|
||||
(self) MockRequest against to see if they are equivalent.
|
||||
"""
|
||||
# More accurate matching logic will likely be required.
|
||||
return (self.operation == other_request.operation and self.uri ==
|
||||
other_request.uri)
|
||||
|
||||
|
||||
def _ConstructFullUrlBase(uri, host=None, ssl=False):
|
||||
"""Puts URL components into the form http(s)://full.host.strinf/uri/path
|
||||
|
||||
Used to construct a roughly canonical URL so that URLs which begin with
|
||||
'http://example.com/' can be compared to a uri of '/' when the host is
|
||||
set to 'example.com'
|
||||
|
||||
If the uri contains 'http://host' already, the host and ssl parameters
|
||||
are ignored.
|
||||
|
||||
Args:
|
||||
uri: str The path component of the URL, examples include '/'
|
||||
host: str (optional) The host name which should prepend the URL. Example:
|
||||
'example.com'
|
||||
ssl: boolean (optional) If true, the returned URL will begin with https
|
||||
instead of http.
|
||||
|
||||
Returns:
|
||||
String which has the form http(s)://example.com/uri/string/contents
|
||||
"""
|
||||
if uri.startswith('http'):
|
||||
return uri
|
||||
if ssl:
|
||||
return 'https://%s%s' % (host, uri)
|
||||
else:
|
||||
return 'http://%s%s' % (host, uri)
|
||||
|
||||
|
||||
class MockHttpResponse(object):
|
||||
"""Returned from MockService crud methods as the server's response."""
|
||||
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
"""Construct a mock HTTPResponse and set members.
|
||||
|
||||
Args:
|
||||
body: str (optional) The HTTP body of the server's response.
|
||||
status: int (optional)
|
||||
reason: str (optional)
|
||||
headers: dict (optional)
|
||||
"""
|
||||
self.body = body
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
self.headers = headers or {}
|
||||
|
||||
def read(self):
|
||||
return self.body
|
||||
|
||||
def getheader(self, header_name):
|
||||
return self.headers[header_name]
|
||||
|
||||
@@ -1,746 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2006, 2007, 2008 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.
|
||||
|
||||
AtomService: Encapsulates the ability to perform insert, update and delete
|
||||
operations with the Atom Publishing Protocol on which GData is
|
||||
based. An instance can perform query, insertion, deletion, and
|
||||
update.
|
||||
|
||||
HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
|
||||
to the specified end point. An AtomService object or a subclass can be
|
||||
used to specify information about the request.
|
||||
"""
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
import atom.http
|
||||
import atom.token_store
|
||||
|
||||
import os
|
||||
import types
|
||||
import httplib
|
||||
import urllib
|
||||
import re
|
||||
import base64
|
||||
import socket
|
||||
import warnings
|
||||
try:
|
||||
from xml.etree import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
try:
|
||||
import cElementTree as ElementTree
|
||||
except ImportError:
|
||||
try:
|
||||
from xml.etree import ElementTree
|
||||
except ImportError:
|
||||
from elementtree import ElementTree
|
||||
import atom
|
||||
|
||||
|
||||
class AtomService(object):
|
||||
"""Performs Atom Publishing Protocol CRUD operations.
|
||||
|
||||
The AtomService contains methods to perform HTTP CRUD operations.
|
||||
"""
|
||||
|
||||
# Default values for members
|
||||
port = 80
|
||||
ssl = False
|
||||
# Set the current_token to force the AtomService to use this token
|
||||
# instead of searching for an appropriate token in the token_store.
|
||||
current_token = None
|
||||
auto_store_tokens = True
|
||||
auto_set_current_token = True
|
||||
|
||||
def _get_override_token(self):
|
||||
return self.current_token
|
||||
|
||||
def _set_override_token(self, token):
|
||||
self.current_token = token
|
||||
|
||||
override_token = property(_get_override_token, _set_override_token)
|
||||
|
||||
#@atom.v1_deprecated('Please use atom.client.AtomPubClient instead.')
|
||||
def __init__(self, server=None, additional_headers=None,
|
||||
application_name='', http_client=None, token_store=None):
|
||||
"""Creates a new AtomService client.
|
||||
|
||||
Args:
|
||||
server: string (optional) The start of a URL for the server
|
||||
to which all operations should be directed. Example:
|
||||
'www.google.com'
|
||||
additional_headers: dict (optional) Any additional HTTP headers which
|
||||
should be included with CRUD operations.
|
||||
http_client: An object responsible for making HTTP requests using a
|
||||
request method. If none is provided, a new instance of
|
||||
atom.http.ProxiedHttpClient will be used.
|
||||
token_store: Keeps a collection of authorization tokens which can be
|
||||
applied to requests for a specific URLs. Critical methods are
|
||||
find_token based on a URL (atom.url.Url or a string), add_token,
|
||||
and remove_token.
|
||||
"""
|
||||
self.http_client = http_client or atom.http.ProxiedHttpClient()
|
||||
self.token_store = token_store or atom.token_store.TokenStore()
|
||||
self.server = server
|
||||
self.additional_headers = additional_headers or {}
|
||||
self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % (
|
||||
application_name,)
|
||||
# If debug is True, the HTTPConnection will display debug information
|
||||
self._set_debug(False)
|
||||
|
||||
__init__ = atom.v1_deprecated(
|
||||
'Please use atom.client.AtomPubClient instead.')(
|
||||
__init__)
|
||||
|
||||
def _get_debug(self):
|
||||
return self.http_client.debug
|
||||
|
||||
def _set_debug(self, value):
|
||||
self.http_client.debug = value
|
||||
|
||||
debug = property(_get_debug, _set_debug,
|
||||
doc='If True, HTTP debug information is printed.')
|
||||
|
||||
def use_basic_auth(self, username, password, scopes=None):
|
||||
if username is not None and password is not None:
|
||||
if scopes is None:
|
||||
scopes = [atom.token_store.SCOPE_ALL]
|
||||
base_64_string = base64.encodestring('%s:%s' % (username, password))
|
||||
token = BasicAuthToken('Basic %s' % base_64_string.strip(),
|
||||
scopes=[atom.token_store.SCOPE_ALL])
|
||||
if self.auto_set_current_token:
|
||||
self.current_token = token
|
||||
if self.auto_store_tokens:
|
||||
return self.token_store.add_token(token)
|
||||
return True
|
||||
return False
|
||||
|
||||
def UseBasicAuth(self, username, password, for_proxy=False):
|
||||
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
|
||||
|
||||
Deprecated, use use_basic_auth instead.
|
||||
|
||||
The username and password are base64 encoded and added to an HTTP header
|
||||
which will be included in each request. Note that your username and
|
||||
password are sent in plaintext.
|
||||
|
||||
Args:
|
||||
username: str
|
||||
password: str
|
||||
"""
|
||||
self.use_basic_auth(username, password)
|
||||
|
||||
#@atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.')
|
||||
def request(self, operation, url, data=None, headers=None,
|
||||
url_params=None):
|
||||
if isinstance(url, (str, unicode)):
|
||||
if url.startswith('http:') and self.ssl:
|
||||
# Force all requests to be https if self.ssl is True.
|
||||
url = atom.url.parse_url('https:' + url[5:])
|
||||
elif not url.startswith('http') and self.ssl:
|
||||
url = atom.url.parse_url('https://%s%s' % (self.server, url))
|
||||
elif not url.startswith('http'):
|
||||
url = atom.url.parse_url('http://%s%s' % (self.server, url))
|
||||
else:
|
||||
url = atom.url.parse_url(url)
|
||||
|
||||
if url_params:
|
||||
for name, value in url_params.iteritems():
|
||||
url.params[name] = value
|
||||
|
||||
all_headers = self.additional_headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
if isinstance(data, types.StringTypes):
|
||||
data = data.encode('utf-8')
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
content_length = CalculateDataLength(data)
|
||||
if content_length:
|
||||
all_headers['Content-Length'] = str(content_length)
|
||||
|
||||
if 'GData-Version' not in all_headers:
|
||||
all_headers['GData-Version'] = '2.0'
|
||||
# Find an Authorization token for this URL if one is available.
|
||||
if self.override_token:
|
||||
auth_token = self.override_token
|
||||
else:
|
||||
auth_token = self.token_store.find_token(url)
|
||||
return auth_token.perform_request(self.http_client, operation, url,
|
||||
data=data, headers=all_headers)
|
||||
|
||||
request = atom.v1_deprecated(
|
||||
'Please use atom.client.AtomPubClient for requests.')(
|
||||
request)
|
||||
|
||||
# CRUD operations
|
||||
def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
|
||||
"""Query the APP server with the given URI
|
||||
|
||||
The uri is the portion of the URI after the server value
|
||||
(server example: 'www.google.com').
|
||||
|
||||
Example use:
|
||||
To perform a query against Google Base, set the server to
|
||||
'base.google.com' and set the uri to '/base/feeds/...', where ... is
|
||||
your query. For example, to find snippets for all digital cameras uri
|
||||
should be set to: '/base/feeds/snippets?bq=digital+camera'
|
||||
|
||||
Args:
|
||||
uri: string The query in the form of a URI. Example:
|
||||
'/base/feeds/snippets?bq=digital+camera'.
|
||||
extra_headers: dicty (optional) Extra HTTP headers to be included
|
||||
in the GET request. These headers are in addition to
|
||||
those stored in the client's additional_headers property.
|
||||
The client automatically sets the Content-Type and
|
||||
Authorization headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the query. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse The server's response to the GET request.
|
||||
"""
|
||||
return self.request('GET', uri, data=None, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Post(self, data, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True, content_type='application/atom+xml; charset=UTF-8'):
|
||||
"""Insert data into an APP server at the given URI.
|
||||
|
||||
Args:
|
||||
data: string, ElementTree._Element, or something with a __str__ method
|
||||
The XML to be sent to the uri.
|
||||
uri: string The location (feed) to which the data should be inserted.
|
||||
Example: '/base/feeds/items'.
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type,
|
||||
Authorization, and Content-Length headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the POST request.
|
||||
"""
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
return self.request('POST', uri, data=data, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Put(self, data, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True, content_type='application/atom+xml; charset=UTF-8'):
|
||||
"""Updates an entry at the given URI.
|
||||
|
||||
Args:
|
||||
data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The
|
||||
XML containing the updated data.
|
||||
uri: string A URI indicating entry to which the update will be applied.
|
||||
Example: '/base/feeds/items/ITEM-ID'
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type,
|
||||
Authorization, and Content-Length headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the PUT request.
|
||||
"""
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
return self.request('PUT', uri, data=data, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Delete(self, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True):
|
||||
"""Deletes the entry at the given URI.
|
||||
|
||||
Args:
|
||||
uri: string The URI of the entry to be deleted. Example:
|
||||
'/base/feeds/items/ITEM-ID'
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type and
|
||||
Authorization headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the DELETE request.
|
||||
"""
|
||||
return self.request('DELETE', uri, data=None, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
|
||||
class BasicAuthToken(atom.http_interface.GenericToken):
|
||||
def __init__(self, auth_header, scopes=None):
|
||||
"""Creates a token used to add Basic Auth headers to HTTP requests.
|
||||
|
||||
Args:
|
||||
auth_header: str The value for the Authorization header.
|
||||
scopes: list of str or atom.url.Url specifying the beginnings of URLs
|
||||
for which this token can be used. For example, if scopes contains
|
||||
'http://example.com/foo', then this token can be used for a request to
|
||||
'http://example.com/foo/bar' but it cannot be used for a request to
|
||||
'http://example.com/baz'
|
||||
"""
|
||||
self.auth_header = auth_header
|
||||
self.scopes = scopes or []
|
||||
|
||||
def perform_request(self, http_client, operation, url, data=None,
|
||||
headers=None):
|
||||
"""Sets the Authorization header to the basic auth string."""
|
||||
if headers is None:
|
||||
headers = {'Authorization':self.auth_header}
|
||||
else:
|
||||
headers['Authorization'] = self.auth_header
|
||||
return http_client.request(operation, url, data=data, headers=headers)
|
||||
|
||||
def __str__(self):
|
||||
return self.auth_header
|
||||
|
||||
def valid_for_scope(self, url):
|
||||
"""Tells the caller if the token authorizes access to the desired URL.
|
||||
"""
|
||||
if isinstance(url, (str, unicode)):
|
||||
url = atom.url.parse_url(url)
|
||||
for scope in self.scopes:
|
||||
if scope == atom.token_store.SCOPE_ALL:
|
||||
return True
|
||||
if isinstance(scope, (str, unicode)):
|
||||
scope = atom.url.parse_url(scope)
|
||||
if scope == url:
|
||||
return True
|
||||
# Check the host and the path, but ignore the port and protocol.
|
||||
elif scope.host == url.host and not scope.path:
|
||||
return True
|
||||
elif scope.host == url.host and scope.path and not url.path:
|
||||
continue
|
||||
elif scope.host == url.host and url.path.startswith(scope.path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def PrepareConnection(service, full_uri):
|
||||
"""Opens a connection to the server based on the full URI.
|
||||
|
||||
This method is deprecated, instead use atom.http.HttpClient.request.
|
||||
|
||||
Examines the target URI and the proxy settings, which are set as
|
||||
environment variables, to open a connection with the server. This
|
||||
connection is used to make an HTTP request.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService or a subclass. It must have a server string which
|
||||
represents the server host to which the request should be made. It may also
|
||||
have a dictionary of additional_headers to send in the HTTP request.
|
||||
full_uri: str Which is the target relative (lacks protocol and host) or
|
||||
absolute URL to be opened. Example:
|
||||
'https://www.google.com/accounts/ClientLogin' or
|
||||
'base/feeds/snippets' where the server is set to www.google.com.
|
||||
|
||||
Returns:
|
||||
A tuple containing the httplib.HTTPConnection and the full_uri for the
|
||||
request.
|
||||
"""
|
||||
deprecation('calling deprecated function PrepareConnection')
|
||||
(server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
|
||||
if ssl:
|
||||
# destination is https
|
||||
proxy = os.environ.get('https_proxy')
|
||||
if proxy:
|
||||
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
user_auth = base64.encodestring('%s:%s' % (proxy_username,
|
||||
proxy_password))
|
||||
proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
|
||||
user_auth.strip()))
|
||||
else:
|
||||
proxy_authorization = ''
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
|
||||
user_agent = 'User-Agent: %s\r\n' % (
|
||||
service.additional_headers['User-Agent'])
|
||||
proxy_pieces = (proxy_connect + proxy_authorization + user_agent
|
||||
+ '\r\n')
|
||||
|
||||
#now connect, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
|
||||
p_sock.connect((p_server,p_port))
|
||||
p_sock.sendall(proxy_pieces)
|
||||
response = ''
|
||||
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
response += p_sock.recv(8192)
|
||||
|
||||
p_status=response.split()[1]
|
||||
if p_status!=str(200):
|
||||
raise atom.http.ProxyError('Error status=%s' % p_status)
|
||||
|
||||
# Trivial setup for ssl socket.
|
||||
ssl = socket.ssl(p_sock, None, None)
|
||||
fake_sock = httplib.FakeSocket(p_sock, ssl)
|
||||
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = httplib.HTTPConnection(server)
|
||||
connection.sock=fake_sock
|
||||
full_uri = partial_uri
|
||||
|
||||
else:
|
||||
connection = httplib.HTTPSConnection(server, port)
|
||||
full_uri = partial_uri
|
||||
|
||||
else:
|
||||
# destination is http
|
||||
proxy = os.environ.get('http_proxy')
|
||||
if proxy:
|
||||
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
UseBasicAuth(service, proxy_username, proxy_password, True)
|
||||
connection = httplib.HTTPConnection(p_server, p_port)
|
||||
if not full_uri.startswith("http://"):
|
||||
if full_uri.startswith("/"):
|
||||
full_uri = "http://%s%s" % (service.server, full_uri)
|
||||
else:
|
||||
full_uri = "http://%s/%s" % (service.server, full_uri)
|
||||
else:
|
||||
connection = httplib.HTTPConnection(server, port)
|
||||
full_uri = partial_uri
|
||||
|
||||
return (connection, full_uri)
|
||||
|
||||
|
||||
def UseBasicAuth(service, username, password, for_proxy=False):
|
||||
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
|
||||
|
||||
Deprecated, use AtomService.use_basic_auth insread.
|
||||
|
||||
The username and password are base64 encoded and added to an HTTP header
|
||||
which will be included in each request. Note that your username and
|
||||
password are sent in plaintext. The auth header is added to the
|
||||
additional_headers dictionary in the service object.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService or a subclass which has an
|
||||
additional_headers dict as a member.
|
||||
username: str
|
||||
password: str
|
||||
"""
|
||||
deprecation('calling deprecated function UseBasicAuth')
|
||||
base_64_string = base64.encodestring('%s:%s' % (username, password))
|
||||
base_64_string = base_64_string.strip()
|
||||
if for_proxy:
|
||||
header_name = 'Proxy-Authorization'
|
||||
else:
|
||||
header_name = 'Authorization'
|
||||
service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
|
||||
|
||||
|
||||
def ProcessUrl(service, url, for_proxy=False):
|
||||
"""Processes a passed URL. If the URL does not begin with https?, then
|
||||
the default value for server is used
|
||||
|
||||
This method is deprecated, use atom.url.parse_url instead.
|
||||
"""
|
||||
if not isinstance(url, atom.url.Url):
|
||||
url = atom.url.parse_url(url)
|
||||
|
||||
server = url.host
|
||||
ssl = False
|
||||
port = 80
|
||||
|
||||
if not server:
|
||||
if hasattr(service, 'server'):
|
||||
server = service.server
|
||||
else:
|
||||
server = service
|
||||
if not url.protocol and hasattr(service, 'ssl'):
|
||||
ssl = service.ssl
|
||||
if hasattr(service, 'port'):
|
||||
port = service.port
|
||||
else:
|
||||
if url.protocol == 'https':
|
||||
ssl = True
|
||||
elif url.protocol == 'http':
|
||||
ssl = False
|
||||
if url.port:
|
||||
port = int(url.port)
|
||||
elif port == 80 and ssl:
|
||||
port = 443
|
||||
|
||||
return (server, port, ssl, url.get_request_uri())
|
||||
|
||||
def DictionaryToParamList(url_parameters, escape_params=True):
|
||||
"""Convert a dictionary of URL arguments into a URL parameter string.
|
||||
|
||||
This function is deprcated, use atom.url.Url instead.
|
||||
|
||||
Args:
|
||||
url_parameters: The dictionaty of key-value pairs which will be converted
|
||||
into URL parameters. For example,
|
||||
{'dry-run': 'true', 'foo': 'bar'}
|
||||
will become ['dry-run=true', 'foo=bar'].
|
||||
|
||||
Returns:
|
||||
A list which contains a string for each key-value pair. The strings are
|
||||
ready to be incorporated into a URL by using '&'.join([] + parameter_list)
|
||||
"""
|
||||
# Choose which function to use when modifying the query and parameters.
|
||||
# Use quote_plus when escape_params is true.
|
||||
transform_op = [str, urllib.quote_plus][bool(escape_params)]
|
||||
# Create a list of tuples containing the escaped version of the
|
||||
# parameter-value pairs.
|
||||
parameter_tuples = [(transform_op(param), transform_op(value))
|
||||
for param, value in (url_parameters or {}).items()]
|
||||
# Turn parameter-value tuples into a list of strings in the form
|
||||
# 'PARAMETER=VALUE'.
|
||||
return ['='.join(x) for x in parameter_tuples]
|
||||
|
||||
|
||||
def BuildUri(uri, url_params=None, escape_params=True):
|
||||
"""Converts a uri string and a collection of parameters into a URI.
|
||||
|
||||
This function is deprcated, use atom.url.Url instead.
|
||||
|
||||
Args:
|
||||
uri: string
|
||||
url_params: dict (optional)
|
||||
escape_params: boolean (optional)
|
||||
uri: string The start of the desired URI. This string can alrady contain
|
||||
URL parameters. Examples: '/base/feeds/snippets',
|
||||
'/base/feeds/snippets?bq=digital+camera'
|
||||
url_parameters: dict (optional) Additional URL parameters to be included
|
||||
in the query. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
string The URI consisting of the escaped URL parameters appended to the
|
||||
initial uri string.
|
||||
"""
|
||||
# Prepare URL parameters for inclusion into the GET request.
|
||||
parameter_list = DictionaryToParamList(url_params, escape_params)
|
||||
|
||||
# Append the URL parameters to the URL.
|
||||
if parameter_list:
|
||||
if uri.find('?') != -1:
|
||||
# If there are already URL parameters in the uri string, add the
|
||||
# parameters after a new & character.
|
||||
full_uri = '&'.join([uri] + parameter_list)
|
||||
else:
|
||||
# The uri string did not have any URL parameters (no ? character)
|
||||
# so put a ? between the uri and URL parameters.
|
||||
full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))
|
||||
else:
|
||||
full_uri = uri
|
||||
|
||||
return full_uri
|
||||
|
||||
|
||||
def HttpRequest(service, operation, data, uri, extra_headers=None,
|
||||
url_params=None, escape_params=True, content_type='application/atom+xml; charset=UTF-8'):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
|
||||
|
||||
This method is deprecated, use atom.http.HttpClient.request instead.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.service
|
||||
client = atom.service.AtomService()
|
||||
http_response = client.Get('http://www.google.com/')
|
||||
or you could set the client.server to 'www.google.com' and use the
|
||||
following:
|
||||
client.server = 'www.google.com'
|
||||
http_response = client.Get('/')
|
||||
|
||||
Args:
|
||||
service: atom.AtomService object which contains some of the parameters
|
||||
needed to make the request. The following members are used to
|
||||
construct the HTTP call: server (str), additional_headers (dict),
|
||||
port (int), and ssl (bool).
|
||||
operation: str The HTTP operation to be performed. This is usually one of
|
||||
'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string.
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, this method will read
|
||||
a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be evaluated
|
||||
and sent.
|
||||
uri: The beginning of the URL to which the request should be sent.
|
||||
Examples: '/', '/base/feeds/snippets',
|
||||
'/m8/feeds/contacts/default/base'
|
||||
extra_headers: dict of strings. HTTP headers which should be sent
|
||||
in the request. These headers are in addition to those stored in
|
||||
service.additional_headers.
|
||||
url_params: dict of strings. Key value pairs to be added to the URL as
|
||||
URL parameters. For example {'foo':'bar', 'test':'param'} will
|
||||
become ?foo=bar&test=param.
|
||||
escape_params: bool default True. If true, the keys and values in
|
||||
url_params will be URL escaped when the form is constructed
|
||||
(Special characters converted to %XX form.)
|
||||
content_type: str The MIME type for the data being sent. Defaults to
|
||||
'application/atom+xml', this is only used if data is set.
|
||||
"""
|
||||
deprecation('call to deprecated function HttpRequest')
|
||||
full_uri = BuildUri(uri, url_params, escape_params)
|
||||
(connection, full_uri) = PrepareConnection(service, full_uri)
|
||||
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
|
||||
# Turn on debug mode if the debug member is set.
|
||||
if service.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
connection.putrequest(operation, full_uri)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if (data and not service.additional_headers.has_key('Content-Length') and
|
||||
not extra_headers.has_key('Content-Length')):
|
||||
content_length = CalculateDataLength(data)
|
||||
if content_length:
|
||||
extra_headers['Content-Length'] = str(content_length)
|
||||
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
|
||||
# Send the HTTP headers.
|
||||
if isinstance(service.additional_headers, dict):
|
||||
for header in service.additional_headers:
|
||||
connection.putheader(header, service.additional_headers[header])
|
||||
if isinstance(extra_headers, dict):
|
||||
for header in extra_headers:
|
||||
connection.putheader(header, extra_headers[header])
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
for data_part in data:
|
||||
__SendDataPart(data_part, connection)
|
||||
else:
|
||||
__SendDataPart(data, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
|
||||
def __SendDataPart(data, connection):
|
||||
"""This method is deprecated, use atom.http._send_data_part"""
|
||||
deprecated('call to deprecated function __SendDataPart')
|
||||
if isinstance(data, str):
|
||||
#TODO add handling for unicode.
|
||||
connection.send(data)
|
||||
return
|
||||
elif ElementTree.iselement(data):
|
||||
connection.send(ElementTree.tostring(data))
|
||||
return
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == '': break
|
||||
connection.send(binarydata)
|
||||
return
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
connection.send(str(data))
|
||||
return
|
||||
|
||||
|
||||
def CalculateDataLength(data):
|
||||
"""Attempts to determine the length of the data to send.
|
||||
|
||||
This method will respond with a length only if the data is a string or
|
||||
and ElementTree element.
|
||||
|
||||
Args:
|
||||
data: object If this is not a string or ElementTree element this funtion
|
||||
will return None.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
return len(data)
|
||||
elif isinstance(data, list):
|
||||
return None
|
||||
elif ElementTree.iselement(data):
|
||||
return len(ElementTree.tostring(data))
|
||||
elif hasattr(data, 'read'):
|
||||
# If this is a file-like object, don't try to guess the length.
|
||||
return None
|
||||
else:
|
||||
return len(str(data))
|
||||
|
||||
|
||||
def deprecation(message):
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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 provides a TokenStore class which is designed to manage
|
||||
auth tokens required for different services.
|
||||
|
||||
Each token is valid for a set of scopes which is the start of a URL. An HTTP
|
||||
client will use a token store to find a valid Authorization header to send
|
||||
in requests to the specified URL. If the HTTP client determines that a token
|
||||
has expired or been revoked, it can remove the token from the store so that
|
||||
it will not be used in future requests.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
|
||||
|
||||
SCOPE_ALL = 'http'
|
||||
|
||||
|
||||
class TokenStore(object):
|
||||
"""Manages Authorization tokens which will be sent in HTTP headers."""
|
||||
def __init__(self, scoped_tokens=None):
|
||||
self._tokens = scoped_tokens or {}
|
||||
|
||||
def add_token(self, token):
|
||||
"""Adds a new token to the store (replaces tokens with the same scope).
|
||||
|
||||
Args:
|
||||
token: A subclass of http_interface.GenericToken. The token object is
|
||||
responsible for adding the Authorization header to the HTTP request.
|
||||
The scopes defined in the token are used to determine if the token
|
||||
is valid for a requested scope when find_token is called.
|
||||
|
||||
Returns:
|
||||
True if the token was added, False if the token was not added becase
|
||||
no scopes were provided.
|
||||
"""
|
||||
if not hasattr(token, 'scopes') or not token.scopes:
|
||||
return False
|
||||
|
||||
for scope in token.scopes:
|
||||
self._tokens[str(scope)] = token
|
||||
return True
|
||||
|
||||
def find_token(self, url):
|
||||
"""Selects an Authorization header token which can be used for the URL.
|
||||
|
||||
Args:
|
||||
url: str or atom.url.Url or a list containing the same.
|
||||
The URL which is going to be requested. All
|
||||
tokens are examined to see if any scopes begin match the beginning
|
||||
of the URL. The first match found is returned.
|
||||
|
||||
Returns:
|
||||
The token object which should execute the HTTP request. If there was
|
||||
no token for the url (the url did not begin with any of the token
|
||||
scopes available), then the atom.http_interface.GenericToken will be
|
||||
returned because the GenericToken calls through to the http client
|
||||
without adding an Authorization header.
|
||||
"""
|
||||
if url is None:
|
||||
return None
|
||||
if isinstance(url, (str, unicode)):
|
||||
url = atom.url.parse_url(url)
|
||||
if url in self._tokens:
|
||||
token = self._tokens[url]
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
else:
|
||||
del self._tokens[url]
|
||||
for scope, token in self._tokens.iteritems():
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
return atom.http_interface.GenericToken()
|
||||
|
||||
def remove_token(self, token):
|
||||
"""Removes the token from the token_store.
|
||||
|
||||
This method is used when a token is determined to be invalid. If the
|
||||
token was found by find_token, but resulted in a 401 or 403 error stating
|
||||
that the token was invlid, then the token should be removed to prevent
|
||||
future use.
|
||||
|
||||
Returns:
|
||||
True if a token was found and then removed from the token
|
||||
store. False if the token was not in the TokenStore.
|
||||
"""
|
||||
token_found = False
|
||||
scopes_to_delete = []
|
||||
for scope, stored_token in self._tokens.iteritems():
|
||||
if stored_token == token:
|
||||
scopes_to_delete.append(scope)
|
||||
token_found = True
|
||||
for scope in scopes_to_delete:
|
||||
del self._tokens[scope]
|
||||
return token_found
|
||||
|
||||
def remove_all_tokens(self):
|
||||
self._tokens = {}
|
||||
139
src/atom/url.py
139
src/atom/url.py
@@ -1,139 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import urlparse
|
||||
import urllib
|
||||
|
||||
|
||||
DEFAULT_PROTOCOL = 'http'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
|
||||
def parse_url(url_string):
|
||||
"""Creates a Url object which corresponds to the URL string.
|
||||
|
||||
This method can accept partial URLs, but it will leave missing
|
||||
members of the Url unset.
|
||||
"""
|
||||
parts = urlparse.urlparse(url_string)
|
||||
url = Url()
|
||||
if parts[0]:
|
||||
url.protocol = parts[0]
|
||||
if parts[1]:
|
||||
host_parts = parts[1].split(':')
|
||||
if host_parts[0]:
|
||||
url.host = host_parts[0]
|
||||
if len(host_parts) > 1:
|
||||
url.port = host_parts[1]
|
||||
if parts[2]:
|
||||
url.path = parts[2]
|
||||
if parts[4]:
|
||||
param_pairs = parts[4].split('&')
|
||||
for pair in param_pairs:
|
||||
pair_parts = pair.split('=')
|
||||
if len(pair_parts) > 1:
|
||||
url.params[urllib.unquote_plus(pair_parts[0])] = (
|
||||
urllib.unquote_plus(pair_parts[1]))
|
||||
elif len(pair_parts) == 1:
|
||||
url.params[urllib.unquote_plus(pair_parts[0])] = None
|
||||
return url
|
||||
|
||||
class Url(object):
|
||||
"""Represents a URL and implements comparison logic.
|
||||
|
||||
URL strings which are not identical can still be equivalent, so this object
|
||||
provides a better interface for comparing and manipulating URLs than
|
||||
strings. URL parameters are represented as a dictionary of strings, and
|
||||
defaults are used for the protocol (http) and port (80) if not provided.
|
||||
"""
|
||||
def __init__(self, protocol=None, host=None, port=None, path=None,
|
||||
params=None):
|
||||
self.protocol = protocol
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.params = params or {}
|
||||
|
||||
def to_string(self):
|
||||
url_parts = ['', '', '', '', '', '']
|
||||
if self.protocol:
|
||||
url_parts[0] = self.protocol
|
||||
if self.host:
|
||||
if self.port:
|
||||
url_parts[1] = ':'.join((self.host, str(self.port)))
|
||||
else:
|
||||
url_parts[1] = self.host
|
||||
if self.path:
|
||||
url_parts[2] = self.path
|
||||
if self.params:
|
||||
url_parts[4] = self.get_param_string()
|
||||
return urlparse.urlunparse(url_parts)
|
||||
|
||||
def get_param_string(self):
|
||||
param_pairs = []
|
||||
for key, value in self.params.iteritems():
|
||||
param_pairs.append('='.join((urllib.quote_plus(key),
|
||||
urllib.quote_plus(str(value)))))
|
||||
return '&'.join(param_pairs)
|
||||
|
||||
def get_request_uri(self):
|
||||
"""Returns the path with the parameters escaped and appended."""
|
||||
param_string = self.get_param_string()
|
||||
if param_string:
|
||||
return '?'.join([self.path, param_string])
|
||||
else:
|
||||
return self.path
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Url):
|
||||
return cmp(self.to_string(), str(other))
|
||||
difference = 0
|
||||
# Compare the protocol
|
||||
if self.protocol and other.protocol:
|
||||
difference = cmp(self.protocol, other.protocol)
|
||||
elif self.protocol and not other.protocol:
|
||||
difference = cmp(self.protocol, DEFAULT_PROTOCOL)
|
||||
elif not self.protocol and other.protocol:
|
||||
difference = cmp(DEFAULT_PROTOCOL, other.protocol)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the host
|
||||
difference = cmp(self.host, other.host)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the port
|
||||
if self.port and other.port:
|
||||
difference = cmp(self.port, other.port)
|
||||
elif self.port and not other.port:
|
||||
difference = cmp(self.port, DEFAULT_PORT)
|
||||
elif not self.port and other.port:
|
||||
difference = cmp(DEFAULT_PORT, other.port)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the path
|
||||
difference = cmp(self.path, other.path)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the parameters
|
||||
return cmp(self.params, other.params)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
rmdir /q /s gam
|
||||
rmdir /q /s gam-64
|
||||
rmdir /q /s build
|
||||
rmdir /q /s dist
|
||||
del /q /f gam-%1-windows.zip
|
||||
del /q /f gam-%1-windows-x64.zip
|
||||
|
||||
c:\python27-32\scripts\pyinstaller -F --distpath=gam gam.spec
|
||||
xcopy LICENSE gam\
|
||||
xcopy whatsnew.txt gam\
|
||||
del gam\w9xpopen.exe
|
||||
"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows.zip gam\ -xr!.svn
|
||||
|
||||
c:\python27\scripts\pyinstaller -F --distpath=gam-64 gam.spec
|
||||
xcopy LICENSE gam-64\
|
||||
xcopy whatsnew.txt gam-64\
|
||||
"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn
|
||||
112
src/cachetools/__init__.py
Normal file
112
src/cachetools/__init__.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Extensible memoizing collections and decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
|
||||
from . import keys
|
||||
from .cache import Cache
|
||||
from .lfu import LFUCache
|
||||
from .lru import LRUCache
|
||||
from .rr import RRCache
|
||||
from .ttl import TTLCache
|
||||
|
||||
__all__ = (
|
||||
'Cache', 'LFUCache', 'LRUCache', 'RRCache', 'TTLCache',
|
||||
'cached', 'cachedmethod'
|
||||
)
|
||||
|
||||
__version__ = '3.1.0'
|
||||
|
||||
if hasattr(functools.update_wrapper(lambda f: f(), lambda: 42), '__wrapped__'):
|
||||
_update_wrapper = functools.update_wrapper
|
||||
else:
|
||||
def _update_wrapper(wrapper, wrapped):
|
||||
functools.update_wrapper(wrapper, wrapped)
|
||||
wrapper.__wrapped__ = wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
def cached(cache, key=keys.hashkey, lock=None):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
results in a cache.
|
||||
|
||||
"""
|
||||
def decorator(func):
|
||||
if cache is None:
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
elif lock is None:
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
return cache[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
else:
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
return cache[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
return _update_wrapper(wrapper, func)
|
||||
return decorator
|
||||
|
||||
|
||||
def cachedmethod(cache, key=keys.hashkey, lock=None):
|
||||
"""Decorator to wrap a class or instance method with a memoizing
|
||||
callable that saves results in a cache.
|
||||
|
||||
"""
|
||||
def decorator(method):
|
||||
if lock is None:
|
||||
def wrapper(self, *args, **kwargs):
|
||||
c = cache(self)
|
||||
if c is None:
|
||||
return method(self, *args, **kwargs)
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
return c[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = method(self, *args, **kwargs)
|
||||
try:
|
||||
c[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
else:
|
||||
def wrapper(self, *args, **kwargs):
|
||||
c = cache(self)
|
||||
if c is None:
|
||||
return method(self, *args, **kwargs)
|
||||
k = key(*args, **kwargs)
|
||||
try:
|
||||
with lock(self):
|
||||
return c[k]
|
||||
except KeyError:
|
||||
pass # key not found
|
||||
v = method(self, *args, **kwargs)
|
||||
try:
|
||||
with lock(self):
|
||||
c[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
return _update_wrapper(wrapper, method)
|
||||
return decorator
|
||||
52
src/cachetools/abc.py
Normal file
52
src/cachetools/abc.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
try:
|
||||
from collections.abc import MutableMapping
|
||||
except ImportError:
|
||||
from collections import MutableMapping
|
||||
|
||||
|
||||
class DefaultMapping(MutableMapping):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def __contains__(self, key): # pragma: nocover
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, key): # pragma: nocover
|
||||
if hasattr(self.__class__, '__missing__'):
|
||||
return self.__class__.__missing__(self, key)
|
||||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
else:
|
||||
return default
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
if key in self:
|
||||
value = self[key]
|
||||
del self[key]
|
||||
elif default is self.__marker:
|
||||
raise KeyError(key)
|
||||
else:
|
||||
value = default
|
||||
return value
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key in self:
|
||||
value = self[key]
|
||||
else:
|
||||
self[key] = value = default
|
||||
return value
|
||||
|
||||
|
||||
DefaultMapping.register(dict)
|
||||
91
src/cachetools/cache.py
Normal file
91
src/cachetools/cache.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .abc import DefaultMapping
|
||||
|
||||
|
||||
class _DefaultSize(object):
|
||||
def __getitem__(self, _):
|
||||
return 1
|
||||
|
||||
def __setitem__(self, _, value):
|
||||
assert value == 1
|
||||
|
||||
def pop(self, _):
|
||||
return 1
|
||||
|
||||
|
||||
class Cache(DefaultMapping):
|
||||
"""Mutable mapping to serve as a simple cache or cache base class."""
|
||||
|
||||
__size = _DefaultSize()
|
||||
|
||||
def __init__(self, maxsize, getsizeof=None):
|
||||
if getsizeof:
|
||||
self.getsizeof = getsizeof
|
||||
if self.getsizeof is not Cache.getsizeof:
|
||||
self.__size = dict()
|
||||
self.__data = dict()
|
||||
self.__currsize = 0
|
||||
self.__maxsize = maxsize
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, maxsize=%r, currsize=%r)' % (
|
||||
self.__class__.__name__,
|
||||
list(self.__data.items()),
|
||||
self.__maxsize,
|
||||
self.__currsize,
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self.__data[key]
|
||||
except KeyError:
|
||||
return self.__missing__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
maxsize = self.__maxsize
|
||||
size = self.getsizeof(value)
|
||||
if size > maxsize:
|
||||
raise ValueError('value too large')
|
||||
if key not in self.__data or self.__size[key] < size:
|
||||
while self.__currsize + size > maxsize:
|
||||
self.popitem()
|
||||
if key in self.__data:
|
||||
diffsize = size - self.__size[key]
|
||||
else:
|
||||
diffsize = size
|
||||
self.__data[key] = value
|
||||
self.__size[key] = size
|
||||
self.__currsize += diffsize
|
||||
|
||||
def __delitem__(self, key):
|
||||
size = self.__size.pop(key)
|
||||
del self.__data[key]
|
||||
self.__currsize -= size
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__data
|
||||
|
||||
def __missing__(self, key):
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__data)
|
||||
|
||||
@property
|
||||
def maxsize(self):
|
||||
"""The maximum size of the cache."""
|
||||
return self.__maxsize
|
||||
|
||||
@property
|
||||
def currsize(self):
|
||||
"""The current size of the cache."""
|
||||
return self.__currsize
|
||||
|
||||
@staticmethod
|
||||
def getsizeof(value):
|
||||
"""Return the size of a cache element's value."""
|
||||
return 1
|
||||
140
src/cachetools/func.py
Normal file
140
src/cachetools/func.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""`functools.lru_cache` compatible memoizing function decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import random
|
||||
|
||||
try:
|
||||
from time import monotonic as default_timer
|
||||
except ImportError:
|
||||
from time import time as default_timer
|
||||
|
||||
try:
|
||||
from threading import RLock
|
||||
except ImportError: # pragma: no cover
|
||||
from dummy_threading import RLock
|
||||
|
||||
from . import keys
|
||||
from .lfu import LFUCache
|
||||
from .lru import LRUCache
|
||||
from .rr import RRCache
|
||||
from .ttl import TTLCache
|
||||
|
||||
__all__ = ('lfu_cache', 'lru_cache', 'rr_cache', 'ttl_cache')
|
||||
|
||||
|
||||
_CacheInfo = collections.namedtuple('CacheInfo', [
|
||||
'hits', 'misses', 'maxsize', 'currsize'
|
||||
])
|
||||
|
||||
|
||||
class _UnboundCache(dict):
|
||||
|
||||
maxsize = None
|
||||
|
||||
@property
|
||||
def currsize(self):
|
||||
return len(self)
|
||||
|
||||
|
||||
class _UnboundTTLCache(TTLCache):
|
||||
def __init__(self, ttl, timer):
|
||||
TTLCache.__init__(self, float('inf'), ttl, timer)
|
||||
|
||||
@property
|
||||
def maxsize(self):
|
||||
return None
|
||||
|
||||
|
||||
def _cache(cache, typed=False):
|
||||
def decorator(func):
|
||||
key = keys.typedkey if typed else keys.hashkey
|
||||
lock = RLock()
|
||||
stats = [0, 0]
|
||||
|
||||
def cache_info():
|
||||
with lock:
|
||||
hits, misses = stats
|
||||
maxsize = cache.maxsize
|
||||
currsize = cache.currsize
|
||||
return _CacheInfo(hits, misses, maxsize, currsize)
|
||||
|
||||
def cache_clear():
|
||||
with lock:
|
||||
try:
|
||||
cache.clear()
|
||||
finally:
|
||||
stats[:] = [0, 0]
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
k = key(*args, **kwargs)
|
||||
with lock:
|
||||
try:
|
||||
v = cache[k]
|
||||
stats[0] += 1
|
||||
return v
|
||||
except KeyError:
|
||||
stats[1] += 1
|
||||
v = func(*args, **kwargs)
|
||||
try:
|
||||
with lock:
|
||||
cache[k] = v
|
||||
except ValueError:
|
||||
pass # value too large
|
||||
return v
|
||||
functools.update_wrapper(wrapper, func)
|
||||
if not hasattr(wrapper, '__wrapped__'):
|
||||
wrapper.__wrapped__ = func # Python 2.7
|
||||
wrapper.cache_info = cache_info
|
||||
wrapper.cache_clear = cache_clear
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def lfu_cache(maxsize=128, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Frequently Used (LFU)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
if maxsize is None:
|
||||
return _cache(_UnboundCache(), typed)
|
||||
else:
|
||||
return _cache(LFUCache(maxsize), typed)
|
||||
|
||||
|
||||
def lru_cache(maxsize=128, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Recently Used (LRU)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
if maxsize is None:
|
||||
return _cache(_UnboundCache(), typed)
|
||||
else:
|
||||
return _cache(LRUCache(maxsize), typed)
|
||||
|
||||
|
||||
def rr_cache(maxsize=128, choice=random.choice, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Random Replacement (RR)
|
||||
algorithm.
|
||||
|
||||
"""
|
||||
if maxsize is None:
|
||||
return _cache(_UnboundCache(), typed)
|
||||
else:
|
||||
return _cache(RRCache(maxsize, choice), typed)
|
||||
|
||||
|
||||
def ttl_cache(maxsize=128, ttl=600, timer=default_timer, typed=False):
|
||||
"""Decorator to wrap a function with a memoizing callable that saves
|
||||
up to `maxsize` results based on a Least Recently Used (LRU)
|
||||
algorithm with a per-item time-to-live (TTL) value.
|
||||
"""
|
||||
if maxsize is None:
|
||||
return _cache(_UnboundTTLCache(ttl, timer), typed)
|
||||
else:
|
||||
return _cache(TTLCache(maxsize, ttl, timer), typed)
|
||||
43
src/cachetools/keys.py
Normal file
43
src/cachetools/keys.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Key functions for memoizing decorators."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__all__ = ('hashkey', 'typedkey')
|
||||
|
||||
|
||||
class _HashedTuple(tuple):
|
||||
|
||||
__hashvalue = None
|
||||
|
||||
def __hash__(self, hash=tuple.__hash__):
|
||||
hashvalue = self.__hashvalue
|
||||
if hashvalue is None:
|
||||
self.__hashvalue = hashvalue = hash(self)
|
||||
return hashvalue
|
||||
|
||||
def __add__(self, other, add=tuple.__add__):
|
||||
return _HashedTuple(add(self, other))
|
||||
|
||||
def __radd__(self, other, add=tuple.__add__):
|
||||
return _HashedTuple(add(other, self))
|
||||
|
||||
|
||||
_kwmark = (object(),)
|
||||
|
||||
|
||||
def hashkey(*args, **kwargs):
|
||||
"""Return a cache key for the specified hashable arguments."""
|
||||
|
||||
if kwargs:
|
||||
return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark))
|
||||
else:
|
||||
return _HashedTuple(args)
|
||||
|
||||
|
||||
def typedkey(*args, **kwargs):
|
||||
"""Return a typed cache key for the specified hashable arguments."""
|
||||
|
||||
key = hashkey(*args, **kwargs)
|
||||
key += tuple(type(v) for v in args)
|
||||
key += tuple(type(v) for _, v in sorted(kwargs.items()))
|
||||
return key
|
||||
35
src/cachetools/lfu.py
Normal file
35
src/cachetools/lfu.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
from .cache import Cache
|
||||
|
||||
|
||||
class LFUCache(Cache):
|
||||
"""Least Frequently Used (LFU) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, getsizeof)
|
||||
self.__counter = collections.Counter()
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
value = cache_getitem(self, key)
|
||||
self.__counter[key] -= 1
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
cache_setitem(self, key, value)
|
||||
self.__counter[key] -= 1
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
del self.__counter[key]
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least frequently used."""
|
||||
try:
|
||||
(key, _), = self.__counter.most_common(1)
|
||||
except ValueError:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
48
src/cachetools/lru.py
Normal file
48
src/cachetools/lru.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
from .cache import Cache
|
||||
|
||||
|
||||
class LRUCache(Cache):
|
||||
"""Least Recently Used (LRU) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, getsizeof)
|
||||
self.__order = collections.OrderedDict()
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
value = cache_getitem(self, key)
|
||||
self.__update(key)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
cache_setitem(self, key, value)
|
||||
self.__update(key)
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
del self.__order[key]
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least recently used."""
|
||||
try:
|
||||
key = next(iter(self.__order))
|
||||
except StopIteration:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
|
||||
if hasattr(collections.OrderedDict, 'move_to_end'):
|
||||
def __update(self, key):
|
||||
try:
|
||||
self.__order.move_to_end(key)
|
||||
except KeyError:
|
||||
self.__order[key] = None
|
||||
else:
|
||||
def __update(self, key):
|
||||
try:
|
||||
self.__order[key] = self.__order.pop(key)
|
||||
except KeyError:
|
||||
self.__order[key] = None
|
||||
36
src/cachetools/rr.py
Normal file
36
src/cachetools/rr.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import random
|
||||
|
||||
from .cache import Cache
|
||||
|
||||
|
||||
# random.choice cannot be pickled in Python 2.7
|
||||
def _choice(seq):
|
||||
return random.choice(seq)
|
||||
|
||||
|
||||
class RRCache(Cache):
|
||||
"""Random Replacement (RR) cache implementation."""
|
||||
|
||||
def __init__(self, maxsize, choice=random.choice, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, getsizeof)
|
||||
# TODO: use None as default, assing to self.choice directly?
|
||||
if choice is random.choice:
|
||||
self.__choice = _choice
|
||||
else:
|
||||
self.__choice = choice
|
||||
|
||||
@property
|
||||
def choice(self):
|
||||
"""The `choice` function used by the cache."""
|
||||
return self.__choice
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return a random `(key, value)` pair."""
|
||||
try:
|
||||
key = self.__choice(list(self))
|
||||
except IndexError:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
220
src/cachetools/ttl.py
Normal file
220
src/cachetools/ttl.py
Normal file
@@ -0,0 +1,220 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
try:
|
||||
from time import monotonic as default_timer
|
||||
except ImportError:
|
||||
from time import time as default_timer
|
||||
|
||||
from .cache import Cache
|
||||
|
||||
|
||||
class _Link(object):
|
||||
|
||||
__slots__ = ('key', 'expire', 'next', 'prev')
|
||||
|
||||
def __init__(self, key=None, expire=None):
|
||||
self.key = key
|
||||
self.expire = expire
|
||||
|
||||
def __reduce__(self):
|
||||
return _Link, (self.key, self.expire)
|
||||
|
||||
def unlink(self):
|
||||
next = self.next
|
||||
prev = self.prev
|
||||
prev.next = next
|
||||
next.prev = prev
|
||||
|
||||
|
||||
class _Timer(object):
|
||||
|
||||
def __init__(self, timer):
|
||||
self.__timer = timer
|
||||
self.__nesting = 0
|
||||
|
||||
def __call__(self):
|
||||
if self.__nesting == 0:
|
||||
return self.__timer()
|
||||
else:
|
||||
return self.__time
|
||||
|
||||
def __enter__(self):
|
||||
if self.__nesting == 0:
|
||||
self.__time = time = self.__timer()
|
||||
else:
|
||||
time = self.__time
|
||||
self.__nesting += 1
|
||||
return time
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.__nesting -= 1
|
||||
|
||||
def __reduce__(self):
|
||||
return _Timer, (self.__timer,)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__timer, name)
|
||||
|
||||
|
||||
class TTLCache(Cache):
|
||||
"""LRU Cache implementation with per-item time-to-live (TTL) value."""
|
||||
|
||||
def __init__(self, maxsize, ttl, timer=default_timer, getsizeof=None):
|
||||
Cache.__init__(self, maxsize, getsizeof)
|
||||
self.__root = root = _Link()
|
||||
root.prev = root.next = root
|
||||
self.__links = collections.OrderedDict()
|
||||
self.__timer = _Timer(timer)
|
||||
self.__ttl = ttl
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
link = self.__links[key] # no reordering
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return not (link.expire < self.__timer())
|
||||
|
||||
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
|
||||
try:
|
||||
link = self.__getlink(key)
|
||||
except KeyError:
|
||||
expired = False
|
||||
else:
|
||||
expired = link.expire < self.__timer()
|
||||
if expired:
|
||||
return self.__missing__(key)
|
||||
else:
|
||||
return cache_getitem(self, key)
|
||||
|
||||
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
cache_setitem(self, key, value)
|
||||
try:
|
||||
link = self.__getlink(key)
|
||||
except KeyError:
|
||||
self.__links[key] = link = _Link(key)
|
||||
else:
|
||||
link.unlink()
|
||||
link.expire = time + self.__ttl
|
||||
link.next = root = self.__root
|
||||
link.prev = prev = root.prev
|
||||
prev.next = root.prev = link
|
||||
|
||||
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
||||
cache_delitem(self, key)
|
||||
link = self.__links.pop(key)
|
||||
link.unlink()
|
||||
if link.expire < self.__timer():
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self):
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
while curr is not root:
|
||||
# "freeze" time for iterator access
|
||||
with self.__timer as time:
|
||||
if not (curr.expire < time):
|
||||
yield curr.key
|
||||
curr = curr.next
|
||||
|
||||
def __len__(self):
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
time = self.__timer()
|
||||
count = len(self.__links)
|
||||
while curr is not root and curr.expire < time:
|
||||
count -= 1
|
||||
curr = curr.next
|
||||
return count
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
root = self.__root
|
||||
root.prev = root.next = root
|
||||
for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
|
||||
link.next = root
|
||||
link.prev = prev = root.prev
|
||||
prev.next = root.prev = link
|
||||
self.expire(self.__timer())
|
||||
|
||||
def __repr__(self, cache_repr=Cache.__repr__):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
return cache_repr(self)
|
||||
|
||||
@property
|
||||
def currsize(self):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
return super(TTLCache, self).currsize
|
||||
|
||||
@property
|
||||
def timer(self):
|
||||
"""The timer function used by the cache."""
|
||||
return self.__timer
|
||||
|
||||
@property
|
||||
def ttl(self):
|
||||
"""The time-to-live value of the cache's items."""
|
||||
return self.__ttl
|
||||
|
||||
def expire(self, time=None):
|
||||
"""Remove expired items from the cache."""
|
||||
if time is None:
|
||||
time = self.__timer()
|
||||
root = self.__root
|
||||
curr = root.next
|
||||
links = self.__links
|
||||
cache_delitem = Cache.__delitem__
|
||||
while curr is not root and curr.expire < time:
|
||||
cache_delitem(self, curr.key)
|
||||
del links[curr.key]
|
||||
next = curr.next
|
||||
curr.unlink()
|
||||
curr = next
|
||||
|
||||
def clear(self):
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
Cache.clear(self)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.get(self, *args, **kwargs)
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.pop(self, *args, **kwargs)
|
||||
|
||||
def setdefault(self, *args, **kwargs):
|
||||
with self.__timer:
|
||||
return Cache.setdefault(self, *args, **kwargs)
|
||||
|
||||
def popitem(self):
|
||||
"""Remove and return the `(key, value)` pair least recently used that
|
||||
has not already expired.
|
||||
|
||||
"""
|
||||
with self.__timer as time:
|
||||
self.expire(time)
|
||||
try:
|
||||
key = next(iter(self.__links))
|
||||
except StopIteration:
|
||||
raise KeyError('%s is empty' % self.__class__.__name__)
|
||||
else:
|
||||
return (key, self.pop(key))
|
||||
|
||||
if hasattr(collections.OrderedDict, 'move_to_end'):
|
||||
def __getlink(self, key):
|
||||
value = self.__links[key]
|
||||
self.__links.move_to_end(key)
|
||||
return value
|
||||
else:
|
||||
def __getlink(self, key):
|
||||
value = self.__links.pop(key)
|
||||
self.__links[key] = value
|
||||
return value
|
||||
56
src/dns/__init__.py
Normal file
56
src/dns/__init__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""dnspython DNS toolkit"""
|
||||
|
||||
__all__ = [
|
||||
'dnssec',
|
||||
'e164',
|
||||
'edns',
|
||||
'entropy',
|
||||
'exception',
|
||||
'flags',
|
||||
'hash',
|
||||
'inet',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'message',
|
||||
'name',
|
||||
'namedict',
|
||||
'node',
|
||||
'opcode',
|
||||
'query',
|
||||
'rcode',
|
||||
'rdata',
|
||||
'rdataclass',
|
||||
'rdataset',
|
||||
'rdatatype',
|
||||
'renderer',
|
||||
'resolver',
|
||||
'reversename',
|
||||
'rrset',
|
||||
'set',
|
||||
'tokenizer',
|
||||
'tsig',
|
||||
'tsigkeyring',
|
||||
'ttl',
|
||||
'rdtypes',
|
||||
'update',
|
||||
'version',
|
||||
'wiredata',
|
||||
'zone',
|
||||
]
|
||||
59
src/dns/_compat.py
Normal file
59
src/dns/_compat.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
import decimal
|
||||
from decimal import Context
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
|
||||
if PY3:
|
||||
long = int
|
||||
xrange = range
|
||||
else:
|
||||
long = long # pylint: disable=long-builtin
|
||||
xrange = xrange # pylint: disable=xrange-builtin
|
||||
|
||||
# unicode / binary types
|
||||
if PY3:
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
string_types = (str,)
|
||||
unichr = chr
|
||||
def maybe_decode(x):
|
||||
return x.decode()
|
||||
def maybe_encode(x):
|
||||
return x.encode()
|
||||
def maybe_chr(x):
|
||||
return x
|
||||
def maybe_ord(x):
|
||||
return x
|
||||
else:
|
||||
text_type = unicode # pylint: disable=unicode-builtin, undefined-variable
|
||||
binary_type = str
|
||||
string_types = (
|
||||
basestring, # pylint: disable=basestring-builtin, undefined-variable
|
||||
)
|
||||
unichr = unichr # pylint: disable=unichr-builtin
|
||||
def maybe_decode(x):
|
||||
return x
|
||||
def maybe_encode(x):
|
||||
return x
|
||||
def maybe_chr(x):
|
||||
return chr(x)
|
||||
def maybe_ord(x):
|
||||
return ord(x)
|
||||
|
||||
|
||||
def round_py2_compat(what):
|
||||
"""
|
||||
Python 2 and Python 3 use different rounding strategies in round(). This
|
||||
function ensures that results are python2/3 compatible and backward
|
||||
compatible with previous py2 releases
|
||||
:param what: float
|
||||
:return: rounded long
|
||||
"""
|
||||
d = Context(
|
||||
prec=len(str(long(what))), # round to integer with max precision
|
||||
rounding=decimal.ROUND_HALF_UP
|
||||
).create_decimal(str(what)) # str(): python 2.6 compat
|
||||
return long(d)
|
||||
519
src/dns/dnssec.py
Normal file
519
src/dns/dnssec.py
Normal file
@@ -0,0 +1,519 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""Common DNSSEC-related functions and constants."""
|
||||
|
||||
from io import BytesIO
|
||||
import struct
|
||||
import time
|
||||
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.node
|
||||
import dns.rdataset
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
from ._compat import string_types
|
||||
|
||||
|
||||
class UnsupportedAlgorithm(dns.exception.DNSException):
|
||||
"""The DNSSEC algorithm is not supported."""
|
||||
|
||||
|
||||
class ValidationFailure(dns.exception.DNSException):
|
||||
"""The DNSSEC signature is invalid."""
|
||||
|
||||
|
||||
#: RSAMD5
|
||||
RSAMD5 = 1
|
||||
#: DH
|
||||
DH = 2
|
||||
#: DSA
|
||||
DSA = 3
|
||||
#: ECC
|
||||
ECC = 4
|
||||
#: RSASHA1
|
||||
RSASHA1 = 5
|
||||
#: DSANSEC3SHA1
|
||||
DSANSEC3SHA1 = 6
|
||||
#: RSASHA1NSEC3SHA1
|
||||
RSASHA1NSEC3SHA1 = 7
|
||||
#: RSASHA256
|
||||
RSASHA256 = 8
|
||||
#: RSASHA512
|
||||
RSASHA512 = 10
|
||||
#: ECDSAP256SHA256
|
||||
ECDSAP256SHA256 = 13
|
||||
#: ECDSAP384SHA384
|
||||
ECDSAP384SHA384 = 14
|
||||
#: INDIRECT
|
||||
INDIRECT = 252
|
||||
#: PRIVATEDNS
|
||||
PRIVATEDNS = 253
|
||||
#: PRIVATEOID
|
||||
PRIVATEOID = 254
|
||||
|
||||
_algorithm_by_text = {
|
||||
'RSAMD5': RSAMD5,
|
||||
'DH': DH,
|
||||
'DSA': DSA,
|
||||
'ECC': ECC,
|
||||
'RSASHA1': RSASHA1,
|
||||
'DSANSEC3SHA1': DSANSEC3SHA1,
|
||||
'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1,
|
||||
'RSASHA256': RSASHA256,
|
||||
'RSASHA512': RSASHA512,
|
||||
'INDIRECT': INDIRECT,
|
||||
'ECDSAP256SHA256': ECDSAP256SHA256,
|
||||
'ECDSAP384SHA384': ECDSAP384SHA384,
|
||||
'PRIVATEDNS': PRIVATEDNS,
|
||||
'PRIVATEOID': PRIVATEOID,
|
||||
}
|
||||
|
||||
# We construct the inverse mapping programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mapping not to be true inverse.
|
||||
|
||||
_algorithm_by_value = {y: x for x, y in _algorithm_by_text.items()}
|
||||
|
||||
|
||||
def algorithm_from_text(text):
|
||||
"""Convert text into a DNSSEC algorithm value.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
value = _algorithm_by_text.get(text.upper())
|
||||
if value is None:
|
||||
value = int(text)
|
||||
return value
|
||||
|
||||
|
||||
def algorithm_to_text(value):
|
||||
"""Convert a DNSSEC algorithm value to text
|
||||
|
||||
Returns a ``str``.
|
||||
"""
|
||||
|
||||
text = _algorithm_by_value.get(value)
|
||||
if text is None:
|
||||
text = str(value)
|
||||
return text
|
||||
|
||||
|
||||
def _to_rdata(record, origin):
|
||||
s = BytesIO()
|
||||
record.to_wire(s, origin=origin)
|
||||
return s.getvalue()
|
||||
|
||||
|
||||
def key_id(key, origin=None):
|
||||
"""Return the key id (a 16-bit number) for the specified key.
|
||||
|
||||
Note the *origin* parameter of this function is historical and
|
||||
is not needed.
|
||||
|
||||
Returns an ``int`` between 0 and 65535.
|
||||
"""
|
||||
|
||||
rdata = _to_rdata(key, origin)
|
||||
rdata = bytearray(rdata)
|
||||
if key.algorithm == RSAMD5:
|
||||
return (rdata[-3] << 8) + rdata[-2]
|
||||
else:
|
||||
total = 0
|
||||
for i in range(len(rdata) // 2):
|
||||
total += (rdata[2 * i] << 8) + \
|
||||
rdata[2 * i + 1]
|
||||
if len(rdata) % 2 != 0:
|
||||
total += rdata[len(rdata) - 1] << 8
|
||||
total += ((total >> 16) & 0xffff)
|
||||
return total & 0xffff
|
||||
|
||||
|
||||
def make_ds(name, key, algorithm, origin=None):
|
||||
"""Create a DS record for a DNSSEC key.
|
||||
|
||||
*name* is the owner name of the DS record.
|
||||
|
||||
*key* is a ``dns.rdtypes.ANY.DNSKEY``.
|
||||
|
||||
*algorithm* is a string describing which hash algorithm to use. The
|
||||
currently supported hashes are "SHA1" and "SHA256". Case does not
|
||||
matter for these strings.
|
||||
|
||||
*origin* is a ``dns.name.Name`` and will be used as the origin
|
||||
if *key* is a relative name.
|
||||
|
||||
Returns a ``dns.rdtypes.ANY.DS``.
|
||||
"""
|
||||
|
||||
if algorithm.upper() == 'SHA1':
|
||||
dsalg = 1
|
||||
hash = SHA1.new()
|
||||
elif algorithm.upper() == 'SHA256':
|
||||
dsalg = 2
|
||||
hash = SHA256.new()
|
||||
else:
|
||||
raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
|
||||
|
||||
if isinstance(name, string_types):
|
||||
name = dns.name.from_text(name, origin)
|
||||
hash.update(name.canonicalize().to_wire())
|
||||
hash.update(_to_rdata(key, origin))
|
||||
digest = hash.digest()
|
||||
|
||||
dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest
|
||||
return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
|
||||
len(dsrdata))
|
||||
|
||||
|
||||
def _find_candidate_keys(keys, rrsig):
|
||||
candidate_keys = []
|
||||
value = keys.get(rrsig.signer)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, dns.node.Node):
|
||||
try:
|
||||
rdataset = value.find_rdataset(dns.rdataclass.IN,
|
||||
dns.rdatatype.DNSKEY)
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
rdataset = value
|
||||
for rdata in rdataset:
|
||||
if rdata.algorithm == rrsig.algorithm and \
|
||||
key_id(rdata) == rrsig.key_tag:
|
||||
candidate_keys.append(rdata)
|
||||
return candidate_keys
|
||||
|
||||
|
||||
def _is_rsa(algorithm):
|
||||
return algorithm in (RSAMD5, RSASHA1,
|
||||
RSASHA1NSEC3SHA1, RSASHA256,
|
||||
RSASHA512)
|
||||
|
||||
|
||||
def _is_dsa(algorithm):
|
||||
return algorithm in (DSA, DSANSEC3SHA1)
|
||||
|
||||
|
||||
def _is_ecdsa(algorithm):
|
||||
return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384))
|
||||
|
||||
|
||||
def _is_md5(algorithm):
|
||||
return algorithm == RSAMD5
|
||||
|
||||
|
||||
def _is_sha1(algorithm):
|
||||
return algorithm in (DSA, RSASHA1,
|
||||
DSANSEC3SHA1, RSASHA1NSEC3SHA1)
|
||||
|
||||
|
||||
def _is_sha256(algorithm):
|
||||
return algorithm in (RSASHA256, ECDSAP256SHA256)
|
||||
|
||||
|
||||
def _is_sha384(algorithm):
|
||||
return algorithm == ECDSAP384SHA384
|
||||
|
||||
|
||||
def _is_sha512(algorithm):
|
||||
return algorithm == RSASHA512
|
||||
|
||||
|
||||
def _make_hash(algorithm):
|
||||
if _is_md5(algorithm):
|
||||
return MD5.new()
|
||||
if _is_sha1(algorithm):
|
||||
return SHA1.new()
|
||||
if _is_sha256(algorithm):
|
||||
return SHA256.new()
|
||||
if _is_sha384(algorithm):
|
||||
return SHA384.new()
|
||||
if _is_sha512(algorithm):
|
||||
return SHA512.new()
|
||||
raise ValidationFailure('unknown hash for algorithm %u' % algorithm)
|
||||
|
||||
|
||||
def _make_algorithm_id(algorithm):
|
||||
if _is_md5(algorithm):
|
||||
oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05]
|
||||
elif _is_sha1(algorithm):
|
||||
oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a]
|
||||
elif _is_sha256(algorithm):
|
||||
oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]
|
||||
elif _is_sha512(algorithm):
|
||||
oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]
|
||||
else:
|
||||
raise ValidationFailure('unknown algorithm %u' % algorithm)
|
||||
olen = len(oid)
|
||||
dlen = _make_hash(algorithm).digest_size
|
||||
idbytes = [0x30] + [8 + olen + dlen] + \
|
||||
[0x30, olen + 4] + [0x06, olen] + oid + \
|
||||
[0x05, 0x00] + [0x04, dlen]
|
||||
return struct.pack('!%dB' % len(idbytes), *idbytes)
|
||||
|
||||
|
||||
def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
||||
"""Validate an RRset against a single signature rdata
|
||||
|
||||
The owner name of *rrsig* is assumed to be the same as the owner name
|
||||
of *rrset*.
|
||||
|
||||
*rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or
|
||||
a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
||||
|
||||
*rrsig* is a ``dns.rdata.Rdata``, the signature to validate.
|
||||
|
||||
*keys* is the key dictionary, used to find the DNSKEY associated with
|
||||
a given name. The dictionary is keyed by a ``dns.name.Name``, and has
|
||||
``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.
|
||||
|
||||
*origin* is a ``dns.name.Name``, the origin to use for relative names.
|
||||
|
||||
*now* is an ``int``, the time to use when validating the signatures,
|
||||
in seconds since the UNIX epoch. The default is the current time.
|
||||
"""
|
||||
|
||||
if isinstance(origin, string_types):
|
||||
origin = dns.name.from_text(origin, dns.name.root)
|
||||
|
||||
candidate_keys = _find_candidate_keys(keys, rrsig)
|
||||
if candidate_keys is None:
|
||||
raise ValidationFailure('unknown key')
|
||||
|
||||
for candidate_key in candidate_keys:
|
||||
# For convenience, allow the rrset to be specified as a (name,
|
||||
# rdataset) tuple as well as a proper rrset
|
||||
if isinstance(rrset, tuple):
|
||||
rrname = rrset[0]
|
||||
rdataset = rrset[1]
|
||||
else:
|
||||
rrname = rrset.name
|
||||
rdataset = rrset
|
||||
|
||||
if now is None:
|
||||
now = time.time()
|
||||
if rrsig.expiration < now:
|
||||
raise ValidationFailure('expired')
|
||||
if rrsig.inception > now:
|
||||
raise ValidationFailure('not yet valid')
|
||||
|
||||
hash = _make_hash(rrsig.algorithm)
|
||||
|
||||
if _is_rsa(rrsig.algorithm):
|
||||
keyptr = candidate_key.key
|
||||
(bytes_,) = struct.unpack('!B', keyptr[0:1])
|
||||
keyptr = keyptr[1:]
|
||||
if bytes_ == 0:
|
||||
(bytes_,) = struct.unpack('!H', keyptr[0:2])
|
||||
keyptr = keyptr[2:]
|
||||
rsa_e = keyptr[0:bytes_]
|
||||
rsa_n = keyptr[bytes_:]
|
||||
try:
|
||||
pubkey = CryptoRSA.construct(
|
||||
(number.bytes_to_long(rsa_n),
|
||||
number.bytes_to_long(rsa_e)))
|
||||
except ValueError:
|
||||
raise ValidationFailure('invalid public key')
|
||||
sig = rrsig.signature
|
||||
elif _is_dsa(rrsig.algorithm):
|
||||
keyptr = candidate_key.key
|
||||
(t,) = struct.unpack('!B', keyptr[0:1])
|
||||
keyptr = keyptr[1:]
|
||||
octets = 64 + t * 8
|
||||
dsa_q = keyptr[0:20]
|
||||
keyptr = keyptr[20:]
|
||||
dsa_p = keyptr[0:octets]
|
||||
keyptr = keyptr[octets:]
|
||||
dsa_g = keyptr[0:octets]
|
||||
keyptr = keyptr[octets:]
|
||||
dsa_y = keyptr[0:octets]
|
||||
pubkey = CryptoDSA.construct(
|
||||
(number.bytes_to_long(dsa_y),
|
||||
number.bytes_to_long(dsa_g),
|
||||
number.bytes_to_long(dsa_p),
|
||||
number.bytes_to_long(dsa_q)))
|
||||
sig = rrsig.signature[1:]
|
||||
elif _is_ecdsa(rrsig.algorithm):
|
||||
# use ecdsa for NIST-384p -- not currently supported by pycryptodome
|
||||
|
||||
keyptr = candidate_key.key
|
||||
|
||||
if rrsig.algorithm == ECDSAP256SHA256:
|
||||
curve = ecdsa.curves.NIST256p
|
||||
key_len = 32
|
||||
elif rrsig.algorithm == ECDSAP384SHA384:
|
||||
curve = ecdsa.curves.NIST384p
|
||||
key_len = 48
|
||||
|
||||
x = number.bytes_to_long(keyptr[0:key_len])
|
||||
y = number.bytes_to_long(keyptr[key_len:key_len * 2])
|
||||
if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y):
|
||||
raise ValidationFailure('invalid ECDSA key')
|
||||
point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order)
|
||||
verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point,
|
||||
curve)
|
||||
pubkey = ECKeyWrapper(verifying_key, key_len)
|
||||
r = rrsig.signature[:key_len]
|
||||
s = rrsig.signature[key_len:]
|
||||
sig = ecdsa.ecdsa.Signature(number.bytes_to_long(r),
|
||||
number.bytes_to_long(s))
|
||||
|
||||
else:
|
||||
raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
|
||||
|
||||
hash.update(_to_rdata(rrsig, origin)[:18])
|
||||
hash.update(rrsig.signer.to_digestable(origin))
|
||||
|
||||
if rrsig.labels < len(rrname) - 1:
|
||||
suffix = rrname.split(rrsig.labels + 1)[1]
|
||||
rrname = dns.name.from_text('*', suffix)
|
||||
rrnamebuf = rrname.to_digestable(origin)
|
||||
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
|
||||
rrsig.original_ttl)
|
||||
rrlist = sorted(rdataset)
|
||||
for rr in rrlist:
|
||||
hash.update(rrnamebuf)
|
||||
hash.update(rrfixed)
|
||||
rrdata = rr.to_digestable(origin)
|
||||
rrlen = struct.pack('!H', len(rrdata))
|
||||
hash.update(rrlen)
|
||||
hash.update(rrdata)
|
||||
|
||||
try:
|
||||
if _is_rsa(rrsig.algorithm):
|
||||
verifier = pkcs1_15.new(pubkey)
|
||||
# will raise ValueError if verify fails:
|
||||
verifier.verify(hash, sig)
|
||||
elif _is_dsa(rrsig.algorithm):
|
||||
verifier = DSS.new(pubkey, 'fips-186-3')
|
||||
verifier.verify(hash, sig)
|
||||
elif _is_ecdsa(rrsig.algorithm):
|
||||
digest = hash.digest()
|
||||
if not pubkey.verify(digest, sig):
|
||||
raise ValueError
|
||||
else:
|
||||
# Raise here for code clarity; this won't actually ever happen
|
||||
# since if the algorithm is really unknown we'd already have
|
||||
# raised an exception above
|
||||
raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
|
||||
# If we got here, we successfully verified so we can return without error
|
||||
return
|
||||
except ValueError:
|
||||
# this happens on an individual validation failure
|
||||
continue
|
||||
# nothing verified -- raise failure:
|
||||
raise ValidationFailure('verify failure')
|
||||
|
||||
|
||||
def _validate(rrset, rrsigset, keys, origin=None, now=None):
|
||||
"""Validate an RRset.
|
||||
|
||||
*rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or
|
||||
a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
||||
|
||||
*rrsigset* is the signature RRset to be validated. It can be a
|
||||
``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
||||
|
||||
*keys* is the key dictionary, used to find the DNSKEY associated with
|
||||
a given name. The dictionary is keyed by a ``dns.name.Name``, and has
|
||||
``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.
|
||||
|
||||
*origin* is a ``dns.name.Name``, the origin to use for relative names.
|
||||
|
||||
*now* is an ``int``, the time to use when validating the signatures,
|
||||
in seconds since the UNIX epoch. The default is the current time.
|
||||
"""
|
||||
|
||||
if isinstance(origin, string_types):
|
||||
origin = dns.name.from_text(origin, dns.name.root)
|
||||
|
||||
if isinstance(rrset, tuple):
|
||||
rrname = rrset[0]
|
||||
else:
|
||||
rrname = rrset.name
|
||||
|
||||
if isinstance(rrsigset, tuple):
|
||||
rrsigname = rrsigset[0]
|
||||
rrsigrdataset = rrsigset[1]
|
||||
else:
|
||||
rrsigname = rrsigset.name
|
||||
rrsigrdataset = rrsigset
|
||||
|
||||
rrname = rrname.choose_relativity(origin)
|
||||
rrsigname = rrsigname.choose_relativity(origin)
|
||||
if rrname != rrsigname:
|
||||
raise ValidationFailure("owner names do not match")
|
||||
|
||||
for rrsig in rrsigrdataset:
|
||||
try:
|
||||
_validate_rrsig(rrset, rrsig, keys, origin, now)
|
||||
return
|
||||
except ValidationFailure:
|
||||
pass
|
||||
raise ValidationFailure("no RRSIGs validated")
|
||||
|
||||
|
||||
def _need_pycrypto(*args, **kwargs):
|
||||
raise NotImplementedError("DNSSEC validation requires pycryptodome/pycryptodomex")
|
||||
|
||||
|
||||
try:
|
||||
try:
|
||||
# test we're using pycryptodome, not pycrypto (which misses SHA1 for example)
|
||||
from Crypto.Hash import MD5, SHA1, SHA256, SHA384, SHA512
|
||||
from Crypto.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA
|
||||
from Crypto.Signature import pkcs1_15, DSS
|
||||
from Crypto.Util import number
|
||||
except ImportError:
|
||||
from Cryptodome.Hash import MD5, SHA1, SHA256, SHA384, SHA512
|
||||
from Cryptodome.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA
|
||||
from Cryptodome.Signature import pkcs1_15, DSS
|
||||
from Cryptodome.Util import number
|
||||
except ImportError:
|
||||
validate = _need_pycrypto
|
||||
validate_rrsig = _need_pycrypto
|
||||
_have_pycrypto = False
|
||||
_have_ecdsa = False
|
||||
else:
|
||||
validate = _validate
|
||||
validate_rrsig = _validate_rrsig
|
||||
_have_pycrypto = True
|
||||
|
||||
try:
|
||||
import ecdsa
|
||||
import ecdsa.ecdsa
|
||||
import ecdsa.ellipticcurve
|
||||
import ecdsa.keys
|
||||
except ImportError:
|
||||
_have_ecdsa = False
|
||||
else:
|
||||
_have_ecdsa = True
|
||||
|
||||
class ECKeyWrapper(object):
|
||||
|
||||
def __init__(self, key, key_len):
|
||||
self.key = key
|
||||
self.key_len = key_len
|
||||
|
||||
def verify(self, digest, sig):
|
||||
diglong = number.bytes_to_long(digest)
|
||||
return self.key.pubkey.verifies(diglong, sig)
|
||||
19
src/dns/dnssec.pyi
Normal file
19
src/dns/dnssec.pyi
Normal file
@@ -0,0 +1,19 @@
|
||||
from typing import Union, Dict, Tuple, Optional
|
||||
from . import rdataset, rrset, exception, name, rdtypes, rdata, node
|
||||
import dns.rdtypes.ANY.DS as DS
|
||||
import dns.rdtypes.ANY.DNSKEY as DNSKEY
|
||||
|
||||
_have_ecdsa : bool
|
||||
_have_pycrypto : bool
|
||||
|
||||
def validate_rrsig(rrset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsig : rdata.Rdata, keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin : Optional[name.Name] = None, now : Optional[int] = None) -> None:
|
||||
...
|
||||
|
||||
def validate(rrset: Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsigset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin=None, now=None) -> None:
|
||||
...
|
||||
|
||||
class ValidationFailure(exception.DNSException):
|
||||
...
|
||||
|
||||
def make_ds(name : name.Name, key : DNSKEY.DNSKEY, algorithm : str, origin : Optional[name.Name] = None) -> DS.DS:
|
||||
...
|
||||
105
src/dns/e164.py
Normal file
105
src/dns/e164.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2006-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS E.164 helpers."""
|
||||
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.resolver
|
||||
from ._compat import string_types, maybe_decode
|
||||
|
||||
#: The public E.164 domain.
|
||||
public_enum_domain = dns.name.from_text('e164.arpa.')
|
||||
|
||||
|
||||
def from_e164(text, origin=public_enum_domain):
|
||||
"""Convert an E.164 number in textual form into a Name object whose
|
||||
value is the ENUM domain name for that number.
|
||||
|
||||
Non-digits in the text are ignored, i.e. "16505551212",
|
||||
"+1.650.555.1212" and "1 (650) 555-1212" are all the same.
|
||||
|
||||
*text*, a ``text``, is an E.164 number in textual form.
|
||||
|
||||
*origin*, a ``dns.name.Name``, the domain in which the number
|
||||
should be constructed. The default is ``e164.arpa.``.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
parts = [d for d in text if d.isdigit()]
|
||||
parts.reverse()
|
||||
return dns.name.from_text('.'.join(parts), origin=origin)
|
||||
|
||||
|
||||
def to_e164(name, origin=public_enum_domain, want_plus_prefix=True):
|
||||
"""Convert an ENUM domain name into an E.164 number.
|
||||
|
||||
Note that dnspython does not have any information about preferred
|
||||
number formats within national numbering plans, so all numbers are
|
||||
emitted as a simple string of digits, prefixed by a '+' (unless
|
||||
*want_plus_prefix* is ``False``).
|
||||
|
||||
*name* is a ``dns.name.Name``, the ENUM domain name.
|
||||
|
||||
*origin* is a ``dns.name.Name``, a domain containing the ENUM
|
||||
domain name. The name is relativized to this domain before being
|
||||
converted to text. If ``None``, no relativization is done.
|
||||
|
||||
*want_plus_prefix* is a ``bool``. If True, add a '+' to the beginning of
|
||||
the returned number.
|
||||
|
||||
Returns a ``text``.
|
||||
|
||||
"""
|
||||
if origin is not None:
|
||||
name = name.relativize(origin)
|
||||
dlabels = [d for d in name.labels if d.isdigit() and len(d) == 1]
|
||||
if len(dlabels) != len(name.labels):
|
||||
raise dns.exception.SyntaxError('non-digit labels in ENUM domain name')
|
||||
dlabels.reverse()
|
||||
text = b''.join(dlabels)
|
||||
if want_plus_prefix:
|
||||
text = b'+' + text
|
||||
return maybe_decode(text)
|
||||
|
||||
|
||||
def query(number, domains, resolver=None):
|
||||
"""Look for NAPTR RRs for the specified number in the specified domains.
|
||||
|
||||
e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.'])
|
||||
|
||||
*number*, a ``text`` is the number to look for.
|
||||
|
||||
*domains* is an iterable containing ``dns.name.Name`` values.
|
||||
|
||||
*resolver*, a ``dns.resolver.Resolver``, is the resolver to use. If
|
||||
``None``, the default resolver is used.
|
||||
"""
|
||||
|
||||
if resolver is None:
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
e_nx = dns.resolver.NXDOMAIN()
|
||||
for domain in domains:
|
||||
if isinstance(domain, string_types):
|
||||
domain = dns.name.from_text(domain)
|
||||
qname = dns.e164.from_e164(number, domain)
|
||||
try:
|
||||
return resolver.query(qname, 'NAPTR')
|
||||
except dns.resolver.NXDOMAIN as e:
|
||||
e_nx += e
|
||||
raise e_nx
|
||||
10
src/dns/e164.pyi
Normal file
10
src/dns/e164.pyi
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Optional, Iterable
|
||||
from . import name, resolver
|
||||
def from_e164(text : str, origin=name.Name(".")) -> name.Name:
|
||||
...
|
||||
|
||||
def to_e164(name : name.Name, origin : Optional[name.Name] = None, want_plus_prefix=True) -> str:
|
||||
...
|
||||
|
||||
def query(number : str, domains : Iterable[str], resolver : Optional[resolver.Resolver] = None) -> resolver.Answer:
|
||||
...
|
||||
269
src/dns/edns.py
Normal file
269
src/dns/edns.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2009-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""EDNS Options"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import math
|
||||
import struct
|
||||
|
||||
import dns.inet
|
||||
|
||||
#: NSID
|
||||
NSID = 3
|
||||
#: DAU
|
||||
DAU = 5
|
||||
#: DHU
|
||||
DHU = 6
|
||||
#: N3U
|
||||
N3U = 7
|
||||
#: ECS (client-subnet)
|
||||
ECS = 8
|
||||
#: EXPIRE
|
||||
EXPIRE = 9
|
||||
#: COOKIE
|
||||
COOKIE = 10
|
||||
#: KEEPALIVE
|
||||
KEEPALIVE = 11
|
||||
#: PADDING
|
||||
PADDING = 12
|
||||
#: CHAIN
|
||||
CHAIN = 13
|
||||
|
||||
class Option(object):
|
||||
|
||||
"""Base class for all EDNS option types."""
|
||||
|
||||
def __init__(self, otype):
|
||||
"""Initialize an option.
|
||||
|
||||
*otype*, an ``int``, is the option type.
|
||||
"""
|
||||
self.otype = otype
|
||||
|
||||
def to_wire(self, file):
|
||||
"""Convert an option to wire format.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, otype, wire, current, olen):
|
||||
"""Build an EDNS option object from wire format.
|
||||
|
||||
*otype*, an ``int``, is the option type.
|
||||
|
||||
*wire*, a ``binary``, is the wire-format message.
|
||||
|
||||
*current*, an ``int``, is the offset in *wire* of the beginning
|
||||
of the rdata.
|
||||
|
||||
*olen*, an ``int``, is the length of the wire-format option data
|
||||
|
||||
Returns a ``dns.edns.Option``.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def _cmp(self, other):
|
||||
"""Compare an EDNS option with another option of the same type.
|
||||
|
||||
Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Option):
|
||||
return False
|
||||
if self.otype != other.otype:
|
||||
return False
|
||||
return self._cmp(other) == 0
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Option):
|
||||
return False
|
||||
if self.otype != other.otype:
|
||||
return False
|
||||
return self._cmp(other) != 0
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Option) or \
|
||||
self.otype != other.otype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) < 0
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, Option) or \
|
||||
self.otype != other.otype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) <= 0
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, Option) or \
|
||||
self.otype != other.otype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) >= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, Option) or \
|
||||
self.otype != other.otype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) > 0
|
||||
|
||||
|
||||
class GenericOption(Option):
|
||||
|
||||
"""Generic Option Class
|
||||
|
||||
This class is used for EDNS option types for which we have no better
|
||||
implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, otype, data):
|
||||
super(GenericOption, self).__init__(otype)
|
||||
self.data = data
|
||||
|
||||
def to_wire(self, file):
|
||||
file.write(self.data)
|
||||
|
||||
def to_text(self):
|
||||
return "Generic %d" % self.otype
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, otype, wire, current, olen):
|
||||
return cls(otype, wire[current: current + olen])
|
||||
|
||||
def _cmp(self, other):
|
||||
if self.data == other.data:
|
||||
return 0
|
||||
if self.data > other.data:
|
||||
return 1
|
||||
return -1
|
||||
|
||||
|
||||
class ECSOption(Option):
|
||||
"""EDNS Client Subnet (ECS, RFC7871)"""
|
||||
|
||||
def __init__(self, address, srclen=None, scopelen=0):
|
||||
"""*address*, a ``text``, is the client address information.
|
||||
|
||||
*srclen*, an ``int``, the source prefix length, which is the
|
||||
leftmost number of bits of the address to be used for the
|
||||
lookup. The default is 24 for IPv4 and 56 for IPv6.
|
||||
|
||||
*scopelen*, an ``int``, the scope prefix length. This value
|
||||
must be 0 in queries, and should be set in responses.
|
||||
"""
|
||||
|
||||
super(ECSOption, self).__init__(ECS)
|
||||
af = dns.inet.af_for_address(address)
|
||||
|
||||
if af == dns.inet.AF_INET6:
|
||||
self.family = 2
|
||||
if srclen is None:
|
||||
srclen = 56
|
||||
elif af == dns.inet.AF_INET:
|
||||
self.family = 1
|
||||
if srclen is None:
|
||||
srclen = 24
|
||||
else:
|
||||
raise ValueError('Bad ip family')
|
||||
|
||||
self.address = address
|
||||
self.srclen = srclen
|
||||
self.scopelen = scopelen
|
||||
|
||||
addrdata = dns.inet.inet_pton(af, address)
|
||||
nbytes = int(math.ceil(srclen/8.0))
|
||||
|
||||
# Truncate to srclen and pad to the end of the last octet needed
|
||||
# See RFC section 6
|
||||
self.addrdata = addrdata[:nbytes]
|
||||
nbits = srclen % 8
|
||||
if nbits != 0:
|
||||
last = struct.pack('B', ord(self.addrdata[-1:]) & (0xff << nbits))
|
||||
self.addrdata = self.addrdata[:-1] + last
|
||||
|
||||
def to_text(self):
|
||||
return "ECS {}/{} scope/{}".format(self.address, self.srclen,
|
||||
self.scopelen)
|
||||
|
||||
def to_wire(self, file):
|
||||
file.write(struct.pack('!H', self.family))
|
||||
file.write(struct.pack('!BB', self.srclen, self.scopelen))
|
||||
file.write(self.addrdata)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, otype, wire, cur, olen):
|
||||
family, src, scope = struct.unpack('!HBB', wire[cur:cur+4])
|
||||
cur += 4
|
||||
|
||||
addrlen = int(math.ceil(src/8.0))
|
||||
|
||||
if family == 1:
|
||||
af = dns.inet.AF_INET
|
||||
pad = 4 - addrlen
|
||||
elif family == 2:
|
||||
af = dns.inet.AF_INET6
|
||||
pad = 16 - addrlen
|
||||
else:
|
||||
raise ValueError('unsupported family')
|
||||
|
||||
addr = dns.inet.inet_ntop(af, wire[cur:cur+addrlen] + b'\x00' * pad)
|
||||
return cls(addr, src, scope)
|
||||
|
||||
def _cmp(self, other):
|
||||
if self.addrdata == other.addrdata:
|
||||
return 0
|
||||
if self.addrdata > other.addrdata:
|
||||
return 1
|
||||
return -1
|
||||
|
||||
_type_to_class = {
|
||||
ECS: ECSOption
|
||||
}
|
||||
|
||||
def get_option_class(otype):
|
||||
"""Return the class for the specified option type.
|
||||
|
||||
The GenericOption class is used if a more specific class is not
|
||||
known.
|
||||
"""
|
||||
|
||||
cls = _type_to_class.get(otype)
|
||||
if cls is None:
|
||||
cls = GenericOption
|
||||
return cls
|
||||
|
||||
|
||||
def option_from_wire(otype, wire, current, olen):
|
||||
"""Build an EDNS option object from wire format.
|
||||
|
||||
*otype*, an ``int``, is the option type.
|
||||
|
||||
*wire*, a ``binary``, is the wire-format message.
|
||||
|
||||
*current*, an ``int``, is the offset in *wire* of the beginning
|
||||
of the rdata.
|
||||
|
||||
*olen*, an ``int``, is the length of the wire-format option data
|
||||
|
||||
Returns an instance of a subclass of ``dns.edns.Option``.
|
||||
"""
|
||||
|
||||
cls = get_option_class(otype)
|
||||
return cls.from_wire(otype, wire, current, olen)
|
||||
148
src/dns/entropy.py
Normal file
148
src/dns/entropy.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2009-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from ._compat import long, binary_type
|
||||
try:
|
||||
import threading as _threading
|
||||
except ImportError:
|
||||
import dummy_threading as _threading
|
||||
|
||||
|
||||
class EntropyPool(object):
|
||||
|
||||
# This is an entropy pool for Python implementations that do not
|
||||
# have a working SystemRandom. I'm not sure there are any, but
|
||||
# leaving this code doesn't hurt anything as the library code
|
||||
# is used if present.
|
||||
|
||||
def __init__(self, seed=None):
|
||||
self.pool_index = 0
|
||||
self.digest = None
|
||||
self.next_byte = 0
|
||||
self.lock = _threading.Lock()
|
||||
try:
|
||||
import hashlib
|
||||
self.hash = hashlib.sha1()
|
||||
self.hash_len = 20
|
||||
except ImportError:
|
||||
try:
|
||||
import sha
|
||||
self.hash = sha.new()
|
||||
self.hash_len = 20
|
||||
except ImportError:
|
||||
import md5 # pylint: disable=import-error
|
||||
self.hash = md5.new()
|
||||
self.hash_len = 16
|
||||
self.pool = bytearray(b'\0' * self.hash_len)
|
||||
if seed is not None:
|
||||
self.stir(bytearray(seed))
|
||||
self.seeded = True
|
||||
self.seed_pid = os.getpid()
|
||||
else:
|
||||
self.seeded = False
|
||||
self.seed_pid = 0
|
||||
|
||||
def stir(self, entropy, already_locked=False):
|
||||
if not already_locked:
|
||||
self.lock.acquire()
|
||||
try:
|
||||
for c in entropy:
|
||||
if self.pool_index == self.hash_len:
|
||||
self.pool_index = 0
|
||||
b = c & 0xff
|
||||
self.pool[self.pool_index] ^= b
|
||||
self.pool_index += 1
|
||||
finally:
|
||||
if not already_locked:
|
||||
self.lock.release()
|
||||
|
||||
def _maybe_seed(self):
|
||||
if not self.seeded or self.seed_pid != os.getpid():
|
||||
try:
|
||||
seed = os.urandom(16)
|
||||
except Exception:
|
||||
try:
|
||||
r = open('/dev/urandom', 'rb', 0)
|
||||
try:
|
||||
seed = r.read(16)
|
||||
finally:
|
||||
r.close()
|
||||
except Exception:
|
||||
seed = str(time.time())
|
||||
self.seeded = True
|
||||
self.seed_pid = os.getpid()
|
||||
self.digest = None
|
||||
seed = bytearray(seed)
|
||||
self.stir(seed, True)
|
||||
|
||||
def random_8(self):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
self._maybe_seed()
|
||||
if self.digest is None or self.next_byte == self.hash_len:
|
||||
self.hash.update(binary_type(self.pool))
|
||||
self.digest = bytearray(self.hash.digest())
|
||||
self.stir(self.digest, True)
|
||||
self.next_byte = 0
|
||||
value = self.digest[self.next_byte]
|
||||
self.next_byte += 1
|
||||
finally:
|
||||
self.lock.release()
|
||||
return value
|
||||
|
||||
def random_16(self):
|
||||
return self.random_8() * 256 + self.random_8()
|
||||
|
||||
def random_32(self):
|
||||
return self.random_16() * 65536 + self.random_16()
|
||||
|
||||
def random_between(self, first, last):
|
||||
size = last - first + 1
|
||||
if size > long(4294967296):
|
||||
raise ValueError('too big')
|
||||
if size > 65536:
|
||||
rand = self.random_32
|
||||
max = long(4294967295)
|
||||
elif size > 256:
|
||||
rand = self.random_16
|
||||
max = 65535
|
||||
else:
|
||||
rand = self.random_8
|
||||
max = 255
|
||||
return first + size * rand() // (max + 1)
|
||||
|
||||
pool = EntropyPool()
|
||||
|
||||
try:
|
||||
system_random = random.SystemRandom()
|
||||
except Exception:
|
||||
system_random = None
|
||||
|
||||
def random_16():
|
||||
if system_random is not None:
|
||||
return system_random.randrange(0, 65536)
|
||||
else:
|
||||
return pool.random_16()
|
||||
|
||||
def between(first, last):
|
||||
if system_random is not None:
|
||||
return system_random.randrange(first, last + 1)
|
||||
else:
|
||||
return pool.random_between(first, last)
|
||||
10
src/dns/entropy.pyi
Normal file
10
src/dns/entropy.pyi
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
from random import SystemRandom
|
||||
|
||||
system_random : Optional[SystemRandom]
|
||||
|
||||
def random_16() -> int:
|
||||
pass
|
||||
|
||||
def between(first: int, last: int) -> int:
|
||||
pass
|
||||
128
src/dns/exception.py
Normal file
128
src/dns/exception.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""Common DNS Exceptions.
|
||||
|
||||
Dnspython modules may also define their own exceptions, which will
|
||||
always be subclasses of ``DNSException``.
|
||||
"""
|
||||
|
||||
class DNSException(Exception):
|
||||
"""Abstract base class shared by all dnspython exceptions.
|
||||
|
||||
It supports two basic modes of operation:
|
||||
|
||||
a) Old/compatible mode is used if ``__init__`` was called with
|
||||
empty *kwargs*. In compatible mode all *args* are passed
|
||||
to the standard Python Exception class as before and all *args* are
|
||||
printed by the standard ``__str__`` implementation. Class variable
|
||||
``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()``
|
||||
if *args* is empty.
|
||||
|
||||
b) New/parametrized mode is used if ``__init__`` was called with
|
||||
non-empty *kwargs*.
|
||||
In the new mode *args* must be empty and all kwargs must match
|
||||
those set in class variable ``supp_kwargs``. All kwargs are stored inside
|
||||
``self.kwargs`` and used in a new ``__str__`` implementation to construct
|
||||
a formatted message based on the ``fmt`` class variable, a ``string``.
|
||||
|
||||
In the simplest case it is enough to override the ``supp_kwargs``
|
||||
and ``fmt`` class variables to get nice parametrized messages.
|
||||
"""
|
||||
|
||||
msg = None # non-parametrized message
|
||||
supp_kwargs = set() # accepted parameters for _fmt_kwargs (sanity check)
|
||||
fmt = None # message parametrized with results from _fmt_kwargs
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._check_params(*args, **kwargs)
|
||||
if kwargs:
|
||||
self.kwargs = self._check_kwargs(**kwargs)
|
||||
self.msg = str(self)
|
||||
else:
|
||||
self.kwargs = dict() # defined but empty for old mode exceptions
|
||||
if self.msg is None:
|
||||
# doc string is better implicit message than empty string
|
||||
self.msg = self.__doc__
|
||||
if args:
|
||||
super(DNSException, self).__init__(*args)
|
||||
else:
|
||||
super(DNSException, self).__init__(self.msg)
|
||||
|
||||
def _check_params(self, *args, **kwargs):
|
||||
"""Old exceptions supported only args and not kwargs.
|
||||
|
||||
For sanity we do not allow to mix old and new behavior."""
|
||||
if args or kwargs:
|
||||
assert bool(args) != bool(kwargs), \
|
||||
'keyword arguments are mutually exclusive with positional args'
|
||||
|
||||
def _check_kwargs(self, **kwargs):
|
||||
if kwargs:
|
||||
assert set(kwargs.keys()) == self.supp_kwargs, \
|
||||
'following set of keyword args is required: %s' % (
|
||||
self.supp_kwargs)
|
||||
return kwargs
|
||||
|
||||
def _fmt_kwargs(self, **kwargs):
|
||||
"""Format kwargs before printing them.
|
||||
|
||||
Resulting dictionary has to have keys necessary for str.format call
|
||||
on fmt class variable.
|
||||
"""
|
||||
fmtargs = {}
|
||||
for kw, data in kwargs.items():
|
||||
if isinstance(data, (list, set)):
|
||||
# convert list of <someobj> to list of str(<someobj>)
|
||||
fmtargs[kw] = list(map(str, data))
|
||||
if len(fmtargs[kw]) == 1:
|
||||
# remove list brackets [] from single-item lists
|
||||
fmtargs[kw] = fmtargs[kw].pop()
|
||||
else:
|
||||
fmtargs[kw] = data
|
||||
return fmtargs
|
||||
|
||||
def __str__(self):
|
||||
if self.kwargs and self.fmt:
|
||||
# provide custom message constructed from keyword arguments
|
||||
fmtargs = self._fmt_kwargs(**self.kwargs)
|
||||
return self.fmt.format(**fmtargs)
|
||||
else:
|
||||
# print *args directly in the same way as old DNSException
|
||||
return super(DNSException, self).__str__()
|
||||
|
||||
|
||||
class FormError(DNSException):
|
||||
"""DNS message is malformed."""
|
||||
|
||||
|
||||
class SyntaxError(DNSException):
|
||||
"""Text input is malformed."""
|
||||
|
||||
|
||||
class UnexpectedEnd(SyntaxError):
|
||||
"""Text input ended unexpectedly."""
|
||||
|
||||
|
||||
class TooBig(DNSException):
|
||||
"""The DNS message is too big."""
|
||||
|
||||
|
||||
class Timeout(DNSException):
|
||||
"""The DNS operation timed out."""
|
||||
supp_kwargs = {'timeout'}
|
||||
fmt = "The DNS operation timed out after {timeout} seconds"
|
||||
9
src/dns/exception.pyi
Normal file
9
src/dns/exception.pyi
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Set, Optional, Dict
|
||||
|
||||
class DNSException(Exception):
|
||||
supp_kwargs : Set[str]
|
||||
kwargs : Optional[Dict]
|
||||
|
||||
class SyntaxError(DNSException): ...
|
||||
class FormError(DNSException): ...
|
||||
class Timeout(DNSException): ...
|
||||
130
src/dns/flags.py
Normal file
130
src/dns/flags.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Message Flags."""
|
||||
|
||||
# Standard DNS flags
|
||||
|
||||
#: Query Response
|
||||
QR = 0x8000
|
||||
#: Authoritative Answer
|
||||
AA = 0x0400
|
||||
#: Truncated Response
|
||||
TC = 0x0200
|
||||
#: Recursion Desired
|
||||
RD = 0x0100
|
||||
#: Recursion Available
|
||||
RA = 0x0080
|
||||
#: Authentic Data
|
||||
AD = 0x0020
|
||||
#: Checking Disabled
|
||||
CD = 0x0010
|
||||
|
||||
# EDNS flags
|
||||
|
||||
#: DNSSEC answer OK
|
||||
DO = 0x8000
|
||||
|
||||
_by_text = {
|
||||
'QR': QR,
|
||||
'AA': AA,
|
||||
'TC': TC,
|
||||
'RD': RD,
|
||||
'RA': RA,
|
||||
'AD': AD,
|
||||
'CD': CD
|
||||
}
|
||||
|
||||
_edns_by_text = {
|
||||
'DO': DO
|
||||
}
|
||||
|
||||
|
||||
# We construct the inverse mappings programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mappings not to be true inverses.
|
||||
|
||||
_by_value = {y: x for x, y in _by_text.items()}
|
||||
|
||||
_edns_by_value = {y: x for x, y in _edns_by_text.items()}
|
||||
|
||||
|
||||
def _order_flags(table):
|
||||
order = list(table.items())
|
||||
order.sort()
|
||||
order.reverse()
|
||||
return order
|
||||
|
||||
_flags_order = _order_flags(_by_value)
|
||||
|
||||
_edns_flags_order = _order_flags(_edns_by_value)
|
||||
|
||||
|
||||
def _from_text(text, table):
|
||||
flags = 0
|
||||
tokens = text.split()
|
||||
for t in tokens:
|
||||
flags = flags | table[t.upper()]
|
||||
return flags
|
||||
|
||||
|
||||
def _to_text(flags, table, order):
|
||||
text_flags = []
|
||||
for k, v in order:
|
||||
if flags & k != 0:
|
||||
text_flags.append(v)
|
||||
return ' '.join(text_flags)
|
||||
|
||||
|
||||
def from_text(text):
|
||||
"""Convert a space-separated list of flag text values into a flags
|
||||
value.
|
||||
|
||||
Returns an ``int``
|
||||
"""
|
||||
|
||||
return _from_text(text, _by_text)
|
||||
|
||||
|
||||
def to_text(flags):
|
||||
"""Convert a flags value into a space-separated list of flag text
|
||||
values.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
return _to_text(flags, _by_value, _flags_order)
|
||||
|
||||
|
||||
def edns_from_text(text):
|
||||
"""Convert a space-separated list of EDNS flag text values into a EDNS
|
||||
flags value.
|
||||
|
||||
Returns an ``int``
|
||||
"""
|
||||
|
||||
return _from_text(text, _edns_by_text)
|
||||
|
||||
|
||||
def edns_to_text(flags):
|
||||
"""Convert an EDNS flags value into a space-separated list of EDNS flag
|
||||
text values.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
return _to_text(flags, _edns_by_value, _edns_flags_order)
|
||||
69
src/dns/grange.py
Normal file
69
src/dns/grange.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2012-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS GENERATE range conversion."""
|
||||
|
||||
import dns
|
||||
|
||||
def from_text(text):
|
||||
"""Convert the text form of a range in a ``$GENERATE`` statement to an
|
||||
integer.
|
||||
|
||||
*text*, a ``str``, the textual range in ``$GENERATE`` form.
|
||||
|
||||
Returns a tuple of three ``int`` values ``(start, stop, step)``.
|
||||
"""
|
||||
|
||||
# TODO, figure out the bounds on start, stop and step.
|
||||
step = 1
|
||||
cur = ''
|
||||
state = 0
|
||||
# state 0 1 2 3 4
|
||||
# x - y / z
|
||||
|
||||
if text and text[0] == '-':
|
||||
raise dns.exception.SyntaxError("Start cannot be a negative number")
|
||||
|
||||
for c in text:
|
||||
if c == '-' and state == 0:
|
||||
start = int(cur)
|
||||
cur = ''
|
||||
state = 2
|
||||
elif c == '/':
|
||||
stop = int(cur)
|
||||
cur = ''
|
||||
state = 4
|
||||
elif c.isdigit():
|
||||
cur += c
|
||||
else:
|
||||
raise dns.exception.SyntaxError("Could not parse %s" % (c))
|
||||
|
||||
if state in (1, 3):
|
||||
raise dns.exception.SyntaxError()
|
||||
|
||||
if state == 2:
|
||||
stop = int(cur)
|
||||
|
||||
if state == 4:
|
||||
step = int(cur)
|
||||
|
||||
assert step >= 1
|
||||
assert start >= 0
|
||||
assert start <= stop
|
||||
# TODO, can start == stop?
|
||||
|
||||
return (start, stop, step)
|
||||
37
src/dns/hash.py
Normal file
37
src/dns/hash.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""Hashing backwards compatibility wrapper"""
|
||||
|
||||
import hashlib
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"dns.hash module will be removed in future versions. Please use hashlib instead.",
|
||||
DeprecationWarning)
|
||||
|
||||
hashes = {}
|
||||
hashes['MD5'] = hashlib.md5
|
||||
hashes['SHA1'] = hashlib.sha1
|
||||
hashes['SHA224'] = hashlib.sha224
|
||||
hashes['SHA256'] = hashlib.sha256
|
||||
hashes['SHA384'] = hashlib.sha384
|
||||
hashes['SHA512'] = hashlib.sha512
|
||||
|
||||
|
||||
def get(algorithm):
|
||||
return hashes[algorithm.upper()]
|
||||
124
src/dns/inet.py
Normal file
124
src/dns/inet.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""Generic Internet address helper functions."""
|
||||
|
||||
import socket
|
||||
|
||||
import dns.ipv4
|
||||
import dns.ipv6
|
||||
|
||||
from ._compat import maybe_ord
|
||||
|
||||
# We assume that AF_INET is always defined.
|
||||
|
||||
AF_INET = socket.AF_INET
|
||||
|
||||
# AF_INET6 might not be defined in the socket module, but we need it.
|
||||
# We'll try to use the socket module's value, and if it doesn't work,
|
||||
# we'll use our own value.
|
||||
|
||||
try:
|
||||
AF_INET6 = socket.AF_INET6
|
||||
except AttributeError:
|
||||
AF_INET6 = 9999
|
||||
|
||||
|
||||
def inet_pton(family, text):
|
||||
"""Convert the textual form of a network address into its binary form.
|
||||
|
||||
*family* is an ``int``, the address family.
|
||||
|
||||
*text* is a ``text``, the textual address.
|
||||
|
||||
Raises ``NotImplementedError`` if the address family specified is not
|
||||
implemented.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
if family == AF_INET:
|
||||
return dns.ipv4.inet_aton(text)
|
||||
elif family == AF_INET6:
|
||||
return dns.ipv6.inet_aton(text)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def inet_ntop(family, address):
|
||||
"""Convert the binary form of a network address into its textual form.
|
||||
|
||||
*family* is an ``int``, the address family.
|
||||
|
||||
*address* is a ``binary``, the network address in binary form.
|
||||
|
||||
Raises ``NotImplementedError`` if the address family specified is not
|
||||
implemented.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if family == AF_INET:
|
||||
return dns.ipv4.inet_ntoa(address)
|
||||
elif family == AF_INET6:
|
||||
return dns.ipv6.inet_ntoa(address)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def af_for_address(text):
|
||||
"""Determine the address family of a textual-form network address.
|
||||
|
||||
*text*, a ``text``, the textual address.
|
||||
|
||||
Raises ``ValueError`` if the address family cannot be determined
|
||||
from the input.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
try:
|
||||
dns.ipv4.inet_aton(text)
|
||||
return AF_INET
|
||||
except Exception:
|
||||
try:
|
||||
dns.ipv6.inet_aton(text)
|
||||
return AF_INET6
|
||||
except:
|
||||
raise ValueError
|
||||
|
||||
|
||||
def is_multicast(text):
|
||||
"""Is the textual-form network address a multicast address?
|
||||
|
||||
*text*, a ``text``, the textual address.
|
||||
|
||||
Raises ``ValueError`` if the address family cannot be determined
|
||||
from the input.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
try:
|
||||
first = maybe_ord(dns.ipv4.inet_aton(text)[0])
|
||||
return first >= 224 and first <= 239
|
||||
except Exception:
|
||||
try:
|
||||
first = maybe_ord(dns.ipv6.inet_aton(text)[0])
|
||||
return first == 255
|
||||
except Exception:
|
||||
raise ValueError
|
||||
4
src/dns/inet.pyi
Normal file
4
src/dns/inet.pyi
Normal file
@@ -0,0 +1,4 @@
|
||||
from typing import Union
|
||||
from socket import AddressFamily
|
||||
|
||||
AF_INET6 : Union[int, AddressFamily]
|
||||
63
src/dns/ipv4.py
Normal file
63
src/dns/ipv4.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""IPv4 helper functions."""
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
from ._compat import binary_type
|
||||
|
||||
def inet_ntoa(address):
|
||||
"""Convert an IPv4 address in binary form to text form.
|
||||
|
||||
*address*, a ``binary``, the IPv4 address in binary form.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if len(address) != 4:
|
||||
raise dns.exception.SyntaxError
|
||||
if not isinstance(address, bytearray):
|
||||
address = bytearray(address)
|
||||
return ('%u.%u.%u.%u' % (address[0], address[1],
|
||||
address[2], address[3]))
|
||||
|
||||
def inet_aton(text):
|
||||
"""Convert an IPv4 address in text form to binary form.
|
||||
|
||||
*text*, a ``text``, the IPv4 address in textual form.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
if not isinstance(text, binary_type):
|
||||
text = text.encode()
|
||||
parts = text.split(b'.')
|
||||
if len(parts) != 4:
|
||||
raise dns.exception.SyntaxError
|
||||
for part in parts:
|
||||
if not part.isdigit():
|
||||
raise dns.exception.SyntaxError
|
||||
if len(part) > 1 and part[0] == '0':
|
||||
# No leading zeros
|
||||
raise dns.exception.SyntaxError
|
||||
try:
|
||||
bytes = [int(part) for part in parts]
|
||||
return struct.pack('BBBB', *bytes)
|
||||
except:
|
||||
raise dns.exception.SyntaxError
|
||||
181
src/dns/ipv6.py
Normal file
181
src/dns/ipv6.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""IPv6 helper functions."""
|
||||
|
||||
import re
|
||||
import binascii
|
||||
|
||||
import dns.exception
|
||||
import dns.ipv4
|
||||
from ._compat import xrange, binary_type, maybe_decode
|
||||
|
||||
_leading_zero = re.compile(r'0+([0-9a-f]+)')
|
||||
|
||||
def inet_ntoa(address):
|
||||
"""Convert an IPv6 address in binary form to text form.
|
||||
|
||||
*address*, a ``binary``, the IPv6 address in binary form.
|
||||
|
||||
Raises ``ValueError`` if the address isn't 16 bytes long.
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if len(address) != 16:
|
||||
raise ValueError("IPv6 addresses are 16 bytes long")
|
||||
hex = binascii.hexlify(address)
|
||||
chunks = []
|
||||
i = 0
|
||||
l = len(hex)
|
||||
while i < l:
|
||||
chunk = maybe_decode(hex[i : i + 4])
|
||||
# strip leading zeros. we do this with an re instead of
|
||||
# with lstrip() because lstrip() didn't support chars until
|
||||
# python 2.2.2
|
||||
m = _leading_zero.match(chunk)
|
||||
if not m is None:
|
||||
chunk = m.group(1)
|
||||
chunks.append(chunk)
|
||||
i += 4
|
||||
#
|
||||
# Compress the longest subsequence of 0-value chunks to ::
|
||||
#
|
||||
best_start = 0
|
||||
best_len = 0
|
||||
start = -1
|
||||
last_was_zero = False
|
||||
for i in xrange(8):
|
||||
if chunks[i] != '0':
|
||||
if last_was_zero:
|
||||
end = i
|
||||
current_len = end - start
|
||||
if current_len > best_len:
|
||||
best_start = start
|
||||
best_len = current_len
|
||||
last_was_zero = False
|
||||
elif not last_was_zero:
|
||||
start = i
|
||||
last_was_zero = True
|
||||
if last_was_zero:
|
||||
end = 8
|
||||
current_len = end - start
|
||||
if current_len > best_len:
|
||||
best_start = start
|
||||
best_len = current_len
|
||||
if best_len > 1:
|
||||
if best_start == 0 and \
|
||||
(best_len == 6 or
|
||||
best_len == 5 and chunks[5] == 'ffff'):
|
||||
# We have an embedded IPv4 address
|
||||
if best_len == 6:
|
||||
prefix = '::'
|
||||
else:
|
||||
prefix = '::ffff:'
|
||||
hex = prefix + dns.ipv4.inet_ntoa(address[12:])
|
||||
else:
|
||||
hex = ':'.join(chunks[:best_start]) + '::' + \
|
||||
':'.join(chunks[best_start + best_len:])
|
||||
else:
|
||||
hex = ':'.join(chunks)
|
||||
return hex
|
||||
|
||||
_v4_ending = re.compile(br'(.*):(\d+\.\d+\.\d+\.\d+)$')
|
||||
_colon_colon_start = re.compile(br'::.*')
|
||||
_colon_colon_end = re.compile(br'.*::$')
|
||||
|
||||
def inet_aton(text):
|
||||
"""Convert an IPv6 address in text form to binary form.
|
||||
|
||||
*text*, a ``text``, the IPv6 address in textual form.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
#
|
||||
# Our aim here is not something fast; we just want something that works.
|
||||
#
|
||||
if not isinstance(text, binary_type):
|
||||
text = text.encode()
|
||||
|
||||
if text == b'::':
|
||||
text = b'0::'
|
||||
#
|
||||
# Get rid of the icky dot-quad syntax if we have it.
|
||||
#
|
||||
m = _v4_ending.match(text)
|
||||
if not m is None:
|
||||
b = bytearray(dns.ipv4.inet_aton(m.group(2)))
|
||||
text = (u"{}:{:02x}{:02x}:{:02x}{:02x}".format(m.group(1).decode(),
|
||||
b[0], b[1], b[2],
|
||||
b[3])).encode()
|
||||
#
|
||||
# Try to turn '::<whatever>' into ':<whatever>'; if no match try to
|
||||
# turn '<whatever>::' into '<whatever>:'
|
||||
#
|
||||
m = _colon_colon_start.match(text)
|
||||
if not m is None:
|
||||
text = text[1:]
|
||||
else:
|
||||
m = _colon_colon_end.match(text)
|
||||
if not m is None:
|
||||
text = text[:-1]
|
||||
#
|
||||
# Now canonicalize into 8 chunks of 4 hex digits each
|
||||
#
|
||||
chunks = text.split(b':')
|
||||
l = len(chunks)
|
||||
if l > 8:
|
||||
raise dns.exception.SyntaxError
|
||||
seen_empty = False
|
||||
canonical = []
|
||||
for c in chunks:
|
||||
if c == b'':
|
||||
if seen_empty:
|
||||
raise dns.exception.SyntaxError
|
||||
seen_empty = True
|
||||
for i in xrange(0, 8 - l + 1):
|
||||
canonical.append(b'0000')
|
||||
else:
|
||||
lc = len(c)
|
||||
if lc > 4:
|
||||
raise dns.exception.SyntaxError
|
||||
if lc != 4:
|
||||
c = (b'0' * (4 - lc)) + c
|
||||
canonical.append(c)
|
||||
if l < 8 and not seen_empty:
|
||||
raise dns.exception.SyntaxError
|
||||
text = b''.join(canonical)
|
||||
|
||||
#
|
||||
# Finally we can go to binary.
|
||||
#
|
||||
try:
|
||||
return binascii.unhexlify(text)
|
||||
except (binascii.Error, TypeError):
|
||||
raise dns.exception.SyntaxError
|
||||
|
||||
_mapped_prefix = b'\x00' * 10 + b'\xff\xff'
|
||||
|
||||
def is_mapped(address):
|
||||
"""Is the specified address a mapped IPv4 address?
|
||||
|
||||
*address*, a ``binary`` is an IPv6 address in binary form.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
return address.startswith(_mapped_prefix)
|
||||
1175
src/dns/message.py
Normal file
1175
src/dns/message.py
Normal file
File diff suppressed because it is too large
Load Diff
55
src/dns/message.pyi
Normal file
55
src/dns/message.pyi
Normal file
@@ -0,0 +1,55 @@
|
||||
from typing import Optional, Dict, List, Tuple, Union
|
||||
from . import name, rrset, tsig, rdatatype, entropy, edns, rdataclass
|
||||
import hmac
|
||||
|
||||
class Message:
|
||||
def to_wire(self, origin : Optional[name.Name]=None, max_size=0, **kw) -> bytes:
|
||||
...
|
||||
def find_rrset(self, section : List[rrset.RRset], name : name.Name, rdclass : int, rdtype : int,
|
||||
covers=rdatatype.NONE, deleting : Optional[int]=None, create=False,
|
||||
force_unique=False) -> rrset.RRset:
|
||||
...
|
||||
def __init__(self, id : Optional[int] =None) -> None:
|
||||
self.id : int
|
||||
self.flags = 0
|
||||
self.question : List[rrset.RRset] = []
|
||||
self.answer : List[rrset.RRset] = []
|
||||
self.authority : List[rrset.RRset] = []
|
||||
self.additional : List[rrset.RRset] = []
|
||||
self.edns = -1
|
||||
self.ednsflags = 0
|
||||
self.payload = 0
|
||||
self.options : List[edns.Option] = []
|
||||
self.request_payload = 0
|
||||
self.keyring = None
|
||||
self.keyname = None
|
||||
self.keyalgorithm = tsig.default_algorithm
|
||||
self.request_mac = b''
|
||||
self.other_data = b''
|
||||
self.tsig_error = 0
|
||||
self.fudge = 300
|
||||
self.original_id = self.id
|
||||
self.mac = b''
|
||||
self.xfr = False
|
||||
self.origin = None
|
||||
self.tsig_ctx = None
|
||||
self.had_tsig = False
|
||||
self.multi = False
|
||||
self.first = True
|
||||
self.index : Dict[Tuple[rrset.RRset, name.Name, int, int, Union[int,str], int], rrset.RRset] = {}
|
||||
def from_text(a : str) -> Message:
|
||||
...
|
||||
|
||||
def from_wire(wire, keyring : Optional[Dict[name.Name,bytes]] = None, request_mac = b'', xfr=False, origin=None,
|
||||
tsig_ctx : Optional[hmac.HMAC] = None, multi=False, first=True,
|
||||
question_only=False, one_rr_per_rrset=False,
|
||||
ignore_trailing=False) -> Message:
|
||||
...
|
||||
def make_response(query : Message, recursion_available=False, our_payload=8192,
|
||||
fudge=300) -> Message:
|
||||
...
|
||||
|
||||
def make_query(qname : Union[name.Name,str], rdtype : Union[str,int], rdclass : Union[int,str] =rdataclass.IN, use_edns : Optional[bool] = None,
|
||||
want_dnssec=False, ednsflags : Optional[int] = None, payload : Optional[int] = None,
|
||||
request_payload : Optional[int] = None, options : Optional[List[edns.Option]] = None) -> Message:
|
||||
...
|
||||
994
src/dns/name.py
Normal file
994
src/dns/name.py
Normal file
@@ -0,0 +1,994 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Names.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
import struct
|
||||
import sys
|
||||
import copy
|
||||
import encodings.idna
|
||||
try:
|
||||
import idna
|
||||
have_idna_2008 = True
|
||||
except ImportError:
|
||||
have_idna_2008 = False
|
||||
|
||||
import dns.exception
|
||||
import dns.wiredata
|
||||
|
||||
from ._compat import long, binary_type, text_type, unichr, maybe_decode
|
||||
|
||||
try:
|
||||
maxint = sys.maxint # pylint: disable=sys-max-int
|
||||
except AttributeError:
|
||||
maxint = (1 << (8 * struct.calcsize("P"))) // 2 - 1
|
||||
|
||||
|
||||
# fullcompare() result values
|
||||
|
||||
#: The compared names have no relationship to each other.
|
||||
NAMERELN_NONE = 0
|
||||
#: the first name is a superdomain of the second.
|
||||
NAMERELN_SUPERDOMAIN = 1
|
||||
#: The first name is a subdomain of the second.
|
||||
NAMERELN_SUBDOMAIN = 2
|
||||
#: The compared names are equal.
|
||||
NAMERELN_EQUAL = 3
|
||||
#: The compared names have a common ancestor.
|
||||
NAMERELN_COMMONANCESTOR = 4
|
||||
|
||||
|
||||
class EmptyLabel(dns.exception.SyntaxError):
|
||||
"""A DNS label is empty."""
|
||||
|
||||
|
||||
class BadEscape(dns.exception.SyntaxError):
|
||||
"""An escaped code in a text format of DNS name is invalid."""
|
||||
|
||||
|
||||
class BadPointer(dns.exception.FormError):
|
||||
"""A DNS compression pointer points forward instead of backward."""
|
||||
|
||||
|
||||
class BadLabelType(dns.exception.FormError):
|
||||
"""The label type in DNS name wire format is unknown."""
|
||||
|
||||
|
||||
class NeedAbsoluteNameOrOrigin(dns.exception.DNSException):
|
||||
"""An attempt was made to convert a non-absolute name to
|
||||
wire when there was also a non-absolute (or missing) origin."""
|
||||
|
||||
|
||||
class NameTooLong(dns.exception.FormError):
|
||||
"""A DNS name is > 255 octets long."""
|
||||
|
||||
|
||||
class LabelTooLong(dns.exception.SyntaxError):
|
||||
"""A DNS label is > 63 octets long."""
|
||||
|
||||
|
||||
class AbsoluteConcatenation(dns.exception.DNSException):
|
||||
"""An attempt was made to append anything other than the
|
||||
empty name to an absolute DNS name."""
|
||||
|
||||
|
||||
class NoParent(dns.exception.DNSException):
|
||||
"""An attempt was made to get the parent of the root name
|
||||
or the empty name."""
|
||||
|
||||
class NoIDNA2008(dns.exception.DNSException):
|
||||
"""IDNA 2008 processing was requested but the idna module is not
|
||||
available."""
|
||||
|
||||
|
||||
class IDNAException(dns.exception.DNSException):
|
||||
"""IDNA processing raised an exception."""
|
||||
|
||||
supp_kwargs = {'idna_exception'}
|
||||
fmt = "IDNA processing exception: {idna_exception}"
|
||||
|
||||
|
||||
class IDNACodec(object):
|
||||
"""Abstract base class for IDNA encoder/decoders."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def encode(self, label):
|
||||
raise NotImplementedError
|
||||
|
||||
def decode(self, label):
|
||||
# We do not apply any IDNA policy on decode; we just
|
||||
downcased = label.lower()
|
||||
if downcased.startswith(b'xn--'):
|
||||
try:
|
||||
label = downcased[4:].decode('punycode')
|
||||
except Exception as e:
|
||||
raise IDNAException(idna_exception=e)
|
||||
else:
|
||||
label = maybe_decode(label)
|
||||
return _escapify(label, True)
|
||||
|
||||
|
||||
class IDNA2003Codec(IDNACodec):
|
||||
"""IDNA 2003 encoder/decoder."""
|
||||
|
||||
def __init__(self, strict_decode=False):
|
||||
"""Initialize the IDNA 2003 encoder/decoder.
|
||||
|
||||
*strict_decode* is a ``bool``. If `True`, then IDNA2003 checking
|
||||
is done when decoding. This can cause failures if the name
|
||||
was encoded with IDNA2008. The default is `False`.
|
||||
"""
|
||||
|
||||
super(IDNA2003Codec, self).__init__()
|
||||
self.strict_decode = strict_decode
|
||||
|
||||
def encode(self, label):
|
||||
"""Encode *label*."""
|
||||
|
||||
if label == '':
|
||||
return b''
|
||||
try:
|
||||
return encodings.idna.ToASCII(label)
|
||||
except UnicodeError:
|
||||
raise LabelTooLong
|
||||
|
||||
def decode(self, label):
|
||||
"""Decode *label*."""
|
||||
if not self.strict_decode:
|
||||
return super(IDNA2003Codec, self).decode(label)
|
||||
if label == b'':
|
||||
return u''
|
||||
try:
|
||||
return _escapify(encodings.idna.ToUnicode(label), True)
|
||||
except Exception as e:
|
||||
raise IDNAException(idna_exception=e)
|
||||
|
||||
|
||||
class IDNA2008Codec(IDNACodec):
|
||||
"""IDNA 2008 encoder/decoder.
|
||||
|
||||
*uts_46* is a ``bool``. If True, apply Unicode IDNA
|
||||
compatibility processing as described in Unicode Technical
|
||||
Standard #46 (http://unicode.org/reports/tr46/).
|
||||
If False, do not apply the mapping. The default is False.
|
||||
|
||||
*transitional* is a ``bool``: If True, use the
|
||||
"transitional" mode described in Unicode Technical Standard
|
||||
#46. The default is False.
|
||||
|
||||
*allow_pure_ascii* is a ``bool``. If True, then a label which
|
||||
consists of only ASCII characters is allowed. This is less
|
||||
strict than regular IDNA 2008, but is also necessary for mixed
|
||||
names, e.g. a name with starting with "_sip._tcp." and ending
|
||||
in an IDN suffix which would otherwise be disallowed. The
|
||||
default is False.
|
||||
|
||||
*strict_decode* is a ``bool``: If True, then IDNA2008 checking
|
||||
is done when decoding. This can cause failures if the name
|
||||
was encoded with IDNA2003. The default is False.
|
||||
"""
|
||||
|
||||
def __init__(self, uts_46=False, transitional=False,
|
||||
allow_pure_ascii=False, strict_decode=False):
|
||||
"""Initialize the IDNA 2008 encoder/decoder."""
|
||||
super(IDNA2008Codec, self).__init__()
|
||||
self.uts_46 = uts_46
|
||||
self.transitional = transitional
|
||||
self.allow_pure_ascii = allow_pure_ascii
|
||||
self.strict_decode = strict_decode
|
||||
|
||||
def is_all_ascii(self, label):
|
||||
for c in label:
|
||||
if ord(c) > 0x7f:
|
||||
return False
|
||||
return True
|
||||
|
||||
def encode(self, label):
|
||||
if label == '':
|
||||
return b''
|
||||
if self.allow_pure_ascii and self.is_all_ascii(label):
|
||||
return label.encode('ascii')
|
||||
if not have_idna_2008:
|
||||
raise NoIDNA2008
|
||||
try:
|
||||
if self.uts_46:
|
||||
label = idna.uts46_remap(label, False, self.transitional)
|
||||
return idna.alabel(label)
|
||||
except idna.IDNAError as e:
|
||||
raise IDNAException(idna_exception=e)
|
||||
|
||||
def decode(self, label):
|
||||
if not self.strict_decode:
|
||||
return super(IDNA2008Codec, self).decode(label)
|
||||
if label == b'':
|
||||
return u''
|
||||
if not have_idna_2008:
|
||||
raise NoIDNA2008
|
||||
try:
|
||||
if self.uts_46:
|
||||
label = idna.uts46_remap(label, False, False)
|
||||
return _escapify(idna.ulabel(label), True)
|
||||
except idna.IDNAError as e:
|
||||
raise IDNAException(idna_exception=e)
|
||||
|
||||
_escaped = bytearray(b'"().;\\@$')
|
||||
|
||||
IDNA_2003_Practical = IDNA2003Codec(False)
|
||||
IDNA_2003_Strict = IDNA2003Codec(True)
|
||||
IDNA_2003 = IDNA_2003_Practical
|
||||
IDNA_2008_Practical = IDNA2008Codec(True, False, True, False)
|
||||
IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False)
|
||||
IDNA_2008_Strict = IDNA2008Codec(False, False, False, True)
|
||||
IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False)
|
||||
IDNA_2008 = IDNA_2008_Practical
|
||||
|
||||
def _escapify(label, unicode_mode=False):
|
||||
"""Escape the characters in label which need it.
|
||||
@param unicode_mode: escapify only special and whitespace (<= 0x20)
|
||||
characters
|
||||
@returns: the escaped string
|
||||
@rtype: string"""
|
||||
if not unicode_mode:
|
||||
text = ''
|
||||
if isinstance(label, text_type):
|
||||
label = label.encode()
|
||||
for c in bytearray(label):
|
||||
if c in _escaped:
|
||||
text += '\\' + chr(c)
|
||||
elif c > 0x20 and c < 0x7F:
|
||||
text += chr(c)
|
||||
else:
|
||||
text += '\\%03d' % c
|
||||
return text.encode()
|
||||
|
||||
text = u''
|
||||
if isinstance(label, binary_type):
|
||||
label = label.decode()
|
||||
for c in label:
|
||||
if c > u'\x20' and c < u'\x7f':
|
||||
text += c
|
||||
else:
|
||||
if c >= u'\x7f':
|
||||
text += c
|
||||
else:
|
||||
text += u'\\%03d' % ord(c)
|
||||
return text
|
||||
|
||||
def _validate_labels(labels):
|
||||
"""Check for empty labels in the middle of a label sequence,
|
||||
labels that are too long, and for too many labels.
|
||||
|
||||
Raises ``dns.name.NameTooLong`` if the name as a whole is too long.
|
||||
|
||||
Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root
|
||||
label) and appears in a position other than the end of the label
|
||||
sequence
|
||||
|
||||
"""
|
||||
|
||||
l = len(labels)
|
||||
total = 0
|
||||
i = -1
|
||||
j = 0
|
||||
for label in labels:
|
||||
ll = len(label)
|
||||
total += ll + 1
|
||||
if ll > 63:
|
||||
raise LabelTooLong
|
||||
if i < 0 and label == b'':
|
||||
i = j
|
||||
j += 1
|
||||
if total > 255:
|
||||
raise NameTooLong
|
||||
if i >= 0 and i != l - 1:
|
||||
raise EmptyLabel
|
||||
|
||||
|
||||
def _maybe_convert_to_binary(label):
|
||||
"""If label is ``text``, convert it to ``binary``. If it is already
|
||||
``binary`` just return it.
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(label, binary_type):
|
||||
return label
|
||||
if isinstance(label, text_type):
|
||||
return label.encode()
|
||||
raise ValueError
|
||||
|
||||
|
||||
class Name(object):
|
||||
|
||||
"""A DNS name.
|
||||
|
||||
The dns.name.Name class represents a DNS name as a tuple of
|
||||
labels. Each label is a `binary` in DNS wire format. Instances
|
||||
of the class are immutable.
|
||||
"""
|
||||
|
||||
__slots__ = ['labels']
|
||||
|
||||
def __init__(self, labels):
|
||||
"""*labels* is any iterable whose values are ``text`` or ``binary``.
|
||||
"""
|
||||
|
||||
labels = [_maybe_convert_to_binary(x) for x in labels]
|
||||
super(Name, self).__setattr__('labels', tuple(labels))
|
||||
_validate_labels(self.labels)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# Names are immutable
|
||||
raise TypeError("object doesn't support attribute assignment")
|
||||
|
||||
def __copy__(self):
|
||||
return Name(self.labels)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return Name(copy.deepcopy(self.labels, memo))
|
||||
|
||||
def __getstate__(self):
|
||||
# Names can be pickled
|
||||
return {'labels': self.labels}
|
||||
|
||||
def __setstate__(self, state):
|
||||
super(Name, self).__setattr__('labels', state['labels'])
|
||||
_validate_labels(self.labels)
|
||||
|
||||
def is_absolute(self):
|
||||
"""Is the most significant label of this name the root label?
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
return len(self.labels) > 0 and self.labels[-1] == b''
|
||||
|
||||
def is_wild(self):
|
||||
"""Is this name wild? (I.e. Is the least significant label '*'?)
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
return len(self.labels) > 0 and self.labels[0] == b'*'
|
||||
|
||||
def __hash__(self):
|
||||
"""Return a case-insensitive hash of the name.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
h = long(0)
|
||||
for label in self.labels:
|
||||
for c in bytearray(label.lower()):
|
||||
h += (h << 3) + c
|
||||
return int(h % maxint)
|
||||
|
||||
def fullcompare(self, other):
|
||||
"""Compare two names, returning a 3-tuple
|
||||
``(relation, order, nlabels)``.
|
||||
|
||||
*relation* describes the relation ship between the names,
|
||||
and is one of: ``dns.name.NAMERELN_NONE``,
|
||||
``dns.name.NAMERELN_SUPERDOMAIN``, ``dns.name.NAMERELN_SUBDOMAIN``,
|
||||
``dns.name.NAMERELN_EQUAL``, or ``dns.name.NAMERELN_COMMONANCESTOR``.
|
||||
|
||||
*order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and ==
|
||||
0 if *self* == *other*. A relative name is always less than an
|
||||
absolute name. If both names have the same relativity, then
|
||||
the DNSSEC order relation is used to order them.
|
||||
|
||||
*nlabels* is the number of significant labels that the two names
|
||||
have in common.
|
||||
|
||||
Here are some examples. Names ending in "." are absolute names,
|
||||
those not ending in "." are relative names.
|
||||
|
||||
============= ============= =========== ===== =======
|
||||
self other relation order nlabels
|
||||
============= ============= =========== ===== =======
|
||||
www.example. www.example. equal 0 3
|
||||
www.example. example. subdomain > 0 2
|
||||
example. www.example. superdomain < 0 2
|
||||
example1.com. example2.com. common anc. < 0 2
|
||||
example1 example2. none < 0 0
|
||||
example1. example2 none > 0 0
|
||||
============= ============= =========== ===== =======
|
||||
"""
|
||||
|
||||
sabs = self.is_absolute()
|
||||
oabs = other.is_absolute()
|
||||
if sabs != oabs:
|
||||
if sabs:
|
||||
return (NAMERELN_NONE, 1, 0)
|
||||
else:
|
||||
return (NAMERELN_NONE, -1, 0)
|
||||
l1 = len(self.labels)
|
||||
l2 = len(other.labels)
|
||||
ldiff = l1 - l2
|
||||
if ldiff < 0:
|
||||
l = l1
|
||||
else:
|
||||
l = l2
|
||||
|
||||
order = 0
|
||||
nlabels = 0
|
||||
namereln = NAMERELN_NONE
|
||||
while l > 0:
|
||||
l -= 1
|
||||
l1 -= 1
|
||||
l2 -= 1
|
||||
label1 = self.labels[l1].lower()
|
||||
label2 = other.labels[l2].lower()
|
||||
if label1 < label2:
|
||||
order = -1
|
||||
if nlabels > 0:
|
||||
namereln = NAMERELN_COMMONANCESTOR
|
||||
return (namereln, order, nlabels)
|
||||
elif label1 > label2:
|
||||
order = 1
|
||||
if nlabels > 0:
|
||||
namereln = NAMERELN_COMMONANCESTOR
|
||||
return (namereln, order, nlabels)
|
||||
nlabels += 1
|
||||
order = ldiff
|
||||
if ldiff < 0:
|
||||
namereln = NAMERELN_SUPERDOMAIN
|
||||
elif ldiff > 0:
|
||||
namereln = NAMERELN_SUBDOMAIN
|
||||
else:
|
||||
namereln = NAMERELN_EQUAL
|
||||
return (namereln, order, nlabels)
|
||||
|
||||
def is_subdomain(self, other):
|
||||
"""Is self a subdomain of other?
|
||||
|
||||
Note that the notion of subdomain includes equality, e.g.
|
||||
"dnpython.org" is a subdomain of itself.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
(nr, o, nl) = self.fullcompare(other)
|
||||
if nr == NAMERELN_SUBDOMAIN or nr == NAMERELN_EQUAL:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_superdomain(self, other):
|
||||
"""Is self a superdomain of other?
|
||||
|
||||
Note that the notion of superdomain includes equality, e.g.
|
||||
"dnpython.org" is a superdomain of itself.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
(nr, o, nl) = self.fullcompare(other)
|
||||
if nr == NAMERELN_SUPERDOMAIN or nr == NAMERELN_EQUAL:
|
||||
return True
|
||||
return False
|
||||
|
||||
def canonicalize(self):
|
||||
"""Return a name which is equal to the current name, but is in
|
||||
DNSSEC canonical form.
|
||||
"""
|
||||
|
||||
return Name([x.lower() for x in self.labels])
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] == 0
|
||||
else:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] != 0
|
||||
else:
|
||||
return True
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] < 0
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] <= 0
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] >= 0
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, Name):
|
||||
return self.fullcompare(other)[1] > 0
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __repr__(self):
|
||||
return '<DNS name ' + self.__str__() + '>'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_text(False)
|
||||
|
||||
def to_text(self, omit_final_dot=False):
|
||||
"""Convert name to DNS text format.
|
||||
|
||||
*omit_final_dot* is a ``bool``. If True, don't emit the final
|
||||
dot (denoting the root label) for absolute names. The default
|
||||
is False.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if len(self.labels) == 0:
|
||||
return maybe_decode(b'@')
|
||||
if len(self.labels) == 1 and self.labels[0] == b'':
|
||||
return maybe_decode(b'.')
|
||||
if omit_final_dot and self.is_absolute():
|
||||
l = self.labels[:-1]
|
||||
else:
|
||||
l = self.labels
|
||||
s = b'.'.join(map(_escapify, l))
|
||||
return maybe_decode(s)
|
||||
|
||||
def to_unicode(self, omit_final_dot=False, idna_codec=None):
|
||||
"""Convert name to Unicode text format.
|
||||
|
||||
IDN ACE labels are converted to Unicode.
|
||||
|
||||
*omit_final_dot* is a ``bool``. If True, don't emit the final
|
||||
dot (denoting the root label) for absolute names. The default
|
||||
is False.
|
||||
*idna_codec* specifies the IDNA encoder/decoder. If None, the
|
||||
dns.name.IDNA_2003_Practical encoder/decoder is used.
|
||||
The IDNA_2003_Practical decoder does
|
||||
not impose any policy, it just decodes punycode, so if you
|
||||
don't want checking for compliance, you can use this decoder
|
||||
for IDNA2008 as well.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if len(self.labels) == 0:
|
||||
return u'@'
|
||||
if len(self.labels) == 1 and self.labels[0] == b'':
|
||||
return u'.'
|
||||
if omit_final_dot and self.is_absolute():
|
||||
l = self.labels[:-1]
|
||||
else:
|
||||
l = self.labels
|
||||
if idna_codec is None:
|
||||
idna_codec = IDNA_2003_Practical
|
||||
return u'.'.join([idna_codec.decode(x) for x in l])
|
||||
|
||||
def to_digestable(self, origin=None):
|
||||
"""Convert name to a format suitable for digesting in hashes.
|
||||
|
||||
The name is canonicalized and converted to uncompressed wire
|
||||
format. All names in wire format are absolute. If the name
|
||||
is a relative name, then an origin must be supplied.
|
||||
|
||||
*origin* is a ``dns.name.Name`` or ``None``. If the name is
|
||||
relative and origin is not ``None``, then origin will be appended
|
||||
to the name.
|
||||
|
||||
Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
|
||||
relative and no origin was provided.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
if not self.is_absolute():
|
||||
if origin is None or not origin.is_absolute():
|
||||
raise NeedAbsoluteNameOrOrigin
|
||||
labels = list(self.labels)
|
||||
labels.extend(list(origin.labels))
|
||||
else:
|
||||
labels = self.labels
|
||||
dlabels = [struct.pack('!B%ds' % len(x), len(x), x.lower())
|
||||
for x in labels]
|
||||
return b''.join(dlabels)
|
||||
|
||||
def to_wire(self, file=None, compress=None, origin=None):
|
||||
"""Convert name to wire format, possibly compressing it.
|
||||
|
||||
*file* is the file where the name is emitted (typically a
|
||||
BytesIO file). If ``None`` (the default), a ``binary``
|
||||
containing the wire name will be returned.
|
||||
|
||||
*compress*, a ``dict``, is the compression table to use. If
|
||||
``None`` (the default), names will not be compressed.
|
||||
|
||||
*origin* is a ``dns.name.Name`` or ``None``. If the name is
|
||||
relative and origin is not ``None``, then *origin* will be appended
|
||||
to it.
|
||||
|
||||
Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
|
||||
relative and no origin was provided.
|
||||
|
||||
Returns a ``binary`` or ``None``.
|
||||
"""
|
||||
|
||||
if file is None:
|
||||
file = BytesIO()
|
||||
want_return = True
|
||||
else:
|
||||
want_return = False
|
||||
|
||||
if not self.is_absolute():
|
||||
if origin is None or not origin.is_absolute():
|
||||
raise NeedAbsoluteNameOrOrigin
|
||||
labels = list(self.labels)
|
||||
labels.extend(list(origin.labels))
|
||||
else:
|
||||
labels = self.labels
|
||||
i = 0
|
||||
for label in labels:
|
||||
n = Name(labels[i:])
|
||||
i += 1
|
||||
if compress is not None:
|
||||
pos = compress.get(n)
|
||||
else:
|
||||
pos = None
|
||||
if pos is not None:
|
||||
value = 0xc000 + pos
|
||||
s = struct.pack('!H', value)
|
||||
file.write(s)
|
||||
break
|
||||
else:
|
||||
if compress is not None and len(n) > 1:
|
||||
pos = file.tell()
|
||||
if pos <= 0x3fff:
|
||||
compress[n] = pos
|
||||
l = len(label)
|
||||
file.write(struct.pack('!B', l))
|
||||
if l > 0:
|
||||
file.write(label)
|
||||
if want_return:
|
||||
return file.getvalue()
|
||||
|
||||
def __len__(self):
|
||||
"""The length of the name (in labels).
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
return len(self.labels)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.labels[index]
|
||||
|
||||
def __add__(self, other):
|
||||
return self.concatenate(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.relativize(other)
|
||||
|
||||
def split(self, depth):
|
||||
"""Split a name into a prefix and suffix names at the specified depth.
|
||||
|
||||
*depth* is an ``int`` specifying the number of labels in the suffix
|
||||
|
||||
Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the
|
||||
name.
|
||||
|
||||
Returns the tuple ``(prefix, suffix)``.
|
||||
"""
|
||||
|
||||
l = len(self.labels)
|
||||
if depth == 0:
|
||||
return (self, dns.name.empty)
|
||||
elif depth == l:
|
||||
return (dns.name.empty, self)
|
||||
elif depth < 0 or depth > l:
|
||||
raise ValueError(
|
||||
'depth must be >= 0 and <= the length of the name')
|
||||
return (Name(self[: -depth]), Name(self[-depth:]))
|
||||
|
||||
def concatenate(self, other):
|
||||
"""Return a new name which is the concatenation of self and other.
|
||||
|
||||
Raises ``dns.name.AbsoluteConcatenation`` if the name is
|
||||
absolute and *other* is not the empty name.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if self.is_absolute() and len(other) > 0:
|
||||
raise AbsoluteConcatenation
|
||||
labels = list(self.labels)
|
||||
labels.extend(list(other.labels))
|
||||
return Name(labels)
|
||||
|
||||
def relativize(self, origin):
|
||||
"""If the name is a subdomain of *origin*, return a new name which is
|
||||
the name relative to origin. Otherwise return the name.
|
||||
|
||||
For example, relativizing ``www.dnspython.org.`` to origin
|
||||
``dnspython.org.`` returns the name ``www``. Relativizing ``example.``
|
||||
to origin ``dnspython.org.`` returns ``example.``.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if origin is not None and self.is_subdomain(origin):
|
||||
return Name(self[: -len(origin)])
|
||||
else:
|
||||
return self
|
||||
|
||||
def derelativize(self, origin):
|
||||
"""If the name is a relative name, return a new name which is the
|
||||
concatenation of the name and origin. Otherwise return the name.
|
||||
|
||||
For example, derelativizing ``www`` to origin ``dnspython.org.``
|
||||
returns the name ``www.dnspython.org.``. Derelativizing ``example.``
|
||||
to origin ``dnspython.org.`` returns ``example.``.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if not self.is_absolute():
|
||||
return self.concatenate(origin)
|
||||
else:
|
||||
return self
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
"""Return a name with the relativity desired by the caller.
|
||||
|
||||
If *origin* is ``None``, then the name is returned.
|
||||
Otherwise, if *relativize* is ``True`` the name is
|
||||
relativized, and if *relativize* is ``False`` the name is
|
||||
derelativized.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if origin:
|
||||
if relativize:
|
||||
return self.relativize(origin)
|
||||
else:
|
||||
return self.derelativize(origin)
|
||||
else:
|
||||
return self
|
||||
|
||||
def parent(self):
|
||||
"""Return the parent of the name.
|
||||
|
||||
For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``.
|
||||
|
||||
Raises ``dns.name.NoParent`` if the name is either the root name or the
|
||||
empty name, and thus has no parent.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if self == root or self == empty:
|
||||
raise NoParent
|
||||
return Name(self.labels[1:])
|
||||
|
||||
#: The root name, '.'
|
||||
root = Name([b''])
|
||||
|
||||
#: The empty name.
|
||||
empty = Name([])
|
||||
|
||||
def from_unicode(text, origin=root, idna_codec=None):
|
||||
"""Convert unicode text into a Name object.
|
||||
|
||||
Labels are encoded in IDN ACE form according to rules specified by
|
||||
the IDNA codec.
|
||||
|
||||
*text*, a ``text``, is the text to convert into a name.
|
||||
|
||||
*origin*, a ``dns.name.Name``, specifies the origin to
|
||||
append to non-absolute names. The default is the root name.
|
||||
|
||||
*idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
|
||||
encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
|
||||
is used.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if not isinstance(text, text_type):
|
||||
raise ValueError("input to from_unicode() must be a unicode string")
|
||||
if not (origin is None or isinstance(origin, Name)):
|
||||
raise ValueError("origin must be a Name or None")
|
||||
labels = []
|
||||
label = u''
|
||||
escaping = False
|
||||
edigits = 0
|
||||
total = 0
|
||||
if idna_codec is None:
|
||||
idna_codec = IDNA_2003
|
||||
if text == u'@':
|
||||
text = u''
|
||||
if text:
|
||||
if text == u'.':
|
||||
return Name([b'']) # no Unicode "u" on this constant!
|
||||
for c in text:
|
||||
if escaping:
|
||||
if edigits == 0:
|
||||
if c.isdigit():
|
||||
total = int(c)
|
||||
edigits += 1
|
||||
else:
|
||||
label += c
|
||||
escaping = False
|
||||
else:
|
||||
if not c.isdigit():
|
||||
raise BadEscape
|
||||
total *= 10
|
||||
total += int(c)
|
||||
edigits += 1
|
||||
if edigits == 3:
|
||||
escaping = False
|
||||
label += unichr(total)
|
||||
elif c in [u'.', u'\u3002', u'\uff0e', u'\uff61']:
|
||||
if len(label) == 0:
|
||||
raise EmptyLabel
|
||||
labels.append(idna_codec.encode(label))
|
||||
label = u''
|
||||
elif c == u'\\':
|
||||
escaping = True
|
||||
edigits = 0
|
||||
total = 0
|
||||
else:
|
||||
label += c
|
||||
if escaping:
|
||||
raise BadEscape
|
||||
if len(label) > 0:
|
||||
labels.append(idna_codec.encode(label))
|
||||
else:
|
||||
labels.append(b'')
|
||||
|
||||
if (len(labels) == 0 or labels[-1] != b'') and origin is not None:
|
||||
labels.extend(list(origin.labels))
|
||||
return Name(labels)
|
||||
|
||||
|
||||
def from_text(text, origin=root, idna_codec=None):
|
||||
"""Convert text into a Name object.
|
||||
|
||||
*text*, a ``text``, is the text to convert into a name.
|
||||
|
||||
*origin*, a ``dns.name.Name``, specifies the origin to
|
||||
append to non-absolute names. The default is the root name.
|
||||
|
||||
*idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
|
||||
encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder
|
||||
is used.
|
||||
|
||||
Returns a ``dns.name.Name``.
|
||||
"""
|
||||
|
||||
if isinstance(text, text_type):
|
||||
return from_unicode(text, origin, idna_codec)
|
||||
if not isinstance(text, binary_type):
|
||||
raise ValueError("input to from_text() must be a string")
|
||||
if not (origin is None or isinstance(origin, Name)):
|
||||
raise ValueError("origin must be a Name or None")
|
||||
labels = []
|
||||
label = b''
|
||||
escaping = False
|
||||
edigits = 0
|
||||
total = 0
|
||||
if text == b'@':
|
||||
text = b''
|
||||
if text:
|
||||
if text == b'.':
|
||||
return Name([b''])
|
||||
for c in bytearray(text):
|
||||
byte_ = struct.pack('!B', c)
|
||||
if escaping:
|
||||
if edigits == 0:
|
||||
if byte_.isdigit():
|
||||
total = int(byte_)
|
||||
edigits += 1
|
||||
else:
|
||||
label += byte_
|
||||
escaping = False
|
||||
else:
|
||||
if not byte_.isdigit():
|
||||
raise BadEscape
|
||||
total *= 10
|
||||
total += int(byte_)
|
||||
edigits += 1
|
||||
if edigits == 3:
|
||||
escaping = False
|
||||
label += struct.pack('!B', total)
|
||||
elif byte_ == b'.':
|
||||
if len(label) == 0:
|
||||
raise EmptyLabel
|
||||
labels.append(label)
|
||||
label = b''
|
||||
elif byte_ == b'\\':
|
||||
escaping = True
|
||||
edigits = 0
|
||||
total = 0
|
||||
else:
|
||||
label += byte_
|
||||
if escaping:
|
||||
raise BadEscape
|
||||
if len(label) > 0:
|
||||
labels.append(label)
|
||||
else:
|
||||
labels.append(b'')
|
||||
if (len(labels) == 0 or labels[-1] != b'') and origin is not None:
|
||||
labels.extend(list(origin.labels))
|
||||
return Name(labels)
|
||||
|
||||
|
||||
def from_wire(message, current):
|
||||
"""Convert possibly compressed wire format into a Name.
|
||||
|
||||
*message* is a ``binary`` containing an entire DNS message in DNS
|
||||
wire form.
|
||||
|
||||
*current*, an ``int``, is the offset of the beginning of the name
|
||||
from the start of the message
|
||||
|
||||
Raises ``dns.name.BadPointer`` if a compression pointer did not
|
||||
point backwards in the message.
|
||||
|
||||
Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
|
||||
|
||||
Returns a ``(dns.name.Name, int)`` tuple consisting of the name
|
||||
that was read and the number of bytes of the wire format message
|
||||
which were consumed reading it.
|
||||
"""
|
||||
|
||||
if not isinstance(message, binary_type):
|
||||
raise ValueError("input to from_wire() must be a byte string")
|
||||
message = dns.wiredata.maybe_wrap(message)
|
||||
labels = []
|
||||
biggest_pointer = current
|
||||
hops = 0
|
||||
count = message[current]
|
||||
current += 1
|
||||
cused = 1
|
||||
while count != 0:
|
||||
if count < 64:
|
||||
labels.append(message[current: current + count].unwrap())
|
||||
current += count
|
||||
if hops == 0:
|
||||
cused += count
|
||||
elif count >= 192:
|
||||
current = (count & 0x3f) * 256 + message[current]
|
||||
if hops == 0:
|
||||
cused += 1
|
||||
if current >= biggest_pointer:
|
||||
raise BadPointer
|
||||
biggest_pointer = current
|
||||
hops += 1
|
||||
else:
|
||||
raise BadLabelType
|
||||
count = message[current]
|
||||
current += 1
|
||||
if hops == 0:
|
||||
cused += 1
|
||||
labels.append('')
|
||||
return (Name(labels), cused)
|
||||
35
src/dns/name.pyi
Normal file
35
src/dns/name.pyi
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Optional, Union, Tuple, Iterable, List
|
||||
|
||||
class Name:
|
||||
def is_subdomain(self, o : Name) -> bool: ...
|
||||
def is_superdomain(self, o : Name) -> bool: ...
|
||||
def __init__(self, labels : Iterable[Union[bytes,str]]) -> None:
|
||||
self.labels : List[bytes]
|
||||
def is_absolute(self) -> bool: ...
|
||||
def is_wild(self) -> bool: ...
|
||||
def fullcompare(self, other) -> Tuple[int,int,int]: ...
|
||||
def canonicalize(self) -> Name: ...
|
||||
def __lt__(self, other : Name): ...
|
||||
def __le__(self, other : Name): ...
|
||||
def __ge__(self, other : Name): ...
|
||||
def __gt__(self, other : Name): ...
|
||||
def to_text(self, omit_final_dot=False) -> str: ...
|
||||
def to_unicode(self, omit_final_dot=False, idna_codec=None) -> str: ...
|
||||
def to_digestable(self, origin=None) -> bytes: ...
|
||||
def to_wire(self, file=None, compress=None, origin=None) -> Optional[bytes]: ...
|
||||
def __add__(self, other : Name): ...
|
||||
def __sub__(self, other : Name): ...
|
||||
def split(self, depth) -> List[Tuple[str,str]]: ...
|
||||
def concatenate(self, other : Name) -> Name: ...
|
||||
def relativize(self, origin): ...
|
||||
def derelativize(self, origin): ...
|
||||
def choose_relativity(self, origin : Optional[Name] = None, relativize=True): ...
|
||||
def parent(self) -> Name: ...
|
||||
|
||||
class IDNACodec:
|
||||
pass
|
||||
|
||||
def from_text(text, origin : Optional[Name] = Name('.'), idna_codec : Optional[IDNACodec] = None) -> Name:
|
||||
...
|
||||
|
||||
empty : Name
|
||||
108
src/dns/namedict.py
Normal file
108
src/dns/namedict.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
# Copyright (C) 2016 Coresec Systems AB
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND CORESEC SYSTEMS AB DISCLAIMS ALL
|
||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CORESEC
|
||||
# SYSTEMS AB BE LIABLE FOR ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS name dictionary"""
|
||||
|
||||
import collections
|
||||
import dns.name
|
||||
from ._compat import xrange
|
||||
|
||||
|
||||
class NameDict(collections.MutableMapping):
|
||||
"""A dictionary whose keys are dns.name.Name objects.
|
||||
|
||||
In addition to being like a regular Python dictionary, this
|
||||
dictionary can also get the deepest match for a given key.
|
||||
"""
|
||||
|
||||
__slots__ = ["max_depth", "max_depth_items", "__store"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NameDict, self).__init__()
|
||||
self.__store = dict()
|
||||
#: the maximum depth of the keys that have ever been added
|
||||
self.max_depth = 0
|
||||
#: the number of items of maximum depth
|
||||
self.max_depth_items = 0
|
||||
self.update(dict(*args, **kwargs))
|
||||
|
||||
def __update_max_depth(self, key):
|
||||
if len(key) == self.max_depth:
|
||||
self.max_depth_items = self.max_depth_items + 1
|
||||
elif len(key) > self.max_depth:
|
||||
self.max_depth = len(key)
|
||||
self.max_depth_items = 1
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__store[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if not isinstance(key, dns.name.Name):
|
||||
raise ValueError('NameDict key must be a name')
|
||||
self.__store[key] = value
|
||||
self.__update_max_depth(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
value = self.__store.pop(key)
|
||||
if len(value) == self.max_depth:
|
||||
self.max_depth_items = self.max_depth_items - 1
|
||||
if self.max_depth_items == 0:
|
||||
self.max_depth = 0
|
||||
for k in self.__store:
|
||||
self.__update_max_depth(k)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__store)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__store)
|
||||
|
||||
def has_key(self, key):
|
||||
return key in self.__store
|
||||
|
||||
def get_deepest_match(self, name):
|
||||
"""Find the deepest match to *fname* in the dictionary.
|
||||
|
||||
The deepest match is the longest name in the dictionary which is
|
||||
a superdomain of *name*. Note that *superdomain* includes matching
|
||||
*name* itself.
|
||||
|
||||
*name*, a ``dns.name.Name``, the name to find.
|
||||
|
||||
Returns a ``(key, value)`` where *key* is the deepest
|
||||
``dns.name.Name``, and *value* is the value associated with *key*.
|
||||
"""
|
||||
|
||||
depth = len(name)
|
||||
if depth > self.max_depth:
|
||||
depth = self.max_depth
|
||||
for i in xrange(-depth, 0):
|
||||
n = dns.name.Name(name[i:])
|
||||
if n in self:
|
||||
return (n, self[n])
|
||||
v = self[dns.name.empty]
|
||||
return (dns.name.empty, v)
|
||||
182
src/dns/node.py
Normal file
182
src/dns/node.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS nodes. A node is a set of rdatasets."""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
import dns.rdataset
|
||||
import dns.rdatatype
|
||||
import dns.renderer
|
||||
|
||||
|
||||
class Node(object):
|
||||
|
||||
"""A Node is a set of rdatasets."""
|
||||
|
||||
__slots__ = ['rdatasets']
|
||||
|
||||
def __init__(self):
|
||||
#: the set of rdatsets, represented as a list.
|
||||
self.rdatasets = []
|
||||
|
||||
def to_text(self, name, **kw):
|
||||
"""Convert a node to text format.
|
||||
|
||||
Each rdataset at the node is printed. Any keyword arguments
|
||||
to this method are passed on to the rdataset's to_text() method.
|
||||
|
||||
*name*, a ``dns.name.Name`` or ``text``, the owner name of the rdatasets.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
s = StringIO()
|
||||
for rds in self.rdatasets:
|
||||
if len(rds) > 0:
|
||||
s.write(rds.to_text(name, **kw))
|
||||
s.write(u'\n')
|
||||
return s.getvalue()[:-1]
|
||||
|
||||
def __repr__(self):
|
||||
return '<DNS node ' + str(id(self)) + '>'
|
||||
|
||||
def __eq__(self, other):
|
||||
#
|
||||
# This is inefficient. Good thing we don't need to do it much.
|
||||
#
|
||||
for rd in self.rdatasets:
|
||||
if rd not in other.rdatasets:
|
||||
return False
|
||||
for rd in other.rdatasets:
|
||||
if rd not in self.rdatasets:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rdatasets)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rdatasets)
|
||||
|
||||
def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
|
||||
create=False):
|
||||
"""Find an rdataset matching the specified properties in the
|
||||
current node.
|
||||
|
||||
*rdclass*, an ``int``, the class of the rdataset.
|
||||
|
||||
*rdtype*, an ``int``, the type of the rdataset.
|
||||
|
||||
*covers*, an ``int``, the covered type. Usually this value is
|
||||
dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or
|
||||
dns.rdatatype.RRSIG, then the covers value will be the rdata
|
||||
type the SIG/RRSIG covers. The library treats the SIG and RRSIG
|
||||
types as if they were a family of
|
||||
types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much
|
||||
easier to work with than if RRSIGs covering different rdata
|
||||
types were aggregated into a single RRSIG rdataset.
|
||||
|
||||
*create*, a ``bool``. If True, create the rdataset if it is not found.
|
||||
|
||||
Raises ``KeyError`` if an rdataset of the desired type and class does
|
||||
not exist and *create* is not ``True``.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset``.
|
||||
"""
|
||||
|
||||
for rds in self.rdatasets:
|
||||
if rds.match(rdclass, rdtype, covers):
|
||||
return rds
|
||||
if not create:
|
||||
raise KeyError
|
||||
rds = dns.rdataset.Rdataset(rdclass, rdtype)
|
||||
self.rdatasets.append(rds)
|
||||
return rds
|
||||
|
||||
def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
|
||||
create=False):
|
||||
"""Get an rdataset matching the specified properties in the
|
||||
current node.
|
||||
|
||||
None is returned if an rdataset of the specified type and
|
||||
class does not exist and *create* is not ``True``.
|
||||
|
||||
*rdclass*, an ``int``, the class of the rdataset.
|
||||
|
||||
*rdtype*, an ``int``, the type of the rdataset.
|
||||
|
||||
*covers*, an ``int``, the covered type. Usually this value is
|
||||
dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or
|
||||
dns.rdatatype.RRSIG, then the covers value will be the rdata
|
||||
type the SIG/RRSIG covers. The library treats the SIG and RRSIG
|
||||
types as if they were a family of
|
||||
types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much
|
||||
easier to work with than if RRSIGs covering different rdata
|
||||
types were aggregated into a single RRSIG rdataset.
|
||||
|
||||
*create*, a ``bool``. If True, create the rdataset if it is not found.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset`` or ``None``.
|
||||
"""
|
||||
|
||||
try:
|
||||
rds = self.find_rdataset(rdclass, rdtype, covers, create)
|
||||
except KeyError:
|
||||
rds = None
|
||||
return rds
|
||||
|
||||
def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE):
|
||||
"""Delete the rdataset matching the specified properties in the
|
||||
current node.
|
||||
|
||||
If a matching rdataset does not exist, it is not an error.
|
||||
|
||||
*rdclass*, an ``int``, the class of the rdataset.
|
||||
|
||||
*rdtype*, an ``int``, the type of the rdataset.
|
||||
|
||||
*covers*, an ``int``, the covered type.
|
||||
"""
|
||||
|
||||
rds = self.get_rdataset(rdclass, rdtype, covers)
|
||||
if rds is not None:
|
||||
self.rdatasets.remove(rds)
|
||||
|
||||
def replace_rdataset(self, replacement):
|
||||
"""Replace an rdataset.
|
||||
|
||||
It is not an error if there is no rdataset matching *replacement*.
|
||||
|
||||
Ownership of the *replacement* object is transferred to the node;
|
||||
in other words, this method does not store a copy of *replacement*
|
||||
at the node, it stores *replacement* itself.
|
||||
|
||||
*replacement*, a ``dns.rdataset.Rdataset``.
|
||||
|
||||
Raises ``ValueError`` if *replacement* is not a
|
||||
``dns.rdataset.Rdataset``.
|
||||
"""
|
||||
|
||||
if not isinstance(replacement, dns.rdataset.Rdataset):
|
||||
raise ValueError('replacement is not an rdataset')
|
||||
self.delete_rdataset(replacement.rdclass, replacement.rdtype,
|
||||
replacement.covers)
|
||||
self.rdatasets.append(replacement)
|
||||
17
src/dns/node.pyi
Normal file
17
src/dns/node.pyi
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import List, Optional, Union
|
||||
from . import rdataset, rdatatype, name
|
||||
class Node:
|
||||
def __init__(self):
|
||||
self.rdatasets : List[rdataset.Rdataset]
|
||||
def to_text(self, name : Union[str,name.Name], **kw) -> str:
|
||||
...
|
||||
def find_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE,
|
||||
create=False) -> rdataset.Rdataset:
|
||||
...
|
||||
def get_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE,
|
||||
create=False) -> Optional[rdataset.Rdataset]:
|
||||
...
|
||||
def delete_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE):
|
||||
...
|
||||
def replace_rdataset(self, replacement : rdataset.Rdataset) -> None:
|
||||
...
|
||||
119
src/dns/opcode.py
Normal file
119
src/dns/opcode.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Opcodes."""
|
||||
|
||||
import dns.exception
|
||||
|
||||
#: Query
|
||||
QUERY = 0
|
||||
#: Inverse Query (historical)
|
||||
IQUERY = 1
|
||||
#: Server Status (unspecified and unimplemented anywhere)
|
||||
STATUS = 2
|
||||
#: Notify
|
||||
NOTIFY = 4
|
||||
#: Dynamic Update
|
||||
UPDATE = 5
|
||||
|
||||
_by_text = {
|
||||
'QUERY': QUERY,
|
||||
'IQUERY': IQUERY,
|
||||
'STATUS': STATUS,
|
||||
'NOTIFY': NOTIFY,
|
||||
'UPDATE': UPDATE
|
||||
}
|
||||
|
||||
# We construct the inverse mapping programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mapping not to be true inverse.
|
||||
|
||||
_by_value = {y: x for x, y in _by_text.items()}
|
||||
|
||||
|
||||
class UnknownOpcode(dns.exception.DNSException):
|
||||
"""An DNS opcode is unknown."""
|
||||
|
||||
|
||||
def from_text(text):
|
||||
"""Convert text into an opcode.
|
||||
|
||||
*text*, a ``text``, the textual opcode
|
||||
|
||||
Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
if text.isdigit():
|
||||
value = int(text)
|
||||
if value >= 0 and value <= 15:
|
||||
return value
|
||||
value = _by_text.get(text.upper())
|
||||
if value is None:
|
||||
raise UnknownOpcode
|
||||
return value
|
||||
|
||||
|
||||
def from_flags(flags):
|
||||
"""Extract an opcode from DNS message flags.
|
||||
|
||||
*flags*, an ``int``, the DNS flags.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
return (flags & 0x7800) >> 11
|
||||
|
||||
|
||||
def to_flags(value):
|
||||
"""Convert an opcode to a value suitable for ORing into DNS message
|
||||
flags.
|
||||
|
||||
*value*, an ``int``, the DNS opcode value.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
return (value << 11) & 0x7800
|
||||
|
||||
|
||||
def to_text(value):
|
||||
"""Convert an opcode to text.
|
||||
|
||||
*value*, an ``int`` the opcode value,
|
||||
|
||||
Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
text = _by_value.get(value)
|
||||
if text is None:
|
||||
text = str(value)
|
||||
return text
|
||||
|
||||
|
||||
def is_update(flags):
|
||||
"""Is the opcode in flags UPDATE?
|
||||
|
||||
*flags*, an ``int``, the DNS message flags.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
return from_flags(flags) == UPDATE
|
||||
0
src/dns/py.typed
Normal file
0
src/dns/py.typed
Normal file
683
src/dns/query.py
Normal file
683
src/dns/query.py
Normal file
@@ -0,0 +1,683 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""Talk to a DNS server."""
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
import errno
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import dns.exception
|
||||
import dns.inet
|
||||
import dns.name
|
||||
import dns.message
|
||||
import dns.rcode
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
from ._compat import long, string_types, PY3
|
||||
|
||||
if PY3:
|
||||
select_error = OSError
|
||||
else:
|
||||
select_error = select.error
|
||||
|
||||
# Function used to create a socket. Can be overridden if needed in special
|
||||
# situations.
|
||||
socket_factory = socket.socket
|
||||
|
||||
class UnexpectedSource(dns.exception.DNSException):
|
||||
"""A DNS query response came from an unexpected address or port."""
|
||||
|
||||
|
||||
class BadResponse(dns.exception.FormError):
|
||||
"""A DNS query response does not respond to the question asked."""
|
||||
|
||||
|
||||
class TransferError(dns.exception.DNSException):
|
||||
"""A zone transfer response got a non-zero rcode."""
|
||||
|
||||
def __init__(self, rcode):
|
||||
message = 'Zone transfer error: %s' % dns.rcode.to_text(rcode)
|
||||
super(TransferError, self).__init__(message)
|
||||
self.rcode = rcode
|
||||
|
||||
|
||||
def _compute_expiration(timeout):
|
||||
if timeout is None:
|
||||
return None
|
||||
else:
|
||||
return time.time() + timeout
|
||||
|
||||
# This module can use either poll() or select() as the "polling backend".
|
||||
#
|
||||
# A backend function takes an fd, bools for readability, writablity, and
|
||||
# error detection, and a timeout.
|
||||
|
||||
def _poll_for(fd, readable, writable, error, timeout):
|
||||
"""Poll polling backend."""
|
||||
|
||||
event_mask = 0
|
||||
if readable:
|
||||
event_mask |= select.POLLIN
|
||||
if writable:
|
||||
event_mask |= select.POLLOUT
|
||||
if error:
|
||||
event_mask |= select.POLLERR
|
||||
|
||||
pollable = select.poll()
|
||||
pollable.register(fd, event_mask)
|
||||
|
||||
if timeout:
|
||||
event_list = pollable.poll(long(timeout * 1000))
|
||||
else:
|
||||
event_list = pollable.poll()
|
||||
|
||||
return bool(event_list)
|
||||
|
||||
|
||||
def _select_for(fd, readable, writable, error, timeout):
|
||||
"""Select polling backend."""
|
||||
|
||||
rset, wset, xset = [], [], []
|
||||
|
||||
if readable:
|
||||
rset = [fd]
|
||||
if writable:
|
||||
wset = [fd]
|
||||
if error:
|
||||
xset = [fd]
|
||||
|
||||
if timeout is None:
|
||||
(rcount, wcount, xcount) = select.select(rset, wset, xset)
|
||||
else:
|
||||
(rcount, wcount, xcount) = select.select(rset, wset, xset, timeout)
|
||||
|
||||
return bool((rcount or wcount or xcount))
|
||||
|
||||
|
||||
def _wait_for(fd, readable, writable, error, expiration):
|
||||
# Use the selected polling backend to wait for any of the specified
|
||||
# events. An "expiration" absolute time is converted into a relative
|
||||
# timeout.
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
if expiration is None:
|
||||
timeout = None
|
||||
else:
|
||||
timeout = expiration - time.time()
|
||||
if timeout <= 0.0:
|
||||
raise dns.exception.Timeout
|
||||
try:
|
||||
if not _polling_backend(fd, readable, writable, error, timeout):
|
||||
raise dns.exception.Timeout
|
||||
except select_error as e:
|
||||
if e.args[0] != errno.EINTR:
|
||||
raise e
|
||||
done = True
|
||||
|
||||
|
||||
def _set_polling_backend(fn):
|
||||
# Internal API. Do not use.
|
||||
|
||||
global _polling_backend
|
||||
|
||||
_polling_backend = fn
|
||||
|
||||
if hasattr(select, 'poll'):
|
||||
# Prefer poll() on platforms that support it because it has no
|
||||
# limits on the maximum value of a file descriptor (plus it will
|
||||
# be more efficient for high values).
|
||||
_polling_backend = _poll_for
|
||||
else:
|
||||
_polling_backend = _select_for
|
||||
|
||||
|
||||
def _wait_for_readable(s, expiration):
|
||||
_wait_for(s, True, False, True, expiration)
|
||||
|
||||
|
||||
def _wait_for_writable(s, expiration):
|
||||
_wait_for(s, False, True, True, expiration)
|
||||
|
||||
|
||||
def _addresses_equal(af, a1, a2):
|
||||
# Convert the first value of the tuple, which is a textual format
|
||||
# address into binary form, so that we are not confused by different
|
||||
# textual representations of the same address
|
||||
try:
|
||||
n1 = dns.inet.inet_pton(af, a1[0])
|
||||
n2 = dns.inet.inet_pton(af, a2[0])
|
||||
except dns.exception.SyntaxError:
|
||||
return False
|
||||
return n1 == n2 and a1[1:] == a2[1:]
|
||||
|
||||
|
||||
def _destination_and_source(af, where, port, source, source_port):
|
||||
# Apply defaults and compute destination and source tuples
|
||||
# suitable for use in connect(), sendto(), or bind().
|
||||
if af is None:
|
||||
try:
|
||||
af = dns.inet.af_for_address(where)
|
||||
except Exception:
|
||||
af = dns.inet.AF_INET
|
||||
if af == dns.inet.AF_INET:
|
||||
destination = (where, port)
|
||||
if source is not None or source_port != 0:
|
||||
if source is None:
|
||||
source = '0.0.0.0'
|
||||
source = (source, source_port)
|
||||
elif af == dns.inet.AF_INET6:
|
||||
destination = (where, port, 0, 0)
|
||||
if source is not None or source_port != 0:
|
||||
if source is None:
|
||||
source = '::'
|
||||
source = (source, source_port, 0, 0)
|
||||
return (af, destination, source)
|
||||
|
||||
|
||||
def send_udp(sock, what, destination, expiration=None):
|
||||
"""Send a DNS message to the specified UDP socket.
|
||||
|
||||
*sock*, a ``socket``.
|
||||
|
||||
*what*, a ``binary`` or ``dns.message.Message``, the message to send.
|
||||
|
||||
*destination*, a destination tuple appropriate for the address family
|
||||
of the socket, specifying where to send the query.
|
||||
|
||||
*expiration*, a ``float`` or ``None``, the absolute time at which
|
||||
a timeout exception should be raised. If ``None``, no timeout will
|
||||
occur.
|
||||
|
||||
Returns an ``(int, float)`` tuple of bytes sent and the sent time.
|
||||
"""
|
||||
|
||||
if isinstance(what, dns.message.Message):
|
||||
what = what.to_wire()
|
||||
_wait_for_writable(sock, expiration)
|
||||
sent_time = time.time()
|
||||
n = sock.sendto(what, destination)
|
||||
return (n, sent_time)
|
||||
|
||||
|
||||
def receive_udp(sock, destination, expiration=None,
|
||||
ignore_unexpected=False, one_rr_per_rrset=False,
|
||||
keyring=None, request_mac=b'', ignore_trailing=False):
|
||||
"""Read a DNS message from a UDP socket.
|
||||
|
||||
*sock*, a ``socket``.
|
||||
|
||||
*destination*, a destination tuple appropriate for the address family
|
||||
of the socket, specifying where the associated query was sent.
|
||||
|
||||
*expiration*, a ``float`` or ``None``, the absolute time at which
|
||||
a timeout exception should be raised. If ``None``, no timeout will
|
||||
occur.
|
||||
|
||||
*ignore_unexpected*, a ``bool``. If ``True``, ignore responses from
|
||||
unexpected sources.
|
||||
|
||||
*one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
|
||||
RRset.
|
||||
|
||||
*keyring*, a ``dict``, the keyring to use for TSIG.
|
||||
|
||||
*request_mac*, a ``binary``, the MAC of the request (for TSIG).
|
||||
|
||||
*ignore_trailing*, a ``bool``. If ``True``, ignore trailing
|
||||
junk at end of the received message.
|
||||
|
||||
Raises if the message is malformed, if network errors occur, of if
|
||||
there is a timeout.
|
||||
|
||||
Returns a ``dns.message.Message`` object.
|
||||
"""
|
||||
|
||||
wire = b''
|
||||
while 1:
|
||||
_wait_for_readable(sock, expiration)
|
||||
(wire, from_address) = sock.recvfrom(65535)
|
||||
if _addresses_equal(sock.family, from_address, destination) or \
|
||||
(dns.inet.is_multicast(destination[0]) and
|
||||
from_address[1:] == destination[1:]):
|
||||
break
|
||||
if not ignore_unexpected:
|
||||
raise UnexpectedSource('got a response from '
|
||||
'%s instead of %s' % (from_address,
|
||||
destination))
|
||||
received_time = time.time()
|
||||
r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
|
||||
one_rr_per_rrset=one_rr_per_rrset,
|
||||
ignore_trailing=ignore_trailing)
|
||||
return (r, received_time)
|
||||
|
||||
def udp(q, where, timeout=None, port=53, af=None, source=None, source_port=0,
|
||||
ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False):
|
||||
"""Return the response obtained after sending a query via UDP.
|
||||
|
||||
*q*, a ``dns.message.Message``, the query to send
|
||||
|
||||
*where*, a ``text`` containing an IPv4 or IPv6 address, where
|
||||
to send the message.
|
||||
|
||||
*timeout*, a ``float`` or ``None``, the number of seconds to wait before the
|
||||
query times out. If ``None``, the default, wait forever.
|
||||
|
||||
*port*, an ``int``, the port send the message to. The default is 53.
|
||||
|
||||
*af*, an ``int``, the address family to use. The default is ``None``,
|
||||
which causes the address family to use to be inferred from the form of
|
||||
*where*. If the inference attempt fails, AF_INET is used. This
|
||||
parameter is historical; you need never set it.
|
||||
|
||||
*source*, a ``text`` containing an IPv4 or IPv6 address, specifying
|
||||
the source address. The default is the wildcard address.
|
||||
|
||||
*source_port*, an ``int``, the port from which to send the message.
|
||||
The default is 0.
|
||||
|
||||
*ignore_unexpected*, a ``bool``. If ``True``, ignore responses from
|
||||
unexpected sources.
|
||||
|
||||
*one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
|
||||
RRset.
|
||||
|
||||
*ignore_trailing*, a ``bool``. If ``True``, ignore trailing
|
||||
junk at end of the received message.
|
||||
|
||||
Returns a ``dns.message.Message``.
|
||||
"""
|
||||
|
||||
wire = q.to_wire()
|
||||
(af, destination, source) = _destination_and_source(af, where, port,
|
||||
source, source_port)
|
||||
s = socket_factory(af, socket.SOCK_DGRAM, 0)
|
||||
received_time = None
|
||||
sent_time = None
|
||||
try:
|
||||
expiration = _compute_expiration(timeout)
|
||||
s.setblocking(0)
|
||||
if source is not None:
|
||||
s.bind(source)
|
||||
(_, sent_time) = send_udp(s, wire, destination, expiration)
|
||||
(r, received_time) = receive_udp(s, destination, expiration,
|
||||
ignore_unexpected, one_rr_per_rrset,
|
||||
q.keyring, q.mac, ignore_trailing)
|
||||
finally:
|
||||
if sent_time is None or received_time is None:
|
||||
response_time = 0
|
||||
else:
|
||||
response_time = received_time - sent_time
|
||||
s.close()
|
||||
r.time = response_time
|
||||
if not q.is_response(r):
|
||||
raise BadResponse
|
||||
return r
|
||||
|
||||
|
||||
def _net_read(sock, count, expiration):
|
||||
"""Read the specified number of bytes from sock. Keep trying until we
|
||||
either get the desired amount, or we hit EOF.
|
||||
A Timeout exception will be raised if the operation is not completed
|
||||
by the expiration time.
|
||||
"""
|
||||
s = b''
|
||||
while count > 0:
|
||||
_wait_for_readable(sock, expiration)
|
||||
n = sock.recv(count)
|
||||
if n == b'':
|
||||
raise EOFError
|
||||
count = count - len(n)
|
||||
s = s + n
|
||||
return s
|
||||
|
||||
|
||||
def _net_write(sock, data, expiration):
|
||||
"""Write the specified data to the socket.
|
||||
A Timeout exception will be raised if the operation is not completed
|
||||
by the expiration time.
|
||||
"""
|
||||
current = 0
|
||||
l = len(data)
|
||||
while current < l:
|
||||
_wait_for_writable(sock, expiration)
|
||||
current += sock.send(data[current:])
|
||||
|
||||
|
||||
def send_tcp(sock, what, expiration=None):
|
||||
"""Send a DNS message to the specified TCP socket.
|
||||
|
||||
*sock*, a ``socket``.
|
||||
|
||||
*what*, a ``binary`` or ``dns.message.Message``, the message to send.
|
||||
|
||||
*expiration*, a ``float`` or ``None``, the absolute time at which
|
||||
a timeout exception should be raised. If ``None``, no timeout will
|
||||
occur.
|
||||
|
||||
Returns an ``(int, float)`` tuple of bytes sent and the sent time.
|
||||
"""
|
||||
|
||||
if isinstance(what, dns.message.Message):
|
||||
what = what.to_wire()
|
||||
l = len(what)
|
||||
# copying the wire into tcpmsg is inefficient, but lets us
|
||||
# avoid writev() or doing a short write that would get pushed
|
||||
# onto the net
|
||||
tcpmsg = struct.pack("!H", l) + what
|
||||
_wait_for_writable(sock, expiration)
|
||||
sent_time = time.time()
|
||||
_net_write(sock, tcpmsg, expiration)
|
||||
return (len(tcpmsg), sent_time)
|
||||
|
||||
def receive_tcp(sock, expiration=None, one_rr_per_rrset=False,
|
||||
keyring=None, request_mac=b'', ignore_trailing=False):
|
||||
"""Read a DNS message from a TCP socket.
|
||||
|
||||
*sock*, a ``socket``.
|
||||
|
||||
*expiration*, a ``float`` or ``None``, the absolute time at which
|
||||
a timeout exception should be raised. If ``None``, no timeout will
|
||||
occur.
|
||||
|
||||
*one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
|
||||
RRset.
|
||||
|
||||
*keyring*, a ``dict``, the keyring to use for TSIG.
|
||||
|
||||
*request_mac*, a ``binary``, the MAC of the request (for TSIG).
|
||||
|
||||
*ignore_trailing*, a ``bool``. If ``True``, ignore trailing
|
||||
junk at end of the received message.
|
||||
|
||||
Raises if the message is malformed, if network errors occur, of if
|
||||
there is a timeout.
|
||||
|
||||
Returns a ``dns.message.Message`` object.
|
||||
"""
|
||||
|
||||
ldata = _net_read(sock, 2, expiration)
|
||||
(l,) = struct.unpack("!H", ldata)
|
||||
wire = _net_read(sock, l, expiration)
|
||||
received_time = time.time()
|
||||
r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
|
||||
one_rr_per_rrset=one_rr_per_rrset,
|
||||
ignore_trailing=ignore_trailing)
|
||||
return (r, received_time)
|
||||
|
||||
def _connect(s, address):
|
||||
try:
|
||||
s.connect(address)
|
||||
except socket.error:
|
||||
(ty, v) = sys.exc_info()[:2]
|
||||
|
||||
if hasattr(v, 'errno'):
|
||||
v_err = v.errno
|
||||
else:
|
||||
v_err = v[0]
|
||||
if v_err not in [errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY]:
|
||||
raise v
|
||||
|
||||
|
||||
def tcp(q, where, timeout=None, port=53, af=None, source=None, source_port=0,
|
||||
one_rr_per_rrset=False, ignore_trailing=False):
|
||||
"""Return the response obtained after sending a query via TCP.
|
||||
|
||||
*q*, a ``dns.message.Message``, the query to send
|
||||
|
||||
*where*, a ``text`` containing an IPv4 or IPv6 address, where
|
||||
to send the message.
|
||||
|
||||
*timeout*, a ``float`` or ``None``, the number of seconds to wait before the
|
||||
query times out. If ``None``, the default, wait forever.
|
||||
|
||||
*port*, an ``int``, the port send the message to. The default is 53.
|
||||
|
||||
*af*, an ``int``, the address family to use. The default is ``None``,
|
||||
which causes the address family to use to be inferred from the form of
|
||||
*where*. If the inference attempt fails, AF_INET is used. This
|
||||
parameter is historical; you need never set it.
|
||||
|
||||
*source*, a ``text`` containing an IPv4 or IPv6 address, specifying
|
||||
the source address. The default is the wildcard address.
|
||||
|
||||
*source_port*, an ``int``, the port from which to send the message.
|
||||
The default is 0.
|
||||
|
||||
*one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
|
||||
RRset.
|
||||
|
||||
*ignore_trailing*, a ``bool``. If ``True``, ignore trailing
|
||||
junk at end of the received message.
|
||||
|
||||
Returns a ``dns.message.Message``.
|
||||
"""
|
||||
|
||||
wire = q.to_wire()
|
||||
(af, destination, source) = _destination_and_source(af, where, port,
|
||||
source, source_port)
|
||||
s = socket_factory(af, socket.SOCK_STREAM, 0)
|
||||
begin_time = None
|
||||
received_time = None
|
||||
try:
|
||||
expiration = _compute_expiration(timeout)
|
||||
s.setblocking(0)
|
||||
begin_time = time.time()
|
||||
if source is not None:
|
||||
s.bind(source)
|
||||
_connect(s, destination)
|
||||
send_tcp(s, wire, expiration)
|
||||
(r, received_time) = receive_tcp(s, expiration, one_rr_per_rrset,
|
||||
q.keyring, q.mac, ignore_trailing)
|
||||
finally:
|
||||
if begin_time is None or received_time is None:
|
||||
response_time = 0
|
||||
else:
|
||||
response_time = received_time - begin_time
|
||||
s.close()
|
||||
r.time = response_time
|
||||
if not q.is_response(r):
|
||||
raise BadResponse
|
||||
return r
|
||||
|
||||
|
||||
def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN,
|
||||
timeout=None, port=53, keyring=None, keyname=None, relativize=True,
|
||||
af=None, lifetime=None, source=None, source_port=0, serial=0,
|
||||
use_udp=False, keyalgorithm=dns.tsig.default_algorithm):
|
||||
"""Return a generator for the responses to a zone transfer.
|
||||
|
||||
*where*. If the inference attempt fails, AF_INET is used. This
|
||||
parameter is historical; you need never set it.
|
||||
|
||||
*zone*, a ``dns.name.Name`` or ``text``, the name of the zone to transfer.
|
||||
|
||||
*rdtype*, an ``int`` or ``text``, the type of zone transfer. The
|
||||
default is ``dns.rdatatype.AXFR``. ``dns.rdatatype.IXFR`` can be
|
||||
used to do an incremental transfer instead.
|
||||
|
||||
*rdclass*, an ``int`` or ``text``, the class of the zone transfer.
|
||||
The default is ``dns.rdataclass.IN``.
|
||||
|
||||
*timeout*, a ``float``, the number of seconds to wait for each
|
||||
response message. If None, the default, wait forever.
|
||||
|
||||
*port*, an ``int``, the port send the message to. The default is 53.
|
||||
|
||||
*keyring*, a ``dict``, the keyring to use for TSIG.
|
||||
|
||||
*keyname*, a ``dns.name.Name`` or ``text``, the name of the TSIG
|
||||
key to use.
|
||||
|
||||
*relativize*, a ``bool``. If ``True``, all names in the zone will be
|
||||
relativized to the zone origin. It is essential that the
|
||||
relativize setting matches the one specified to
|
||||
``dns.zone.from_xfr()`` if using this generator to make a zone.
|
||||
|
||||
*af*, an ``int``, the address family to use. The default is ``None``,
|
||||
which causes the address family to use to be inferred from the form of
|
||||
*where*. If the inference attempt fails, AF_INET is used. This
|
||||
parameter is historical; you need never set it.
|
||||
|
||||
*lifetime*, a ``float``, the total number of seconds to spend
|
||||
doing the transfer. If ``None``, the default, then there is no
|
||||
limit on the time the transfer may take.
|
||||
|
||||
*source*, a ``text`` containing an IPv4 or IPv6 address, specifying
|
||||
the source address. The default is the wildcard address.
|
||||
|
||||
*source_port*, an ``int``, the port from which to send the message.
|
||||
The default is 0.
|
||||
|
||||
*serial*, an ``int``, the SOA serial number to use as the base for
|
||||
an IXFR diff sequence (only meaningful if *rdtype* is
|
||||
``dns.rdatatype.IXFR``).
|
||||
|
||||
*use_udp*, a ``bool``. If ``True``, use UDP (only meaningful for IXFR).
|
||||
|
||||
*keyalgorithm*, a ``dns.name.Name`` or ``text``, the TSIG algorithm to use.
|
||||
|
||||
Raises on errors, and so does the generator.
|
||||
|
||||
Returns a generator of ``dns.message.Message`` objects.
|
||||
"""
|
||||
|
||||
if isinstance(zone, string_types):
|
||||
zone = dns.name.from_text(zone)
|
||||
if isinstance(rdtype, string_types):
|
||||
rdtype = dns.rdatatype.from_text(rdtype)
|
||||
q = dns.message.make_query(zone, rdtype, rdclass)
|
||||
if rdtype == dns.rdatatype.IXFR:
|
||||
rrset = dns.rrset.from_text(zone, 0, 'IN', 'SOA',
|
||||
'. . %u 0 0 0 0' % serial)
|
||||
q.authority.append(rrset)
|
||||
if keyring is not None:
|
||||
q.use_tsig(keyring, keyname, algorithm=keyalgorithm)
|
||||
wire = q.to_wire()
|
||||
(af, destination, source) = _destination_and_source(af, where, port,
|
||||
source, source_port)
|
||||
if use_udp:
|
||||
if rdtype != dns.rdatatype.IXFR:
|
||||
raise ValueError('cannot do a UDP AXFR')
|
||||
s = socket_factory(af, socket.SOCK_DGRAM, 0)
|
||||
else:
|
||||
s = socket_factory(af, socket.SOCK_STREAM, 0)
|
||||
s.setblocking(0)
|
||||
if source is not None:
|
||||
s.bind(source)
|
||||
expiration = _compute_expiration(lifetime)
|
||||
_connect(s, destination)
|
||||
l = len(wire)
|
||||
if use_udp:
|
||||
_wait_for_writable(s, expiration)
|
||||
s.send(wire)
|
||||
else:
|
||||
tcpmsg = struct.pack("!H", l) + wire
|
||||
_net_write(s, tcpmsg, expiration)
|
||||
done = False
|
||||
delete_mode = True
|
||||
expecting_SOA = False
|
||||
soa_rrset = None
|
||||
if relativize:
|
||||
origin = zone
|
||||
oname = dns.name.empty
|
||||
else:
|
||||
origin = None
|
||||
oname = zone
|
||||
tsig_ctx = None
|
||||
first = True
|
||||
while not done:
|
||||
mexpiration = _compute_expiration(timeout)
|
||||
if mexpiration is None or mexpiration > expiration:
|
||||
mexpiration = expiration
|
||||
if use_udp:
|
||||
_wait_for_readable(s, expiration)
|
||||
(wire, from_address) = s.recvfrom(65535)
|
||||
else:
|
||||
ldata = _net_read(s, 2, mexpiration)
|
||||
(l,) = struct.unpack("!H", ldata)
|
||||
wire = _net_read(s, l, mexpiration)
|
||||
is_ixfr = (rdtype == dns.rdatatype.IXFR)
|
||||
r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac,
|
||||
xfr=True, origin=origin, tsig_ctx=tsig_ctx,
|
||||
multi=True, first=first,
|
||||
one_rr_per_rrset=is_ixfr)
|
||||
rcode = r.rcode()
|
||||
if rcode != dns.rcode.NOERROR:
|
||||
raise TransferError(rcode)
|
||||
tsig_ctx = r.tsig_ctx
|
||||
first = False
|
||||
answer_index = 0
|
||||
if soa_rrset is None:
|
||||
if not r.answer or r.answer[0].name != oname:
|
||||
raise dns.exception.FormError(
|
||||
"No answer or RRset not for qname")
|
||||
rrset = r.answer[0]
|
||||
if rrset.rdtype != dns.rdatatype.SOA:
|
||||
raise dns.exception.FormError("first RRset is not an SOA")
|
||||
answer_index = 1
|
||||
soa_rrset = rrset.copy()
|
||||
if rdtype == dns.rdatatype.IXFR:
|
||||
if soa_rrset[0].serial <= serial:
|
||||
#
|
||||
# We're already up-to-date.
|
||||
#
|
||||
done = True
|
||||
else:
|
||||
expecting_SOA = True
|
||||
#
|
||||
# Process SOAs in the answer section (other than the initial
|
||||
# SOA in the first message).
|
||||
#
|
||||
for rrset in r.answer[answer_index:]:
|
||||
if done:
|
||||
raise dns.exception.FormError("answers after final SOA")
|
||||
if rrset.rdtype == dns.rdatatype.SOA and rrset.name == oname:
|
||||
if expecting_SOA:
|
||||
if rrset[0].serial != serial:
|
||||
raise dns.exception.FormError(
|
||||
"IXFR base serial mismatch")
|
||||
expecting_SOA = False
|
||||
elif rdtype == dns.rdatatype.IXFR:
|
||||
delete_mode = not delete_mode
|
||||
#
|
||||
# If this SOA RRset is equal to the first we saw then we're
|
||||
# finished. If this is an IXFR we also check that we're seeing
|
||||
# the record in the expected part of the response.
|
||||
#
|
||||
if rrset == soa_rrset and \
|
||||
(rdtype == dns.rdatatype.AXFR or
|
||||
(rdtype == dns.rdatatype.IXFR and delete_mode)):
|
||||
done = True
|
||||
elif expecting_SOA:
|
||||
#
|
||||
# We made an IXFR request and are expecting another
|
||||
# SOA RR, but saw something else, so this must be an
|
||||
# AXFR response.
|
||||
#
|
||||
rdtype = dns.rdatatype.AXFR
|
||||
expecting_SOA = False
|
||||
if done and q.keyring and not r.had_tsig:
|
||||
raise dns.exception.FormError("missing TSIG")
|
||||
yield r
|
||||
s.close()
|
||||
15
src/dns/query.pyi
Normal file
15
src/dns/query.pyi
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import Optional, Union, Dict, Generator, Any
|
||||
from . import message, tsig, rdatatype, rdataclass, name, message
|
||||
def tcp(q : message.Message, where : str, timeout : float = None, port=53, af : Optional[int] = None, source : Optional[str] = None, source_port : int = 0,
|
||||
one_rr_per_rrset=False) -> message.Message:
|
||||
pass
|
||||
|
||||
def xfr(where : None, zone : Union[name.Name,str], rdtype=rdatatype.AXFR, rdclass=rdataclass.IN,
|
||||
timeout : Optional[float] =None, port=53, keyring : Optional[Dict[name.Name, bytes]] =None, keyname : Union[str,name.Name]=None, relativize=True,
|
||||
af : Optional[int] =None, lifetime : Optional[float]=None, source : Optional[str] =None, source_port=0, serial=0,
|
||||
use_udp=False, keyalgorithm=tsig.default_algorithm) -> Generator[Any,Any,message.Message]:
|
||||
pass
|
||||
|
||||
def udp(q : message.Message, where : str, timeout : Optional[float] = None, port=53, af : Optional[int] = None, source : Optional[str] = None, source_port=0,
|
||||
ignore_unexpected=False, one_rr_per_rrset=False) -> message.Message:
|
||||
...
|
||||
144
src/dns/rcode.py
Normal file
144
src/dns/rcode.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Result Codes."""
|
||||
|
||||
import dns.exception
|
||||
from ._compat import long
|
||||
|
||||
#: No error
|
||||
NOERROR = 0
|
||||
#: Form error
|
||||
FORMERR = 1
|
||||
#: Server failure
|
||||
SERVFAIL = 2
|
||||
#: Name does not exist ("Name Error" in RFC 1025 terminology).
|
||||
NXDOMAIN = 3
|
||||
#: Not implemented
|
||||
NOTIMP = 4
|
||||
#: Refused
|
||||
REFUSED = 5
|
||||
#: Name exists.
|
||||
YXDOMAIN = 6
|
||||
#: RRset exists.
|
||||
YXRRSET = 7
|
||||
#: RRset does not exist.
|
||||
NXRRSET = 8
|
||||
#: Not authoritative.
|
||||
NOTAUTH = 9
|
||||
#: Name not in zone.
|
||||
NOTZONE = 10
|
||||
#: Bad EDNS version.
|
||||
BADVERS = 16
|
||||
|
||||
_by_text = {
|
||||
'NOERROR': NOERROR,
|
||||
'FORMERR': FORMERR,
|
||||
'SERVFAIL': SERVFAIL,
|
||||
'NXDOMAIN': NXDOMAIN,
|
||||
'NOTIMP': NOTIMP,
|
||||
'REFUSED': REFUSED,
|
||||
'YXDOMAIN': YXDOMAIN,
|
||||
'YXRRSET': YXRRSET,
|
||||
'NXRRSET': NXRRSET,
|
||||
'NOTAUTH': NOTAUTH,
|
||||
'NOTZONE': NOTZONE,
|
||||
'BADVERS': BADVERS
|
||||
}
|
||||
|
||||
# We construct the inverse mapping programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mapping not to be a true inverse.
|
||||
|
||||
_by_value = {y: x for x, y in _by_text.items()}
|
||||
|
||||
|
||||
class UnknownRcode(dns.exception.DNSException):
|
||||
"""A DNS rcode is unknown."""
|
||||
|
||||
|
||||
def from_text(text):
|
||||
"""Convert text into an rcode.
|
||||
|
||||
*text*, a ``text``, the textual rcode or an integer in textual form.
|
||||
|
||||
Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
if text.isdigit():
|
||||
v = int(text)
|
||||
if v >= 0 and v <= 4095:
|
||||
return v
|
||||
v = _by_text.get(text.upper())
|
||||
if v is None:
|
||||
raise UnknownRcode
|
||||
return v
|
||||
|
||||
|
||||
def from_flags(flags, ednsflags):
|
||||
"""Return the rcode value encoded by flags and ednsflags.
|
||||
|
||||
*flags*, an ``int``, the DNS flags field.
|
||||
|
||||
*ednsflags*, an ``int``, the EDNS flags field.
|
||||
|
||||
Raises ``ValueError`` if rcode is < 0 or > 4095
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
value = (flags & 0x000f) | ((ednsflags >> 20) & 0xff0)
|
||||
if value < 0 or value > 4095:
|
||||
raise ValueError('rcode must be >= 0 and <= 4095')
|
||||
return value
|
||||
|
||||
|
||||
def to_flags(value):
|
||||
"""Return a (flags, ednsflags) tuple which encodes the rcode.
|
||||
|
||||
*value*, an ``int``, the rcode.
|
||||
|
||||
Raises ``ValueError`` if rcode is < 0 or > 4095.
|
||||
|
||||
Returns an ``(int, int)`` tuple.
|
||||
"""
|
||||
|
||||
if value < 0 or value > 4095:
|
||||
raise ValueError('rcode must be >= 0 and <= 4095')
|
||||
v = value & 0xf
|
||||
ev = long(value & 0xff0) << 20
|
||||
return (v, ev)
|
||||
|
||||
|
||||
def to_text(value):
|
||||
"""Convert rcode into text.
|
||||
|
||||
*value*, and ``int``, the rcode.
|
||||
|
||||
Raises ``ValueError`` if rcode is < 0 or > 4095.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
if value < 0 or value > 4095:
|
||||
raise ValueError('rcode must be >= 0 and <= 4095')
|
||||
text = _by_value.get(value)
|
||||
if text is None:
|
||||
text = str(value)
|
||||
return text
|
||||
456
src/dns/rdata.py
Normal file
456
src/dns/rdata.py
Normal file
@@ -0,0 +1,456 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS rdata."""
|
||||
|
||||
from io import BytesIO
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.tokenizer
|
||||
import dns.wiredata
|
||||
from ._compat import xrange, string_types, text_type
|
||||
|
||||
try:
|
||||
import threading as _threading
|
||||
except ImportError:
|
||||
import dummy_threading as _threading
|
||||
|
||||
_hex_chunksize = 32
|
||||
|
||||
|
||||
def _hexify(data, chunksize=_hex_chunksize):
|
||||
"""Convert a binary string into its hex encoding, broken up into chunks
|
||||
of chunksize characters separated by a space.
|
||||
"""
|
||||
|
||||
line = binascii.hexlify(data)
|
||||
return b' '.join([line[i:i + chunksize]
|
||||
for i
|
||||
in range(0, len(line), chunksize)]).decode()
|
||||
|
||||
_base64_chunksize = 32
|
||||
|
||||
|
||||
def _base64ify(data, chunksize=_base64_chunksize):
|
||||
"""Convert a binary string into its base64 encoding, broken up into chunks
|
||||
of chunksize characters separated by a space.
|
||||
"""
|
||||
|
||||
line = base64.b64encode(data)
|
||||
return b' '.join([line[i:i + chunksize]
|
||||
for i
|
||||
in range(0, len(line), chunksize)]).decode()
|
||||
|
||||
__escaped = bytearray(b'"\\')
|
||||
|
||||
def _escapify(qstring):
|
||||
"""Escape the characters in a quoted string which need it."""
|
||||
|
||||
if isinstance(qstring, text_type):
|
||||
qstring = qstring.encode()
|
||||
if not isinstance(qstring, bytearray):
|
||||
qstring = bytearray(qstring)
|
||||
|
||||
text = ''
|
||||
for c in qstring:
|
||||
if c in __escaped:
|
||||
text += '\\' + chr(c)
|
||||
elif c >= 0x20 and c < 0x7F:
|
||||
text += chr(c)
|
||||
else:
|
||||
text += '\\%03d' % c
|
||||
return text
|
||||
|
||||
|
||||
def _truncate_bitmap(what):
|
||||
"""Determine the index of greatest byte that isn't all zeros, and
|
||||
return the bitmap that contains all the bytes less than that index.
|
||||
"""
|
||||
|
||||
for i in xrange(len(what) - 1, -1, -1):
|
||||
if what[i] != 0:
|
||||
return what[0: i + 1]
|
||||
return what[0:1]
|
||||
|
||||
|
||||
class Rdata(object):
|
||||
"""Base class for all DNS rdata types."""
|
||||
|
||||
__slots__ = ['rdclass', 'rdtype']
|
||||
|
||||
def __init__(self, rdclass, rdtype):
|
||||
"""Initialize an rdata.
|
||||
|
||||
*rdclass*, an ``int`` is the rdataclass of the Rdata.
|
||||
*rdtype*, an ``int`` is the rdatatype of the Rdata.
|
||||
"""
|
||||
|
||||
self.rdclass = rdclass
|
||||
self.rdtype = rdtype
|
||||
|
||||
def covers(self):
|
||||
"""Return the type a Rdata covers.
|
||||
|
||||
DNS SIG/RRSIG rdatas apply to a specific type; this type is
|
||||
returned by the covers() function. If the rdata type is not
|
||||
SIG or RRSIG, dns.rdatatype.NONE is returned. This is useful when
|
||||
creating rdatasets, allowing the rdataset to contain only RRSIGs
|
||||
of a particular type, e.g. RRSIG(NS).
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
return dns.rdatatype.NONE
|
||||
|
||||
def extended_rdatatype(self):
|
||||
"""Return a 32-bit type value, the least significant 16 bits of
|
||||
which are the ordinary DNS type, and the upper 16 bits of which are
|
||||
the "covered" type, if any.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
return self.covers() << 16 | self.rdtype
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
"""Convert an rdata to text format.
|
||||
|
||||
Returns a ``text``.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
"""Convert an rdata to wire format.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def to_digestable(self, origin=None):
|
||||
"""Convert rdata to a format suitable for digesting in hashes. This
|
||||
is also the DNSSEC canonical form.
|
||||
|
||||
Returns a ``binary``.
|
||||
"""
|
||||
|
||||
f = BytesIO()
|
||||
self.to_wire(f, None, origin)
|
||||
return f.getvalue()
|
||||
|
||||
def validate(self):
|
||||
"""Check that the current contents of the rdata's fields are
|
||||
valid.
|
||||
|
||||
If you change an rdata by assigning to its fields,
|
||||
it is a good idea to call validate() when you are done making
|
||||
changes.
|
||||
|
||||
Raises various exceptions if there are problems.
|
||||
|
||||
Returns ``None``.
|
||||
"""
|
||||
|
||||
dns.rdata.from_text(self.rdclass, self.rdtype, self.to_text())
|
||||
|
||||
def __repr__(self):
|
||||
covers = self.covers()
|
||||
if covers == dns.rdatatype.NONE:
|
||||
ctext = ''
|
||||
else:
|
||||
ctext = '(' + dns.rdatatype.to_text(covers) + ')'
|
||||
return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
|
||||
dns.rdatatype.to_text(self.rdtype) + ctext + ' rdata: ' + \
|
||||
str(self) + '>'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_text()
|
||||
|
||||
def _cmp(self, other):
|
||||
"""Compare an rdata with another rdata of the same rdtype and
|
||||
rdclass.
|
||||
|
||||
Return < 0 if self < other in the DNSSEC ordering, 0 if self
|
||||
== other, and > 0 if self > other.
|
||||
|
||||
"""
|
||||
our = self.to_digestable(dns.name.root)
|
||||
their = other.to_digestable(dns.name.root)
|
||||
if our == their:
|
||||
return 0
|
||||
elif our > their:
|
||||
return 1
|
||||
else:
|
||||
return -1
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Rdata):
|
||||
return False
|
||||
if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
return False
|
||||
return self._cmp(other) == 0
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Rdata):
|
||||
return True
|
||||
if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
return True
|
||||
return self._cmp(other) != 0
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Rdata) or \
|
||||
self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
|
||||
return NotImplemented
|
||||
return self._cmp(other) < 0
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, Rdata) or \
|
||||
self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) <= 0
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, Rdata) or \
|
||||
self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) >= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, Rdata) or \
|
||||
self.rdclass != other.rdclass or self.rdtype != other.rdtype:
|
||||
return NotImplemented
|
||||
return self._cmp(other) > 0
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.to_digestable(dns.name.root))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
"""Convert any domain names in the rdata to the specified
|
||||
relativization.
|
||||
"""
|
||||
|
||||
class GenericRdata(Rdata):
|
||||
|
||||
"""Generic Rdata Class
|
||||
|
||||
This class is used for rdata types for which we have no better
|
||||
implementation. It implements the DNS "unknown RRs" scheme.
|
||||
"""
|
||||
|
||||
__slots__ = ['data']
|
||||
|
||||
def __init__(self, rdclass, rdtype, data):
|
||||
super(GenericRdata, self).__init__(rdclass, rdtype)
|
||||
self.data = data
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return r'\# %d ' % len(self.data) + _hexify(self.data)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
token = tok.get()
|
||||
if not token.is_identifier() or token.value != r'\#':
|
||||
raise dns.exception.SyntaxError(
|
||||
r'generic rdata does not start with \#')
|
||||
length = tok.get_int()
|
||||
chunks = []
|
||||
while 1:
|
||||
token = tok.get()
|
||||
if token.is_eol_or_eof():
|
||||
break
|
||||
chunks.append(token.value.encode())
|
||||
hex = b''.join(chunks)
|
||||
data = binascii.unhexlify(hex)
|
||||
if len(data) != length:
|
||||
raise dns.exception.SyntaxError(
|
||||
'generic rdata hex data has wrong length')
|
||||
return cls(rdclass, rdtype, data)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
file.write(self.data)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
return cls(rdclass, rdtype, wire[current: current + rdlen])
|
||||
|
||||
_rdata_modules = {}
|
||||
_module_prefix = 'dns.rdtypes'
|
||||
_import_lock = _threading.Lock()
|
||||
|
||||
def get_rdata_class(rdclass, rdtype):
|
||||
|
||||
def import_module(name):
|
||||
with _import_lock:
|
||||
mod = __import__(name)
|
||||
components = name.split('.')
|
||||
for comp in components[1:]:
|
||||
mod = getattr(mod, comp)
|
||||
return mod
|
||||
|
||||
mod = _rdata_modules.get((rdclass, rdtype))
|
||||
rdclass_text = dns.rdataclass.to_text(rdclass)
|
||||
rdtype_text = dns.rdatatype.to_text(rdtype)
|
||||
rdtype_text = rdtype_text.replace('-', '_')
|
||||
if not mod:
|
||||
mod = _rdata_modules.get((dns.rdatatype.ANY, rdtype))
|
||||
if not mod:
|
||||
try:
|
||||
mod = import_module('.'.join([_module_prefix,
|
||||
rdclass_text, rdtype_text]))
|
||||
_rdata_modules[(rdclass, rdtype)] = mod
|
||||
except ImportError:
|
||||
try:
|
||||
mod = import_module('.'.join([_module_prefix,
|
||||
'ANY', rdtype_text]))
|
||||
_rdata_modules[(dns.rdataclass.ANY, rdtype)] = mod
|
||||
except ImportError:
|
||||
mod = None
|
||||
if mod:
|
||||
cls = getattr(mod, rdtype_text)
|
||||
else:
|
||||
cls = GenericRdata
|
||||
return cls
|
||||
|
||||
|
||||
def from_text(rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
"""Build an rdata object from text format.
|
||||
|
||||
This function attempts to dynamically load a class which
|
||||
implements the specified rdata class and type. If there is no
|
||||
class-and-type-specific implementation, the GenericRdata class
|
||||
is used.
|
||||
|
||||
Once a class is chosen, its from_text() class method is called
|
||||
with the parameters to this function.
|
||||
|
||||
If *tok* is a ``text``, then a tokenizer is created and the string
|
||||
is used as its input.
|
||||
|
||||
*rdclass*, an ``int``, the rdataclass.
|
||||
|
||||
*rdtype*, an ``int``, the rdatatype.
|
||||
|
||||
*tok*, a ``dns.tokenizer.Tokenizer`` or a ``text``.
|
||||
|
||||
*origin*, a ``dns.name.Name`` (or ``None``), the
|
||||
origin to use for relative names.
|
||||
|
||||
*relativize*, a ``bool``. If true, name will be relativized to
|
||||
the specified origin.
|
||||
|
||||
Returns an instance of the chosen Rdata subclass.
|
||||
"""
|
||||
|
||||
if isinstance(tok, string_types):
|
||||
tok = dns.tokenizer.Tokenizer(tok)
|
||||
cls = get_rdata_class(rdclass, rdtype)
|
||||
if cls != GenericRdata:
|
||||
# peek at first token
|
||||
token = tok.get()
|
||||
tok.unget(token)
|
||||
if token.is_identifier() and \
|
||||
token.value == r'\#':
|
||||
#
|
||||
# Known type using the generic syntax. Extract the
|
||||
# wire form from the generic syntax, and then run
|
||||
# from_wire on it.
|
||||
#
|
||||
rdata = GenericRdata.from_text(rdclass, rdtype, tok, origin,
|
||||
relativize)
|
||||
return from_wire(rdclass, rdtype, rdata.data, 0, len(rdata.data),
|
||||
origin)
|
||||
return cls.from_text(rdclass, rdtype, tok, origin, relativize)
|
||||
|
||||
|
||||
def from_wire(rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
"""Build an rdata object from wire format
|
||||
|
||||
This function attempts to dynamically load a class which
|
||||
implements the specified rdata class and type. If there is no
|
||||
class-and-type-specific implementation, the GenericRdata class
|
||||
is used.
|
||||
|
||||
Once a class is chosen, its from_wire() class method is called
|
||||
with the parameters to this function.
|
||||
|
||||
*rdclass*, an ``int``, the rdataclass.
|
||||
|
||||
*rdtype*, an ``int``, the rdatatype.
|
||||
|
||||
*wire*, a ``binary``, the wire-format message.
|
||||
|
||||
*current*, an ``int``, the offset in wire of the beginning of
|
||||
the rdata.
|
||||
|
||||
*rdlen*, an ``int``, the length of the wire-format rdata
|
||||
|
||||
*origin*, a ``dns.name.Name`` (or ``None``). If not ``None``,
|
||||
then names will be relativized to this origin.
|
||||
|
||||
Returns an instance of the chosen Rdata subclass.
|
||||
"""
|
||||
|
||||
wire = dns.wiredata.maybe_wrap(wire)
|
||||
cls = get_rdata_class(rdclass, rdtype)
|
||||
return cls.from_wire(rdclass, rdtype, wire, current, rdlen, origin)
|
||||
|
||||
|
||||
class RdatatypeExists(dns.exception.DNSException):
|
||||
"""DNS rdatatype already exists."""
|
||||
supp_kwargs = {'rdclass', 'rdtype'}
|
||||
fmt = "The rdata type with class {rdclass} and rdtype {rdtype} " + \
|
||||
"already exists."
|
||||
|
||||
|
||||
def register_type(implementation, rdtype, rdtype_text, is_singleton=False,
|
||||
rdclass=dns.rdataclass.IN):
|
||||
"""Dynamically register a module to handle an rdatatype.
|
||||
|
||||
*implementation*, a module implementing the type in the usual dnspython
|
||||
way.
|
||||
|
||||
*rdtype*, an ``int``, the rdatatype to register.
|
||||
|
||||
*rdtype_text*, a ``text``, the textual form of the rdatatype.
|
||||
|
||||
*is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
|
||||
RRsets of the type can have only one member.)
|
||||
|
||||
*rdclass*, the rdataclass of the type, or ``dns.rdataclass.ANY`` if
|
||||
it applies to all classes.
|
||||
"""
|
||||
|
||||
existing_cls = get_rdata_class(rdclass, rdtype)
|
||||
if existing_cls != GenericRdata:
|
||||
raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype)
|
||||
_rdata_modules[(rdclass, rdtype)] = implementation
|
||||
dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton)
|
||||
17
src/dns/rdata.pyi
Normal file
17
src/dns/rdata.pyi
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Dict, Tuple, Any, Optional
|
||||
from .name import Name
|
||||
class Rdata:
|
||||
def __init__(self):
|
||||
self.address : str
|
||||
def to_wire(self, file, compress : Optional[Dict[Name,int]], origin : Optional[Name]) -> bytes:
|
||||
...
|
||||
@classmethod
|
||||
def from_text(cls, rdclass : int, rdtype : int, tok, origin=None, relativize=True):
|
||||
...
|
||||
_rdata_modules : Dict[Tuple[Any,Rdata],Any]
|
||||
|
||||
def from_text(rdclass : int, rdtype : int, tok : Optional[str], origin : Optional[Name] = None, relativize : bool = True):
|
||||
...
|
||||
|
||||
def from_wire(rdclass : int, rdtype : int, wire : bytes, current : int, rdlen : int, origin : Optional[Name] = None):
|
||||
...
|
||||
122
src/dns/rdataclass.py
Normal file
122
src/dns/rdataclass.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Rdata Classes."""
|
||||
|
||||
import re
|
||||
|
||||
import dns.exception
|
||||
|
||||
RESERVED0 = 0
|
||||
IN = 1
|
||||
CH = 3
|
||||
HS = 4
|
||||
NONE = 254
|
||||
ANY = 255
|
||||
|
||||
_by_text = {
|
||||
'RESERVED0': RESERVED0,
|
||||
'IN': IN,
|
||||
'CH': CH,
|
||||
'HS': HS,
|
||||
'NONE': NONE,
|
||||
'ANY': ANY
|
||||
}
|
||||
|
||||
# We construct the inverse mapping programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mapping not to be true inverse.
|
||||
|
||||
_by_value = {y: x for x, y in _by_text.items()}
|
||||
|
||||
# Now that we've built the inverse map, we can add class aliases to
|
||||
# the _by_text mapping.
|
||||
|
||||
_by_text.update({
|
||||
'INTERNET': IN,
|
||||
'CHAOS': CH,
|
||||
'HESIOD': HS
|
||||
})
|
||||
|
||||
_metaclasses = {
|
||||
NONE: True,
|
||||
ANY: True
|
||||
}
|
||||
|
||||
_unknown_class_pattern = re.compile('CLASS([0-9]+)$', re.I)
|
||||
|
||||
|
||||
class UnknownRdataclass(dns.exception.DNSException):
|
||||
"""A DNS class is unknown."""
|
||||
|
||||
|
||||
def from_text(text):
|
||||
"""Convert text into a DNS rdata class value.
|
||||
|
||||
The input text can be a defined DNS RR class mnemonic or
|
||||
instance of the DNS generic class syntax.
|
||||
|
||||
For example, "IN" and "CLASS1" will both result in a value of 1.
|
||||
|
||||
Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown.
|
||||
|
||||
Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
value = _by_text.get(text.upper())
|
||||
if value is None:
|
||||
match = _unknown_class_pattern.match(text)
|
||||
if match is None:
|
||||
raise UnknownRdataclass
|
||||
value = int(match.group(1))
|
||||
if value < 0 or value > 65535:
|
||||
raise ValueError("class must be between >= 0 and <= 65535")
|
||||
return value
|
||||
|
||||
|
||||
def to_text(value):
|
||||
"""Convert a DNS rdata type value to text.
|
||||
|
||||
If the value has a known mnemonic, it will be used, otherwise the
|
||||
DNS generic class syntax will be used.
|
||||
|
||||
Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
|
||||
|
||||
Returns a ``str``.
|
||||
"""
|
||||
|
||||
if value < 0 or value > 65535:
|
||||
raise ValueError("class must be between >= 0 and <= 65535")
|
||||
text = _by_value.get(value)
|
||||
if text is None:
|
||||
text = 'CLASS' + repr(value)
|
||||
return text
|
||||
|
||||
|
||||
def is_metaclass(rdclass):
|
||||
"""True if the specified class is a metaclass.
|
||||
|
||||
The currently defined metaclasses are ANY and NONE.
|
||||
|
||||
*rdclass* is an ``int``.
|
||||
"""
|
||||
|
||||
if rdclass in _metaclasses:
|
||||
return True
|
||||
return False
|
||||
347
src/dns/rdataset.py
Normal file
347
src/dns/rdataset.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS rdatasets (an rdataset is a set of rdatas of a given type and class)"""
|
||||
|
||||
import random
|
||||
from io import StringIO
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
import dns.rdata
|
||||
import dns.set
|
||||
from ._compat import string_types
|
||||
|
||||
# define SimpleSet here for backwards compatibility
|
||||
SimpleSet = dns.set.Set
|
||||
|
||||
|
||||
class DifferingCovers(dns.exception.DNSException):
|
||||
"""An attempt was made to add a DNS SIG/RRSIG whose covered type
|
||||
is not the same as that of the other rdatas in the rdataset."""
|
||||
|
||||
|
||||
class IncompatibleTypes(dns.exception.DNSException):
|
||||
"""An attempt was made to add DNS RR data of an incompatible type."""
|
||||
|
||||
|
||||
class Rdataset(dns.set.Set):
|
||||
|
||||
"""A DNS rdataset."""
|
||||
|
||||
__slots__ = ['rdclass', 'rdtype', 'covers', 'ttl']
|
||||
|
||||
def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0):
|
||||
"""Create a new rdataset of the specified class and type.
|
||||
|
||||
*rdclass*, an ``int``, the rdataclass.
|
||||
|
||||
*rdtype*, an ``int``, the rdatatype.
|
||||
|
||||
*covers*, an ``int``, the covered rdatatype.
|
||||
|
||||
*ttl*, an ``int``, the TTL.
|
||||
"""
|
||||
|
||||
super(Rdataset, self).__init__()
|
||||
self.rdclass = rdclass
|
||||
self.rdtype = rdtype
|
||||
self.covers = covers
|
||||
self.ttl = ttl
|
||||
|
||||
def _clone(self):
|
||||
obj = super(Rdataset, self)._clone()
|
||||
obj.rdclass = self.rdclass
|
||||
obj.rdtype = self.rdtype
|
||||
obj.covers = self.covers
|
||||
obj.ttl = self.ttl
|
||||
return obj
|
||||
|
||||
def update_ttl(self, ttl):
|
||||
"""Perform TTL minimization.
|
||||
|
||||
Set the TTL of the rdataset to be the lesser of the set's current
|
||||
TTL or the specified TTL. If the set contains no rdatas, set the TTL
|
||||
to the specified TTL.
|
||||
|
||||
*ttl*, an ``int``.
|
||||
"""
|
||||
|
||||
if len(self) == 0:
|
||||
self.ttl = ttl
|
||||
elif ttl < self.ttl:
|
||||
self.ttl = ttl
|
||||
|
||||
def add(self, rd, ttl=None):
|
||||
"""Add the specified rdata to the rdataset.
|
||||
|
||||
If the optional *ttl* parameter is supplied, then
|
||||
``self.update_ttl(ttl)`` will be called prior to adding the rdata.
|
||||
|
||||
*rd*, a ``dns.rdata.Rdata``, the rdata
|
||||
|
||||
*ttl*, an ``int``, the TTL.
|
||||
|
||||
Raises ``dns.rdataset.IncompatibleTypes`` if the type and class
|
||||
do not match the type and class of the rdataset.
|
||||
|
||||
Raises ``dns.rdataset.DifferingCovers`` if the type is a signature
|
||||
type and the covered type does not match that of the rdataset.
|
||||
"""
|
||||
|
||||
#
|
||||
# If we're adding a signature, do some special handling to
|
||||
# check that the signature covers the same type as the
|
||||
# other rdatas in this rdataset. If this is the first rdata
|
||||
# in the set, initialize the covers field.
|
||||
#
|
||||
if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:
|
||||
raise IncompatibleTypes
|
||||
if ttl is not None:
|
||||
self.update_ttl(ttl)
|
||||
if self.rdtype == dns.rdatatype.RRSIG or \
|
||||
self.rdtype == dns.rdatatype.SIG:
|
||||
covers = rd.covers()
|
||||
if len(self) == 0 and self.covers == dns.rdatatype.NONE:
|
||||
self.covers = covers
|
||||
elif self.covers != covers:
|
||||
raise DifferingCovers
|
||||
if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:
|
||||
self.clear()
|
||||
super(Rdataset, self).add(rd)
|
||||
|
||||
def union_update(self, other):
|
||||
self.update_ttl(other.ttl)
|
||||
super(Rdataset, self).union_update(other)
|
||||
|
||||
def intersection_update(self, other):
|
||||
self.update_ttl(other.ttl)
|
||||
super(Rdataset, self).intersection_update(other)
|
||||
|
||||
def update(self, other):
|
||||
"""Add all rdatas in other to self.
|
||||
|
||||
*other*, a ``dns.rdataset.Rdataset``, the rdataset from which
|
||||
to update.
|
||||
"""
|
||||
|
||||
self.update_ttl(other.ttl)
|
||||
super(Rdataset, self).update(other)
|
||||
|
||||
def __repr__(self):
|
||||
if self.covers == 0:
|
||||
ctext = ''
|
||||
else:
|
||||
ctext = '(' + dns.rdatatype.to_text(self.covers) + ')'
|
||||
return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
|
||||
dns.rdatatype.to_text(self.rdtype) + ctext + ' rdataset>'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_text()
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Rdataset):
|
||||
return False
|
||||
if self.rdclass != other.rdclass or \
|
||||
self.rdtype != other.rdtype or \
|
||||
self.covers != other.covers:
|
||||
return False
|
||||
return super(Rdataset, self).__eq__(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def to_text(self, name=None, origin=None, relativize=True,
|
||||
override_rdclass=None, **kw):
|
||||
"""Convert the rdataset into DNS master file format.
|
||||
|
||||
See ``dns.name.Name.choose_relativity`` for more information
|
||||
on how *origin* and *relativize* determine the way names
|
||||
are emitted.
|
||||
|
||||
Any additional keyword arguments are passed on to the rdata
|
||||
``to_text()`` method.
|
||||
|
||||
*name*, a ``dns.name.Name``. If name is not ``None``, emit RRs with
|
||||
*name* as the owner name.
|
||||
|
||||
*origin*, a ``dns.name.Name`` or ``None``, the origin for relative
|
||||
names.
|
||||
|
||||
*relativize*, a ``bool``. If ``True``, names will be relativized
|
||||
to *origin*.
|
||||
"""
|
||||
|
||||
if name is not None:
|
||||
name = name.choose_relativity(origin, relativize)
|
||||
ntext = str(name)
|
||||
pad = ' '
|
||||
else:
|
||||
ntext = ''
|
||||
pad = ''
|
||||
s = StringIO()
|
||||
if override_rdclass is not None:
|
||||
rdclass = override_rdclass
|
||||
else:
|
||||
rdclass = self.rdclass
|
||||
if len(self) == 0:
|
||||
#
|
||||
# Empty rdatasets are used for the question section, and in
|
||||
# some dynamic updates, so we don't need to print out the TTL
|
||||
# (which is meaningless anyway).
|
||||
#
|
||||
s.write(u'{}{}{} {}\n'.format(ntext, pad,
|
||||
dns.rdataclass.to_text(rdclass),
|
||||
dns.rdatatype.to_text(self.rdtype)))
|
||||
else:
|
||||
for rd in self:
|
||||
s.write(u'%s%s%d %s %s %s\n' %
|
||||
(ntext, pad, self.ttl, dns.rdataclass.to_text(rdclass),
|
||||
dns.rdatatype.to_text(self.rdtype),
|
||||
rd.to_text(origin=origin, relativize=relativize,
|
||||
**kw)))
|
||||
#
|
||||
# We strip off the final \n for the caller's convenience in printing
|
||||
#
|
||||
return s.getvalue()[:-1]
|
||||
|
||||
def to_wire(self, name, file, compress=None, origin=None,
|
||||
override_rdclass=None, want_shuffle=True):
|
||||
"""Convert the rdataset to wire format.
|
||||
|
||||
*name*, a ``dns.name.Name`` is the owner name to use.
|
||||
|
||||
*file* is the file where the name is emitted (typically a
|
||||
BytesIO file).
|
||||
|
||||
*compress*, a ``dict``, is the compression table to use. If
|
||||
``None`` (the default), names will not be compressed.
|
||||
|
||||
*origin* is a ``dns.name.Name`` or ``None``. If the name is
|
||||
relative and origin is not ``None``, then *origin* will be appended
|
||||
to it.
|
||||
|
||||
*override_rdclass*, an ``int``, is used as the class instead of the
|
||||
class of the rdataset. This is useful when rendering rdatasets
|
||||
associated with dynamic updates.
|
||||
|
||||
*want_shuffle*, a ``bool``. If ``True``, then the order of the
|
||||
Rdatas within the Rdataset will be shuffled before rendering.
|
||||
|
||||
Returns an ``int``, the number of records emitted.
|
||||
"""
|
||||
|
||||
if override_rdclass is not None:
|
||||
rdclass = override_rdclass
|
||||
want_shuffle = False
|
||||
else:
|
||||
rdclass = self.rdclass
|
||||
file.seek(0, 2)
|
||||
if len(self) == 0:
|
||||
name.to_wire(file, compress, origin)
|
||||
stuff = struct.pack("!HHIH", self.rdtype, rdclass, 0, 0)
|
||||
file.write(stuff)
|
||||
return 1
|
||||
else:
|
||||
if want_shuffle:
|
||||
l = list(self)
|
||||
random.shuffle(l)
|
||||
else:
|
||||
l = self
|
||||
for rd in l:
|
||||
name.to_wire(file, compress, origin)
|
||||
stuff = struct.pack("!HHIH", self.rdtype, rdclass,
|
||||
self.ttl, 0)
|
||||
file.write(stuff)
|
||||
start = file.tell()
|
||||
rd.to_wire(file, compress, origin)
|
||||
end = file.tell()
|
||||
assert end - start < 65536
|
||||
file.seek(start - 2)
|
||||
stuff = struct.pack("!H", end - start)
|
||||
file.write(stuff)
|
||||
file.seek(0, 2)
|
||||
return len(self)
|
||||
|
||||
def match(self, rdclass, rdtype, covers):
|
||||
"""Returns ``True`` if this rdataset matches the specified class,
|
||||
type, and covers.
|
||||
"""
|
||||
if self.rdclass == rdclass and \
|
||||
self.rdtype == rdtype and \
|
||||
self.covers == covers:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def from_text_list(rdclass, rdtype, ttl, text_rdatas):
|
||||
"""Create an rdataset with the specified class, type, and TTL, and with
|
||||
the specified list of rdatas in text format.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset`` object.
|
||||
"""
|
||||
|
||||
if isinstance(rdclass, string_types):
|
||||
rdclass = dns.rdataclass.from_text(rdclass)
|
||||
if isinstance(rdtype, string_types):
|
||||
rdtype = dns.rdatatype.from_text(rdtype)
|
||||
r = Rdataset(rdclass, rdtype)
|
||||
r.update_ttl(ttl)
|
||||
for t in text_rdatas:
|
||||
rd = dns.rdata.from_text(r.rdclass, r.rdtype, t)
|
||||
r.add(rd)
|
||||
return r
|
||||
|
||||
|
||||
def from_text(rdclass, rdtype, ttl, *text_rdatas):
|
||||
"""Create an rdataset with the specified class, type, and TTL, and with
|
||||
the specified rdatas in text format.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset`` object.
|
||||
"""
|
||||
|
||||
return from_text_list(rdclass, rdtype, ttl, text_rdatas)
|
||||
|
||||
|
||||
def from_rdata_list(ttl, rdatas):
|
||||
"""Create an rdataset with the specified TTL, and with
|
||||
the specified list of rdata objects.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset`` object.
|
||||
"""
|
||||
|
||||
if len(rdatas) == 0:
|
||||
raise ValueError("rdata list must not be empty")
|
||||
r = None
|
||||
for rd in rdatas:
|
||||
if r is None:
|
||||
r = Rdataset(rd.rdclass, rd.rdtype)
|
||||
r.update_ttl(ttl)
|
||||
r.add(rd)
|
||||
return r
|
||||
|
||||
|
||||
def from_rdata(ttl, *rdatas):
|
||||
"""Create an rdataset with the specified TTL, and with
|
||||
the specified rdata objects.
|
||||
|
||||
Returns a ``dns.rdataset.Rdataset`` object.
|
||||
"""
|
||||
|
||||
return from_rdata_list(ttl, rdatas)
|
||||
58
src/dns/rdataset.pyi
Normal file
58
src/dns/rdataset.pyi
Normal file
@@ -0,0 +1,58 @@
|
||||
from typing import Optional, Dict, List, Union
|
||||
from io import BytesIO
|
||||
from . import exception, name, set, rdatatype, rdata, rdataset
|
||||
|
||||
class DifferingCovers(exception.DNSException):
|
||||
"""An attempt was made to add a DNS SIG/RRSIG whose covered type
|
||||
is not the same as that of the other rdatas in the rdataset."""
|
||||
|
||||
|
||||
class IncompatibleTypes(exception.DNSException):
|
||||
"""An attempt was made to add DNS RR data of an incompatible type."""
|
||||
|
||||
|
||||
class Rdataset(set.Set):
|
||||
def __init__(self, rdclass, rdtype, covers=rdatatype.NONE, ttl=0):
|
||||
self.rdclass : int = rdclass
|
||||
self.rdtype : int = rdtype
|
||||
self.covers : int = covers
|
||||
self.ttl : int = ttl
|
||||
|
||||
def update_ttl(self, ttl : int) -> None:
|
||||
...
|
||||
|
||||
def add(self, rd : rdata.Rdata, ttl : Optional[int] =None):
|
||||
...
|
||||
|
||||
def union_update(self, other : Rdataset):
|
||||
...
|
||||
|
||||
def intersection_update(self, other : Rdataset):
|
||||
...
|
||||
|
||||
def update(self, other : Rdataset):
|
||||
...
|
||||
|
||||
def to_text(self, name : Optional[name.Name] =None, origin : Optional[name.Name] =None, relativize=True,
|
||||
override_rdclass : Optional[int] =None, **kw) -> bytes:
|
||||
...
|
||||
|
||||
def to_wire(self, name : Optional[name.Name], file : BytesIO, compress : Optional[Dict[name.Name, int]] = None, origin : Optional[name.Name] = None,
|
||||
override_rdclass : Optional[int] = None, want_shuffle=True) -> int:
|
||||
...
|
||||
|
||||
def match(self, rdclass : int, rdtype : int, covers : int) -> bool:
|
||||
...
|
||||
|
||||
|
||||
def from_text_list(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, text_rdatas : str) -> rdataset.Rdataset:
|
||||
...
|
||||
|
||||
def from_text(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, *text_rdatas : str) -> rdataset.Rdataset:
|
||||
...
|
||||
|
||||
def from_rdata_list(ttl : int, rdatas : List[rdata.Rdata]) -> rdataset.Rdataset:
|
||||
...
|
||||
|
||||
def from_rdata(ttl : int, *rdatas : List[rdata.Rdata]) -> rdataset.Rdataset:
|
||||
...
|
||||
287
src/dns/rdatatype.py
Normal file
287
src/dns/rdatatype.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2001-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
"""DNS Rdata Types."""
|
||||
|
||||
import re
|
||||
|
||||
import dns.exception
|
||||
|
||||
NONE = 0
|
||||
A = 1
|
||||
NS = 2
|
||||
MD = 3
|
||||
MF = 4
|
||||
CNAME = 5
|
||||
SOA = 6
|
||||
MB = 7
|
||||
MG = 8
|
||||
MR = 9
|
||||
NULL = 10
|
||||
WKS = 11
|
||||
PTR = 12
|
||||
HINFO = 13
|
||||
MINFO = 14
|
||||
MX = 15
|
||||
TXT = 16
|
||||
RP = 17
|
||||
AFSDB = 18
|
||||
X25 = 19
|
||||
ISDN = 20
|
||||
RT = 21
|
||||
NSAP = 22
|
||||
NSAP_PTR = 23
|
||||
SIG = 24
|
||||
KEY = 25
|
||||
PX = 26
|
||||
GPOS = 27
|
||||
AAAA = 28
|
||||
LOC = 29
|
||||
NXT = 30
|
||||
SRV = 33
|
||||
NAPTR = 35
|
||||
KX = 36
|
||||
CERT = 37
|
||||
A6 = 38
|
||||
DNAME = 39
|
||||
OPT = 41
|
||||
APL = 42
|
||||
DS = 43
|
||||
SSHFP = 44
|
||||
IPSECKEY = 45
|
||||
RRSIG = 46
|
||||
NSEC = 47
|
||||
DNSKEY = 48
|
||||
DHCID = 49
|
||||
NSEC3 = 50
|
||||
NSEC3PARAM = 51
|
||||
TLSA = 52
|
||||
HIP = 55
|
||||
CDS = 59
|
||||
CDNSKEY = 60
|
||||
OPENPGPKEY = 61
|
||||
CSYNC = 62
|
||||
SPF = 99
|
||||
UNSPEC = 103
|
||||
EUI48 = 108
|
||||
EUI64 = 109
|
||||
TKEY = 249
|
||||
TSIG = 250
|
||||
IXFR = 251
|
||||
AXFR = 252
|
||||
MAILB = 253
|
||||
MAILA = 254
|
||||
ANY = 255
|
||||
URI = 256
|
||||
CAA = 257
|
||||
AVC = 258
|
||||
TA = 32768
|
||||
DLV = 32769
|
||||
|
||||
_by_text = {
|
||||
'NONE': NONE,
|
||||
'A': A,
|
||||
'NS': NS,
|
||||
'MD': MD,
|
||||
'MF': MF,
|
||||
'CNAME': CNAME,
|
||||
'SOA': SOA,
|
||||
'MB': MB,
|
||||
'MG': MG,
|
||||
'MR': MR,
|
||||
'NULL': NULL,
|
||||
'WKS': WKS,
|
||||
'PTR': PTR,
|
||||
'HINFO': HINFO,
|
||||
'MINFO': MINFO,
|
||||
'MX': MX,
|
||||
'TXT': TXT,
|
||||
'RP': RP,
|
||||
'AFSDB': AFSDB,
|
||||
'X25': X25,
|
||||
'ISDN': ISDN,
|
||||
'RT': RT,
|
||||
'NSAP': NSAP,
|
||||
'NSAP-PTR': NSAP_PTR,
|
||||
'SIG': SIG,
|
||||
'KEY': KEY,
|
||||
'PX': PX,
|
||||
'GPOS': GPOS,
|
||||
'AAAA': AAAA,
|
||||
'LOC': LOC,
|
||||
'NXT': NXT,
|
||||
'SRV': SRV,
|
||||
'NAPTR': NAPTR,
|
||||
'KX': KX,
|
||||
'CERT': CERT,
|
||||
'A6': A6,
|
||||
'DNAME': DNAME,
|
||||
'OPT': OPT,
|
||||
'APL': APL,
|
||||
'DS': DS,
|
||||
'SSHFP': SSHFP,
|
||||
'IPSECKEY': IPSECKEY,
|
||||
'RRSIG': RRSIG,
|
||||
'NSEC': NSEC,
|
||||
'DNSKEY': DNSKEY,
|
||||
'DHCID': DHCID,
|
||||
'NSEC3': NSEC3,
|
||||
'NSEC3PARAM': NSEC3PARAM,
|
||||
'TLSA': TLSA,
|
||||
'HIP': HIP,
|
||||
'CDS': CDS,
|
||||
'CDNSKEY': CDNSKEY,
|
||||
'OPENPGPKEY': OPENPGPKEY,
|
||||
'CSYNC': CSYNC,
|
||||
'SPF': SPF,
|
||||
'UNSPEC': UNSPEC,
|
||||
'EUI48': EUI48,
|
||||
'EUI64': EUI64,
|
||||
'TKEY': TKEY,
|
||||
'TSIG': TSIG,
|
||||
'IXFR': IXFR,
|
||||
'AXFR': AXFR,
|
||||
'MAILB': MAILB,
|
||||
'MAILA': MAILA,
|
||||
'ANY': ANY,
|
||||
'URI': URI,
|
||||
'CAA': CAA,
|
||||
'AVC': AVC,
|
||||
'TA': TA,
|
||||
'DLV': DLV,
|
||||
}
|
||||
|
||||
# We construct the inverse mapping programmatically to ensure that we
|
||||
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
||||
# would cause the mapping not to be true inverse.
|
||||
|
||||
_by_value = {y: x for x, y in _by_text.items()}
|
||||
|
||||
_metatypes = {
|
||||
OPT: True
|
||||
}
|
||||
|
||||
_singletons = {
|
||||
SOA: True,
|
||||
NXT: True,
|
||||
DNAME: True,
|
||||
NSEC: True,
|
||||
CNAME: True,
|
||||
}
|
||||
|
||||
_unknown_type_pattern = re.compile('TYPE([0-9]+)$', re.I)
|
||||
|
||||
|
||||
class UnknownRdatatype(dns.exception.DNSException):
|
||||
"""DNS resource record type is unknown."""
|
||||
|
||||
|
||||
def from_text(text):
|
||||
"""Convert text into a DNS rdata type value.
|
||||
|
||||
The input text can be a defined DNS RR type mnemonic or
|
||||
instance of the DNS generic type syntax.
|
||||
|
||||
For example, "NS" and "TYPE2" will both result in a value of 2.
|
||||
|
||||
Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown.
|
||||
|
||||
Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
|
||||
|
||||
Returns an ``int``.
|
||||
"""
|
||||
|
||||
value = _by_text.get(text.upper())
|
||||
if value is None:
|
||||
match = _unknown_type_pattern.match(text)
|
||||
if match is None:
|
||||
raise UnknownRdatatype
|
||||
value = int(match.group(1))
|
||||
if value < 0 or value > 65535:
|
||||
raise ValueError("type must be between >= 0 and <= 65535")
|
||||
return value
|
||||
|
||||
|
||||
def to_text(value):
|
||||
"""Convert a DNS rdata type value to text.
|
||||
|
||||
If the value has a known mnemonic, it will be used, otherwise the
|
||||
DNS generic type syntax will be used.
|
||||
|
||||
Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
|
||||
|
||||
Returns a ``str``.
|
||||
"""
|
||||
|
||||
if value < 0 or value > 65535:
|
||||
raise ValueError("type must be between >= 0 and <= 65535")
|
||||
text = _by_value.get(value)
|
||||
if text is None:
|
||||
text = 'TYPE' + repr(value)
|
||||
return text
|
||||
|
||||
|
||||
def is_metatype(rdtype):
|
||||
"""True if the specified type is a metatype.
|
||||
|
||||
*rdtype* is an ``int``.
|
||||
|
||||
The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA,
|
||||
MAILB, ANY, and OPT.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
if rdtype >= TKEY and rdtype <= ANY or rdtype in _metatypes:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_singleton(rdtype):
|
||||
"""Is the specified type a singleton type?
|
||||
|
||||
Singleton types can only have a single rdata in an rdataset, or a single
|
||||
RR in an RRset.
|
||||
|
||||
The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and
|
||||
SOA.
|
||||
|
||||
*rdtype* is an ``int``.
|
||||
|
||||
Returns a ``bool``.
|
||||
"""
|
||||
|
||||
if rdtype in _singletons:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def register_type(rdtype, rdtype_text, is_singleton=False): # pylint: disable=redefined-outer-name
|
||||
"""Dynamically register an rdatatype.
|
||||
|
||||
*rdtype*, an ``int``, the rdatatype to register.
|
||||
|
||||
*rdtype_text*, a ``text``, the textual form of the rdatatype.
|
||||
|
||||
*is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
|
||||
RRsets of the type can have only one member.)
|
||||
"""
|
||||
|
||||
_by_text[rdtype_text] = rdtype
|
||||
_by_value[rdtype] = rdtype_text
|
||||
if is_singleton:
|
||||
_singletons[rdtype] = True
|
||||
55
src/dns/rdtypes/ANY/AFSDB.py
Normal file
55
src/dns/rdtypes/ANY/AFSDB.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.mxbase
|
||||
|
||||
|
||||
class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX):
|
||||
|
||||
"""AFSDB record
|
||||
|
||||
@ivar subtype: the subtype value
|
||||
@type subtype: int
|
||||
@ivar hostname: the hostname name
|
||||
@type hostname: dns.name.Name object"""
|
||||
|
||||
# Use the property mechanism to make "subtype" an alias for the
|
||||
# "preference" attribute, and "hostname" an alias for the "exchange"
|
||||
# attribute.
|
||||
#
|
||||
# This lets us inherit the UncompressedMX implementation but lets
|
||||
# the caller use appropriate attribute names for the rdata type.
|
||||
#
|
||||
# We probably lose some performance vs. a cut-and-paste
|
||||
# implementation, but this way we don't copy code, and that's
|
||||
# good.
|
||||
|
||||
def get_subtype(self):
|
||||
return self.preference
|
||||
|
||||
def set_subtype(self, subtype):
|
||||
self.preference = subtype
|
||||
|
||||
subtype = property(get_subtype, set_subtype)
|
||||
|
||||
def get_hostname(self):
|
||||
return self.exchange
|
||||
|
||||
def set_hostname(self, hostname):
|
||||
self.exchange = hostname
|
||||
|
||||
hostname = property(get_hostname, set_hostname)
|
||||
25
src/dns/rdtypes/ANY/AVC.py
Normal file
25
src/dns/rdtypes/ANY/AVC.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2016 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.txtbase
|
||||
|
||||
|
||||
class AVC(dns.rdtypes.txtbase.TXTBase):
|
||||
|
||||
"""AVC record
|
||||
|
||||
@see: U{http://www.iana.org/assignments/dns-parameters/AVC/avc-completed-template}"""
|
||||
75
src/dns/rdtypes/ANY/CAA.py
Normal file
75
src/dns/rdtypes/ANY/CAA.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
|
||||
|
||||
class CAA(dns.rdata.Rdata):
|
||||
|
||||
"""CAA (Certification Authority Authorization) record
|
||||
|
||||
@ivar flags: the flags
|
||||
@type flags: int
|
||||
@ivar tag: the tag
|
||||
@type tag: string
|
||||
@ivar value: the value
|
||||
@type value: string
|
||||
@see: RFC 6844"""
|
||||
|
||||
__slots__ = ['flags', 'tag', 'value']
|
||||
|
||||
def __init__(self, rdclass, rdtype, flags, tag, value):
|
||||
super(CAA, self).__init__(rdclass, rdtype)
|
||||
self.flags = flags
|
||||
self.tag = tag
|
||||
self.value = value
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '%u %s "%s"' % (self.flags,
|
||||
dns.rdata._escapify(self.tag),
|
||||
dns.rdata._escapify(self.value))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
flags = tok.get_uint8()
|
||||
tag = tok.get_string().encode()
|
||||
if len(tag) > 255:
|
||||
raise dns.exception.SyntaxError("tag too long")
|
||||
if not tag.isalnum():
|
||||
raise dns.exception.SyntaxError("tag is not alphanumeric")
|
||||
value = tok.get_string().encode()
|
||||
return cls(rdclass, rdtype, flags, tag, value)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
file.write(struct.pack('!B', self.flags))
|
||||
l = len(self.tag)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.tag)
|
||||
file.write(self.value)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(flags, l) = struct.unpack('!BB', wire[current: current + 2])
|
||||
current += 2
|
||||
tag = wire[current: current + l]
|
||||
value = wire[current + l:current + rdlen - 2]
|
||||
return cls(rdclass, rdtype, flags, tag, value)
|
||||
27
src/dns/rdtypes/ANY/CDNSKEY.py
Normal file
27
src/dns/rdtypes/ANY/CDNSKEY.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.dnskeybase
|
||||
from dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set
|
||||
|
||||
|
||||
__all__ = ['flags_to_text_set', 'flags_from_text_set']
|
||||
|
||||
|
||||
class CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
|
||||
|
||||
"""CDNSKEY record"""
|
||||
23
src/dns/rdtypes/ANY/CDS.py
Normal file
23
src/dns/rdtypes/ANY/CDS.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.dsbase
|
||||
|
||||
|
||||
class CDS(dns.rdtypes.dsbase.DSBase):
|
||||
|
||||
"""CDS record"""
|
||||
123
src/dns/rdtypes/ANY/CERT.py
Normal file
123
src/dns/rdtypes/ANY/CERT.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
import base64
|
||||
|
||||
import dns.exception
|
||||
import dns.dnssec
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
|
||||
_ctype_by_value = {
|
||||
1: 'PKIX',
|
||||
2: 'SPKI',
|
||||
3: 'PGP',
|
||||
253: 'URI',
|
||||
254: 'OID',
|
||||
}
|
||||
|
||||
_ctype_by_name = {
|
||||
'PKIX': 1,
|
||||
'SPKI': 2,
|
||||
'PGP': 3,
|
||||
'URI': 253,
|
||||
'OID': 254,
|
||||
}
|
||||
|
||||
|
||||
def _ctype_from_text(what):
|
||||
v = _ctype_by_name.get(what)
|
||||
if v is not None:
|
||||
return v
|
||||
return int(what)
|
||||
|
||||
|
||||
def _ctype_to_text(what):
|
||||
v = _ctype_by_value.get(what)
|
||||
if v is not None:
|
||||
return v
|
||||
return str(what)
|
||||
|
||||
|
||||
class CERT(dns.rdata.Rdata):
|
||||
|
||||
"""CERT record
|
||||
|
||||
@ivar certificate_type: certificate type
|
||||
@type certificate_type: int
|
||||
@ivar key_tag: key tag
|
||||
@type key_tag: int
|
||||
@ivar algorithm: algorithm
|
||||
@type algorithm: int
|
||||
@ivar certificate: the certificate or CRL
|
||||
@type certificate: string
|
||||
@see: RFC 2538"""
|
||||
|
||||
__slots__ = ['certificate_type', 'key_tag', 'algorithm', 'certificate']
|
||||
|
||||
def __init__(self, rdclass, rdtype, certificate_type, key_tag, algorithm,
|
||||
certificate):
|
||||
super(CERT, self).__init__(rdclass, rdtype)
|
||||
self.certificate_type = certificate_type
|
||||
self.key_tag = key_tag
|
||||
self.algorithm = algorithm
|
||||
self.certificate = certificate
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
certificate_type = _ctype_to_text(self.certificate_type)
|
||||
return "%s %d %s %s" % (certificate_type, self.key_tag,
|
||||
dns.dnssec.algorithm_to_text(self.algorithm),
|
||||
dns.rdata._base64ify(self.certificate))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
certificate_type = _ctype_from_text(tok.get_string())
|
||||
key_tag = tok.get_uint16()
|
||||
algorithm = dns.dnssec.algorithm_from_text(tok.get_string())
|
||||
if algorithm < 0 or algorithm > 255:
|
||||
raise dns.exception.SyntaxError("bad algorithm type")
|
||||
chunks = []
|
||||
while 1:
|
||||
t = tok.get().unescape()
|
||||
if t.is_eol_or_eof():
|
||||
break
|
||||
if not t.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
chunks.append(t.value.encode())
|
||||
b64 = b''.join(chunks)
|
||||
certificate = base64.b64decode(b64)
|
||||
return cls(rdclass, rdtype, certificate_type, key_tag,
|
||||
algorithm, certificate)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
prefix = struct.pack("!HHB", self.certificate_type, self.key_tag,
|
||||
self.algorithm)
|
||||
file.write(prefix)
|
||||
file.write(self.certificate)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
prefix = wire[current: current + 5].unwrap()
|
||||
current += 5
|
||||
rdlen -= 5
|
||||
if rdlen < 0:
|
||||
raise dns.exception.FormError
|
||||
(certificate_type, key_tag, algorithm) = struct.unpack("!HHB", prefix)
|
||||
certificate = wire[current: current + rdlen].unwrap()
|
||||
return cls(rdclass, rdtype, certificate_type, key_tag, algorithm,
|
||||
certificate)
|
||||
27
src/dns/rdtypes/ANY/CNAME.py
Normal file
27
src/dns/rdtypes/ANY/CNAME.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.nsbase
|
||||
|
||||
|
||||
class CNAME(dns.rdtypes.nsbase.NSBase):
|
||||
|
||||
"""CNAME record
|
||||
|
||||
Note: although CNAME is officially a singleton type, dnspython allows
|
||||
non-singleton CNAME rdatasets because such sets have been commonly
|
||||
used by BIND and other nameservers for load balancing."""
|
||||
126
src/dns/rdtypes/ANY/CSYNC.py
Normal file
126
src/dns/rdtypes/ANY/CSYNC.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011, 2016 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
import dns.name
|
||||
from dns._compat import xrange
|
||||
|
||||
class CSYNC(dns.rdata.Rdata):
|
||||
|
||||
"""CSYNC record
|
||||
|
||||
@ivar serial: the SOA serial number
|
||||
@type serial: int
|
||||
@ivar flags: the CSYNC flags
|
||||
@type flags: int
|
||||
@ivar windows: the windowed bitmap list
|
||||
@type windows: list of (window number, string) tuples"""
|
||||
|
||||
__slots__ = ['serial', 'flags', 'windows']
|
||||
|
||||
def __init__(self, rdclass, rdtype, serial, flags, windows):
|
||||
super(CSYNC, self).__init__(rdclass, rdtype)
|
||||
self.serial = serial
|
||||
self.flags = flags
|
||||
self.windows = windows
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
text = ''
|
||||
for (window, bitmap) in self.windows:
|
||||
bits = []
|
||||
for i in xrange(0, len(bitmap)):
|
||||
byte = bitmap[i]
|
||||
for j in xrange(0, 8):
|
||||
if byte & (0x80 >> j):
|
||||
bits.append(dns.rdatatype.to_text(window * 256 +
|
||||
i * 8 + j))
|
||||
text += (' ' + ' '.join(bits))
|
||||
return '%d %d%s' % (self.serial, self.flags, text)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
serial = tok.get_uint32()
|
||||
flags = tok.get_uint16()
|
||||
rdtypes = []
|
||||
while 1:
|
||||
token = tok.get().unescape()
|
||||
if token.is_eol_or_eof():
|
||||
break
|
||||
nrdtype = dns.rdatatype.from_text(token.value)
|
||||
if nrdtype == 0:
|
||||
raise dns.exception.SyntaxError("CSYNC with bit 0")
|
||||
if nrdtype > 65535:
|
||||
raise dns.exception.SyntaxError("CSYNC with bit > 65535")
|
||||
rdtypes.append(nrdtype)
|
||||
rdtypes.sort()
|
||||
window = 0
|
||||
octets = 0
|
||||
prior_rdtype = 0
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
windows = []
|
||||
for nrdtype in rdtypes:
|
||||
if nrdtype == prior_rdtype:
|
||||
continue
|
||||
prior_rdtype = nrdtype
|
||||
new_window = nrdtype // 256
|
||||
if new_window != window:
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
window = new_window
|
||||
offset = nrdtype % 256
|
||||
byte = offset // 8
|
||||
bit = offset % 8
|
||||
octets = byte + 1
|
||||
bitmap[byte] = bitmap[byte] | (0x80 >> bit)
|
||||
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
return cls(rdclass, rdtype, serial, flags, windows)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
file.write(struct.pack('!IH', self.serial, self.flags))
|
||||
for (window, bitmap) in self.windows:
|
||||
file.write(struct.pack('!BB', window, len(bitmap)))
|
||||
file.write(bitmap)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
if rdlen < 6:
|
||||
raise dns.exception.FormError("CSYNC too short")
|
||||
(serial, flags) = struct.unpack("!IH", wire[current: current + 6])
|
||||
current += 6
|
||||
rdlen -= 6
|
||||
windows = []
|
||||
while rdlen > 0:
|
||||
if rdlen < 3:
|
||||
raise dns.exception.FormError("CSYNC too short")
|
||||
window = wire[current]
|
||||
octets = wire[current + 1]
|
||||
if octets == 0 or octets > 32:
|
||||
raise dns.exception.FormError("bad CSYNC octets")
|
||||
current += 2
|
||||
rdlen -= 2
|
||||
if rdlen < octets:
|
||||
raise dns.exception.FormError("bad CSYNC bitmap length")
|
||||
bitmap = bytearray(wire[current: current + octets].unwrap())
|
||||
current += octets
|
||||
rdlen -= octets
|
||||
windows.append((window, bitmap))
|
||||
return cls(rdclass, rdtype, serial, flags, windows)
|
||||
23
src/dns/rdtypes/ANY/DLV.py
Normal file
23
src/dns/rdtypes/ANY/DLV.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.dsbase
|
||||
|
||||
|
||||
class DLV(dns.rdtypes.dsbase.DSBase):
|
||||
|
||||
"""DLV record"""
|
||||
26
src/dns/rdtypes/ANY/DNAME.py
Normal file
26
src/dns/rdtypes/ANY/DNAME.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.nsbase
|
||||
|
||||
|
||||
class DNAME(dns.rdtypes.nsbase.UncompressedNS):
|
||||
|
||||
"""DNAME record"""
|
||||
|
||||
def to_digestable(self, origin=None):
|
||||
return self.target.to_digestable(origin)
|
||||
27
src/dns/rdtypes/ANY/DNSKEY.py
Normal file
27
src/dns/rdtypes/ANY/DNSKEY.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.dnskeybase
|
||||
from dns.rdtypes.dnskeybase import flags_to_text_set, flags_from_text_set
|
||||
|
||||
|
||||
__all__ = ['flags_to_text_set', 'flags_from_text_set']
|
||||
|
||||
|
||||
class DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
|
||||
|
||||
"""DNSKEY record"""
|
||||
23
src/dns/rdtypes/ANY/DS.py
Normal file
23
src/dns/rdtypes/ANY/DS.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.dsbase
|
||||
|
||||
|
||||
class DS(dns.rdtypes.dsbase.DSBase):
|
||||
|
||||
"""DS record"""
|
||||
29
src/dns/rdtypes/ANY/EUI48.py
Normal file
29
src/dns/rdtypes/ANY/EUI48.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (C) 2015 Red Hat, Inc.
|
||||
# Author: Petr Spacek <pspacek@redhat.com>
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.euibase
|
||||
|
||||
|
||||
class EUI48(dns.rdtypes.euibase.EUIBase):
|
||||
|
||||
"""EUI48 record
|
||||
|
||||
@ivar fingerprint: 48-bit Extended Unique Identifier (EUI-48)
|
||||
@type fingerprint: string
|
||||
@see: rfc7043.txt"""
|
||||
|
||||
byte_len = 6 # 0123456789ab (in hex)
|
||||
text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab
|
||||
29
src/dns/rdtypes/ANY/EUI64.py
Normal file
29
src/dns/rdtypes/ANY/EUI64.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (C) 2015 Red Hat, Inc.
|
||||
# Author: Petr Spacek <pspacek@redhat.com>
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.euibase
|
||||
|
||||
|
||||
class EUI64(dns.rdtypes.euibase.EUIBase):
|
||||
|
||||
"""EUI64 record
|
||||
|
||||
@ivar fingerprint: 64-bit Extended Unique Identifier (EUI-64)
|
||||
@type fingerprint: string
|
||||
@see: rfc7043.txt"""
|
||||
|
||||
byte_len = 8 # 0123456789abcdef (in hex)
|
||||
text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab-cd-ef
|
||||
162
src/dns/rdtypes/ANY/GPOS.py
Normal file
162
src/dns/rdtypes/ANY/GPOS.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
from dns._compat import long, text_type
|
||||
|
||||
|
||||
def _validate_float_string(what):
|
||||
if what[0] == b'-'[0] or what[0] == b'+'[0]:
|
||||
what = what[1:]
|
||||
if what.isdigit():
|
||||
return
|
||||
(left, right) = what.split(b'.')
|
||||
if left == b'' and right == b'':
|
||||
raise dns.exception.FormError
|
||||
if not left == b'' and not left.decode().isdigit():
|
||||
raise dns.exception.FormError
|
||||
if not right == b'' and not right.decode().isdigit():
|
||||
raise dns.exception.FormError
|
||||
|
||||
|
||||
def _sanitize(value):
|
||||
if isinstance(value, text_type):
|
||||
return value.encode()
|
||||
return value
|
||||
|
||||
|
||||
class GPOS(dns.rdata.Rdata):
|
||||
|
||||
"""GPOS record
|
||||
|
||||
@ivar latitude: latitude
|
||||
@type latitude: string
|
||||
@ivar longitude: longitude
|
||||
@type longitude: string
|
||||
@ivar altitude: altitude
|
||||
@type altitude: string
|
||||
@see: RFC 1712"""
|
||||
|
||||
__slots__ = ['latitude', 'longitude', 'altitude']
|
||||
|
||||
def __init__(self, rdclass, rdtype, latitude, longitude, altitude):
|
||||
super(GPOS, self).__init__(rdclass, rdtype)
|
||||
if isinstance(latitude, float) or \
|
||||
isinstance(latitude, int) or \
|
||||
isinstance(latitude, long):
|
||||
latitude = str(latitude)
|
||||
if isinstance(longitude, float) or \
|
||||
isinstance(longitude, int) or \
|
||||
isinstance(longitude, long):
|
||||
longitude = str(longitude)
|
||||
if isinstance(altitude, float) or \
|
||||
isinstance(altitude, int) or \
|
||||
isinstance(altitude, long):
|
||||
altitude = str(altitude)
|
||||
latitude = _sanitize(latitude)
|
||||
longitude = _sanitize(longitude)
|
||||
altitude = _sanitize(altitude)
|
||||
_validate_float_string(latitude)
|
||||
_validate_float_string(longitude)
|
||||
_validate_float_string(altitude)
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.altitude = altitude
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '{} {} {}'.format(self.latitude.decode(),
|
||||
self.longitude.decode(),
|
||||
self.altitude.decode())
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
latitude = tok.get_string()
|
||||
longitude = tok.get_string()
|
||||
altitude = tok.get_string()
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, latitude, longitude, altitude)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
l = len(self.latitude)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.latitude)
|
||||
l = len(self.longitude)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.longitude)
|
||||
l = len(self.altitude)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.altitude)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l > rdlen:
|
||||
raise dns.exception.FormError
|
||||
latitude = wire[current: current + l].unwrap()
|
||||
current += l
|
||||
rdlen -= l
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l > rdlen:
|
||||
raise dns.exception.FormError
|
||||
longitude = wire[current: current + l].unwrap()
|
||||
current += l
|
||||
rdlen -= l
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l != rdlen:
|
||||
raise dns.exception.FormError
|
||||
altitude = wire[current: current + l].unwrap()
|
||||
return cls(rdclass, rdtype, latitude, longitude, altitude)
|
||||
|
||||
def _get_float_latitude(self):
|
||||
return float(self.latitude)
|
||||
|
||||
def _set_float_latitude(self, value):
|
||||
self.latitude = str(value)
|
||||
|
||||
float_latitude = property(_get_float_latitude, _set_float_latitude,
|
||||
doc="latitude as a floating point value")
|
||||
|
||||
def _get_float_longitude(self):
|
||||
return float(self.longitude)
|
||||
|
||||
def _set_float_longitude(self, value):
|
||||
self.longitude = str(value)
|
||||
|
||||
float_longitude = property(_get_float_longitude, _set_float_longitude,
|
||||
doc="longitude as a floating point value")
|
||||
|
||||
def _get_float_altitude(self):
|
||||
return float(self.altitude)
|
||||
|
||||
def _set_float_altitude(self, value):
|
||||
self.altitude = str(value)
|
||||
|
||||
float_altitude = property(_get_float_altitude, _set_float_altitude,
|
||||
doc="altitude as a floating point value")
|
||||
86
src/dns/rdtypes/ANY/HINFO.py
Normal file
86
src/dns/rdtypes/ANY/HINFO.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
from dns._compat import text_type
|
||||
|
||||
|
||||
class HINFO(dns.rdata.Rdata):
|
||||
|
||||
"""HINFO record
|
||||
|
||||
@ivar cpu: the CPU type
|
||||
@type cpu: string
|
||||
@ivar os: the OS type
|
||||
@type os: string
|
||||
@see: RFC 1035"""
|
||||
|
||||
__slots__ = ['cpu', 'os']
|
||||
|
||||
def __init__(self, rdclass, rdtype, cpu, os):
|
||||
super(HINFO, self).__init__(rdclass, rdtype)
|
||||
if isinstance(cpu, text_type):
|
||||
self.cpu = cpu.encode()
|
||||
else:
|
||||
self.cpu = cpu
|
||||
if isinstance(os, text_type):
|
||||
self.os = os.encode()
|
||||
else:
|
||||
self.os = os
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '"{}" "{}"'.format(dns.rdata._escapify(self.cpu),
|
||||
dns.rdata._escapify(self.os))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
cpu = tok.get_string()
|
||||
os = tok.get_string()
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, cpu, os)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
l = len(self.cpu)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.cpu)
|
||||
l = len(self.os)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.os)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l > rdlen:
|
||||
raise dns.exception.FormError
|
||||
cpu = wire[current:current + l].unwrap()
|
||||
current += l
|
||||
rdlen -= l
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l != rdlen:
|
||||
raise dns.exception.FormError
|
||||
os = wire[current: current + l].unwrap()
|
||||
return cls(rdclass, rdtype, cpu, os)
|
||||
115
src/dns/rdtypes/ANY/HIP.py
Normal file
115
src/dns/rdtypes/ANY/HIP.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2010, 2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
|
||||
|
||||
class HIP(dns.rdata.Rdata):
|
||||
|
||||
"""HIP record
|
||||
|
||||
@ivar hit: the host identity tag
|
||||
@type hit: string
|
||||
@ivar algorithm: the public key cryptographic algorithm
|
||||
@type algorithm: int
|
||||
@ivar key: the public key
|
||||
@type key: string
|
||||
@ivar servers: the rendezvous servers
|
||||
@type servers: list of dns.name.Name objects
|
||||
@see: RFC 5205"""
|
||||
|
||||
__slots__ = ['hit', 'algorithm', 'key', 'servers']
|
||||
|
||||
def __init__(self, rdclass, rdtype, hit, algorithm, key, servers):
|
||||
super(HIP, self).__init__(rdclass, rdtype)
|
||||
self.hit = hit
|
||||
self.algorithm = algorithm
|
||||
self.key = key
|
||||
self.servers = servers
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
hit = binascii.hexlify(self.hit).decode()
|
||||
key = base64.b64encode(self.key).replace(b'\n', b'').decode()
|
||||
text = u''
|
||||
servers = []
|
||||
for server in self.servers:
|
||||
servers.append(server.choose_relativity(origin, relativize))
|
||||
if len(servers) > 0:
|
||||
text += (u' ' + u' '.join((x.to_unicode() for x in servers)))
|
||||
return u'%u %s %s%s' % (self.algorithm, hit, key, text)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
algorithm = tok.get_uint8()
|
||||
hit = binascii.unhexlify(tok.get_string().encode())
|
||||
if len(hit) > 255:
|
||||
raise dns.exception.SyntaxError("HIT too long")
|
||||
key = base64.b64decode(tok.get_string().encode())
|
||||
servers = []
|
||||
while 1:
|
||||
token = tok.get()
|
||||
if token.is_eol_or_eof():
|
||||
break
|
||||
server = dns.name.from_text(token.value, origin)
|
||||
server.choose_relativity(origin, relativize)
|
||||
servers.append(server)
|
||||
return cls(rdclass, rdtype, hit, algorithm, key, servers)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
lh = len(self.hit)
|
||||
lk = len(self.key)
|
||||
file.write(struct.pack("!BBH", lh, self.algorithm, lk))
|
||||
file.write(self.hit)
|
||||
file.write(self.key)
|
||||
for server in self.servers:
|
||||
server.to_wire(file, None, origin)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(lh, algorithm, lk) = struct.unpack('!BBH',
|
||||
wire[current: current + 4])
|
||||
current += 4
|
||||
rdlen -= 4
|
||||
hit = wire[current: current + lh].unwrap()
|
||||
current += lh
|
||||
rdlen -= lh
|
||||
key = wire[current: current + lk].unwrap()
|
||||
current += lk
|
||||
rdlen -= lk
|
||||
servers = []
|
||||
while rdlen > 0:
|
||||
(server, cused) = dns.name.from_wire(wire[: current + rdlen],
|
||||
current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
if origin is not None:
|
||||
server = server.relativize(origin)
|
||||
servers.append(server)
|
||||
return cls(rdclass, rdtype, hit, algorithm, key, servers)
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
servers = []
|
||||
for server in self.servers:
|
||||
server = server.choose_relativity(origin, relativize)
|
||||
servers.append(server)
|
||||
self.servers = servers
|
||||
99
src/dns/rdtypes/ANY/ISDN.py
Normal file
99
src/dns/rdtypes/ANY/ISDN.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
from dns._compat import text_type
|
||||
|
||||
|
||||
class ISDN(dns.rdata.Rdata):
|
||||
|
||||
"""ISDN record
|
||||
|
||||
@ivar address: the ISDN address
|
||||
@type address: string
|
||||
@ivar subaddress: the ISDN subaddress (or '' if not present)
|
||||
@type subaddress: string
|
||||
@see: RFC 1183"""
|
||||
|
||||
__slots__ = ['address', 'subaddress']
|
||||
|
||||
def __init__(self, rdclass, rdtype, address, subaddress):
|
||||
super(ISDN, self).__init__(rdclass, rdtype)
|
||||
if isinstance(address, text_type):
|
||||
self.address = address.encode()
|
||||
else:
|
||||
self.address = address
|
||||
if isinstance(address, text_type):
|
||||
self.subaddress = subaddress.encode()
|
||||
else:
|
||||
self.subaddress = subaddress
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
if self.subaddress:
|
||||
return '"{}" "{}"'.format(dns.rdata._escapify(self.address),
|
||||
dns.rdata._escapify(self.subaddress))
|
||||
else:
|
||||
return '"%s"' % dns.rdata._escapify(self.address)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
address = tok.get_string()
|
||||
t = tok.get()
|
||||
if not t.is_eol_or_eof():
|
||||
tok.unget(t)
|
||||
subaddress = tok.get_string()
|
||||
else:
|
||||
tok.unget(t)
|
||||
subaddress = ''
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, address, subaddress)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
l = len(self.address)
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.address)
|
||||
l = len(self.subaddress)
|
||||
if l > 0:
|
||||
assert l < 256
|
||||
file.write(struct.pack('!B', l))
|
||||
file.write(self.subaddress)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l > rdlen:
|
||||
raise dns.exception.FormError
|
||||
address = wire[current: current + l].unwrap()
|
||||
current += l
|
||||
rdlen -= l
|
||||
if rdlen > 0:
|
||||
l = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
if l != rdlen:
|
||||
raise dns.exception.FormError
|
||||
subaddress = wire[current: current + l].unwrap()
|
||||
else:
|
||||
subaddress = ''
|
||||
return cls(rdclass, rdtype, address, subaddress)
|
||||
327
src/dns/rdtypes/ANY/LOC.py
Normal file
327
src/dns/rdtypes/ANY/LOC.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
from dns._compat import long, xrange, round_py2_compat
|
||||
|
||||
|
||||
_pows = tuple(long(10**i) for i in range(0, 11))
|
||||
|
||||
# default values are in centimeters
|
||||
_default_size = 100.0
|
||||
_default_hprec = 1000000.0
|
||||
_default_vprec = 1000.0
|
||||
|
||||
|
||||
def _exponent_of(what, desc):
|
||||
if what == 0:
|
||||
return 0
|
||||
exp = None
|
||||
for i in xrange(len(_pows)):
|
||||
if what // _pows[i] == long(0):
|
||||
exp = i - 1
|
||||
break
|
||||
if exp is None or exp < 0:
|
||||
raise dns.exception.SyntaxError("%s value out of bounds" % desc)
|
||||
return exp
|
||||
|
||||
|
||||
def _float_to_tuple(what):
|
||||
if what < 0:
|
||||
sign = -1
|
||||
what *= -1
|
||||
else:
|
||||
sign = 1
|
||||
what = round_py2_compat(what * 3600000)
|
||||
degrees = int(what // 3600000)
|
||||
what -= degrees * 3600000
|
||||
minutes = int(what // 60000)
|
||||
what -= minutes * 60000
|
||||
seconds = int(what // 1000)
|
||||
what -= int(seconds * 1000)
|
||||
what = int(what)
|
||||
return (degrees, minutes, seconds, what, sign)
|
||||
|
||||
|
||||
def _tuple_to_float(what):
|
||||
value = float(what[0])
|
||||
value += float(what[1]) / 60.0
|
||||
value += float(what[2]) / 3600.0
|
||||
value += float(what[3]) / 3600000.0
|
||||
return float(what[4]) * value
|
||||
|
||||
|
||||
def _encode_size(what, desc):
|
||||
what = long(what)
|
||||
exponent = _exponent_of(what, desc) & 0xF
|
||||
base = what // pow(10, exponent) & 0xF
|
||||
return base * 16 + exponent
|
||||
|
||||
|
||||
def _decode_size(what, desc):
|
||||
exponent = what & 0x0F
|
||||
if exponent > 9:
|
||||
raise dns.exception.SyntaxError("bad %s exponent" % desc)
|
||||
base = (what & 0xF0) >> 4
|
||||
if base > 9:
|
||||
raise dns.exception.SyntaxError("bad %s base" % desc)
|
||||
return long(base) * pow(10, exponent)
|
||||
|
||||
|
||||
class LOC(dns.rdata.Rdata):
|
||||
|
||||
"""LOC record
|
||||
|
||||
@ivar latitude: latitude
|
||||
@type latitude: (int, int, int, int, sign) tuple specifying the degrees, minutes,
|
||||
seconds, milliseconds, and sign of the coordinate.
|
||||
@ivar longitude: longitude
|
||||
@type longitude: (int, int, int, int, sign) tuple specifying the degrees,
|
||||
minutes, seconds, milliseconds, and sign of the coordinate.
|
||||
@ivar altitude: altitude
|
||||
@type altitude: float
|
||||
@ivar size: size of the sphere
|
||||
@type size: float
|
||||
@ivar horizontal_precision: horizontal precision
|
||||
@type horizontal_precision: float
|
||||
@ivar vertical_precision: vertical precision
|
||||
@type vertical_precision: float
|
||||
@see: RFC 1876"""
|
||||
|
||||
__slots__ = ['latitude', 'longitude', 'altitude', 'size',
|
||||
'horizontal_precision', 'vertical_precision']
|
||||
|
||||
def __init__(self, rdclass, rdtype, latitude, longitude, altitude,
|
||||
size=_default_size, hprec=_default_hprec,
|
||||
vprec=_default_vprec):
|
||||
"""Initialize a LOC record instance.
|
||||
|
||||
The parameters I{latitude} and I{longitude} may be either a 4-tuple
|
||||
of integers specifying (degrees, minutes, seconds, milliseconds),
|
||||
or they may be floating point values specifying the number of
|
||||
degrees. The other parameters are floats. Size, horizontal precision,
|
||||
and vertical precision are specified in centimeters."""
|
||||
|
||||
super(LOC, self).__init__(rdclass, rdtype)
|
||||
if isinstance(latitude, int) or isinstance(latitude, long):
|
||||
latitude = float(latitude)
|
||||
if isinstance(latitude, float):
|
||||
latitude = _float_to_tuple(latitude)
|
||||
self.latitude = latitude
|
||||
if isinstance(longitude, int) or isinstance(longitude, long):
|
||||
longitude = float(longitude)
|
||||
if isinstance(longitude, float):
|
||||
longitude = _float_to_tuple(longitude)
|
||||
self.longitude = longitude
|
||||
self.altitude = float(altitude)
|
||||
self.size = float(size)
|
||||
self.horizontal_precision = float(hprec)
|
||||
self.vertical_precision = float(vprec)
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
if self.latitude[4] > 0:
|
||||
lat_hemisphere = 'N'
|
||||
else:
|
||||
lat_hemisphere = 'S'
|
||||
if self.longitude[4] > 0:
|
||||
long_hemisphere = 'E'
|
||||
else:
|
||||
long_hemisphere = 'W'
|
||||
text = "%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm" % (
|
||||
self.latitude[0], self.latitude[1],
|
||||
self.latitude[2], self.latitude[3], lat_hemisphere,
|
||||
self.longitude[0], self.longitude[1], self.longitude[2],
|
||||
self.longitude[3], long_hemisphere,
|
||||
self.altitude / 100.0
|
||||
)
|
||||
|
||||
# do not print default values
|
||||
if self.size != _default_size or \
|
||||
self.horizontal_precision != _default_hprec or \
|
||||
self.vertical_precision != _default_vprec:
|
||||
text += " {:0.2f}m {:0.2f}m {:0.2f}m".format(
|
||||
self.size / 100.0, self.horizontal_precision / 100.0,
|
||||
self.vertical_precision / 100.0
|
||||
)
|
||||
return text
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
latitude = [0, 0, 0, 0, 1]
|
||||
longitude = [0, 0, 0, 0, 1]
|
||||
size = _default_size
|
||||
hprec = _default_hprec
|
||||
vprec = _default_vprec
|
||||
|
||||
latitude[0] = tok.get_int()
|
||||
t = tok.get_string()
|
||||
if t.isdigit():
|
||||
latitude[1] = int(t)
|
||||
t = tok.get_string()
|
||||
if '.' in t:
|
||||
(seconds, milliseconds) = t.split('.')
|
||||
if not seconds.isdigit():
|
||||
raise dns.exception.SyntaxError(
|
||||
'bad latitude seconds value')
|
||||
latitude[2] = int(seconds)
|
||||
if latitude[2] >= 60:
|
||||
raise dns.exception.SyntaxError('latitude seconds >= 60')
|
||||
l = len(milliseconds)
|
||||
if l == 0 or l > 3 or not milliseconds.isdigit():
|
||||
raise dns.exception.SyntaxError(
|
||||
'bad latitude milliseconds value')
|
||||
if l == 1:
|
||||
m = 100
|
||||
elif l == 2:
|
||||
m = 10
|
||||
else:
|
||||
m = 1
|
||||
latitude[3] = m * int(milliseconds)
|
||||
t = tok.get_string()
|
||||
elif t.isdigit():
|
||||
latitude[2] = int(t)
|
||||
t = tok.get_string()
|
||||
if t == 'S':
|
||||
latitude[4] = -1
|
||||
elif t != 'N':
|
||||
raise dns.exception.SyntaxError('bad latitude hemisphere value')
|
||||
|
||||
longitude[0] = tok.get_int()
|
||||
t = tok.get_string()
|
||||
if t.isdigit():
|
||||
longitude[1] = int(t)
|
||||
t = tok.get_string()
|
||||
if '.' in t:
|
||||
(seconds, milliseconds) = t.split('.')
|
||||
if not seconds.isdigit():
|
||||
raise dns.exception.SyntaxError(
|
||||
'bad longitude seconds value')
|
||||
longitude[2] = int(seconds)
|
||||
if longitude[2] >= 60:
|
||||
raise dns.exception.SyntaxError('longitude seconds >= 60')
|
||||
l = len(milliseconds)
|
||||
if l == 0 or l > 3 or not milliseconds.isdigit():
|
||||
raise dns.exception.SyntaxError(
|
||||
'bad longitude milliseconds value')
|
||||
if l == 1:
|
||||
m = 100
|
||||
elif l == 2:
|
||||
m = 10
|
||||
else:
|
||||
m = 1
|
||||
longitude[3] = m * int(milliseconds)
|
||||
t = tok.get_string()
|
||||
elif t.isdigit():
|
||||
longitude[2] = int(t)
|
||||
t = tok.get_string()
|
||||
if t == 'W':
|
||||
longitude[4] = -1
|
||||
elif t != 'E':
|
||||
raise dns.exception.SyntaxError('bad longitude hemisphere value')
|
||||
|
||||
t = tok.get_string()
|
||||
if t[-1] == 'm':
|
||||
t = t[0: -1]
|
||||
altitude = float(t) * 100.0 # m -> cm
|
||||
|
||||
token = tok.get().unescape()
|
||||
if not token.is_eol_or_eof():
|
||||
value = token.value
|
||||
if value[-1] == 'm':
|
||||
value = value[0: -1]
|
||||
size = float(value) * 100.0 # m -> cm
|
||||
token = tok.get().unescape()
|
||||
if not token.is_eol_or_eof():
|
||||
value = token.value
|
||||
if value[-1] == 'm':
|
||||
value = value[0: -1]
|
||||
hprec = float(value) * 100.0 # m -> cm
|
||||
token = tok.get().unescape()
|
||||
if not token.is_eol_or_eof():
|
||||
value = token.value
|
||||
if value[-1] == 'm':
|
||||
value = value[0: -1]
|
||||
vprec = float(value) * 100.0 # m -> cm
|
||||
tok.get_eol()
|
||||
|
||||
return cls(rdclass, rdtype, latitude, longitude, altitude,
|
||||
size, hprec, vprec)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
milliseconds = (self.latitude[0] * 3600000 +
|
||||
self.latitude[1] * 60000 +
|
||||
self.latitude[2] * 1000 +
|
||||
self.latitude[3]) * self.latitude[4]
|
||||
latitude = long(0x80000000) + milliseconds
|
||||
milliseconds = (self.longitude[0] * 3600000 +
|
||||
self.longitude[1] * 60000 +
|
||||
self.longitude[2] * 1000 +
|
||||
self.longitude[3]) * self.longitude[4]
|
||||
longitude = long(0x80000000) + milliseconds
|
||||
altitude = long(self.altitude) + long(10000000)
|
||||
size = _encode_size(self.size, "size")
|
||||
hprec = _encode_size(self.horizontal_precision, "horizontal precision")
|
||||
vprec = _encode_size(self.vertical_precision, "vertical precision")
|
||||
wire = struct.pack("!BBBBIII", 0, size, hprec, vprec, latitude,
|
||||
longitude, altitude)
|
||||
file.write(wire)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(version, size, hprec, vprec, latitude, longitude, altitude) = \
|
||||
struct.unpack("!BBBBIII", wire[current: current + rdlen])
|
||||
if latitude > long(0x80000000):
|
||||
latitude = float(latitude - long(0x80000000)) / 3600000
|
||||
else:
|
||||
latitude = -1 * float(long(0x80000000) - latitude) / 3600000
|
||||
if latitude < -90.0 or latitude > 90.0:
|
||||
raise dns.exception.FormError("bad latitude")
|
||||
if longitude > long(0x80000000):
|
||||
longitude = float(longitude - long(0x80000000)) / 3600000
|
||||
else:
|
||||
longitude = -1 * float(long(0x80000000) - longitude) / 3600000
|
||||
if longitude < -180.0 or longitude > 180.0:
|
||||
raise dns.exception.FormError("bad longitude")
|
||||
altitude = float(altitude) - 10000000.0
|
||||
size = _decode_size(size, "size")
|
||||
hprec = _decode_size(hprec, "horizontal precision")
|
||||
vprec = _decode_size(vprec, "vertical precision")
|
||||
return cls(rdclass, rdtype, latitude, longitude, altitude,
|
||||
size, hprec, vprec)
|
||||
|
||||
def _get_float_latitude(self):
|
||||
return _tuple_to_float(self.latitude)
|
||||
|
||||
def _set_float_latitude(self, value):
|
||||
self.latitude = _float_to_tuple(value)
|
||||
|
||||
float_latitude = property(_get_float_latitude, _set_float_latitude,
|
||||
doc="latitude as a floating point value")
|
||||
|
||||
def _get_float_longitude(self):
|
||||
return _tuple_to_float(self.longitude)
|
||||
|
||||
def _set_float_longitude(self, value):
|
||||
self.longitude = _float_to_tuple(value)
|
||||
|
||||
float_longitude = property(_get_float_longitude, _set_float_longitude,
|
||||
doc="longitude as a floating point value")
|
||||
23
src/dns/rdtypes/ANY/MX.py
Normal file
23
src/dns/rdtypes/ANY/MX.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.mxbase
|
||||
|
||||
|
||||
class MX(dns.rdtypes.mxbase.MXBase):
|
||||
|
||||
"""MX record"""
|
||||
23
src/dns/rdtypes/ANY/NS.py
Normal file
23
src/dns/rdtypes/ANY/NS.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.nsbase
|
||||
|
||||
|
||||
class NS(dns.rdtypes.nsbase.NSBase):
|
||||
|
||||
"""NS record"""
|
||||
128
src/dns/rdtypes/ANY/NSEC.py
Normal file
128
src/dns/rdtypes/ANY/NSEC.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
import dns.name
|
||||
from dns._compat import xrange
|
||||
|
||||
|
||||
class NSEC(dns.rdata.Rdata):
|
||||
|
||||
"""NSEC record
|
||||
|
||||
@ivar next: the next name
|
||||
@type next: dns.name.Name object
|
||||
@ivar windows: the windowed bitmap list
|
||||
@type windows: list of (window number, string) tuples"""
|
||||
|
||||
__slots__ = ['next', 'windows']
|
||||
|
||||
def __init__(self, rdclass, rdtype, next, windows):
|
||||
super(NSEC, self).__init__(rdclass, rdtype)
|
||||
self.next = next
|
||||
self.windows = windows
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
next = self.next.choose_relativity(origin, relativize)
|
||||
text = ''
|
||||
for (window, bitmap) in self.windows:
|
||||
bits = []
|
||||
for i in xrange(0, len(bitmap)):
|
||||
byte = bitmap[i]
|
||||
for j in xrange(0, 8):
|
||||
if byte & (0x80 >> j):
|
||||
bits.append(dns.rdatatype.to_text(window * 256 +
|
||||
i * 8 + j))
|
||||
text += (' ' + ' '.join(bits))
|
||||
return '{}{}'.format(next, text)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
next = tok.get_name()
|
||||
next = next.choose_relativity(origin, relativize)
|
||||
rdtypes = []
|
||||
while 1:
|
||||
token = tok.get().unescape()
|
||||
if token.is_eol_or_eof():
|
||||
break
|
||||
nrdtype = dns.rdatatype.from_text(token.value)
|
||||
if nrdtype == 0:
|
||||
raise dns.exception.SyntaxError("NSEC with bit 0")
|
||||
if nrdtype > 65535:
|
||||
raise dns.exception.SyntaxError("NSEC with bit > 65535")
|
||||
rdtypes.append(nrdtype)
|
||||
rdtypes.sort()
|
||||
window = 0
|
||||
octets = 0
|
||||
prior_rdtype = 0
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
windows = []
|
||||
for nrdtype in rdtypes:
|
||||
if nrdtype == prior_rdtype:
|
||||
continue
|
||||
prior_rdtype = nrdtype
|
||||
new_window = nrdtype // 256
|
||||
if new_window != window:
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
window = new_window
|
||||
offset = nrdtype % 256
|
||||
byte = offset // 8
|
||||
bit = offset % 8
|
||||
octets = byte + 1
|
||||
bitmap[byte] = bitmap[byte] | (0x80 >> bit)
|
||||
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
return cls(rdclass, rdtype, next, windows)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
self.next.to_wire(file, None, origin)
|
||||
for (window, bitmap) in self.windows:
|
||||
file.write(struct.pack('!BB', window, len(bitmap)))
|
||||
file.write(bitmap)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(next, cused) = dns.name.from_wire(wire[: current + rdlen], current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
windows = []
|
||||
while rdlen > 0:
|
||||
if rdlen < 3:
|
||||
raise dns.exception.FormError("NSEC too short")
|
||||
window = wire[current]
|
||||
octets = wire[current + 1]
|
||||
if octets == 0 or octets > 32:
|
||||
raise dns.exception.FormError("bad NSEC octets")
|
||||
current += 2
|
||||
rdlen -= 2
|
||||
if rdlen < octets:
|
||||
raise dns.exception.FormError("bad NSEC bitmap length")
|
||||
bitmap = bytearray(wire[current: current + octets].unwrap())
|
||||
current += octets
|
||||
rdlen -= octets
|
||||
windows.append((window, bitmap))
|
||||
if origin is not None:
|
||||
next = next.relativize(origin)
|
||||
return cls(rdclass, rdtype, next, windows)
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
self.next = self.next.choose_relativity(origin, relativize)
|
||||
196
src/dns/rdtypes/ANY/NSEC3.py
Normal file
196
src/dns/rdtypes/ANY/NSEC3.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2017 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import string
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
from dns._compat import xrange, text_type, PY3
|
||||
|
||||
# pylint: disable=deprecated-string-function
|
||||
if PY3:
|
||||
b32_hex_to_normal = bytes.maketrans(b'0123456789ABCDEFGHIJKLMNOPQRSTUV',
|
||||
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
|
||||
b32_normal_to_hex = bytes.maketrans(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
||||
b'0123456789ABCDEFGHIJKLMNOPQRSTUV')
|
||||
else:
|
||||
b32_hex_to_normal = string.maketrans('0123456789ABCDEFGHIJKLMNOPQRSTUV',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
|
||||
b32_normal_to_hex = string.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUV')
|
||||
# pylint: enable=deprecated-string-function
|
||||
|
||||
|
||||
# hash algorithm constants
|
||||
SHA1 = 1
|
||||
|
||||
# flag constants
|
||||
OPTOUT = 1
|
||||
|
||||
|
||||
class NSEC3(dns.rdata.Rdata):
|
||||
|
||||
"""NSEC3 record
|
||||
|
||||
@ivar algorithm: the hash algorithm number
|
||||
@type algorithm: int
|
||||
@ivar flags: the flags
|
||||
@type flags: int
|
||||
@ivar iterations: the number of iterations
|
||||
@type iterations: int
|
||||
@ivar salt: the salt
|
||||
@type salt: string
|
||||
@ivar next: the next name hash
|
||||
@type next: string
|
||||
@ivar windows: the windowed bitmap list
|
||||
@type windows: list of (window number, string) tuples"""
|
||||
|
||||
__slots__ = ['algorithm', 'flags', 'iterations', 'salt', 'next', 'windows']
|
||||
|
||||
def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt,
|
||||
next, windows):
|
||||
super(NSEC3, self).__init__(rdclass, rdtype)
|
||||
self.algorithm = algorithm
|
||||
self.flags = flags
|
||||
self.iterations = iterations
|
||||
if isinstance(salt, text_type):
|
||||
self.salt = salt.encode()
|
||||
else:
|
||||
self.salt = salt
|
||||
self.next = next
|
||||
self.windows = windows
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
next = base64.b32encode(self.next).translate(
|
||||
b32_normal_to_hex).lower().decode()
|
||||
if self.salt == b'':
|
||||
salt = '-'
|
||||
else:
|
||||
salt = binascii.hexlify(self.salt).decode()
|
||||
text = u''
|
||||
for (window, bitmap) in self.windows:
|
||||
bits = []
|
||||
for i in xrange(0, len(bitmap)):
|
||||
byte = bitmap[i]
|
||||
for j in xrange(0, 8):
|
||||
if byte & (0x80 >> j):
|
||||
bits.append(dns.rdatatype.to_text(window * 256 +
|
||||
i * 8 + j))
|
||||
text += (u' ' + u' '.join(bits))
|
||||
return u'%u %u %u %s %s%s' % (self.algorithm, self.flags,
|
||||
self.iterations, salt, next, text)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
algorithm = tok.get_uint8()
|
||||
flags = tok.get_uint8()
|
||||
iterations = tok.get_uint16()
|
||||
salt = tok.get_string()
|
||||
if salt == u'-':
|
||||
salt = b''
|
||||
else:
|
||||
salt = binascii.unhexlify(salt.encode('ascii'))
|
||||
next = tok.get_string().encode(
|
||||
'ascii').upper().translate(b32_hex_to_normal)
|
||||
next = base64.b32decode(next)
|
||||
rdtypes = []
|
||||
while 1:
|
||||
token = tok.get().unescape()
|
||||
if token.is_eol_or_eof():
|
||||
break
|
||||
nrdtype = dns.rdatatype.from_text(token.value)
|
||||
if nrdtype == 0:
|
||||
raise dns.exception.SyntaxError("NSEC3 with bit 0")
|
||||
if nrdtype > 65535:
|
||||
raise dns.exception.SyntaxError("NSEC3 with bit > 65535")
|
||||
rdtypes.append(nrdtype)
|
||||
rdtypes.sort()
|
||||
window = 0
|
||||
octets = 0
|
||||
prior_rdtype = 0
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
windows = []
|
||||
for nrdtype in rdtypes:
|
||||
if nrdtype == prior_rdtype:
|
||||
continue
|
||||
prior_rdtype = nrdtype
|
||||
new_window = nrdtype // 256
|
||||
if new_window != window:
|
||||
if octets != 0:
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
bitmap = bytearray(b'\0' * 32)
|
||||
window = new_window
|
||||
offset = nrdtype % 256
|
||||
byte = offset // 8
|
||||
bit = offset % 8
|
||||
octets = byte + 1
|
||||
bitmap[byte] = bitmap[byte] | (0x80 >> bit)
|
||||
if octets != 0:
|
||||
windows.append((window, bitmap[0:octets]))
|
||||
return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,
|
||||
windows)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
l = len(self.salt)
|
||||
file.write(struct.pack("!BBHB", self.algorithm, self.flags,
|
||||
self.iterations, l))
|
||||
file.write(self.salt)
|
||||
l = len(self.next)
|
||||
file.write(struct.pack("!B", l))
|
||||
file.write(self.next)
|
||||
for (window, bitmap) in self.windows:
|
||||
file.write(struct.pack("!BB", window, len(bitmap)))
|
||||
file.write(bitmap)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(algorithm, flags, iterations, slen) = \
|
||||
struct.unpack('!BBHB', wire[current: current + 5])
|
||||
|
||||
current += 5
|
||||
rdlen -= 5
|
||||
salt = wire[current: current + slen].unwrap()
|
||||
current += slen
|
||||
rdlen -= slen
|
||||
nlen = wire[current]
|
||||
current += 1
|
||||
rdlen -= 1
|
||||
next = wire[current: current + nlen].unwrap()
|
||||
current += nlen
|
||||
rdlen -= nlen
|
||||
windows = []
|
||||
while rdlen > 0:
|
||||
if rdlen < 3:
|
||||
raise dns.exception.FormError("NSEC3 too short")
|
||||
window = wire[current]
|
||||
octets = wire[current + 1]
|
||||
if octets == 0 or octets > 32:
|
||||
raise dns.exception.FormError("bad NSEC3 octets")
|
||||
current += 2
|
||||
rdlen -= 2
|
||||
if rdlen < octets:
|
||||
raise dns.exception.FormError("bad NSEC3 bitmap length")
|
||||
bitmap = bytearray(wire[current: current + octets].unwrap())
|
||||
current += octets
|
||||
rdlen -= octets
|
||||
windows.append((window, bitmap))
|
||||
return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,
|
||||
windows)
|
||||
90
src/dns/rdtypes/ANY/NSEC3PARAM.py
Normal file
90
src/dns/rdtypes/ANY/NSEC3PARAM.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
from dns._compat import text_type
|
||||
|
||||
|
||||
class NSEC3PARAM(dns.rdata.Rdata):
|
||||
|
||||
"""NSEC3PARAM record
|
||||
|
||||
@ivar algorithm: the hash algorithm number
|
||||
@type algorithm: int
|
||||
@ivar flags: the flags
|
||||
@type flags: int
|
||||
@ivar iterations: the number of iterations
|
||||
@type iterations: int
|
||||
@ivar salt: the salt
|
||||
@type salt: string"""
|
||||
|
||||
__slots__ = ['algorithm', 'flags', 'iterations', 'salt']
|
||||
|
||||
def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt):
|
||||
super(NSEC3PARAM, self).__init__(rdclass, rdtype)
|
||||
self.algorithm = algorithm
|
||||
self.flags = flags
|
||||
self.iterations = iterations
|
||||
if isinstance(salt, text_type):
|
||||
self.salt = salt.encode()
|
||||
else:
|
||||
self.salt = salt
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
if self.salt == b'':
|
||||
salt = '-'
|
||||
else:
|
||||
salt = binascii.hexlify(self.salt).decode()
|
||||
return '%u %u %u %s' % (self.algorithm, self.flags, self.iterations,
|
||||
salt)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
algorithm = tok.get_uint8()
|
||||
flags = tok.get_uint8()
|
||||
iterations = tok.get_uint16()
|
||||
salt = tok.get_string()
|
||||
if salt == '-':
|
||||
salt = ''
|
||||
else:
|
||||
salt = binascii.unhexlify(salt.encode())
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, algorithm, flags, iterations, salt)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
l = len(self.salt)
|
||||
file.write(struct.pack("!BBHB", self.algorithm, self.flags,
|
||||
self.iterations, l))
|
||||
file.write(self.salt)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(algorithm, flags, iterations, slen) = \
|
||||
struct.unpack('!BBHB',
|
||||
wire[current: current + 5])
|
||||
current += 5
|
||||
rdlen -= 5
|
||||
salt = wire[current: current + slen].unwrap()
|
||||
current += slen
|
||||
rdlen -= slen
|
||||
if rdlen != 0:
|
||||
raise dns.exception.FormError
|
||||
return cls(rdclass, rdtype, algorithm, flags, iterations, salt)
|
||||
60
src/dns/rdtypes/ANY/OPENPGPKEY.py
Normal file
60
src/dns/rdtypes/ANY/OPENPGPKEY.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2016 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import base64
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.tokenizer
|
||||
|
||||
class OPENPGPKEY(dns.rdata.Rdata):
|
||||
|
||||
"""OPENPGPKEY record
|
||||
|
||||
@ivar key: the key
|
||||
@type key: bytes
|
||||
@see: RFC 7929
|
||||
"""
|
||||
|
||||
def __init__(self, rdclass, rdtype, key):
|
||||
super(OPENPGPKEY, self).__init__(rdclass, rdtype)
|
||||
self.key = key
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return dns.rdata._base64ify(self.key)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
chunks = []
|
||||
while 1:
|
||||
t = tok.get().unescape()
|
||||
if t.is_eol_or_eof():
|
||||
break
|
||||
if not t.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
chunks.append(t.value.encode())
|
||||
b64 = b''.join(chunks)
|
||||
key = base64.b64decode(b64)
|
||||
return cls(rdclass, rdtype, key)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
file.write(self.key)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
key = wire[current: current + rdlen].unwrap()
|
||||
return cls(rdclass, rdtype, key)
|
||||
23
src/dns/rdtypes/ANY/PTR.py
Normal file
23
src/dns/rdtypes/ANY/PTR.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.nsbase
|
||||
|
||||
|
||||
class PTR(dns.rdtypes.nsbase.NSBase):
|
||||
|
||||
"""PTR record"""
|
||||
82
src/dns/rdtypes/ANY/RP.py
Normal file
82
src/dns/rdtypes/ANY/RP.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.name
|
||||
|
||||
|
||||
class RP(dns.rdata.Rdata):
|
||||
|
||||
"""RP record
|
||||
|
||||
@ivar mbox: The responsible person's mailbox
|
||||
@type mbox: dns.name.Name object
|
||||
@ivar txt: The owner name of a node with TXT records, or the root name
|
||||
if no TXT records are associated with this RP.
|
||||
@type txt: dns.name.Name object
|
||||
@see: RFC 1183"""
|
||||
|
||||
__slots__ = ['mbox', 'txt']
|
||||
|
||||
def __init__(self, rdclass, rdtype, mbox, txt):
|
||||
super(RP, self).__init__(rdclass, rdtype)
|
||||
self.mbox = mbox
|
||||
self.txt = txt
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
mbox = self.mbox.choose_relativity(origin, relativize)
|
||||
txt = self.txt.choose_relativity(origin, relativize)
|
||||
return "{} {}".format(str(mbox), str(txt))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
mbox = tok.get_name()
|
||||
txt = tok.get_name()
|
||||
mbox = mbox.choose_relativity(origin, relativize)
|
||||
txt = txt.choose_relativity(origin, relativize)
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, mbox, txt)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
self.mbox.to_wire(file, None, origin)
|
||||
self.txt.to_wire(file, None, origin)
|
||||
|
||||
def to_digestable(self, origin=None):
|
||||
return self.mbox.to_digestable(origin) + \
|
||||
self.txt.to_digestable(origin)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(mbox, cused) = dns.name.from_wire(wire[: current + rdlen],
|
||||
current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
if rdlen <= 0:
|
||||
raise dns.exception.FormError
|
||||
(txt, cused) = dns.name.from_wire(wire[: current + rdlen],
|
||||
current)
|
||||
if cused != rdlen:
|
||||
raise dns.exception.FormError
|
||||
if origin is not None:
|
||||
mbox = mbox.relativize(origin)
|
||||
txt = txt.relativize(origin)
|
||||
return cls(rdclass, rdtype, mbox, txt)
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
self.mbox = self.mbox.choose_relativity(origin, relativize)
|
||||
self.txt = self.txt.choose_relativity(origin, relativize)
|
||||
158
src/dns/rdtypes/ANY/RRSIG.py
Normal file
158
src/dns/rdtypes/ANY/RRSIG.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import base64
|
||||
import calendar
|
||||
import struct
|
||||
import time
|
||||
|
||||
import dns.dnssec
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
|
||||
|
||||
class BadSigTime(dns.exception.DNSException):
|
||||
|
||||
"""Time in DNS SIG or RRSIG resource record cannot be parsed."""
|
||||
|
||||
|
||||
def sigtime_to_posixtime(what):
|
||||
if len(what) != 14:
|
||||
raise BadSigTime
|
||||
year = int(what[0:4])
|
||||
month = int(what[4:6])
|
||||
day = int(what[6:8])
|
||||
hour = int(what[8:10])
|
||||
minute = int(what[10:12])
|
||||
second = int(what[12:14])
|
||||
return calendar.timegm((year, month, day, hour, minute, second,
|
||||
0, 0, 0))
|
||||
|
||||
|
||||
def posixtime_to_sigtime(what):
|
||||
return time.strftime('%Y%m%d%H%M%S', time.gmtime(what))
|
||||
|
||||
|
||||
class RRSIG(dns.rdata.Rdata):
|
||||
|
||||
"""RRSIG record
|
||||
|
||||
@ivar type_covered: the rdata type this signature covers
|
||||
@type type_covered: int
|
||||
@ivar algorithm: the algorithm used for the sig
|
||||
@type algorithm: int
|
||||
@ivar labels: number of labels
|
||||
@type labels: int
|
||||
@ivar original_ttl: the original TTL
|
||||
@type original_ttl: long
|
||||
@ivar expiration: signature expiration time
|
||||
@type expiration: long
|
||||
@ivar inception: signature inception time
|
||||
@type inception: long
|
||||
@ivar key_tag: the key tag
|
||||
@type key_tag: int
|
||||
@ivar signer: the signer
|
||||
@type signer: dns.name.Name object
|
||||
@ivar signature: the signature
|
||||
@type signature: string"""
|
||||
|
||||
__slots__ = ['type_covered', 'algorithm', 'labels', 'original_ttl',
|
||||
'expiration', 'inception', 'key_tag', 'signer',
|
||||
'signature']
|
||||
|
||||
def __init__(self, rdclass, rdtype, type_covered, algorithm, labels,
|
||||
original_ttl, expiration, inception, key_tag, signer,
|
||||
signature):
|
||||
super(RRSIG, self).__init__(rdclass, rdtype)
|
||||
self.type_covered = type_covered
|
||||
self.algorithm = algorithm
|
||||
self.labels = labels
|
||||
self.original_ttl = original_ttl
|
||||
self.expiration = expiration
|
||||
self.inception = inception
|
||||
self.key_tag = key_tag
|
||||
self.signer = signer
|
||||
self.signature = signature
|
||||
|
||||
def covers(self):
|
||||
return self.type_covered
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '%s %d %d %d %s %s %d %s %s' % (
|
||||
dns.rdatatype.to_text(self.type_covered),
|
||||
self.algorithm,
|
||||
self.labels,
|
||||
self.original_ttl,
|
||||
posixtime_to_sigtime(self.expiration),
|
||||
posixtime_to_sigtime(self.inception),
|
||||
self.key_tag,
|
||||
self.signer.choose_relativity(origin, relativize),
|
||||
dns.rdata._base64ify(self.signature)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
type_covered = dns.rdatatype.from_text(tok.get_string())
|
||||
algorithm = dns.dnssec.algorithm_from_text(tok.get_string())
|
||||
labels = tok.get_int()
|
||||
original_ttl = tok.get_ttl()
|
||||
expiration = sigtime_to_posixtime(tok.get_string())
|
||||
inception = sigtime_to_posixtime(tok.get_string())
|
||||
key_tag = tok.get_int()
|
||||
signer = tok.get_name()
|
||||
signer = signer.choose_relativity(origin, relativize)
|
||||
chunks = []
|
||||
while 1:
|
||||
t = tok.get().unescape()
|
||||
if t.is_eol_or_eof():
|
||||
break
|
||||
if not t.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
chunks.append(t.value.encode())
|
||||
b64 = b''.join(chunks)
|
||||
signature = base64.b64decode(b64)
|
||||
return cls(rdclass, rdtype, type_covered, algorithm, labels,
|
||||
original_ttl, expiration, inception, key_tag, signer,
|
||||
signature)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
header = struct.pack('!HBBIIIH', self.type_covered,
|
||||
self.algorithm, self.labels,
|
||||
self.original_ttl, self.expiration,
|
||||
self.inception, self.key_tag)
|
||||
file.write(header)
|
||||
self.signer.to_wire(file, None, origin)
|
||||
file.write(self.signature)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
header = struct.unpack('!HBBIIIH', wire[current: current + 18])
|
||||
current += 18
|
||||
rdlen -= 18
|
||||
(signer, cused) = dns.name.from_wire(wire[: current + rdlen], current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
if origin is not None:
|
||||
signer = signer.relativize(origin)
|
||||
signature = wire[current: current + rdlen].unwrap()
|
||||
return cls(rdclass, rdtype, header[0], header[1], header[2],
|
||||
header[3], header[4], header[5], header[6], signer,
|
||||
signature)
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
self.signer = self.signer.choose_relativity(origin, relativize)
|
||||
23
src/dns/rdtypes/ANY/RT.py
Normal file
23
src/dns/rdtypes/ANY/RT.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.mxbase
|
||||
|
||||
|
||||
class RT(dns.rdtypes.mxbase.UncompressedDowncasingMX):
|
||||
|
||||
"""RT record"""
|
||||
116
src/dns/rdtypes/ANY/SOA.py
Normal file
116
src/dns/rdtypes/ANY/SOA.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
|
||||
import dns.exception
|
||||
import dns.rdata
|
||||
import dns.name
|
||||
|
||||
|
||||
class SOA(dns.rdata.Rdata):
|
||||
|
||||
"""SOA record
|
||||
|
||||
@ivar mname: the SOA MNAME (master name) field
|
||||
@type mname: dns.name.Name object
|
||||
@ivar rname: the SOA RNAME (responsible name) field
|
||||
@type rname: dns.name.Name object
|
||||
@ivar serial: The zone's serial number
|
||||
@type serial: int
|
||||
@ivar refresh: The zone's refresh value (in seconds)
|
||||
@type refresh: int
|
||||
@ivar retry: The zone's retry value (in seconds)
|
||||
@type retry: int
|
||||
@ivar expire: The zone's expiration value (in seconds)
|
||||
@type expire: int
|
||||
@ivar minimum: The zone's negative caching time (in seconds, called
|
||||
"minimum" for historical reasons)
|
||||
@type minimum: int
|
||||
@see: RFC 1035"""
|
||||
|
||||
__slots__ = ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire',
|
||||
'minimum']
|
||||
|
||||
def __init__(self, rdclass, rdtype, mname, rname, serial, refresh, retry,
|
||||
expire, minimum):
|
||||
super(SOA, self).__init__(rdclass, rdtype)
|
||||
self.mname = mname
|
||||
self.rname = rname
|
||||
self.serial = serial
|
||||
self.refresh = refresh
|
||||
self.retry = retry
|
||||
self.expire = expire
|
||||
self.minimum = minimum
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
mname = self.mname.choose_relativity(origin, relativize)
|
||||
rname = self.rname.choose_relativity(origin, relativize)
|
||||
return '%s %s %d %d %d %d %d' % (
|
||||
mname, rname, self.serial, self.refresh, self.retry,
|
||||
self.expire, self.minimum)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
mname = tok.get_name()
|
||||
rname = tok.get_name()
|
||||
mname = mname.choose_relativity(origin, relativize)
|
||||
rname = rname.choose_relativity(origin, relativize)
|
||||
serial = tok.get_uint32()
|
||||
refresh = tok.get_ttl()
|
||||
retry = tok.get_ttl()
|
||||
expire = tok.get_ttl()
|
||||
minimum = tok.get_ttl()
|
||||
tok.get_eol()
|
||||
return cls(rdclass, rdtype, mname, rname, serial, refresh, retry,
|
||||
expire, minimum)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
self.mname.to_wire(file, compress, origin)
|
||||
self.rname.to_wire(file, compress, origin)
|
||||
five_ints = struct.pack('!IIIII', self.serial, self.refresh,
|
||||
self.retry, self.expire, self.minimum)
|
||||
file.write(five_ints)
|
||||
|
||||
def to_digestable(self, origin=None):
|
||||
return self.mname.to_digestable(origin) + \
|
||||
self.rname.to_digestable(origin) + \
|
||||
struct.pack('!IIIII', self.serial, self.refresh,
|
||||
self.retry, self.expire, self.minimum)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
(mname, cused) = dns.name.from_wire(wire[: current + rdlen], current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
(rname, cused) = dns.name.from_wire(wire[: current + rdlen], current)
|
||||
current += cused
|
||||
rdlen -= cused
|
||||
if rdlen != 20:
|
||||
raise dns.exception.FormError
|
||||
five_ints = struct.unpack('!IIIII',
|
||||
wire[current: current + rdlen])
|
||||
if origin is not None:
|
||||
mname = mname.relativize(origin)
|
||||
rname = rname.relativize(origin)
|
||||
return cls(rdclass, rdtype, mname, rname,
|
||||
five_ints[0], five_ints[1], five_ints[2], five_ints[3],
|
||||
five_ints[4])
|
||||
|
||||
def choose_relativity(self, origin=None, relativize=True):
|
||||
self.mname = self.mname.choose_relativity(origin, relativize)
|
||||
self.rname = self.rname.choose_relativity(origin, relativize)
|
||||
25
src/dns/rdtypes/ANY/SPF.py
Normal file
25
src/dns/rdtypes/ANY/SPF.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.txtbase
|
||||
|
||||
|
||||
class SPF(dns.rdtypes.txtbase.TXTBase):
|
||||
|
||||
"""SPF record
|
||||
|
||||
@see: RFC 4408"""
|
||||
79
src/dns/rdtypes/ANY/SSHFP.py
Normal file
79
src/dns/rdtypes/ANY/SSHFP.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
|
||||
|
||||
class SSHFP(dns.rdata.Rdata):
|
||||
|
||||
"""SSHFP record
|
||||
|
||||
@ivar algorithm: the algorithm
|
||||
@type algorithm: int
|
||||
@ivar fp_type: the digest type
|
||||
@type fp_type: int
|
||||
@ivar fingerprint: the fingerprint
|
||||
@type fingerprint: string
|
||||
@see: draft-ietf-secsh-dns-05.txt"""
|
||||
|
||||
__slots__ = ['algorithm', 'fp_type', 'fingerprint']
|
||||
|
||||
def __init__(self, rdclass, rdtype, algorithm, fp_type,
|
||||
fingerprint):
|
||||
super(SSHFP, self).__init__(rdclass, rdtype)
|
||||
self.algorithm = algorithm
|
||||
self.fp_type = fp_type
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '%d %d %s' % (self.algorithm,
|
||||
self.fp_type,
|
||||
dns.rdata._hexify(self.fingerprint,
|
||||
chunksize=128))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
algorithm = tok.get_uint8()
|
||||
fp_type = tok.get_uint8()
|
||||
chunks = []
|
||||
while 1:
|
||||
t = tok.get().unescape()
|
||||
if t.is_eol_or_eof():
|
||||
break
|
||||
if not t.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
chunks.append(t.value.encode())
|
||||
fingerprint = b''.join(chunks)
|
||||
fingerprint = binascii.unhexlify(fingerprint)
|
||||
return cls(rdclass, rdtype, algorithm, fp_type, fingerprint)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
header = struct.pack("!BB", self.algorithm, self.fp_type)
|
||||
file.write(header)
|
||||
file.write(self.fingerprint)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
header = struct.unpack("!BB", wire[current: current + 2])
|
||||
current += 2
|
||||
rdlen -= 2
|
||||
fingerprint = wire[current: current + rdlen].unwrap()
|
||||
return cls(rdclass, rdtype, header[0], header[1], fingerprint)
|
||||
84
src/dns/rdtypes/ANY/TLSA.py
Normal file
84
src/dns/rdtypes/ANY/TLSA.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
|
||||
|
||||
class TLSA(dns.rdata.Rdata):
|
||||
|
||||
"""TLSA record
|
||||
|
||||
@ivar usage: The certificate usage
|
||||
@type usage: int
|
||||
@ivar selector: The selector field
|
||||
@type selector: int
|
||||
@ivar mtype: The 'matching type' field
|
||||
@type mtype: int
|
||||
@ivar cert: The 'Certificate Association Data' field
|
||||
@type cert: string
|
||||
@see: RFC 6698"""
|
||||
|
||||
__slots__ = ['usage', 'selector', 'mtype', 'cert']
|
||||
|
||||
def __init__(self, rdclass, rdtype, usage, selector,
|
||||
mtype, cert):
|
||||
super(TLSA, self).__init__(rdclass, rdtype)
|
||||
self.usage = usage
|
||||
self.selector = selector
|
||||
self.mtype = mtype
|
||||
self.cert = cert
|
||||
|
||||
def to_text(self, origin=None, relativize=True, **kw):
|
||||
return '%d %d %d %s' % (self.usage,
|
||||
self.selector,
|
||||
self.mtype,
|
||||
dns.rdata._hexify(self.cert,
|
||||
chunksize=128))
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True):
|
||||
usage = tok.get_uint8()
|
||||
selector = tok.get_uint8()
|
||||
mtype = tok.get_uint8()
|
||||
cert_chunks = []
|
||||
while 1:
|
||||
t = tok.get().unescape()
|
||||
if t.is_eol_or_eof():
|
||||
break
|
||||
if not t.is_identifier():
|
||||
raise dns.exception.SyntaxError
|
||||
cert_chunks.append(t.value.encode())
|
||||
cert = b''.join(cert_chunks)
|
||||
cert = binascii.unhexlify(cert)
|
||||
return cls(rdclass, rdtype, usage, selector, mtype, cert)
|
||||
|
||||
def to_wire(self, file, compress=None, origin=None):
|
||||
header = struct.pack("!BBB", self.usage, self.selector, self.mtype)
|
||||
file.write(header)
|
||||
file.write(self.cert)
|
||||
|
||||
@classmethod
|
||||
def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin=None):
|
||||
header = struct.unpack("!BBB", wire[current: current + 3])
|
||||
current += 3
|
||||
rdlen -= 3
|
||||
cert = wire[current: current + rdlen].unwrap()
|
||||
return cls(rdclass, rdtype, header[0], header[1], header[2], cert)
|
||||
23
src/dns/rdtypes/ANY/TXT.py
Normal file
23
src/dns/rdtypes/ANY/TXT.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
||||
|
||||
# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose with or without fee is hereby granted,
|
||||
# provided that the above copyright notice and this permission notice
|
||||
# appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, 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.
|
||||
|
||||
import dns.rdtypes.txtbase
|
||||
|
||||
|
||||
class TXT(dns.rdtypes.txtbase.TXTBase):
|
||||
|
||||
"""TXT record"""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user