/** * Copyright (C) 2013 Alexander Szczuczko * * This file may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ package ca.szc.keratin.core.net.message; import java.util.LinkedList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Represents, parses and provides general format verification of IRC message lines, as defined by RFC 1459. */ public class IrcMessage { /** * Pattern that will match a DNS host name (ASCII only). Not perfect. */ private static final Pattern hostPat = Pattern.compile( "(?:(?:\\p{Alpha}|\\p{Digit}|-){1,63}\\.)*(?:\\p{Alpha}|\\p{Digit}|-){1,63}" ); /** * Pattern that will match a nick name (Extended charset) */ private static final Pattern nickPat = Pattern.compile( "(?:\\p{IsAlphabetic}|\\p{Punct})(?:\\p{IsAlphabetic}|\\p{Punct}|\\d)*" ); /** * Pattern that will match a RFC 1459 message prefix, not including the leading colon. */ // <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ] private static final Pattern prefixPattern = Pattern.compile( "(?:" + hostPat + "|" + nickPat + "(?:!\\S\\S*){0,1}(?:@" + hostPat + "){0,1})" ); /** * Pattern that will match a RFC 1459 message command, or a reply "command" number. */ // <command> ::= <letter> { <letter> } | <number> <number> <number> private static final Pattern commandPattern = Pattern.compile( "(?:\\p{IsAlphabetic}+|\\d\\d\\d)" ); /** * Pattern that will match a RFC 1459 middle parameter. Middle parameters aren't allowed spaces or a leading colon. */ // <middle> ::= <Any *non-empty* sequence of octets not including SPACE // or NUL or CR or LF, the first of which may not be ':'> private static final Pattern middleParamPattern = Pattern.compile( "[^: ][^ \r\n]*" ); /** * Pattern that will match a RFC 1459 trailing parameter. Trailing parameters are allowed spaces, and should have a * leading colon. It is feasible for the colon to not be required when the trailing parameter has no spaces. */ // <trailing> ::= <Any, possibly *empty*, sequence of octets not including // NUL or CR or LF> private static final Pattern trailingParamPattern = Pattern.compile( ":?[^\r\n]*" ); /** * Pattern that will match a RFC 1459 message line. Group 1 contains the prefix, group 2 contains the command, and * group 3 contains any parameters, as a blob. */ // <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf> // <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ] private static final Pattern parseLinePattern = Pattern.compile( "(?::(" + prefixPattern + ") |)(" + commandPattern + ")((?: " + middleParamPattern + ")* (?:" + trailingParamPattern + "))+" ); /** * Pattern that will match either a middle or trailing parameter, storing it in group 1. Best used with find() * rather than matches(). */ private static final Pattern parseLineParamsPattern = Pattern.compile( " (" + middleParamPattern + "|" + trailingParamPattern + ")" ); /** * The RFC 1459 message prefix */ private final String prefix; /** * The RFC 1459 message command */ private final String command; /** * The RFC 1459 message parameters */ private final String[] params; // Validating based on RFC 1459, grammar excerpt follows: // // The BNF representation for this is: // // <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf> // <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ] // <command> ::= <letter> { <letter> } | <number> <number> <number> // <SPACE> ::= ' ' { ' ' } // <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ] // // <middle> ::= <Any *non-empty* sequence of octets not including SPACE // or NUL or CR or LF, the first of which may not be ':'> // <trailing> ::= <Any, possibly *empty*, sequence of octets not including // NUL or CR or LF> // // <crlf> ::= CR LF // // <target> ::= <to> [ "," <target> ] // <to> ::= <channel> | <user> '@' <servername> | <nick> | <mask> // <channel> ::= ('#' | '&') <chstring> // <servername> ::= <host> // <host> ::= see RFC 952 [DNS:4] for details on allowed hostnames // <nick> ::= <letter> { <letter> | <number> | <special> } // <mask> ::= ('#' | '$') <chstring> // <chstring> ::= <any 8bit code except SPACE, BELL, NUL, CR, LF and // comma (',')> // // Other parameter syntaxes are: // // <user> ::= <nonwhite> { <nonwhite> } // <letter> ::= 'a' ... 'z' | 'A' ... 'Z' // <number> ::= '0' ... '9' // <special> ::= '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}' /** * Create a RFC 1459 message. See the RFC's section 2.3.1 for a detailed specification. * * @param prefix The prefix component, may be null. * @param command The command component, may not be null, may not be zero length. * @param params The params component, may be zero length, no element may be null. * @throws InvalidMessagePrefixException If an invalid prefix component is given. * @throws InvalidMessageCommandException If an invalid command component is given. * @throws InvalidMessageParamException If one of the parts of the given params component is invalid. */ /** * Create IRC message from its three main components. * * @param prefix The message prefix, may be null. * @param command The command * @param params The parameters, may be zero-length. * @throws InvalidMessagePrefixException If the prefix is invalid * @throws InvalidMessageCommandException If the command is invalid * @throws InvalidMessageParamException If a parameter is invalid */ // <message> public IrcMessage( String prefix, String command, String... params ) throws InvalidMessagePrefixException, InvalidMessageCommandException, InvalidMessageParamException { // <prefix> if ( prefix != null ) { if ( prefix.length() <= 0 ) throw new InvalidMessagePrefixException( "Must be greater than zero length" ); if ( !prefixPattern.matcher( prefix ).matches() ) throw new InvalidMessagePrefixException( "Did not match pattern" ); } // <command> if ( command.length() <= 0 ) throw new InvalidMessageCommandException( "Must be greater than zero length" ); if ( !commandPattern.matcher( command ).matches() ) throw new InvalidMessageCommandException( "Did not match pattern" ); // <params> for ( int i = 0; i < params.length; i++ ) { String param = params[i]; if ( i == params.length - 1 ) { // <trailing> if ( param.length() == 0 || param.charAt( 0 ) != ':' ) { param = ":" + param; params[i] = param; } if ( !trailingParamPattern.matcher( param ).matches() ) throw new InvalidMessageParamException( "'" + param + "' did not match trailing param pattern" ); } else { // <middle> if ( param.length() <= 0 ) throw new InvalidMessageParamException( "Must be greater than zero length" ); if ( !middleParamPattern.matcher( param ).matches() ) throw new InvalidMessageParamException( "'" + param + "' did not match middle param pattern" ); } } this.prefix = prefix; this.command = command; this.params = params; } /** * Parse a raw IRC message into an IrcMessage * * @param line The raw line of an IRC message * @return An IrcMessage representation of line * @throws InvalidMessageException */ public static IrcMessage parseMessage( String line ) throws InvalidMessageException { IrcMessage retMsg; Matcher matcher = parseLinePattern.matcher( line ); if ( matcher.matches() ) { String prefix = matcher.group( 1 ); // prefix is null if not present String command = matcher.group( 2 ); String[] params = parseMessageParams( matcher.group( 3 ) ); retMsg = new IrcMessage( prefix, command, params ); } else { throw new InvalidMessageException( "Message didn't match pattern" ); } return retMsg; } /** * Parse a parameter blob into parameters. * * @param paramBlob A bunch of parameters in one string * @return Seperated parameters. Last parameter is the trailing parameter, and may have spaces. * @throws InvalidMessageException If no parameters were matched */ private static String[] parseMessageParams( String paramBlob ) throws InvalidMessageException { Matcher matcher = parseLineParamsPattern.matcher( paramBlob ); LinkedList<String> params = new LinkedList<String>(); while ( matcher.find() ) { params.add( matcher.group( 1 ) ); } if ( params.size() < 1 ) { throw new InvalidMessageException( "Message parameters didn't match pattern" ); } // Standardize the trailing parameter to the RFC String lastParam = params.pollLast(); if ( lastParam.startsWith( ":" ) ) params.add( lastParam ); else params.add( ":" + lastParam ); String[] retArray = new String[params.size()]; params.toArray( retArray ); return retArray; } /** * Compares some text against the nick validation pattern. * * @param text Text to validate as a nick * @return true iff text matches the nick pattern */ public static boolean isNick( String text ) { return nickPat.matcher( text ).matches(); } /** * Compares some text against the host validation pattern. * * @param text Text to validate as a host * @return true iff text matches the host pattern */ public static boolean isHost( String text ) { return hostPat.matcher( text ).matches(); } /** * Get the RFC 1459 message prefix component, if it exists. * * @return A string if there is a prefix, otherwise null. */ public String getPrefix() { return prefix; } /** * Get the RFC 1459 message command component. * * @return A string always. */ public String getCommand() { return command; } /** * Get the RFC 1459 message params component. * * @return An array of parameter strings, which is zero length if there are no parameters. */ public String[] getParams() { return params; } /** * Returns the message as a raw one-line IRC protocol message String */ @Override public String toString() { StringBuilder sb = new StringBuilder(); if ( prefix != null ) { sb.append( prefix ); sb.append( " " ); } sb.append( command ); for ( String param : params ) { sb.append( " " ); sb.append( param ); } // sb.append( "\n" ); return sb.toString(); } }