Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
1import json
2import time
3import io
4
5import pytest
6
7from Crypt import CryptBitcoin
8from Content.ContentManager import VerifyError, SignError
9from util.SafeRe import UnsafePatternError
10
11
12@pytest.mark.usefixtures("resetSettings")
13class TestContent:
14 privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv"
15
16 def testInclude(self, site):
17 # Rules defined in parent content.json
18 rules = site.content_manager.getRules("data/test_include/content.json")
19
20 assert rules["signers"] == ["15ik6LeBWnACWfaika1xqGapRZ1zh3JpCo"] # Valid signer
21 assert rules["user_name"] == "test" # Extra data
22 assert rules["max_size"] == 20000 # Max size of files
23 assert not rules["includes_allowed"] # Don't allow more includes
24 assert rules["files_allowed"] == "data.json" # Allowed file pattern
25
26 # Valid signers for "data/test_include/content.json"
27 valid_signers = site.content_manager.getValidSigners("data/test_include/content.json")
28 assert "15ik6LeBWnACWfaika1xqGapRZ1zh3JpCo" in valid_signers # Extra valid signer defined in parent content.json
29 assert "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT" in valid_signers # The site itself
30 assert len(valid_signers) == 2 # No more
31
32 # Valid signers for "data/users/content.json"
33 valid_signers = site.content_manager.getValidSigners("data/users/content.json")
34 assert "1LSxsKfC9S9TVXGGNSM3vPHjyW82jgCX5f" in valid_signers # Extra valid signer defined in parent content.json
35 assert "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT" in valid_signers # The site itself
36 assert len(valid_signers) == 2
37
38 # Valid signers for root content.json
39 assert site.content_manager.getValidSigners("content.json") == ["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"]
40
41 def testInlcudeLimits(self, site, crypt_bitcoin_lib):
42 # Data validation
43 res = []
44 data_dict = {
45 "files": {
46 "data.json": {
47 "sha512": "369d4e780cc80504285f13774ca327fe725eed2d813aad229e62356b07365906",
48 "size": 505
49 }
50 },
51 "modified": time.time()
52 }
53
54 # Normal data
55 data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)}
56 data_json = json.dumps(data_dict).encode()
57 data = io.BytesIO(data_json)
58 assert site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False)
59
60 # Reset
61 del data_dict["signs"]
62
63 # Too large
64 data_dict["files"]["data.json"]["size"] = 200000 # Emulate 2MB sized data.json
65 data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)}
66 data = io.BytesIO(json.dumps(data_dict).encode())
67 with pytest.raises(VerifyError) as err:
68 site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False)
69 assert "Include too large" in str(err.value)
70
71 # Reset
72 data_dict["files"]["data.json"]["size"] = 505
73 del data_dict["signs"]
74
75 # Not allowed file
76 data_dict["files"]["notallowed.exe"] = data_dict["files"]["data.json"]
77 data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)}
78 data = io.BytesIO(json.dumps(data_dict).encode())
79 with pytest.raises(VerifyError) as err:
80 site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False)
81 assert "File not allowed" in str(err.value)
82
83 # Reset
84 del data_dict["files"]["notallowed.exe"]
85 del data_dict["signs"]
86
87 # Should work again
88 data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)}
89 data = io.BytesIO(json.dumps(data_dict).encode())
90 assert site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False)
91
92 @pytest.mark.parametrize("inner_path", ["content.json", "data/test_include/content.json", "data/users/content.json"])
93 def testSign(self, site, inner_path):
94 # Bad privatekey
95 with pytest.raises(SignError) as err:
96 site.content_manager.sign(inner_path, privatekey="5aaa3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMnaa", filewrite=False)
97 assert "Private key invalid" in str(err.value)
98
99 # Good privatekey
100 content = site.content_manager.sign(inner_path, privatekey=self.privatekey, filewrite=False)
101 content_old = site.content_manager.contents[inner_path] # Content before the sign
102 assert not content_old == content # Timestamp changed
103 assert site.address in content["signs"] # Used the site's private key to sign
104 if inner_path == "content.json":
105 assert len(content["files"]) == 17
106 elif inner_path == "data/test-include/content.json":
107 assert len(content["files"]) == 1
108 elif inner_path == "data/users/content.json":
109 assert len(content["files"]) == 0
110
111 # Everything should be same as before except the modified timestamp and the signs
112 assert (
113 {key: val for key, val in content_old.items() if key not in ["modified", "signs", "sign", "zeronet_version"]}
114 ==
115 {key: val for key, val in content.items() if key not in ["modified", "signs", "sign", "zeronet_version"]}
116 )
117
118 def testSignOptionalFiles(self, site):
119 for hash in list(site.content_manager.hashfield):
120 site.content_manager.hashfield.remove(hash)
121
122 assert len(site.content_manager.hashfield) == 0
123
124 site.content_manager.contents["content.json"]["optional"] = "((data/img/zero.*))"
125 content_optional = site.content_manager.sign(privatekey=self.privatekey, filewrite=False, remove_missing_optional=True)
126
127 del site.content_manager.contents["content.json"]["optional"]
128 content_nooptional = site.content_manager.sign(privatekey=self.privatekey, filewrite=False, remove_missing_optional=True)
129
130 assert len(content_nooptional.get("files_optional", {})) == 0 # No optional files if no pattern
131 assert len(content_optional["files_optional"]) > 0
132 assert len(site.content_manager.hashfield) == len(content_optional["files_optional"]) # Hashed optional files should be added to hashfield
133 assert len(content_nooptional["files"]) > len(content_optional["files"])
134
135 def testFileInfo(self, site):
136 assert "sha512" in site.content_manager.getFileInfo("index.html")
137 assert site.content_manager.getFileInfo("data/img/domain.png")["content_inner_path"] == "content.json"
138 assert site.content_manager.getFileInfo("data/users/hello.png")["content_inner_path"] == "data/users/content.json"
139 assert site.content_manager.getFileInfo("data/users/content.json")["content_inner_path"] == "data/users/content.json"
140 assert not site.content_manager.getFileInfo("notexist")
141
142 # Optional file
143 file_info_optional = site.content_manager.getFileInfo("data/optional.txt")
144 assert "sha512" in file_info_optional
145 assert file_info_optional["optional"] is True
146
147 # Not exists yet user content.json
148 assert "cert_signers" in site.content_manager.getFileInfo("data/users/unknown/content.json")
149
150 # Optional user file
151 file_info_optional = site.content_manager.getFileInfo("data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/peanut-butter-jelly-time.gif")
152 assert "sha512" in file_info_optional
153 assert file_info_optional["optional"] is True
154
155 def testVerify(self, site, crypt_bitcoin_lib):
156 inner_path = "data/test_include/content.json"
157 data_dict = site.storage.loadJson(inner_path)
158 data = io.BytesIO(json.dumps(data_dict).encode("utf8"))
159
160 # Re-sign
161 data_dict["signs"] = {
162 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
163 }
164 assert site.content_manager.verifyFile(inner_path, data, ignore_same=False)
165
166 # Wrong address
167 data_dict["address"] = "Othersite"
168 del data_dict["signs"]
169 data_dict["signs"] = {
170 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
171 }
172 data = io.BytesIO(json.dumps(data_dict).encode())
173 with pytest.raises(VerifyError) as err:
174 site.content_manager.verifyFile(inner_path, data, ignore_same=False)
175 assert "Wrong site address" in str(err.value)
176
177 # Wrong inner_path
178 data_dict["address"] = "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"
179 data_dict["inner_path"] = "content.json"
180 del data_dict["signs"]
181 data_dict["signs"] = {
182 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
183 }
184 data = io.BytesIO(json.dumps(data_dict).encode())
185 with pytest.raises(VerifyError) as err:
186 site.content_manager.verifyFile(inner_path, data, ignore_same=False)
187 assert "Wrong inner_path" in str(err.value)
188
189 # Everything right again
190 data_dict["address"] = "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"
191 data_dict["inner_path"] = inner_path
192 del data_dict["signs"]
193 data_dict["signs"] = {
194 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
195 }
196 data = io.BytesIO(json.dumps(data_dict).encode())
197 assert site.content_manager.verifyFile(inner_path, data, ignore_same=False)
198
199 def testVerifyInnerPath(self, site, crypt_bitcoin_lib):
200 inner_path = "content.json"
201 data_dict = site.storage.loadJson(inner_path)
202
203 for good_relative_path in ["data.json", "out/data.json", "Any File [by none] (1).jpg", "árvzítűrő/tükörfúrógép.txt"]:
204 data_dict["files"] = {good_relative_path: {"sha512": "369d4e780cc80504285f13774ca327fe725eed2d813aad229e62356b07365906", "size": 505}}
205
206 if "sign" in data_dict:
207 del data_dict["sign"]
208 del data_dict["signs"]
209 data_dict["signs"] = {
210 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
211 }
212 data = io.BytesIO(json.dumps(data_dict).encode())
213 assert site.content_manager.verifyFile(inner_path, data, ignore_same=False)
214
215 for bad_relative_path in ["../data.json", "data/" * 100, "invalid|file.jpg", "con.txt", "any/con.txt"]:
216 data_dict["files"] = {bad_relative_path: {"sha512": "369d4e780cc80504285f13774ca327fe725eed2d813aad229e62356b07365906", "size": 505}}
217
218 if "sign" in data_dict:
219 del data_dict["sign"]
220 del data_dict["signs"]
221 data_dict["signs"] = {
222 "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey)
223 }
224 data = io.BytesIO(json.dumps(data_dict).encode())
225 with pytest.raises(VerifyError) as err:
226 site.content_manager.verifyFile(inner_path, data, ignore_same=False)
227 assert "Invalid relative path" in str(err.value)
228
229 @pytest.mark.parametrize("key", ["ignore", "optional"])
230 def testSignUnsafePattern(self, site, key):
231 site.content_manager.contents["content.json"][key] = "([a-zA-Z]+)*"
232 with pytest.raises(UnsafePatternError) as err:
233 site.content_manager.sign("content.json", privatekey=self.privatekey, filewrite=False)
234 assert "Potentially unsafe" in str(err.value)
235
236
237 def testVerifyUnsafePattern(self, site, crypt_bitcoin_lib):
238 site.content_manager.contents["content.json"]["includes"]["data/test_include/content.json"]["files_allowed"] = "([a-zA-Z]+)*"
239 with pytest.raises(UnsafePatternError) as err:
240 with site.storage.open("data/test_include/content.json") as data:
241 site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False)
242 assert "Potentially unsafe" in str(err.value)
243
244 site.content_manager.contents["data/users/content.json"]["user_contents"]["permission_rules"]["([a-zA-Z]+)*"] = {"max_size": 0}
245 with pytest.raises(UnsafePatternError) as err:
246 with site.storage.open("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json") as data:
247 site.content_manager.verifyFile("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json", data, ignore_same=False)
248 assert "Potentially unsafe" in str(err.value)
249
250 def testPathValidation(self, site):
251 assert site.content_manager.isValidRelativePath("test.txt")
252 assert site.content_manager.isValidRelativePath("test/!@#$%^&().txt")
253 assert site.content_manager.isValidRelativePath("ÜøßÂŒƂÆÇ.txt")
254 assert site.content_manager.isValidRelativePath("тест.текст")
255 assert site.content_manager.isValidRelativePath("𝐮𝐧𝐢𝐜𝐨𝐝𝐞𝑖𝑠𝒂𝒘𝒆𝒔𝒐𝒎𝒆")
256
257 # Test rules based on https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
258
259 assert not site.content_manager.isValidRelativePath("any\\hello.txt") # \ not allowed
260 assert not site.content_manager.isValidRelativePath("/hello.txt") # Cannot start with /
261 assert not site.content_manager.isValidRelativePath("\\hello.txt") # Cannot start with \
262 assert not site.content_manager.isValidRelativePath("../hello.txt") # Not allowed .. in path
263 assert not site.content_manager.isValidRelativePath("\0hello.txt") # NULL character
264 assert not site.content_manager.isValidRelativePath("\31hello.txt") # 0-31 (ASCII control characters)
265 assert not site.content_manager.isValidRelativePath("any/hello.txt ") # Cannot end with space
266 assert not site.content_manager.isValidRelativePath("any/hello.txt.") # Cannot end with dot
267 assert site.content_manager.isValidRelativePath(".hello.txt") # Allow start with dot
268 assert not site.content_manager.isValidRelativePath("any/CON") # Protected names on Windows
269 assert not site.content_manager.isValidRelativePath("CON/any.txt")
270 assert not site.content_manager.isValidRelativePath("any/lpt1.txt")
271 assert site.content_manager.isValidRelativePath("any/CONAN")
272 assert not site.content_manager.isValidRelativePath("any/CONOUT$")
273 assert not site.content_manager.isValidRelativePath("a" * 256) # Max 255 characters allowed