WIP on refactoring signzone, verifyzone
This commit is contained in:
parent
1406cd2e68
commit
4478d7e3af
@ -1,3 +1,3 @@
|
|||||||
build.deprecation=true
|
build.deprecation=true
|
||||||
build.debug=false
|
build.debug=true
|
||||||
build.java_version=17
|
build.java_version=17
|
||||||
|
BIN
lib/bcprov-jdk18on-1.78.jar
Normal file
BIN
lib/bcprov-jdk18on-1.78.jar
Normal file
Binary file not shown.
@ -61,7 +61,7 @@ public class JCEDnsSecSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public JCEDnsSecSigner(boolean verboseSigning) {
|
public JCEDnsSecSigner(boolean verboseSigning) {
|
||||||
this.mKeyConverter = null;
|
super();
|
||||||
this.mVerboseSigning = verboseSigning;
|
this.mVerboseSigning = verboseSigning;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,8 +334,8 @@ public class JCEDnsSecSigner {
|
|||||||
// Remove duplicate records
|
// Remove duplicate records
|
||||||
SignUtils.removeDuplicateRecords(records);
|
SignUtils.removeDuplicateRecords(records);
|
||||||
|
|
||||||
// Generate DS records. This replaces any non-zone-apex DNSKEY RRs with DS
|
// Generate DS records. This replaces any non-zone-apex DNSKEY RRs with DS RRs.
|
||||||
// RRs.
|
// This is not a common practice, so this step may be dropped in the future.
|
||||||
SignUtils.generateDSRecords(zonename, records, dsDigestAlg);
|
SignUtils.generateDSRecords(zonename, records, dsDigestAlg);
|
||||||
|
|
||||||
// Generate the NSEC or NSEC3 records based on 'mode'
|
// Generate the NSEC or NSEC3 records based on 'mode'
|
||||||
|
@ -31,6 +31,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@ -61,11 +62,11 @@ public class SignUtils {
|
|||||||
private static final int ASN1_INT = 0x02;
|
private static final int ASN1_INT = 0x02;
|
||||||
private static final int ASN1_SEQ = 0x30;
|
private static final int ASN1_SEQ = 0x30;
|
||||||
|
|
||||||
public static final int RR_NORMAL = 0;
|
// public static final int RR_NORMAL = 0;
|
||||||
public static final int RR_DELEGATION = 1;
|
// public static final int RR_DELEGATION = 1;
|
||||||
public static final int RR_GLUE = 2;
|
// public static final int RR_GLUE = 2;
|
||||||
public static final int RR_INVALID = 3;
|
// public static final int RR_INVALID = 3;
|
||||||
public static final int RR_DNAME = 4;
|
// public static final int RR_DNAME = 4;
|
||||||
|
|
||||||
private static Logger log;
|
private static Logger log;
|
||||||
|
|
||||||
@ -714,6 +715,41 @@ public class SignUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void generateNSECRecord(ZoneData zone) {
|
||||||
|
Name lastCut = null;
|
||||||
|
Name lastDname = null;
|
||||||
|
Map.Entry<Name, Set<Integer>> lastNode = null;
|
||||||
|
int backup;
|
||||||
|
long nsecTTL = 0;
|
||||||
|
|
||||||
|
SOARecord soa = (SOARecord) zone.getSOA();
|
||||||
|
nsecTTL = Math.min(soa.getMinimum(), soa.getTTL());
|
||||||
|
|
||||||
|
for (Map.Entry<Name, Set<Integer>> entry : zone.nodeMap().entrySet()) {
|
||||||
|
ZoneData.NodeType nodeType = zone.determineNodeType(entry.getKey(), entry.getValue(), lastCut, lastDname);
|
||||||
|
|
||||||
|
switch (nodeType) {
|
||||||
|
case INVALID:
|
||||||
|
case GLUE:
|
||||||
|
// we can skip these
|
||||||
|
continue;
|
||||||
|
case DELEGATION:
|
||||||
|
lastCut = entry.getKey();
|
||||||
|
break;
|
||||||
|
case DNAME:
|
||||||
|
lastDname = entry.getKey();
|
||||||
|
break;
|
||||||
|
case NORMAL:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (lastNode != null) {
|
||||||
|
NSECRecord nsec = new NSECRecord(lastNode.getKey(), DClass.IN, nsecTTL, entry.getKey(), lastNode.getValue().toArray());
|
||||||
|
}
|
||||||
|
NSECRecord nsec =
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Given a canonical (by name) ordered list of records in a zone, generate the
|
* Given a canonical (by name) ordered list of records in a zone, generate the
|
||||||
* NSEC records in place.
|
* NSEC records in place.
|
||||||
@ -1307,7 +1343,7 @@ public class SignUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a zone with DNSKEY records at delegation points, convert those KEY
|
* Given a zone with DNSKEY records at delegation points, convert those DNSKEY
|
||||||
* records into their corresponding DS records in place.
|
* records into their corresponding DS records in place.
|
||||||
*
|
*
|
||||||
* @param zonename the name of the zone, used to reliably distinguish the
|
* @param zonename the name of the zone, used to reliably distinguish the
|
||||||
|
397
src/main/java/com/verisignlabs/dnssec/security/ZoneData.java
Normal file
397
src/main/java/com/verisignlabs/dnssec/security/ZoneData.java
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
package com.verisignlabs.dnssec.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.xbill.DNS.Master;
|
||||||
|
import org.xbill.DNS.NSEC3PARAMRecord;
|
||||||
|
import org.xbill.DNS.NSEC3Record;
|
||||||
|
import org.xbill.DNS.Name;
|
||||||
|
import org.xbill.DNS.RRSIGRecord;
|
||||||
|
import org.xbill.DNS.RRset;
|
||||||
|
import org.xbill.DNS.Record;
|
||||||
|
import org.xbill.DNS.SOARecord;
|
||||||
|
import org.xbill.DNS.Type;
|
||||||
|
|
||||||
|
public class ZoneData {
|
||||||
|
private SortedMap<Name, Set<Integer>> mNodeMap;
|
||||||
|
private HashMap<String, RRset> mRRsetMap;
|
||||||
|
private SortedMap<Name, MarkRRset> mNSECMap;
|
||||||
|
private SortedMap<Name, MarkRRset> mNSEC3Map;
|
||||||
|
private Name mZoneName;
|
||||||
|
private DNSSECType mDNSSECType;
|
||||||
|
private NSEC3PARAMRecord mNSEC3params;
|
||||||
|
// Configuration parameters
|
||||||
|
private boolean mIgnoreDuplicateRRs;
|
||||||
|
private boolean mStripDNSSEC;
|
||||||
|
|
||||||
|
private Logger log = Logger.getLogger(ZoneData.class.toString());
|
||||||
|
|
||||||
|
// The various types of signed zones.
|
||||||
|
enum DNSSECType {
|
||||||
|
UNSIGNED, NSEC, NSEC3, NSEC3_OPTOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The types of nodes (a node consists of all RRs with the same name).
|
||||||
|
enum NodeType {
|
||||||
|
NORMAL, DELEGATION, GLUE, DNAME, INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if mStripDNSSEC is true, then RRsets of the following types will be skipped
|
||||||
|
// on load, or stripped.
|
||||||
|
private static final Set<Integer> GENTYPES_SET = Set.of(Type.RRSIG, Type.NSEC, Type.NSEC3, Type.NSEC3PARAM);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a subclass of {@link org.xbill.DNS.RRset} that adds a "mark".
|
||||||
|
*/
|
||||||
|
public class MarkRRset extends RRset {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private boolean mIsMarked = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return super.equals(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return super.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean getMark() {
|
||||||
|
return mIsMarked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMark(boolean value) {
|
||||||
|
mIsMarked = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoneData() {
|
||||||
|
mNodeMap = new TreeMap<>();
|
||||||
|
mRRsetMap = new HashMap<>();
|
||||||
|
mNSECMap = new TreeMap<>();
|
||||||
|
mNSEC3Map = new TreeMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoneData(boolean stripDNSSEC, boolean ignoreDuplicates) {
|
||||||
|
super();
|
||||||
|
setStripDNSSEC(stripDNSSEC);
|
||||||
|
setIgnoreDuplicateRRs(ignoreDuplicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStripDNSSEC(boolean value) {
|
||||||
|
this.mStripDNSSEC = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIgnoreDuplicateRRs(boolean value) {
|
||||||
|
this.mIgnoreDuplicateRRs = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The detected DNSSEC "type" (what form of negative proof it uses), or
|
||||||
|
* UNSIGNED if the zone is unsigned or nothing has been processed yet.
|
||||||
|
*/
|
||||||
|
public DNSSECType getDNSSECType() {
|
||||||
|
return mDNSSECType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Any detected NSEC3PARAM record in the processed zone, or null if
|
||||||
|
* there wasn't one or no zone has been processed yet.
|
||||||
|
*/
|
||||||
|
public NSEC3PARAMRecord getNSEC3Params() {
|
||||||
|
return mNSEC3params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Name zoneName() {
|
||||||
|
return mZoneName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SOARecord getSOA() {
|
||||||
|
assert mRRsetMap != null;
|
||||||
|
|
||||||
|
RRset r = mRRsetMap.get(key(mZoneName, Type.SOA));
|
||||||
|
assert r != null;
|
||||||
|
|
||||||
|
return (SOARecord) r.first();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return The computed "node map" -- this is a mapping of
|
||||||
|
* {@link org.xbill.DNS.Name} objects to simple sets of DNS typecodes.
|
||||||
|
* This will assert if no zone has been processed yet.
|
||||||
|
*/
|
||||||
|
public SortedMap<Name, Set<Integer>> nodeMap() {
|
||||||
|
assert mNodeMap != null;
|
||||||
|
return mNodeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The computed NSEC map.
|
||||||
|
*/
|
||||||
|
public SortedMap<Name, MarkRRset> getNSECMap() {
|
||||||
|
assert mNSECMap != null;
|
||||||
|
|
||||||
|
return mNSECMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the computed NSEC3 map
|
||||||
|
*/
|
||||||
|
public SortedMap<Name, MarkRRset> getNSEC3Map() {
|
||||||
|
assert mNSEC3Map != null;
|
||||||
|
|
||||||
|
return mNSEC3Map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an RRset from our internal map with the given name and type, or null if not found.
|
||||||
|
*/
|
||||||
|
public RRset getRRset(Name n, int type) {
|
||||||
|
assert mRRsetMap != null;
|
||||||
|
|
||||||
|
return mRRsetMap.get(key(n, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formulate a unique key for an RRset in the RRsetMap. Since an RRset is
|
||||||
|
* defined to be the set of Records that all the same ownername and type, we can
|
||||||
|
* formulate a valid key by combining them.
|
||||||
|
*
|
||||||
|
* @param n the ownername of the RRset
|
||||||
|
* @param type The (integer) type of the RRset
|
||||||
|
* @return the key, as a string.
|
||||||
|
*/
|
||||||
|
public static String key(Name n, int type) {
|
||||||
|
return n.toString() + ':' + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an Record (RRSIG or otherwise) to a given RRset
|
||||||
|
*
|
||||||
|
* @param rrset The rrset to add to
|
||||||
|
* @param rr The record to add
|
||||||
|
* @return true if the record was actually added, false if not.
|
||||||
|
*/
|
||||||
|
private boolean addRRtoRRset(RRset rrset, Record rr) {
|
||||||
|
if (mIgnoreDuplicateRRs) {
|
||||||
|
rrset.addRR(rr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rr instanceof RRSIGRecord) {
|
||||||
|
for (RRSIGRecord sigrec : rrset.sigs()) {
|
||||||
|
if (rr.equals(sigrec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Record rec : rrset.rrs()) {
|
||||||
|
if (rr.equals(rec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rrset.addRR(rr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a record to the various maps.
|
||||||
|
*
|
||||||
|
* @param r the record to add
|
||||||
|
* @return true if the RR was added, false if it wasn't (because it was a
|
||||||
|
* duplicate)
|
||||||
|
*/
|
||||||
|
private boolean addRR(Record r) {
|
||||||
|
assert mNSEC3Map != null;
|
||||||
|
assert mRRsetMap != null;
|
||||||
|
|
||||||
|
Name n = r.getName();
|
||||||
|
int t = r.getType();
|
||||||
|
if (t == Type.RRSIG)
|
||||||
|
t = ((RRSIGRecord) r).getTypeCovered();
|
||||||
|
|
||||||
|
// Add NSEC and NSEC3 RRs to their respective maps
|
||||||
|
if (t == Type.NSEC) {
|
||||||
|
MarkRRset rrset = mNSECMap.computeIfAbsent(n, k -> new MarkRRset());
|
||||||
|
return addRRtoRRset(rrset, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t == Type.NSEC3) {
|
||||||
|
MarkRRset rrset = mNSEC3Map.computeIfAbsent(n, k -> new MarkRRset());
|
||||||
|
return addRRtoRRset(rrset, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the name and type to the node map
|
||||||
|
Set<Integer> typeset = mNodeMap.computeIfAbsent(n, k -> new HashSet<>());
|
||||||
|
typeset.add(r.getType()); // add the original type
|
||||||
|
|
||||||
|
// Add the record to the RRset map
|
||||||
|
String k = key(n, t);
|
||||||
|
RRset rrset = mRRsetMap.computeIfAbsent(k, k2 -> new RRset());
|
||||||
|
return addRRtoRRset(rrset, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a record, determine the DNSSEC signing type. If the record doesn't
|
||||||
|
* determine that, DNSSECType.UNSIGNED is returned
|
||||||
|
*
|
||||||
|
* @param r the record to examine
|
||||||
|
* @return a DNSSECType of UNKNOWN, unless r is an NSEC or NSEC3 record, in
|
||||||
|
* which case, the type corresponding to the
|
||||||
|
*/
|
||||||
|
private DNSSECType determineDNSSECType(Record r) {
|
||||||
|
if (r.getType() == Type.NSEC)
|
||||||
|
return DNSSECType.NSEC;
|
||||||
|
if (r.getType() == Type.NSEC3) {
|
||||||
|
NSEC3Record nsec3 = (NSEC3Record) r;
|
||||||
|
if ((nsec3.getFlags() & NSEC3Record.Flags.OPT_OUT) == NSEC3Record.Flags.OPT_OUT) {
|
||||||
|
return DNSSECType.NSEC3_OPTOUT;
|
||||||
|
}
|
||||||
|
return DNSSECType.NSEC3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DNSSECType.UNSIGNED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a resource record to the node map and rrsets map.
|
||||||
|
* @param r The record
|
||||||
|
* @return the number of errors encountered.
|
||||||
|
*/
|
||||||
|
private int addRecord(Record r) {
|
||||||
|
int errors = 0;
|
||||||
|
Name n = r.getName();
|
||||||
|
int t = r.getType();
|
||||||
|
|
||||||
|
// skip any generated DNSSEC records if we are skipping
|
||||||
|
if (mStripDNSSEC && GENTYPES_SET.contains(t)) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the record to the various maps.
|
||||||
|
boolean res = addRR(r);
|
||||||
|
if (!res) {
|
||||||
|
log.warning("Record '" + r + "' detected as a duplicate");
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Learn some things about the zone as we do this pass.
|
||||||
|
if (t == Type.SOA)
|
||||||
|
mZoneName = n;
|
||||||
|
if (t == Type.NSEC3PARAM)
|
||||||
|
mNSEC3params = (NSEC3PARAMRecord) r;
|
||||||
|
if (mDNSSECType == DNSSECType.UNSIGNED)
|
||||||
|
mDNSSECType = determineDNSSECType(r);
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a zone file, load the node and rrset maps, as well as
|
||||||
|
* determine the NSEC3 parameters and signing type.
|
||||||
|
*
|
||||||
|
* @param master A {@link org.xbill.DNS.Master} object.
|
||||||
|
* @return the number of errors encountered.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public int calculateNodes(String zoneFileName, Name origin) {
|
||||||
|
// The zone is unsigned until we get a clue otherwise.
|
||||||
|
mDNSSECType = DNSSECType.UNSIGNED;
|
||||||
|
|
||||||
|
int errors = 0;
|
||||||
|
try (Master m = zoneFileName.equals("-") ? new Master(System.in) : new Master(zoneFileName, origin)) {
|
||||||
|
Record r = null;
|
||||||
|
while ((r = m.nextRecord()) != null) {
|
||||||
|
errors += addRecord(r);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an unsorted list of records, load the node and rrset maps, as well as
|
||||||
|
* determine the NSEC3 parameters and signing type.
|
||||||
|
*
|
||||||
|
* @param records A list of {@link org.xbill.DNS.Record} objects, unsorted.
|
||||||
|
* @return the number of errors encountered.
|
||||||
|
*/
|
||||||
|
public int calculateNodes(List<Record> records) {
|
||||||
|
mNodeMap = new TreeMap<>();
|
||||||
|
mRRsetMap = new HashMap<>();
|
||||||
|
|
||||||
|
// The zone is unsigned until we get a clue otherwise.
|
||||||
|
mDNSSECType = DNSSECType.UNSIGNED;
|
||||||
|
|
||||||
|
int errors = 0;
|
||||||
|
for (Record r : records) {
|
||||||
|
errors += addRecord(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a name, typeset, and name of the last zone cut, determine the node
|
||||||
|
* type.
|
||||||
|
*
|
||||||
|
* @param n The owner name for the node (a collection of RRsets with all
|
||||||
|
* the same owner)
|
||||||
|
* @param typeset The (simple) set of types present at that node
|
||||||
|
* @param lastCut The name of the last "zone cut". This is the last name (if
|
||||||
|
* encountering the name in DNSSEC canonical name order) that was
|
||||||
|
* either a delegation or a DNAME.
|
||||||
|
*/
|
||||||
|
public NodeType determineNodeType(Name n, Set<Integer> typeset, Name lastCut, Name lastDname) {
|
||||||
|
// Names not at or below the zone name are "invalid" -- they shouldn't
|
||||||
|
// be in the zone at all, normally. In practice, they are the same as
|
||||||
|
// glue.
|
||||||
|
if (!n.subdomain(mZoneName)) {
|
||||||
|
return NodeType.INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All RRs at the zone apex are normal
|
||||||
|
if (n.equals(mZoneName))
|
||||||
|
return NodeType.NORMAL;
|
||||||
|
|
||||||
|
// If the node is not below the zone itself, we will treat it as glue (it is
|
||||||
|
// really junk).
|
||||||
|
if (!n.subdomain(mZoneName)) {
|
||||||
|
return NodeType.GLUE;
|
||||||
|
}
|
||||||
|
// If the node is below a zone cut (either a delegation or DNAME), it is
|
||||||
|
// glue.
|
||||||
|
if (lastCut != null && n.subdomain(lastCut) && !n.equals(lastCut)) {
|
||||||
|
return NodeType.GLUE;
|
||||||
|
}
|
||||||
|
if (lastDname != null && n.subdomain(lastDname) && !n.equals(lastDname)) {
|
||||||
|
return NodeType.GLUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has a NS record it is a delegation.
|
||||||
|
if (typeset.contains(Integer.valueOf(Type.NS)))
|
||||||
|
return NodeType.DELEGATION;
|
||||||
|
|
||||||
|
// If the node has a DNAME record, is similar to a delegation
|
||||||
|
if (typeset.contains(Integer.valueOf(Type.DNAME))) {
|
||||||
|
return NodeType.DNAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything else is normal
|
||||||
|
return NodeType.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user