package peergos.server.net; import peergos.shared.cbor.*; import peergos.shared.crypto.asymmetric.*; import peergos.shared.io.ipfs.api.*; import peergos.shared.io.ipfs.multihash.*; import peergos.shared.io.ipfs.cid.*; import peergos.shared.storage.ContentAddressedStorage; import com.sun.net.httpserver.*; import peergos.shared.util.*; import java.io.*; import java.util.*; import java.util.function.*; import java.util.stream.*; public class DHTHandler implements HttpHandler { private static final boolean LOGGING = true; private final ContentAddressedStorage dht; private final String apiPrefix; public DHTHandler(ContentAddressedStorage dht, String apiPrefix) throws IOException { this.dht = dht; this.apiPrefix = apiPrefix; } public DHTHandler(ContentAddressedStorage dht) throws IOException { this(dht, "/api/v0/"); } private Map<String, List<String>> parseQuery(String query) { if (query == null) return Collections.emptyMap(); if (query.startsWith("?")) query = query.substring(1); String[] parts = query.split("&"); Map<String, List<String>> res = new HashMap<>(); for (String part : parts) { int sep = part.indexOf("="); String key = part.substring(0, sep); String value = part.substring(sep + 1); res.putIfAbsent(key, new ArrayList<>()); res.get(key).add(value); } return res; } @Override public void handle(HttpExchange httpExchange) throws IOException { long t1 = System.currentTimeMillis(); String path = httpExchange.getRequestURI().getPath(); try { if (! path.startsWith(apiPrefix)) throw new IllegalStateException("Unsupported api version, required: " + apiPrefix); path = path.substring(apiPrefix.length()); // N.B. URI.getQuery() decodes the query string Map<String, List<String>> params = parseQuery(httpExchange.getRequestURI().getQuery()); List<String> args = params.get("arg"); Function<String, String> last = key -> params.get(key).get(params.get(key).size() - 1); switch (path) { case "block/put": { PublicSigningKey writer = PublicSigningKey.fromString(last.apply("writer")); String boundary = httpExchange.getRequestHeaders().get("Content-Type") .stream() .filter(s -> s.contains("boundary=")) .map(s -> s.substring(s.indexOf("=") + 1)) .findAny() .get(); List<byte[]> data = MultipartReceiver.extractFiles(httpExchange.getRequestBody(), boundary); dht.put(writer, data).thenAccept(hashes -> { List<Object> json = hashes.stream().map(h -> wrapHash(h)).collect(Collectors.toList()); // make stream of JSON objects String jsonStream = json.stream().map(m -> JSONParser.toString(m)).reduce("", (a, b) -> a + b); replyJson(httpExchange, jsonStream, Optional.empty()); }).exceptionally(Futures::logError); break; } case "block/get":{ Multihash hash = Cid.decode(args.get(0)); dht.get(hash) .thenAccept(opt -> replyBytes(httpExchange, opt.map(CborObject::toByteArray).orElse(new byte[0]), opt.map(x -> hash))) .exceptionally(Futures::logError); break; } case "pin/add": { Multihash hash = Cid.decode(args.get(0)); dht.recursivePin(hash).thenAccept(pinned -> { Map<String, Object> json = new TreeMap<>(); json.put("Pins", pinned.stream().map(h -> h.toString()).collect(Collectors.toList())); replyJson(httpExchange, JSONParser.toString(json), Optional.empty()); }).exceptionally(Futures::logError); break; } case "pin/rm": { boolean recursive = params.containsKey("r") && Boolean.parseBoolean(last.apply("r")); if (!recursive) throw new IllegalStateException("Unimplemented: non recursive unpin!"); Multihash hash = Cid.decode(args.get(0)); dht.recursiveUnpin(hash).thenAccept(unpinned -> { Map<String, Object> json = new TreeMap<>(); json.put("Pins", unpinned.stream().map(h -> h.toString()).collect(Collectors.toList())); replyJson(httpExchange, JSONParser.toString(json), Optional.empty()); }).exceptionally(Futures::logError); break; } case "block/stat": { Multihash block = Cid.decode(args.get(0)); dht.getSize(block).thenAccept(sizeOpt -> { Map<String, Object> res = new HashMap<>(); res.put("Size", sizeOpt.orElse(0)); String json = JSONParser.toString(res); replyJson(httpExchange, json, Optional.of(block)); }).exceptionally(Futures::logError); break; } case "refs": { Multihash block = Cid.decode(args.get(0)); dht.getLinks(block).thenAccept(links -> { List<Object> json = links.stream().map(h -> wrapHash("Ref", h)).collect(Collectors.toList()); // make stream of JSON objects String jsonStream = json.stream().map(m -> JSONParser.toString(m)).reduce("", (a, b) -> a + b); replyJson(httpExchange, jsonStream, Optional.of(block)); }).exceptionally(Futures::logError); break; } default: { httpExchange.sendResponseHeaders(404, 0); } } } catch (Exception e) { System.err.println("Error handling " +httpExchange.getRequestURI()); e.printStackTrace(); replyError(httpExchange, e); } finally { long t2 = System.currentTimeMillis(); if (LOGGING) System.out.println("DHT Handler handled " + path + " query in: " + (t2 - t1) + " mS"); } } private static Map<String, Object> wrapHash(Multihash h) { return wrapHash("Hash", h); } private static Map<String, Object> wrapHash(String key, Multihash h) { Map<String, Object> json = new TreeMap<>(); json.put(key, h.toString()); return json; } private static void replyError(HttpExchange exchange, Throwable t) { try { exchange.sendResponseHeaders(500, 0); DataOutputStream dout = new DataOutputStream(exchange.getResponseBody()); String body = t.getMessage(); dout.write(body.getBytes()); dout.flush(); dout.close(); } catch (IOException e) { e.printStackTrace(); } } private static void replyJson(HttpExchange exchange, String json, Optional<Multihash> key) { try { if (key.isPresent()) { exchange.getResponseHeaders().set("Cache-Control", "public, max-age=31622400 immutable"); exchange.getResponseHeaders().set("ETag", "\"" + key.get().toString() + "\""); } exchange.sendResponseHeaders(200, 0); DataOutputStream dout = new DataOutputStream(exchange.getResponseBody()); dout.write(json.getBytes()); dout.flush(); dout.close(); } catch (IOException e) { e.printStackTrace(); } } private static void replyBytes(HttpExchange exchange, byte[] body, Optional<Multihash> key) { try { if (key.isPresent()) { exchange.getResponseHeaders().set("Cache-Control", "public, max-age=31622400 immutable"); exchange.getResponseHeaders().set("ETag", "\"" + key.get().toString() + "\""); } exchange.sendResponseHeaders(200, 0); DataOutputStream dout = new DataOutputStream(exchange.getResponseBody()); dout.write(body); dout.flush(); dout.close(); } catch (IOException e) { e.printStackTrace(); } } }