package peergos.shared; import jsinterop.annotations.*; import peergos.client.*; import peergos.server.tests.*; import peergos.shared.cbor.*; import peergos.shared.corenode.*; import peergos.shared.crypto.*; import peergos.shared.crypto.asymmetric.*; import peergos.shared.crypto.symmetric.*; import peergos.shared.io.ipfs.multihash.*; import peergos.shared.mutable.*; import peergos.shared.storage.*; import peergos.shared.user.*; import peergos.shared.user.fs.*; import peergos.shared.util.*; import java.net.*; import java.time.*; import java.util.*; import java.util.concurrent.*; import java.util.stream.*; /** * This class is unprivileged - doesn't have any private keys */ public class NetworkAccess { public final CoreNode coreNode; public final ContentAddressedStorage dhtClient; public final MutablePointers mutable; public final Btree btree; @JsProperty public final List<String> usernames; private final LocalDateTime creationTime; private final boolean isJavascript; public NetworkAccess(CoreNode coreNode, ContentAddressedStorage dhtClient, MutablePointers mutable, Btree btree, List<String> usernames) { this(coreNode, dhtClient, mutable, btree, usernames, false); } public NetworkAccess(CoreNode coreNode, ContentAddressedStorage dhtClient, MutablePointers mutable, Btree btree, List<String> usernames, boolean isJavascript) { this.coreNode = coreNode; this.dhtClient = dhtClient; this.mutable = mutable; this.btree = btree; this.usernames = usernames; this.creationTime = LocalDateTime.now(); this.isJavascript = isJavascript; } public boolean isJavascript() { return isJavascript; } public NetworkAccess withCorenode(CoreNode newCore) { return new NetworkAccess(newCore, dhtClient, mutable, btree, usernames, isJavascript); } @JsMethod public CompletableFuture<Boolean> isUsernameRegistered(String username) { if (usernames.contains(username)) return CompletableFuture.completedFuture(true); return coreNode.getChain(username).thenApply(chain -> chain.size() > 0); } public NetworkAccess clear() { return new NetworkAccess(coreNode, dhtClient, mutable, new BtreeImpl(mutable, dhtClient), usernames, isJavascript); } public static CompletableFuture<NetworkAccess> build(HttpPoster poster, boolean isJavascript) { int cacheTTL = 7_000; System.out.println("Using caching corenode with TTL: " + cacheTTL + " mS"); CoreNode coreNode = new HTTPCoreNode(poster); MutablePointers mutable = new CachingPointers(new HttpMutablePointers(poster), cacheTTL); // allow 10MiB of ram for caching btree entries ContentAddressedStorage dht = new CachingStorage(new ContentAddressedStorage.HTTP(poster), 10_000, 50 * 1024); Btree btree = new BtreeImpl(mutable, dht); return coreNode.getUsernames("").thenApply(usernames -> new NetworkAccess(coreNode, dht, mutable, btree, usernames, isJavascript)); } @JsMethod public static CompletableFuture<NetworkAccess> buildJS() { System.setOut(new ConsolePrintStream()); System.setErr(new ConsolePrintStream()); return build(new JavaScriptPoster(), true); } public static CompletableFuture<NetworkAccess> buildJava(URL target) { return build(new JavaPoster(target), false); } public static CompletableFuture<NetworkAccess> buildJava(int targetPort) { try { return buildJava(new URL("http://localhost:" + targetPort + "/")); } catch (MalformedURLException e) { throw new RuntimeException(e); } } public CompletableFuture<List<RetrievedFilePointer>> retrieveAllMetadata(List<SymmetricLocationLink> links, SymmetricKey baseKey) { List<CompletableFuture<Optional<RetrievedFilePointer>>> all = links.stream() .map(link -> { Location loc = link.targetLocation(baseKey); return btree.get(loc.writer, loc.getMapKey()) .thenCompose(key -> { if (key.isPresent()) return dhtClient.get(key.get()); System.err.println("Couldn't download link at: " + loc); Optional<CborObject> result = Optional.empty(); return CompletableFuture.completedFuture(result); }).thenApply(dataOpt -> dataOpt .map(cbor -> new RetrievedFilePointer(link.toReadableFilePointer(baseKey), FileAccess.fromCbor(cbor)))); }).collect(Collectors.toList()); return Futures.combineAll(all).thenApply(optSet -> optSet.stream() .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList())); } public CompletableFuture<Set<FileTreeNode>> retrieveAll(List<EntryPoint> entries) { return Futures.reduceAll(entries, Collections.emptySet(), (set, entry) -> retrieveEntryPoint(entry) .thenApply(opt -> opt.map(f -> Stream.concat(set.stream(), Stream.of(f)).collect(Collectors.toSet())) .orElse(set)), (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toSet())); } public CompletableFuture<Optional<FileTreeNode>> retrieveEntryPoint(EntryPoint e) { return downloadEntryPoint(e) .thenApply(faOpt ->faOpt.map(fa -> new FileTreeNode(new RetrievedFilePointer(e.pointer, fa), e.owner, e.readers, e.writers, e.pointer.writer))); } private CompletableFuture<Optional<FileAccess>> downloadEntryPoint(EntryPoint entry) { // download the metadata blob for this entry point return btree.get(entry.pointer.location.writer, entry.pointer.location.getMapKey()).thenCompose(btreeValue -> { if (btreeValue.isPresent()) return dhtClient.get(btreeValue.get()) .thenApply(value -> value.map(FileAccess::fromCbor)); return CompletableFuture.completedFuture(Optional.empty()); }); } private CompletableFuture<Multihash> uploadFragment(Fragment f, PublicSigningKey targetUser) { return dhtClient.put(targetUser, new CborObject.CborByteArray(f.data).toByteArray()); } private CompletableFuture<List<Multihash>> bulkUploadFragments(List<Fragment> fragments, PublicSigningKey targetUser) { return dhtClient.put(targetUser, fragments .stream() .map(f -> new CborObject.CborByteArray(f.data).toByteArray()) .collect(Collectors.toList())); } public CompletableFuture<List<Multihash>> uploadFragments(List<Fragment> fragments, PublicSigningKey owner, ProgressConsumer<Long> progressCounter, double spaceIncreaseFactor) { // upload in groups of 10. This means in a browser we have 6 upload threads with erasure coding on, or 4 without int FRAGMENTs_PER_QUERY = 1; List<List<Fragment>> grouped = IntStream.range(0, (fragments.size() + FRAGMENTs_PER_QUERY - 1) / FRAGMENTs_PER_QUERY) .mapToObj(i -> fragments.stream().skip(FRAGMENTs_PER_QUERY * i).limit(FRAGMENTs_PER_QUERY).collect(Collectors.toList())) .collect(Collectors.toList()); List<CompletableFuture<List<Multihash>>> futures = grouped.stream() .map(g -> bulkUploadFragments(g, owner) .thenApply(hash -> { if (progressCounter != null) progressCounter.accept((long)(g.stream().mapToInt(f -> f.data.length).sum() / spaceIncreaseFactor)); return hash; })) .collect(Collectors.toList()); return Futures.combineAllInOrder(futures) .thenApply(groups -> groups.stream() .flatMap(g -> g.stream()) .collect(Collectors.toList())); } public CompletableFuture<Boolean> uploadChunk(FileAccess metadata, Location location, SigningKeyPair writer) { if (! writer.publicSigningKey.equals(location.writer)) throw new IllegalStateException("Non matching location writer and signing writer key!"); try { byte[] metaBlob = metadata.serialize(); return dhtClient.put(location.owner, metaBlob) .thenCompose(blobHash -> btree.put(writer, location.getMapKey(), blobHash)); } catch (Exception e) { System.out.println(e.getMessage()); throw new RuntimeException(e); } } public CompletableFuture<Optional<FileAccess>> getMetadata(Location loc) { if (loc == null) return CompletableFuture.completedFuture(Optional.empty()); return btree.get(loc.writer, loc.getMapKey()).thenCompose(blobHash -> { if (!blobHash.isPresent()) return CompletableFuture.completedFuture(Optional.empty()); return dhtClient.get(blobHash.get()) .thenApply(rawOpt -> rawOpt.map(FileAccess::fromCbor)); }); } public CompletableFuture<List<FragmentWithHash>> downloadFragments(List<Multihash> hashes, ProgressConsumer<Long> monitor, double spaceIncreaseFactor) { List<CompletableFuture<Optional<FragmentWithHash>>> futures = hashes.stream() .map(h -> dhtClient.get(h) .thenApply(dataOpt -> { Optional<byte[]> bytes = dataOpt.map(cbor -> ((CborObject.CborByteArray) cbor).value); bytes.ifPresent(arr -> monitor.accept((long)(arr.length / spaceIncreaseFactor))); return bytes.map(data -> new FragmentWithHash(new Fragment(data), h)); })) .collect(Collectors.toList()); return Futures.combineAllInOrder(futures) .thenApply(optList -> optList.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())); } }