From a332a22ad3c0a9e358bf86b6ff62f1253ca91b8e Mon Sep 17 00:00:00 2001 From: Trumeet Date: Sun, 4 Apr 2021 14:20:46 -0700 Subject: fix(central): completely rewrite IP validation and compressing --- central/build.gradle | 2 +- .../java/edazdarevic/commons/net/CIDRUtils.java | 142 ---------- .../yuuta/dn42peering/admin/nodes/NodeAdminUI.java | 79 +++--- .../moe/yuuta/dn42peering/manage/ManagementUI.java | 308 +++++++++++---------- 4 files changed, 197 insertions(+), 334 deletions(-) delete mode 100644 central/src/main/java/edazdarevic/commons/net/CIDRUtils.java diff --git a/central/build.gradle b/central/build.gradle index af18c0f..0f18e7f 100644 --- a/central/build.gradle +++ b/central/build.gradle @@ -18,7 +18,6 @@ dependencies { implementation 'org.apache.commons:commons-text:1.9' implementation 'commons-net:commons-net:3.7.2' - implementation 'commons-validator:commons-validator:1.7' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' @@ -41,6 +40,7 @@ dependencies { annotationProcessor "io.vertx:vertx-web-api-service:${project.vertxVersion}" implementation "org.flywaydb:flyway-core:7.7.2" implementation "org.mariadb.jdbc:mariadb-java-client:2.7.2" + implementation "com.github.seancfoley:ipaddress:5.3.3" implementation project(':rpc-common') } diff --git a/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java b/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java deleted file mode 100644 index 159f99d..0000000 --- a/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com) - - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * */ - -package edazdarevic.commons.net; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * A class that enables to get an IP range from CIDR specification. It supports - * both IPv4 and IPv6. - */ -public class CIDRUtils { - private final String cidr; - - private InetAddress inetAddress; - private InetAddress startAddress; - private InetAddress endAddress; - private final int prefixLength; - - - public CIDRUtils(String cidr) throws UnknownHostException { - - this.cidr = cidr; - - /* split CIDR to address and prefix part */ - if (this.cidr.contains("/")) { - int index = this.cidr.indexOf("/"); - String addressPart = this.cidr.substring(0, index); - String networkPart = this.cidr.substring(index + 1); - - inetAddress = InetAddress.getByName(addressPart); - prefixLength = Integer.parseInt(networkPart); - - calculate(); - } else { - throw new IllegalArgumentException("not an valid CIDR format!"); - } - } - - - private void calculate() throws UnknownHostException { - - ByteBuffer maskBuffer; - int targetSize; - if (inetAddress.getAddress().length == 4) { - maskBuffer = - ByteBuffer - .allocate(4) - .putInt(-1); - targetSize = 4; - } else { - maskBuffer = ByteBuffer.allocate(16) - .putLong(-1L) - .putLong(-1L); - targetSize = 16; - } - - BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength); - - ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress()); - BigInteger ipVal = new BigInteger(1, buffer.array()); - - BigInteger startIp = ipVal.and(mask); - BigInteger endIp = startIp.add(mask.not()); - - byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize); - byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize); - - this.startAddress = InetAddress.getByAddress(startIpArr); - this.endAddress = InetAddress.getByAddress(endIpArr); - - } - - private byte[] toBytes(byte[] array, int targetSize) { - int counter = 0; - List newArr = new ArrayList(); - while (counter < targetSize && (array.length - 1 - counter >= 0)) { - newArr.add(0, array[array.length - 1 - counter]); - counter++; - } - - int size = newArr.size(); - for (int i = 0; i < (targetSize - size); i++) { - - newArr.add(0, (byte) 0); - } - - byte[] ret = new byte[newArr.size()]; - for (int i = 0; i < newArr.size(); i++) { - ret[i] = newArr.get(i); - } - return ret; - } - - public String getNetworkAddress() { - - return this.startAddress.getHostAddress(); - } - - public String getBroadcastAddress() { - return this.endAddress.getHostAddress(); - } - - public boolean isInRange(String ipAddress) throws UnknownHostException { - InetAddress address = InetAddress.getByName(ipAddress); - BigInteger start = new BigInteger(1, this.startAddress.getAddress()); - BigInteger end = new BigInteger(1, this.endAddress.getAddress()); - BigInteger target = new BigInteger(1, address.getAddress()); - - int st = start.compareTo(target); - int te = target.compareTo(end); - - return (st == -1 || st == 0) && (te == -1 || te == 0); - } -} \ No newline at end of file diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java index dd625cc..d15f6d4 100644 --- a/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java +++ b/central/src/main/java/moe/yuuta/dn42peering/admin/nodes/NodeAdminUI.java @@ -1,6 +1,7 @@ package moe.yuuta.dn42peering.admin.nodes; -import edazdarevic.commons.net.CIDRUtils; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import io.vertx.core.Future; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; @@ -10,12 +11,9 @@ import moe.yuuta.dn42peering.node.INodeService; import moe.yuuta.dn42peering.node.Node; import moe.yuuta.dn42peering.portal.FormException; import moe.yuuta.dn42peering.portal.RenderingUtils; -import org.apache.commons.validator.routines.InetAddressValidator; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.io.IOException; -import java.net.Inet6Address; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -84,40 +82,45 @@ class NodeAdminUI { errors.add("ASN is invalid"); if(node.getName() == null || node.getName().trim().isEmpty()) errors.add("No name supplied"); - try { - if(node.getDn42Ip4() != null) { - if (InetAddressValidator.getInstance().isValidInet4Address(node.getDn42Ip4())) { - if (!new CIDRUtils("172.20.0.0/14").isInRange(node.getDn42Ip4())) { - errors.add("DN42 IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); - } - } else - errors.add("DN42 IPv4 address is illegal. Cannot parse your address."); - } else { - errors.add("DN42 IPv4 address is not supplied"); - } - if(node.getDn42Ip6() != null) { - if (InetAddressValidator.getInstance().isValidInet6Address(node.getDn42Ip6())) { - if (new CIDRUtils("fd00::/8").isInRange(node.getDn42Ip6())) { - errors.add("DN42 IPv6 address is illegal. It must be a link-local IPv6 address."); - } - } else - errors.add("DN42 Link Local IPv6 address is illegal. Cannot parse your address."); - } else { - errors.add("DN42 IPv6 Link Local address is not supplied"); - } - if(node.getDn42Ip6NonLL() != null) { - if (InetAddressValidator.getInstance().isValidInet6Address(node.getDn42Ip6NonLL())) { - if (!new CIDRUtils("fd00::/8").isInRange(node.getDn42Ip6NonLL()) || - Inet6Address.getByName(node.getDn42Ip6NonLL()).isLinkLocalAddress()) { - errors.add("DN42 IPv6 address is illegal. It must be a dn42 IPv6 address."); - } - } else - errors.add("IPv6 address is illegal. Cannot parse your address."); - } else { - errors.add("DN42 IPv6 address is not supplied"); - } - } catch (IOException e) { - return Future.failedFuture(e); + if(node.getDn42Ip4() != null) { + final IPAddress address = new IPAddressString(node.getDn42Ip4()).getHostAddress(); + if (address != null) { + if (!new IPAddressString("172.20.0.0/14").getAddress().contains(address)) { + errors.add("DN42 IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); + } + // Remove prefix + node.setDn42Ip4(address.toNormalizedString()); + } else + errors.add("DN42 IPv4 address is illegal. Cannot parse your address."); + } else { + errors.add("DN42 IPv4 address is not supplied"); + } + if(node.getDn42Ip6() != null) { + final IPAddress address = new IPAddressString(node.getDn42Ip6()).getHostAddress(); + if (address != null) { + if (!address.isLinkLocal()) { + errors.add("DN42 IPv6 address is illegal. It must be a link-local IPv6 address."); + } + // Compress & remove prefix + node.setDn42Ip6(address.toCanonicalString()); + } else + errors.add("DN42 Link Local IPv6 address is illegal. Cannot parse your address."); + } else { + errors.add("DN42 IPv6 Link Local address is not supplied"); + } + if(node.getDn42Ip6NonLL() != null) { + final IPAddress address = new IPAddressString(node.getDn42Ip6NonLL()).getHostAddress(); + if (address != null) { + if (!new IPAddressString("fd00::/8").getAddress().contains(address) || + address.isLinkLocal()) { + errors.add("DN42 IPv6 address is illegal. It must be a dn42 IPv6 address."); + } + // Compress & remove prefix + node.setDn42Ip6NonLL(address.toCanonicalString()); + } else + errors.add("IPv6 address is illegal. Cannot parse your address."); + } else { + errors.add("DN42 IPv6 address is not supplied"); } if(node.getInternalIp() == null) { diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java index 40a93d7..4f9954b 100644 --- a/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManagementUI.java @@ -2,7 +2,8 @@ package moe.yuuta.dn42peering.manage; import com.wireguard.crypto.Key; import com.wireguard.crypto.KeyFormatException; -import edazdarevic.commons.net.CIDRUtils; +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; @@ -15,12 +16,10 @@ import moe.yuuta.dn42peering.node.Node; import moe.yuuta.dn42peering.peer.Peer; import moe.yuuta.dn42peering.peer.ProvisionStatus; import moe.yuuta.dn42peering.portal.FormException; -import org.apache.commons.validator.routines.InetAddressValidator; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; -import java.net.Inet6Address; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -226,183 +225,186 @@ class ManagementUI { int n = nodeId; return Future.future(f -> nodeService.getNode(n, f)) .compose(node -> { - try { - final List errors = new ArrayList<>(10); - if(node == null) { - errors.add("The node selection is invalid."); - } - Peer.VPNType type = null; - if (form.containsKey("vpn")) { - final String rawVPN = form.getString("vpn"); - if (rawVPN == null) { - errors.add("Tunneling type is not specified."); - } else - switch (rawVPN) { - case "wg": - type = Peer.VPNType.WIREGUARD; - break; - default: - errors.add("Tunneling type is unexpected."); - break; - } - } else { + final List errors = new ArrayList<>(10); + if(node == null) { + errors.add("The node selection is invalid."); + } + Peer.VPNType type = null; + if (form.containsKey("vpn")) { + final String rawVPN = form.getString("vpn"); + if (rawVPN == null) { errors.add("Tunneling type is not specified."); - } - - String ipv4 = null; - if (form.containsKey("ipv4")) { - ipv4 = form.getString("ipv4"); - if (ipv4 == null || ipv4.isEmpty()) { - errors.add("IPv4 address is not specified."); - ipv4 = null; // Non-null but empty values could cause problems. - } else { - if (InetAddressValidator.getInstance().isValidInet4Address(ipv4)) { - if (!new CIDRUtils("172.20.0.0/14").isInRange(ipv4)) { - errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); - } - } else - errors.add("IPv4 address is illegal. Cannot parse your address."); + } else + switch (rawVPN) { + case "wg": + type = Peer.VPNType.WIREGUARD; + break; + default: + errors.add("Tunneling type is unexpected."); + break; } - } else { - errors.add("IPv4 address is not specified."); - } + } else { + errors.add("Tunneling type is not specified."); + } - String ipv6 = null; - if (form.containsKey("ipv6")) { - ipv6 = form.getString("ipv6"); - if (ipv6 != null && !ipv6.isEmpty()) { - if (InetAddressValidator.getInstance().isValidInet6Address(ipv6)) { - if (!new CIDRUtils("fd00::/8").isInRange(ipv6) && - !Inet6Address.getByName(ipv6).isLinkLocalAddress()) { - errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address."); - } - ipv6 = ipv6.replaceAll("((?::0\\b){2,}):?(?!\\S*\\b\\1:0\\b)(\\S*)", "::$2"); - } else - errors.add("IPv6 address is illegal. Cannot parse your address."); - } else { - ipv6 = null; // Non-null but empty values could cause problems. - } + String ipv4 = null; + if (form.containsKey("ipv4")) { + ipv4 = form.getString("ipv4"); + if (ipv4 == null || ipv4.isEmpty()) { + errors.add("IPv4 address is not specified."); + ipv4 = null; // Non-null but empty values could cause problems. + } else { + final IPAddress address = new IPAddressString(ipv4).getHostAddress(); + if (address != null) { + if (!new IPAddressString("172.20.0.0/14").getAddress().contains(address)) { + errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x)."); + } + // Remove prefixes + ipv4 = address.toNormalizedString(); + } else + errors.add("IPv4 address is illegal. Cannot parse your address."); } + } else { + errors.add("IPv4 address is not specified."); + } - boolean mpbgp = false; - if (form.containsKey("mpbgp")) { - if (ipv6 == null) { - errors.add("MP-BGP cannot be enabled if you do not have a valid IPv6 address."); - } else { - mpbgp = true; - } + String ipv6 = null; + if (form.containsKey("ipv6")) { + ipv6 = form.getString("ipv6"); + if (ipv6 != null && !ipv6.isEmpty()) { + final IPAddress address = new IPAddressString(ipv6).getHostAddress(); + if (address != null) { + ipv6 = address.toCanonicalString(); + if(!new IPAddressString("fd00::/8").getAddress().contains(address) && + !address.isLinkLocal()) { + errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address."); + } + } else + errors.add("IPv6 address is illegal. Cannot parse your address."); + } else { + ipv6 = null; // Non-null but empty values could cause problems. } + } - String wgEndpoint = null; - boolean wgEndpointCorrect = false; - if (form.containsKey("wg_endpoint")) { - if (type == Peer.VPNType.WIREGUARD) { - wgEndpoint = form.getString("wg_endpoint"); - if (wgEndpoint != null && !wgEndpoint.isEmpty()) { - if (InetAddressValidator.getInstance().isValidInet4Address(wgEndpoint)) { - if (new CIDRUtils("10.0.0.0/8").isInRange(wgEndpoint) || - new CIDRUtils("192.168.0.0/16").isInRange(wgEndpoint) || - new CIDRUtils("172.16.0.0/23").isInRange(wgEndpoint)) { - errors.add("WireGuard EndPoint is illegal. It must not be an internal address."); - } else { - wgEndpointCorrect = true; - } - } else - errors.add("WireGuard EndPoint is illegal. Cannot parse your address."); - } else { - wgEndpoint = null; // Non-null but empty values could cause problems. - } - } else { - errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); - } + boolean mpbgp = false; + if (form.containsKey("mpbgp")) { + if (ipv6 == null) { + errors.add("MP-BGP cannot be enabled if you do not have a valid IPv6 address."); + } else { + mpbgp = true; } + } - Integer wgEndpointPort = null; - if (form.containsKey("wg_endpoint_port")) { - if (type == Peer.VPNType.WIREGUARD) { - final String rawPort = form.getString("wg_endpoint_port"); - if(rawPort != null && !rawPort.isEmpty()) { - if (wgEndpointCorrect) { - try { - wgEndpointPort = Integer.parseInt(rawPort); - if (wgEndpointPort < 0 || wgEndpointPort > 65535) { - errors.add("WireGuard EndPoint port must be in UDP port range."); - } - } catch (NumberFormatException | NullPointerException ignored) { - errors.add("WireGuard EndPoint port is not valid. It must be a number."); - } + String wgEndpoint = null; + boolean wgEndpointCorrect = false; + if (form.containsKey("wg_endpoint")) { + if (type == Peer.VPNType.WIREGUARD) { + wgEndpoint = form.getString("wg_endpoint"); + if (wgEndpoint != null && !wgEndpoint.isEmpty()) { + final IPAddress address = new IPAddressString(wgEndpoint).getHostAddress(); + if (address != null) { + if(new IPAddressString("10.0.0.0/8").getAddress().contains(address) || + new IPAddressString("192.168.0.0/16").getAddress().contains(address) || + new IPAddressString("172.16.0.0/23").getAddress().contains(address)) { + errors.add("WireGuard EndPoint is illegal. It must not be an internal address."); } else { - errors.add("WireGuard EndPoint IP is not specified or invalid, but port is specified."); + // Remove prefixes + wgEndpoint = address.toNormalizedString(); + wgEndpointCorrect = true; } - } + } else + errors.add("WireGuard EndPoint is illegal. Cannot parse your address."); } else { - errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); + wgEndpoint = null; // Non-null but empty values could cause problems. } + } else { + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); } + } - // When user specified the endpoint without the port. - if(type == Peer.VPNType.WIREGUARD && - wgEndpointCorrect && - wgEndpointPort == null) { - errors.add("WireGuard EndPoint IP is specified, but the port is missing."); - } - - String wgPubKey = null; - if (form.containsKey("wg_pubkey")) { - if (type == Peer.VPNType.WIREGUARD) { - wgPubKey = form.getString("wg_pubkey"); - if (wgPubKey == null || wgPubKey.isEmpty()) { - errors.add("WireGuard public key is not specified."); - wgPubKey = null; // Non-null but empty values could cause problems. - } else { + Integer wgEndpointPort = null; + if (form.containsKey("wg_endpoint_port")) { + if (type == Peer.VPNType.WIREGUARD) { + final String rawPort = form.getString("wg_endpoint_port"); + if(rawPort != null && !rawPort.isEmpty()) { + if (wgEndpointCorrect) { try { - Key.fromBase64(wgPubKey); - } catch (KeyFormatException e) { - errors.add("WireGuard public key is not valid."); + wgEndpointPort = Integer.parseInt(rawPort); + if (wgEndpointPort < 0 || wgEndpointPort > 65535) { + errors.add("WireGuard EndPoint port must be in UDP port range."); + } + } catch (NumberFormatException | NullPointerException ignored) { + errors.add("WireGuard EndPoint port is not valid. It must be a number."); } + } else { + errors.add("WireGuard EndPoint IP is not specified or invalid, but port is specified."); } - } else { - errors.add("WireGuard tunneling is not selected but WireGuard public key appears."); } } else { - if (type == Peer.VPNType.WIREGUARD) { - errors.add("WireGuard public key is not specified."); - } + errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears."); } + } - if(node != null && !node.getSupportedVPNTypes().contains(type)) { - errors.add(String.format("Node %s does not support VPN type %s.", node.getName(), - type)); - } + // When user specified the endpoint without the port. + if(type == Peer.VPNType.WIREGUARD && + wgEndpointCorrect && + wgEndpointPort == null) { + errors.add("WireGuard EndPoint IP is specified, but the port is missing."); + } - Peer peer; + String wgPubKey = null; + if (form.containsKey("wg_pubkey")) { if (type == Peer.VPNType.WIREGUARD) { - peer = new Peer(ipv4, ipv6, wgEndpoint, wgEndpointPort, wgPubKey, mpbgp, n); + wgPubKey = form.getString("wg_pubkey"); + if (wgPubKey == null || wgPubKey.isEmpty()) { + errors.add("WireGuard public key is not specified."); + wgPubKey = null; // Non-null but empty values could cause problems. + } else { + try { + Key.fromBase64(wgPubKey); + } catch (KeyFormatException e) { + errors.add("WireGuard public key is not valid."); + } + } } else { - peer = new Peer( - -1, - type, - null, /* ASN: To be filled later */ - ipv4, - ipv6, - wgEndpoint, - wgEndpointPort, - null, /* Self public key: Generate later if needed */ - null, /* Self private key: Generate later if needed */ - wgPubKey, - null /* Preshared Secret: Generate later if needed */, - ProvisionStatus.NOT_PROVISIONED, - mpbgp, - n - ); + errors.add("WireGuard tunneling is not selected but WireGuard public key appears."); } - if(errors.isEmpty()) { - return Future.succeededFuture(peer); - } else { - return Future.failedFuture(new FormException(peer, errors.toArray(new String[]{}))); + } else { + if (type == Peer.VPNType.WIREGUARD) { + errors.add("WireGuard public key is not specified."); } - } catch (IOException e) { - return Future.failedFuture(e); + } + + if(node != null && !node.getSupportedVPNTypes().contains(type)) { + errors.add(String.format("Node %s does not support VPN type %s.", node.getName(), + type)); + } + + Peer peer; + if (type == Peer.VPNType.WIREGUARD) { + peer = new Peer(ipv4, ipv6, wgEndpoint, wgEndpointPort, wgPubKey, mpbgp, n); + } else { + peer = new Peer( + -1, + type, + null, /* ASN: To be filled later */ + ipv4, + ipv6, + wgEndpoint, + wgEndpointPort, + null, /* Self public key: Generate later if needed */ + null, /* Self private key: Generate later if needed */ + wgPubKey, + null /* Preshared Secret: Generate later if needed */, + ProvisionStatus.NOT_PROVISIONED, + mpbgp, + n + ); + } + if(errors.isEmpty()) { + return Future.succeededFuture(peer); + } else { + return Future.failedFuture(new FormException(peer, errors.toArray(new String[]{}))); } }); } -- cgit v1.2.3