/*
 * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.net.sdp;

import sun.net.NetHooks;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.*;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintStream;
import java.security.AccessController;

import sun.net.sdp.SdpSupport;
import sun.security.action.GetPropertyAction;

/**
 * A NetHooks provider that converts sockets from the TCP to SDP protocol prior
 * to binding or connecting.
 */

public class SdpProvider extends NetHooks.Provider {
    // maximum port
    private static final int MAX_PORT = 65535;

    // indicates if SDP is enabled and the rules for when the protocol is used
    private final boolean enabled;
    private final List<Rule> rules;

    // logging for debug purposes
    private PrintStream log;

    public SdpProvider() {
        // if this property is not defined then there is nothing to do.
        String file = AccessController.doPrivileged(
            new GetPropertyAction("com.sun.sdp.conf"));
        if (file == null) {
            this.enabled = false;
            this.rules = null;
            return;
        }

        // load configuration file
        List<Rule> list = null;
        if (file != null) {
            try {
                list = loadRulesFromFile(file);
            } catch (IOException e) {
                fail("Error reading %s: %s", file, e.getMessage());
            }
        }

        // check if debugging is enabled
        PrintStream out = null;
        String logfile = AccessController.doPrivileged(
            new GetPropertyAction("com.sun.sdp.debug"));
        if (logfile != null) {
            out = System.out;
            if (logfile.length() > 0) {
                try {
                    out = new PrintStream(logfile);
                } catch (IOException ignore) { }
            }
        }

        this.enabled = !list.isEmpty();
        this.rules = list;
        this.log = out;
    }

    // supported actions
    private static enum Action {
        BIND,
        CONNECT;
    }

    // a rule for matching a bind or connect request
    private static interface Rule {
        boolean match(Action action, InetAddress address, int port);
    }

    // rule to match port[-end]
    private static class PortRangeRule implements Rule {
        private final Action action;
        private final int portStart;
        private final int portEnd;
        PortRangeRule(Action action, int portStart, int portEnd) {
            this.action = action;
            this.portStart = portStart;
            this.portEnd = portEnd;
        }
        Action action() {
            return action;
        }
        @Override
        public boolean match(Action action, InetAddress address, int port) {
            return (action == this.action &&
                    port >= this.portStart &&
                    port <= this.portEnd);
        }
    }

    // rule to match address[/prefix] port[-end]
    private static class AddressPortRangeRule extends PortRangeRule {
        private final byte[] addressAsBytes;
        private final int prefixByteCount;
        private final byte mask;
        AddressPortRangeRule(Action action, InetAddress address,
                             int prefix, int port, int end)
        {
            super(action, port, end);
            this.addressAsBytes = address.getAddress();
            this.prefixByteCount = prefix >> 3;
            this.mask = (byte)(0xff << (8 - (prefix % 8)));
        }
        @Override
        public boolean match(Action action, InetAddress address, int port) {
            if (action != action())
                return false;
            byte[] candidate = address.getAddress();
            // same address type?
            if (candidate.length != addressAsBytes.length)
                return false;
            // check bytes
            for (int i=0; i<prefixByteCount; i++) {
                if (candidate[i] != addressAsBytes[i])
                    return false;
            }
            // check remaining bits
            if ((prefixByteCount < addressAsBytes.length) &&
                ((candidate[prefixByteCount] & mask) !=
                 (addressAsBytes[prefixByteCount] & mask)))
                    return false;
            return super.match(action, address, port);
        }
    }

    // parses port:[-end]
    private static int[] parsePortRange(String s) {
        int pos = s.indexOf('-');
        try {
            int[] result = new int[2];
            if (pos < 0) {
                boolean all = s.equals("*");
                result[0] = all ? 0 : Integer.parseInt(s);
                result[1] = all ? MAX_PORT : result[0];
            } else {
                String low = s.substring(0, pos);
                if (low.length() == 0) low = "*";
                String high = s.substring(pos+1);
                if (high.length() == 0) high = "*";
                result[0] = low.equals("*") ? 0 : Integer.parseInt(low);
                result[1] = high.equals("*") ? MAX_PORT : Integer.parseInt(high);
            }
            return result;
        } catch (NumberFormatException e) {
            return new int[0];
        }
    }

    private static void fail(String msg, Object... args) {
        Formatter f = new Formatter();
        f.format(msg, args);
        throw new RuntimeException(f.out().toString());
    }

    // loads rules from the given file
    // Each non-blank/non-comment line must have the format:
    // ("bind" | "connect") 1*LWSP-char (hostname | ipaddress["/" prefix])
    //     1*LWSP-char ("*" | port) [ "-" ("*" | port) ]
    private static List<Rule> loadRulesFromFile(String file)
        throws IOException
    {
        Scanner scanner = new Scanner(new File(file));
        try {
            List<Rule> result = new ArrayList<Rule>();
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine().trim();

                // skip blank lines and comments
                if (line.length() == 0 || line.charAt(0) == '#')
                    continue;

                // must have 3 fields
                String[] s = line.split("\\s+");
                if (s.length != 3) {
                    fail("Malformed line '%s'", line);
                    continue;
                }

                // first field is the action ("bind" or "connect")
                Action action = null;
                for (Action a: Action.values()) {
                    if (s[0].equalsIgnoreCase(a.name())) {
                        action = a;
                        break;
                    }
                }
                if (action == null) {
                    fail("Action '%s' not recognized", s[0]);
                    continue;
                }

                // * port[-end]
                int[] ports = parsePortRange(s[2]);
                if (ports.length == 0) {
                    fail("Malformed port range '%s'", s[2]);
                    continue;
                }

                // match all addresses
                if (s[1].equals("*")) {
                    result.add(new PortRangeRule(action, ports[0], ports[1]));
                    continue;
                }

                // hostname | ipaddress[/prefix]
                int pos = s[1].indexOf('/');
                try {
                    if (pos < 0) {
                        // hostname or ipaddress (no prefix)
                        InetAddress[] addresses = InetAddress.getAllByName(s[1]);
                        for (InetAddress address: addresses) {
                            int prefix =
                                (address instanceof Inet4Address) ? 32 : 128;
                            result.add(new AddressPortRangeRule(action, address,
                                prefix, ports[0], ports[1]));
                        }
                    } else {
                        // ipaddress/prefix
                        InetAddress address = InetAddress
                            .getByName(s[1].substring(0, pos));
                        int prefix = -1;
                        try {
                            prefix = Integer.parseInt(s[1].substring(pos+1));
                            if (address instanceof Inet4Address) {
                                // must be 1-31
                                if (prefix < 0 || prefix > 32) prefix = -1;
                            } else {
                                // must be 1-128
                                if (prefix < 0 || prefix > 128) prefix = -1;
                            }
                        } catch (NumberFormatException e) {
                        }

                        if (prefix > 0) {
                            result.add(new AddressPortRangeRule(action,
                                        address, prefix, ports[0], ports[1]));
                        } else {
                            fail("Malformed prefix '%s'", s[1]);
                            continue;
                        }
                    }
                } catch (UnknownHostException uhe) {
                    fail("Unknown host or malformed IP address '%s'", s[1]);
                    continue;
                }
            }
            return result;
        } finally {
            scanner.close();
        }
    }

    // converts unbound TCP socket to a SDP socket if it matches the rules
    private void convertTcpToSdpIfMatch(FileDescriptor fdObj,
                                               Action action,
                                               InetAddress address,
                                               int port)
        throws IOException
    {
        boolean matched = false;
        for (Rule rule: rules) {
            if (rule.match(action, address, port)) {
                SdpSupport.convertSocket(fdObj);
                matched = true;
                break;
            }
        }
        if (log != null) {
            String addr = (address instanceof Inet4Address) ?
                address.getHostAddress() : "[" + address.getHostAddress() + "]";
            if (matched) {
                log.format("%s to %s:%d (socket converted to SDP protocol)\n", action, addr, port);
            } else {
                log.format("%s to %s:%d (no match)\n", action, addr, port);
            }
        }
    }

    @Override
    public void implBeforeTcpBind(FileDescriptor fdObj,
                              InetAddress address,
                              int port)
        throws IOException
    {
        if (enabled)
            convertTcpToSdpIfMatch(fdObj, Action.BIND, address, port);
    }

    @Override
    public void implBeforeTcpConnect(FileDescriptor fdObj,
                                InetAddress address,
                                int port)
        throws IOException
    {
        if (enabled)
            convertTcpToSdpIfMatch(fdObj, Action.CONNECT, address, port);
    }
}
