Server Header Parsing


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;
        }