Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
at main 273 lines 15 kB view raw
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