All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.subethamail.smtp.internal.proxy.ProxyProtocolV1Handler Maven / Gradle / Ivy

Go to download

A fork of a fork (!) of SubEtha, an easy-to-use server-side SMTP library for Java.

The newest version!
package org.subethamail.smtp.internal.proxy;

import static org.subethamail.smtp.internal.util.HexUtils.toHex;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.internal.util.ArrayUtils;
import org.subethamail.smtp.server.Session;

/**
 * Implements {@link ProxyHandler} for PROXY
 * protocol V1 textual.
 *
 * @author Diego Salvi
 */
public class ProxyProtocolV1Handler implements ProxyHandler {

    private final static Logger log = LoggerFactory.getLogger(ProxyProtocolV1Handler.class);

    public static final ProxyProtocolV1Handler INSTANCE = new ProxyProtocolV1Handler();

    /**
     * Maximum PROXY header length
     */
    public static final int MAX_PROXY_HEADER_LENGTH = 107;

    /**
     * PROXY header prefix: "PROXY "
     */
    private static final byte[] PREFIX = "PROXY ".getBytes(StandardCharsets.US_ASCII);

    private static final Pattern PATTERN = Pattern.compile("PROXY (?UNKNOWN|TCP4|TCP6)"
            + "( (?[0-9a-fA-F.:]+) (?[0-9a-fA-F.:]+) (?[0-9]{1,5}) (?[0-9]{1,5}))?\r\n");

    public enum Family {
        UNKNOWN,
        TCP4,
        TCP6
    }

    /* States for PROXY header reading */
    
    /** Header still in reading */
    private static final int STATE_READING = 0;
    
    /** Found a CR, looking for a LF */
    private static final int STATE_READ_TERMINATING = 1;
    
    /** Found CRLF, reading concluded */
    private static final int STATE_READ_END = 2;
    
    /** Found ad invalid sequence, reading failed */
    private static final int STATE_READ_ERROR = 3;
    
    private ProxyProtocolV1Handler() {
    }

    /**
     * Returns the PROXY protocol v1 prefix size, it can be used to retrieve data from connection and then invoke
     * {@link #isValidPrefix(byte[])}.
     *
     * @return PROXY protocol v1 prefix size
     */
    static int prefixSize() {
        return PREFIX.length;
    }

    /**
     * Check if given prefix is a PROXY protocol v1 prefix. Only the prefix will be checked: it doesn't check any other
     * data following the prefix
     *
     * @param prefix byte array prefix to isValidPrefix
     * @return {@code true} if is a PROXY protocol v1 prefix
     */
    boolean isValidPrefix(byte[] prefix) {
        return prefix.length >= PREFIX.length // Must accomodate every prefix byte
                && ArrayUtils.equals(PREFIX, 0, PREFIX.length, prefix, 0, PREFIX.length);
    }

    @Override
    public ProxyResult handle(InputStream in, OutputStream out, Session session) throws IOException {

        /*
         * Header as seen on specs
         *
         * - a string identifying the protocol : "PROXY" ( \x50 \x52 \x4F \x58 \x59 ) Seeing this string indicates that
         * this is version 1 of the protocol.
         *
         * - exactly one space : " " ( \x20 )
         *
         * - a string indicating the proxied INET protocol and family. As of version 1, only "TCP4" ( \x54 \x43 \x50
         * \x34 ) for TCP over IPv4, and "TCP6" ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed. Other,
         * unsupported, or unknown protocols must be reported with the name "UNKNOWN" ( \x55 \x4E \x4B \x4E \x4F \x57
         * \x4E ). For "UNKNOWN", the rest of the line before the CRLF may be omitted by the sender, and the receiver
         * must ignore anything presented before the CRLF is found. Note that an earlier version of this specification
         * suggested to use this when sending health checks, but this causes issues with servers that reject the
         * "UNKNOWN" keyword. Thus is it now recommended not to send "UNKNOWN" when the connection is expected to be
         * accepted, but only when it is not possible to correctly fill the PROXY line.
         *
         * - exactly one space : " " ( \x20 )
         *
         * - the layer 3 source address in its canonical format. IPv4 addresses must be indicated as a series of exactly
         * 4 integers in the range [0..255] inclusive written in decimal representation separated by exactly one dot
         * between each other. Heading zeroes are not permitted in front of numbers in order to avoid any possible
         * confusion with octal numbers. IPv6 addresses must be indicated as series of sets of 4 hexadecimal digits
         * (upper or lower case) delimited by colons between each other, with the acceptance of one double colon
         * sequence to replace the largest acceptable range of consecutive zeroes. The total number of decoded bits must
         * exactly be 128. The advertised protocol family dictates what format to use.
         *
         *
         * - exactly one space : " " ( \x20 )
         *
         *
         * - the layer 3 destination address in its canonical format. It is the same format as the layer 3 source
         * address and matches the same family.
         *
         *
         * - exactly one space : " " ( \x20 )
         *
         *
         * - the TCP source port represented as a decimal integer in the range [0..65535] inclusive. Heading zeroes are
         * not permitted in front of numbers in order to avoid any possible confusion with octal numbers.
         *
         *
         * - exactly one space : " " ( \x20 )
         *
         *
         *
         * - the TCP destination port represented as a decimal integer in the range [0..65535] inclusive. Heading zeroes
         * are not permitted in front of numbers in order to avoid any possible confusion with octal numbers.
         *
         *
         * - the CRLF sequence ( \x0D \x0A )
         *
         */
        log.debug("(session {}) Starting PROXY protocol v1 handling", session.getSessionId());

        byte[] header = new byte[MAX_PROXY_HEADER_LENGTH];
        int len = in.read(header, 0, PREFIX.length);
        if (len != PREFIX.length) {
            final String headerHex = toHex(header, 0, len);
            log.error("(session {}) Failed to fully read PROXY v1 header prefix. Read {}",
                    session.getSessionId(), headerHex);
            return ProxyResult.FAIL;
        }

        if (!ArrayUtils.equals(PREFIX, 0, PREFIX.length, header, 0, PREFIX.length)) {
            final String receivedHeader = toHex(header, 0, len);
            log.error("(session {}) Invalid PROXY protocol v1 header prefix {}", session.getSessionId(), receivedHeader);
            return ProxyResult.FAIL;
        }

        int state = STATE_READING;
        while (state < STATE_READ_END && len < MAX_PROXY_HEADER_LENGTH) {
            int read = in.read();
            if (read < 0) {
                final String headerHex = toHex(header, 0, len);
                log.error("(session {}) Failed to fully read PROXY v1 header. Read {}",
                        session.getSessionId(), headerHex);
                return ProxyResult.FAIL;
            }

            byte result = (byte) read;
            header[len++] = result;

            switch (state) {
                case STATE_READING:
                    // Check for CR
                    if (read == 0x0D) {
                        state = STATE_READ_TERMINATING;
                    }
                    break;
                case STATE_READ_TERMINATING:
                    // Check for LF
                    if (read == 0x0A) {
                        // Termination
                        state = STATE_READ_END;
                    } else {
                        state = STATE_READ_ERROR;
                    }
                    break;
                default:
                    // State unexpected
                    state = STATE_READ_ERROR;
            }
        }

        final String headerHex = toHex(header, 0, len);
        log.debug("(session {}) Read header {}", session.getSessionId(), headerHex);

        // Check if header read terminated due to max length reached without find a wellformed termination
        if (state != STATE_READ_END) {
            log.error("(session {}) Invalid PROXY protocol v1 header {}", session.getSessionId(), headerHex);
            return ProxyResult.FAIL;
        }

        Matcher matcher = PATTERN.matcher(new String(header, 0, len, StandardCharsets.US_ASCII));
        if (!matcher.matches()) {
            log.error("(session {}) Invalid PROXY protocol v1 header {}", session.getSessionId(), headerHex);
            return ProxyResult.FAIL;
        }

        InetSocketAddress clientAddress;

        // By regex definition family MUST exists and be one of UNKNOWN, TCP4 or TCP6
        String family = matcher.group("family");
        switch (family) {
            case "UNKNOWN":
                return ProxyResult.NOP;
            case "TCP4": {
                String asrc = matcher.group("asrc");
                if (asrc == null) {
                    /*
                     * We need to isValidPrefix only for one group between asrc, adst, psrc or pdst. Ther are all or
                     * noting by regex definition.
                     */
                    log.error("(session {}) Invalid PROXY protocol v1 header {}", session.getSessionId(), headerHex);
                    return ProxyResult.FAIL;
                }

                InetAddress src;
                try {
                    src = InetAddress.getByName(asrc);
                } catch (UnknownHostException ex) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv4 {}", session.getSessionId(), asrc);
                    return ProxyResult.FAIL;
                }

                if (!(src instanceof Inet4Address)) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv4 {}", session.getSessionId(), asrc);
                    return ProxyResult.FAIL;
                }

                // Group psrc cannot be null here
                int psrc = Integer.parseInt(matcher.group("psrc"));
                if (psrc < 1 || psrc > 65535) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv4 port {}", session.getSessionId(), psrc);
                    return ProxyResult.FAIL;
                }
                clientAddress = new InetSocketAddress(src, psrc);
                break;
            }

            case "TCP6": {
                String asrc = matcher.group("asrc");
                if (asrc == null) {
                    /*
                     * We need to isValidPrefix only for one group between asrc, adst, psrc or pdst. Ther are all or
                     * noting by regex definition.
                     */
                    log.error("(session {}) Invalid PROXY protocol v1 header {}", session.getSessionId(), headerHex);
                    return ProxyResult.FAIL;
                }

                InetAddress src;
                try {
                    src = InetAddress.getByName(asrc);
                } catch (UnknownHostException ex) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv6 {}", session.getSessionId(), asrc);
                    return ProxyResult.FAIL;
                }

                if (!(src instanceof Inet6Address)) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv6 {}", session.getSessionId(), asrc);
                    return ProxyResult.FAIL;
                }

                // Group psrc cannot be null here
                int psrc = Integer.parseInt(matcher.group("psrc"));
                if (psrc < 1 || psrc > 65535) {
                    log.error("(session {}) wrong PROXY protocol v1 source IPv6 port {}", session.getSessionId(), psrc);
                    return ProxyResult.FAIL;
                }
                clientAddress = new InetSocketAddress(src, psrc);
                break;
            }
            default:
                // Due to regex we should never end here
                log.error("(session {}) Unknown PROXY protocol v1 address family {}", session.getSessionId(), family);
                return ProxyResult.FAIL;
        }

        log.debug("(session {}) Accepted PROXY connection: family {} client {} original {}",
                session.getSessionId(), family, clientAddress.getHostString(),
                session.getRealRemoteAddress().getHostString());

        return new ProxyResult(clientAddress);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy