package peergos.server.tests;
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.*;
import org.junit.runners.*;
import peergos.server.storage.ResetableFileInputStream;
import peergos.shared.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.asymmetric.*;
import peergos.shared.crypto.asymmetric.curve25519.*;
import peergos.shared.crypto.hash.*;
import peergos.shared.crypto.random.*;
import peergos.shared.crypto.symmetric.*;
import peergos.server.*;
import peergos.shared.user.*;
import peergos.shared.user.fs.*;
import peergos.shared.util.*;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public abstract class UserTests {
public static int RANDOM_SEED = 666;
private final NetworkAccess network;
private final Crypto crypto = Crypto.initJava();
private static Random random = new Random(RANDOM_SEED);
public UserTests(String useIPFS, Random r) throws Exception {
int portMin = 9000;
int portRange = 2000;
int webPort = portMin + r.nextInt(portRange);
int corePort = portMin + portRange + r.nextInt(portRange);
Args args = Args.parse(new String[]{"useIPFS", ""+useIPFS.equals("IPFS"), "-port", Integer.toString(webPort), "-corenodePort", Integer.toString(corePort)});
Start.local(args);
this.network = NetworkAccess.buildJava(new URL("http://localhost:" + webPort)).get();
// use insecure random otherwise tests take ages
setFinalStatic(TweetNaCl.class.getDeclaredField("prng"), new Random(1));
}
static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
private String generateUsername() {
return "test" + (random.nextInt() % 10000);
}
@Test
public void serializationSizesSmall() {
SigningKeyPair signer = SigningKeyPair.random(crypto.random, crypto.signer);
byte[] rawSignPub = signer.publicSigningKey.serialize(); // 36
byte[] rawSignSecret = signer.secretSigningKey.serialize(); // 68
byte[] rawSignBoth = signer.serialize(); // 105
BoxingKeyPair boxer = BoxingKeyPair.random(crypto.random, crypto.boxer);
byte[] rawBoxPub = boxer.publicBoxingKey.serialize(); // 36
byte[] rawBoxSecret = boxer.secretBoxingKey.serialize(); // 36
byte[] rawBoxBoth = boxer.serialize(); // 73
SymmetricKey sym = SymmetricKey.random();
byte[] rawSym = sym.serialize(); // 37
Assert.assertTrue("Serialization overhead isn't too much", rawSignPub.length <= 32 + 4);
Assert.assertTrue("Serialization overhead isn't too much", rawSignSecret.length <= 64 + 4);
Assert.assertTrue("Serialization overhead isn't too much", rawSignBoth.length <= 96 + 9);
Assert.assertTrue("Serialization overhead isn't too much", rawBoxPub.length <= 32 + 4);
Assert.assertTrue("Serialization overhead isn't too much", rawBoxSecret.length <= 32 + 4);
Assert.assertTrue("Serialization overhead isn't too much", rawBoxBoth.length <= 64 + 9);
Assert.assertTrue("Serialization overhead isn't too much", rawSym.length <= 33 + 4);
}
@Test
public void differentLoginTypes() throws Exception {
String username = generateUsername();
String password = "letmein";
Crypto crypto = Crypto.initJava();
List<ScryptEd25519Curve25519> params = Arrays.asList(
new ScryptEd25519Curve25519(17, 8, 1, 96),
new ScryptEd25519Curve25519(18, 8, 1, 96),
new ScryptEd25519Curve25519(19, 8, 1, 96),
new ScryptEd25519Curve25519(17, 9, 1, 96)
);
for (ScryptEd25519Curve25519 p: params) {
long t1 = System.currentTimeMillis();
UserUtil.generateUser(username, password, crypto.hasher, crypto.symmetricProvider, crypto.random, crypto.signer, crypto.boxer, p).get();
long t2 = System.currentTimeMillis();
System.out.println("User gen took " + (t2 - t1) + " mS");
System.gc();
}
}
@Test
public void javascriptCompatible() throws IOException {
String username = generateUsername();
String password = "test01";
UserUtil.generateUser(username, password, new ScryptJava(), new Salsa20Poly1305.Java(),
new SafeRandom.Java(), new Ed25519.Java(), new Curve25519.Java(), UserGenerationAlgorithm.getDefault()).thenAccept(userWithRoot -> {
PublicSigningKey expected = PublicSigningKey.fromString("7HvEWP6yd1UD8rOorfFrieJ8S7yC8+l3VisV9kXNiHmI7Eav7+3GTRSVBRCymItrzebUUoCi39M6rdgeOU9sXXFD");
if (! expected.equals(userWithRoot.getUser().publicSigningKey))
throw new IllegalStateException("Generated user diferent from the Javascript! \n"+userWithRoot.getUser().publicSigningKey + " != \n"+expected);
});
}
@Test
public void randomSignup() throws Exception {
String username = generateUsername();
String password = "password";
ensureSignedUp(username, password, network, crypto);
}
@Test
public void singleSignUp() throws Exception {
// This is to ensure a user can't accidentally sign in rather than login and overwrite all their data
String username = generateUsername();
String password = "password";
ensureSignedUp(username, password, network, crypto);
CompletableFuture<UserContext> secondSignup = UserContext.signUp(username, password, network, crypto);
Assert.assertTrue("Second sign up fails", secondSignup.isCompletedExceptionally());
}
@Test
public void changePassword() throws Exception {
String username = generateUsername();
String password = "password";
UserContext userContext = ensureSignedUp(username, password, network, crypto);
String newPassword = "newPassword";
userContext.changePassword(password, newPassword, UserGenerationAlgorithm.getDefault(), UserGenerationAlgorithm.getDefault()).get();
ensureSignedUp(username, newPassword, network, crypto);
}
@Test
public void changePasswordFAIL() throws Exception {
String username = generateUsername();
String password = "password";
UserContext userContext = ensureSignedUp(username, password, network, crypto);
String newPassword = "passwordtest";
UserContext newContext = userContext.changePassword(password, newPassword, UserGenerationAlgorithm.getDefault(), UserGenerationAlgorithm.getDefault()).get();
try {
UserContext oldContext = ensureSignedUp(username, password, network, crypto);
} catch (Exception e) {
if (! e.getMessage().contains("Incorrect password"))
throw e;
}
}
public static UserContext ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) throws Exception {
return UserContext.ensureSignedUp(username, password, network, crypto).get();
}
@Test
public void writeReadVariations() throws Exception {
String username = generateUsername();
String password = "test";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
String filename = "somedata.txt";
// write empty file
byte[] data = new byte[0];
userRoot.uploadFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,
context.crypto.random, l -> {}, context.fragmenter()).get();
checkFileContents(data, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
// write small 1 chunk file
byte[] data2 = "This is a small amount of data".getBytes();
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data2), 0, data2.length, context.network,
context.crypto.random, l -> {}, context.fragmenter()).get();
checkFileContents(data2, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
// check file size
assertTrue("File size", data2.length == userRoot.getDescendentByPath(filename,
context.network).get().get().getFileProperties().size);
assertTrue("File size", data2.length == context.getByPath(username + "/" + filename).get().get().getFileProperties().size);
// extend file within existing chunk
byte[] data3 = new byte[128 * 1024];
new Random().nextBytes(data3);
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data3), 0, data3.length, context.network,
context.crypto.random, l -> {}, context.fragmenter()).get();
checkFileContents(data3, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
// insert data in the middle
byte[] data4 = "some data to insert somewhere".getBytes();
int startIndex = 100 * 1024;
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data4), startIndex, startIndex + data4.length,
context.network, context.crypto.random, l -> {}, context.fragmenter()).get();
System.arraycopy(data4, 0, data3, startIndex, data4.length);
checkFileContents(data3, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
//rename
String newname = "newname.txt";
userRoot.getDescendentByPath(filename, context.network).get().get()
.rename(newname, context.network, userRoot).get();
checkFileContents(data3, userRoot.getDescendentByPath(newname, context.network).get().get(), context);
// check from the root as well
checkFileContents(data3, context.getByPath(username + "/" + newname).get().get(), context);
// check from a fresh log in too
UserContext context2 = ensureSignedUp(username, password, network.clear(), crypto);
Optional<FileTreeNode> renamed = context2.getByPath(username + "/" + newname).get();
checkFileContents(data3, renamed.get(), context);
}
@Test
public void concurrentWrites() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
// write empty file
int concurrency = 8;
int fileSize = 1024;
ForkJoinPool pool = new ForkJoinPool(concurrency);
Set<CompletableFuture<Boolean>> futs = IntStream.range(0, concurrency)
.mapToObj(i -> CompletableFuture.supplyAsync(() -> {
byte[] data = randomData(fileSize);
String filename = i + ".bin";
try {
boolean result = userRoot.uploadFile(filename,
new AsyncReader.ArrayBacked(data),
data.length,
context.network, context.crypto.random, l -> {
},
context.fragmenter()
).get();
checkFileContents(data, context.getByPath("/" + username + "/" + filename).get().get(), context);
System.out.println("Finished a file");
return true;
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}, pool)).collect(Collectors.toSet());
boolean success = Futures.combineAll(futs).get().stream().reduce(true, (a, b) -> a && b);
Set<FileTreeNode> files = context.getUserRoot().get().getChildren(context.network).get();
Set<String> names = files.stream().filter(f -> ! f.getFileProperties().isHidden).map(f -> f.getName()).collect(Collectors.toSet());
Set<String> expectedNames = IntStream.range(0, concurrency).mapToObj(i -> i + ".bin").collect(Collectors.toSet());
Assert.assertTrue("All children present and accounted for", names.equals(expectedNames));
}
@Test
public void mediumFileWrite() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
String filename = "mediumfile.bin";
byte[] data = new byte[0];
userRoot.uploadFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network,
context.crypto.random, l -> {}, context.fragmenter());
//overwrite with 2 chunk file
byte[] data5 = new byte[10*1024*1024];
random.nextBytes(data5);
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data5), 0, data5.length, context.network,
context.crypto.random, l -> {} , context.fragmenter());
checkFileContents(data5, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
assertTrue("10MiB file size", data5.length == userRoot.getDescendentByPath(filename,
context.network).get().get().getFileProperties().size);
// insert data in the middle of second chunk
System.out.println("\n***** Mid 2nd chunk write test");
byte[] dataInsert = "some data to insert somewhere else".getBytes();
int start = 5*1024*1024 + 4*1024;
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(dataInsert), start, start + dataInsert.length,
context.network, context.crypto.random, l -> {}, context.fragmenter());
System.arraycopy(dataInsert, 0, data5, start, dataInsert.length);
checkFileContents(data5, userRoot.getDescendentByPath(filename, context.network).get().get(), context);
// check used space
long totalSpaceUsed = context.getTotalSpaceUsed(context.signer.publicSigningKey).get();
Assert.assertTrue("Correct used space", totalSpaceUsed > 10*1024*1024);
}
@Test
public void writeTiming() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
String filename = "mediumfile.bin";
byte[] data = new byte[0];
userRoot.uploadFile(filename, new AsyncReader.ArrayBacked(data), data.length, context.network, context.crypto.random, l -> {}, context.fragmenter());
//overwrite with 2 chunk file
byte[] data5 = new byte[10*1024*1024];
random.nextBytes(data5);
long t1 = System.currentTimeMillis();
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data5), 0, data5.length, context.network, context.crypto.random, l -> {}, context.fragmenter());
long t2 = System.currentTimeMillis();
System.out.println("Write time per chunk " + (t2-t1)/2 + "mS");
Assert.assertTrue("Timely write", (t2-t1)/2 < 20000);
}
@Test
public void publicLinkToFile() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
String filename = "mediumfile.bin";
byte[] data = new byte[128*1024];
random.nextBytes(data);
long t1 = System.currentTimeMillis();
userRoot.uploadFileSection(filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network, context.crypto.random, l -> {}, context.fragmenter()).get();
long t2 = System.currentTimeMillis();
String path = "/" + username + "/" + filename;
FileTreeNode file = context.getByPath(path).get().get();
String link = file.toLink();
UserContext linkContext = UserContext.fromPublicLink(link, network, crypto).get();
Optional<FileTreeNode> fileThroughLink = linkContext.getByPath(path).get();
Assert.assertTrue("File present through link", fileThroughLink.isPresent());
}
@Test
public void publicLinkToDir() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
String filename = "mediumfile.bin";
byte[] data = new byte[128*1024];
random.nextBytes(data);
long t1 = System.currentTimeMillis();
String dirName = "subdir";
userRoot.mkdir(dirName, context.network, false, context.crypto.random).get();
FileTreeNode subdir = context.getByPath("/" + username + "/" + dirName).get().get();
String anotherDirName = "anotherDir";
subdir.mkdir(anotherDirName, context.network, false, context.crypto.random).get();
FileTreeNode anotherDir = context.getByPath("/" + username + "/" + dirName + "/" + anotherDirName).get().get();
anotherDir.uploadFileSection(filename, new AsyncReader.ArrayBacked(data), 0, data.length, context.network,
context.crypto.random, l -> {}, context.fragmenter()).get();
long t2 = System.currentTimeMillis();
String path = "/" + username + "/" + dirName + "/" + anotherDirName;
FileTreeNode theDir = context.getByPath(path).get().get();
String link = theDir.toLink();
UserContext linkContext = UserContext.fromPublicLink(link, network, crypto).get();
String entryPath = linkContext.getEntryPath().get();
Assert.assertTrue("public link to folder has correct entry path", entryPath.equals(path));
Optional<FileTreeNode> fileThroughLink = linkContext.getByPath(path + "/" + filename).get();
Assert.assertTrue("File present through link", fileThroughLink.isPresent());
}
// This one takes a while, so disable most of the time
// @Test
public void hugeFolder() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
List<String> names = new ArrayList<>();
IntStream.range(0, 2000).forEach(i -> names.add(randomString()));
for (String filename: names) {
userRoot.mkdir(filename, context.network, false, context.crypto.random);
}
}
private static void checkFileContents(byte[] expected, FileTreeNode f, UserContext context) throws Exception {
byte[] retrievedData = Serialize.readFully(f.getInputStream(context.network, context.crypto.random,
f.getFileProperties().size, l-> {}).get(), f.getSize()).get();
assertTrue("Correct contents", Arrays.equals(retrievedData, expected));
}
@Test
public void readWriteTest() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
Set<FileTreeNode> children = userRoot.getChildren(context.network).get();
children.stream()
.map(FileTreeNode::toString)
.forEach(System.out::println);
String name = randomString();
Path tmpPath = createTmpFile(name);
byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining
Files.write(tmpPath, data);
File tmpFile = tmpPath.toFile();
ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(tmpFile);
boolean b = userRoot.uploadFile(name, resetableFileInputStream, tmpFile.length(), context.network, context.crypto.random, (l) -> {}, context.fragmenter()).get();
assertTrue("file upload", b);
Optional<FileTreeNode> opt = userRoot.getChildren(context.network).get()
.stream()
.filter(e -> e.getFileProperties().name.equals(name))
.findFirst();
assertTrue("found uploaded file", opt.isPresent());
FileTreeNode fileTreeNode = opt.get();
long size = fileTreeNode.getFileProperties().size;
AsyncReader in = fileTreeNode.getInputStream(context.network, context.crypto.random, size, (l) -> {}).get();
byte[] retrievedData = Serialize.readFully(in, fileTreeNode.getSize()).get();
boolean dataEquals = Arrays.equals(data, retrievedData);
assertTrue("retrieved same data", dataEquals);
}
@Test
public void deleteTest() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network.clear(), crypto);
FileTreeNode userRoot = context.getUserRoot().get();
Set<FileTreeNode> children = userRoot.getChildren(context.network).get();
children.stream()
.map(FileTreeNode::toString)
.forEach(System.out::println);
String name = randomString();
Path tmpPath = createTmpFile(name);
byte[] data = randomData(10*1024*1024); // 2 chunks to test block chaining
Files.write(tmpPath, data);
File tmpFile = tmpPath.toFile();
ResetableFileInputStream resetableFileInputStream = new ResetableFileInputStream(tmpFile);
boolean b = userRoot.uploadFile(name, resetableFileInputStream, tmpFile.length(), context.network, context.crypto.random, (l) -> {}, context.fragmenter()).get();
String otherName = name + ".other";
boolean b2 = userRoot.uploadFile(otherName, resetableFileInputStream, tmpFile.length(), context.network, context.crypto.random, (l) -> {}, context.fragmenter()).get();
assertTrue("file upload", b);
Optional<FileTreeNode> opt = userRoot.getChildren(context.network).get()
.stream()
.filter(e -> e.getFileProperties().name.equals(name))
.findFirst();
assertTrue("found uploaded file", opt.isPresent());
FileTreeNode fileTreeNode = opt.get();
long size = fileTreeNode.getFileProperties().size;
AsyncReader in = fileTreeNode.getInputStream(context.network, context.crypto.random, size, (l) -> {}).get();
byte[] retrievedData = Serialize.readFully(in, fileTreeNode.getSize()).get();
boolean dataEquals = Arrays.equals(data, retrievedData);
assertTrue("retrieved same data", dataEquals);
//delete the file
fileTreeNode.remove(context.network, userRoot).get();
//re-create user-context
UserContext context2 = ensureSignedUp(username, password, network.clear(), crypto);
FileTreeNode userRoot2 = context2.getUserRoot().get();
//check the file is no longer present
boolean isPresent = userRoot2.getChildren(context2.network).get()
.stream()
.anyMatch(e -> e.getFileProperties().name.equals(name));
Assert.assertFalse("uploaded file is deleted", isPresent);
//check content of other file in same directory that was not removed
FileTreeNode otherFileTreeNode = userRoot2.getChildren(context2.network).get()
.stream()
.filter(e -> e.getFileProperties().name.equals(otherName))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Missing other file"));
AsyncReader asyncReader = otherFileTreeNode.getInputStream(context2.network, context2.crypto.random, l -> {}).get();
byte[] otherRetrievedData = Serialize.readFully(asyncReader, otherFileTreeNode.getSize()).get();
boolean otherDataEquals = Arrays.equals(data, otherRetrievedData);
Assert.assertTrue("other file data is intact", otherDataEquals);
}
@Test
public void deleteDirectoryTest() throws Exception {
String username = generateUsername();
String password = "test01";
UserContext context = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot = context.getUserRoot().get();
Set<FileTreeNode> children = userRoot.getChildren(context.network).get();
children.stream()
.map(FileTreeNode::toString)
.forEach(System.out::println);
String folderName = "a_folder";
boolean isSystemFolder = false;
//create the directory
userRoot.mkdir(folderName, context.network, isSystemFolder, context.crypto.random).get();
FileTreeNode folderTreeNode = userRoot.getChildren(context.network)
.get()
.stream()
.filter(e -> e.getFileProperties().name.equals(folderName))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Missing created folder " + folderName));
//remove the directory
folderTreeNode.remove(context.network, userRoot).get();
//ensure folder directory not present
boolean isPresent = userRoot.getChildren(context.network)
.get()
.stream()
.filter(e -> e.getFileProperties().name.equals(folderName))
.findFirst()
.isPresent();
Assert.assertFalse("folder not present after remove", isPresent);
//can sign-in again
try {
UserContext context2 = ensureSignedUp(username, password, network, crypto);
FileTreeNode userRoot2 = context2.getUserRoot().get();
} catch (Exception ex) {
fail("Failed to log-in and see user-root " + ex.getMessage());
}
}
public static String randomString() {
return UUID.randomUUID().toString();
}
private static byte[] randomData(int length) {
byte[] data = new byte[length];
random.nextBytes(data);
return data;
}
private static Path TMP_DIR = Paths.get("test","resources","tmp");
private static void ensureTmpDir() {
File dir = TMP_DIR.toFile();
if (! dir.isDirectory() && ! dir.mkdirs())
throw new IllegalStateException("Could not find or create specified tmp directory "+ TMP_DIR);
}
private static Path createTmpFile(String filename) throws IOException {
ensureTmpDir();
Path resolve = TMP_DIR.resolve(filename);
File file = resolve.toFile();
file.createNewFile();
file.deleteOnExit();
return resolve;
}
}