The following are code snippets from javAPRSSrvr demonstrating packet header parsing to clean and mangled headers and reformat AEA headers to TNC2 format.
Note that the base header parsing also applies to third-party header parsing.
Callsign-SSID Parsing
private CallsignSSID(String callsign, String SSID, String callSSID, boolean isValidCallSSID)
{
this.callsign = callsign;
this.SSID = SSID;
this.callSSID = callSSID;
this.isValidCallSSID = isValidCallSSID;
}
/**
* Base callsign-SSID regex group 1=callsign, 2=SSID
*/
private static final Pattern regexcallssid = Pattern.compile("^([0-9A-Z]{1,9}+)(?>-([0-9A-Z]{1,2}+))?+$", Pattern.CASE_INSENSITIVE);
/**
* Creates CallsignSSID. Basic checks are:
* 1) length of callsign {@literal >0 <=9}
* 2) length of SSID {@literal >=0 <=2}
* 3) total length of callsign-SSID {@literal <= 9}
* 4) Callsign and SSID ASCII alphanumeric
* 5) CAUTION: -0 is NOT converted to no SSID due to dupe check issues
*
* @param testcall Callsign-SSID to check and create CallsignSSID from
* @return new CallsignSSID or emptyCall
*/
public static CallsignSSID parse(String testcall)
{
String tcall = testcall.trim();
if (tcall.isEmpty() || tcall.length() > 9)
return emptyCall;
Matcher tmatch = getMatcher(regexcallssid, tcall);
if (tmatch.matches())
{
if ("0".equals(tmatch.group(2)))
return emptyCall;
if (tmatch.group(2) == null)
return new CallsignSSID(tmatch.group(1), "", tmatch.group(1), true);
return new CallsignSSID(tmatch.group(1), tmatch.group(2), tmatch.group(0), true);
}
return emptyCall;
}
/**
* This is the international standard callsign pattern for use in other
* patterns
*/
public static final Pattern intlcallpat = Pattern.compile("(?>[1-9][A-Z][A-Z]?+[0-9]|[A-Z][2-9A-Z]?[0-9])[A-Z]{1,4}+");
private static final Pattern hamcallpat = Pattern.compile("^" + intlcallpat + "$");
/**
* RegEx for this is
* ^(?:(?:[1-9][A-Z][A-Z]?)|(?:[A-Z][2-9A-Z]?))[0-9][A-Z]{1,4}$ Determines
* if the callsign conforms to amateur radio international rules (#A#, #AA#,
* A#, A##, or AA#). Callsign length must be between 3 and 7 characters.
*
* @return true if callsign is compliant
*/
public boolean isHamCall()
{
return (callsign.length() < 3 || callsign.length() > 7) ? false : getMatcher(hamcallpat, callsign).matches();
}
Base TNC2/AEA Parsing
/**
* Group 1: Org Call Group 2: Dest Call Group 3: Path Group 4: Ifield
*/
private final static Pattern baseheaderpat = Pattern.compile("^([0-9A-Z-]{3,9}+)>([0-9A-Z-]{1,9}+)((?>[,>][0-9A-Z-]{1,9}+[*]?+)*):(.{1,256}+)$", Pattern.CASE_INSENSITIVE);
/**
* This parses TNC2 format (and AEA format) packets accounting for errors
* seen from misconfigured sources regarding AEA format. Also parses 3rd
* party embedded packets.
*
* @param orgPacket byte array containing TNC2(AEA) format packet
* @param startpos Starting position of header
* @return null if an invalid packet line
*/
public static APRSPacket parseTNCHeader(byte[] orgPacket, int startpos)
{
if (startpos > orgPacket.length - 8 || orgPacket[startpos] == (byte) '#')
return null;
// Nested third-party could get us here
// Comment line should never get here nor any call beginning with #
if (orgPacket[startpos] == (byte) '}')
return null; // Invalid nesting of third-party packets.
Matcher hdrmtch = getMatcher(baseheaderpat, getASCII(orgPacket, startpos, orgPacket.length - startpos));
if (!hdrmtch.matches()) // Match fails if no Callsign-SSID or Ifield, or if Ifield.length > 256
return null;
int IFieldPtr = hdrmtch.start(4) + startpos;
if (orgPacket[IFieldPtr] < 0x1c || orgPacket[IFieldPtr] == 0x7f)
return null;
String destcallstr = hdrmtch.group(2);
String hdrpathstr = hdrmtch.group(3);
// From call must start with letter or digit
CallsignSSID OrgCall = CallsignSSID.parse(hdrmtch.group(1));
if (OrgCall.callsign.length() < 3 || !OrgCall.isValidCallSSID)
return null; // Callsigns must be at least 3 characters
CallsignSSID DestCall = CallsignSSID.parse(destcallstr);
if (!DestCall.isValidCallSSID)
return null; // ToCalls could be 1 character
int lastdigi = -1;
List<CallsignSSID> path;
if (hdrpathstr.isEmpty())
path = new ArrayList<>(0);
else
{
boolean AEA = hdrpathstr.contains(">");
String[] st = hdrpathstr.split(AEA ? "[>,]" : ",");
path = new ArrayList<>(st.length - 1);
int qloc = -1;
int Ipath = -1;
for (String pathcallstr : st)
{
if (pathcallstr.isEmpty())
continue; // Skips first empty path
Matcher matcher = getMatcher(pathcallpat, pathcallstr);
if (!matcher.matches())
return null; // Invalid path call
final boolean isdigi = (matcher.group(2) != null);
CallsignSSID pathcall = CallsignSSID.parse(matcher.group(1));
if (!pathcall.isValidCallSSID)
return null; // Invalid callsign-SSID
switch (pathcall.callSSID.length())
{
case 0:
return null;
case 1:
if (pathcall.callSSID.charAt(0) == 'I')
{
if (qloc >= 0 || Ipath >= 0)
return null; // Only one q or I construct allowed
Ipath = path.size();
}
break;
case 3:
if (pathcall.callSSID.charAt(0) == 'q')
{
if (qloc >= 0 || Ipath >= 0)
return null; // Only one q construct allowed
qloc = path.size();
}
break;
default:
}
path.add(pathcall);
if (isdigi
&& Ipath < 0
&& qloc < 0
&& !(AEA && path.size() == st.length - 1))
// marked as a digipeater, don't set lastdigi unless before
// Ipath, qloc, or if AEA, destination call
lastdigi = path.size() - 1;
}
if (AEA)
{
CallsignSSID tdest = DestCall;
// Fix bad IGate strings
if (path.size() >= 3)
if (qloc > 0)
// q tacked on after dest call
DestCall = path.remove(qloc - 1);
else if (Ipath == path.size() - 1)
// ,I tacked on after dest call
DestCall = path.remove(Ipath - 1);
else
DestCall = path.remove(path.size() - 1);
else
// OK AEA format now
DestCall = path.remove(path.size() - 1);
path.add(0, tdest);
if (lastdigi == path.size())
--lastdigi; // Adjust for malformed lastdigi
}
if (Ipath == 0 || (Ipath > 0 && Ipath != path.size() - 1))
return null; // I can't be at beginning or any place but the end so this is badly mangled
if (qloc >= 0 && qloc == path.size() - 1)
return null; // q construct must have atleast one callsign following it
// Malformed ,I and q constructs out there showing as digi'ed
// Move digi indicator to before the construct
if (qloc >= 0 && lastdigi >= qloc)
lastdigi = qloc - 1;
else if (Ipath > 0 && lastdigi >= Ipath - 1)
lastdigi = Ipath - 2;
}
if (DestCall.callsign.length() < 2)
return null;
// Handle third party by parsing until not third-party
if (orgPacket[IFieldPtr] == '}')
{
if (IFieldPtr > orgPacket.length - 9 || orgPacket[IFieldPtr + 1] == '}')
return null; // Invalid 3rd party packet from the get-go
APRSPacket tp = parseTNCHeader(orgPacket, IFieldPtr + 1);
if (tp == null || tp.path.size() != 2 || tp.lastdigi != 1)
return null; // Invalid 3rd party
return new APRSPacket(OrgCall, DestCall, path, lastdigi, tp);
}
return new APRSPacket(OrgCall, DestCall, path, lastdigi, Arrays.copyOfRange(orgPacket, IFieldPtr, orgPacket.length));
}
Client Header Mangling Detection/Cleanup
/**
* Group 1: Callsign-SSID Group 2: Path Group 3: First byte of Ifield
*/
private final static Pattern headerpat = Pattern.compile("^(?>[^<>]*<[^>]*>)*+[^>]*?([A-Z0-9-]+)\\s*>([^\\[:]+)(?>[^\\[:]*\\[[^\\]]*\\]?)*+[^:]*:(.{1,256})$", Pattern.CASE_INSENSITIVE);
/**
* This parses TNC2 format (and AEA format) packets accounting for errors
* seen from misconfigured sources.
*
* @param pkt Packet to parse
* @param startpos Starting position to end of bytes
* @return <code>null</code> if an invalid header
*/
@Override
public ClientPacket parseTNC2Header(byte[] pkt, int startpos)
{
if (startpos > pkt.length - 8 || pkt[startpos] == (byte) '#')
return null;
// Nested third-party could get us here
// Comment line should never get here nor any call beginning with #
if (pkt[startpos] == (byte) '}')
return null; // Invalid nesting of third-party packets.
// Potentially mangled so we reparse here
Matcher hdrmtch = getMatcher(headerpat, getASCII(pkt, startpos, pkt.length - startpos));
if (!hdrmtch.matches()) // Match fails if no Callsign-SSID or Ifield, or if Ifield.length > 256
return null;
int IFieldPtr = hdrmtch.start(3) + startpos;
if (pkt[IFieldPtr] < 0x1c || pkt[IFieldPtr] == 0x7f)
return null;
String fullhdrpath = hdrmtch.group(2);
String headerpath = fullhdrpath.trim();
if (headerpath.length() < 3)
return null;
boolean regexclean = (hdrmtch.start(1) == 0 && hdrmtch.end(1) + 1 == hdrmtch.start(2) && hdrmtch.end(2) + 1 == hdrmtch.start(3));
String callstr = hdrmtch.group(1);
CallsignSSID OrgCall = CallsignSSID.parse(callstr);
if (OrgCall.callsign.length() < 3 || !OrgCall.isValidCallSSID)
return null; // Callsigns must be at least 3 characters
// Cleanup embedded <UI> in path
int lb = headerpath.indexOf(" <UI");
if (lb > 0)
{
int rb = headerpath.indexOf('>', lb + 3);
if (rb < 0)
{
rb = headerpath.indexOf(',', lb + 3);
if (rb > 0 && rb < headerpath.length() - 1)
// WinAPRS thought this was sort of AEA
// To field must follow
headerpath = headerpath.substring(rb + 1).trim() + ',' + headerpath.substring(0, lb).trim();
else
headerpath = headerpath.substring(0, lb).trim();
}
else if (rb > 0)
// monitored UI header improperly passed, get rid of it
if (rb < headerpath.length() - 1)
headerpath = headerpath.substring(0, lb).trim() + headerpath.substring(rb + 1).trim();
else
headerpath = headerpath.substring(0, lb).trim();
}
// Delete port indicator, <, and spaces
lb = idxmin(headerpath.indexOf('/'), headerpath.indexOf(' '));
lb = idxmin(lb, headerpath.indexOf('<'));
lb = idxmin(lb, headerpath.length()); // If lb < 0, lb now = length()
// Remove trailing commas
for (; lb > 0 && headerpath.charAt(lb - 1) == ','; lb--)
{
}
// Remove trailing >'s
for (; lb > 0 && headerpath.charAt(lb - 1) == '>'; lb--)
{
}
if (lb < 2)
return null; // 2 char To
if (lb < headerpath.length())
{
headerpath = headerpath.substring(0, lb).trim();
if (headerpath.length() < 2)
return null;
}
ClientPacket retpacket;
if (regexclean && OrgCall.callSSID.equals(callstr) && fullhdrpath.equals(headerpath))
retpacket = super.parseTNC2Header(pkt, startpos); // Original packet ok regarding header (regex didn't eliminate anything)
else
{
// Some part of the header was mangled so recreate for parsing
byte[] fixedpkt = new byte[OrgCall.callSSID.length() + headerpath.length() + 2 + pkt.length - IFieldPtr];
MiscTools.getASCIIBytes(OrgCall.callSSID, fixedpkt, 0);
fixedpkt[OrgCall.callSSID.length()] = '>';
MiscTools.getASCIIBytes(headerpath, fixedpkt, OrgCall.callSSID.length() + 1);
System.arraycopy(pkt, IFieldPtr - 1, fixedpkt, OrgCall.callSSID.length() + headerpath.length() + 1, pkt.length - IFieldPtr + 1); // includes colon
retpacket = super.parseTNC2Header(fixedpkt, 0);
}
return retpacket;
}