1/*
2 * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.net.sdp;
27
28import sun.net.NetHooks;
29import java.net.InetAddress;
30import java.net.Inet4Address;
31import java.net.UnknownHostException;
32import java.util.*;
33import java.io.File;
34import java.io.FileDescriptor;
35import java.io.IOException;
36import java.io.PrintStream;
37
38import sun.net.sdp.SdpSupport;
39import sun.security.action.GetPropertyAction;
40
41/**
42 * A NetHooks provider that converts sockets from the TCP to SDP protocol prior
43 * to binding or connecting.
44 */
45
46public class SdpProvider extends NetHooks.Provider {
47    // maximum port
48    private static final int MAX_PORT = 65535;
49
50    // indicates if SDP is enabled and the rules for when the protocol is used
51    private final boolean enabled;
52    private final List<Rule> rules;
53
54    // logging for debug purposes
55    private PrintStream log;
56
57    public SdpProvider() {
58        Properties props = GetPropertyAction.privilegedGetProperties();
59        // if this property is not defined then there is nothing to do.
60        String file = props.getProperty("com.sun.sdp.conf");
61        if (file == null) {
62            this.enabled = false;
63            this.rules = null;
64            return;
65        }
66
67        // load configuration file
68        List<Rule> list = null;
69        try {
70            list = loadRulesFromFile(file);
71        } catch (IOException e) {
72            fail("Error reading %s: %s", file, e.getMessage());
73        }
74
75        // check if debugging is enabled
76        PrintStream out = null;
77        String logfile = props.getProperty("com.sun.sdp.debug");
78        if (logfile != null) {
79            out = System.out;
80            if (logfile.length() > 0) {
81                try {
82                    out = new PrintStream(logfile);
83                } catch (IOException ignore) { }
84            }
85        }
86
87        this.enabled = !list.isEmpty();
88        this.rules = list;
89        this.log = out;
90    }
91
92    // supported actions
93    private static enum Action {
94        BIND,
95        CONNECT;
96    }
97
98    // a rule for matching a bind or connect request
99    private static interface Rule {
100        boolean match(Action action, InetAddress address, int port);
101    }
102
103    // rule to match port[-end]
104    private static class PortRangeRule implements Rule {
105        private final Action action;
106        private final int portStart;
107        private final int portEnd;
108        PortRangeRule(Action action, int portStart, int portEnd) {
109            this.action = action;
110            this.portStart = portStart;
111            this.portEnd = portEnd;
112        }
113        Action action() {
114            return action;
115        }
116        @Override
117        public boolean match(Action action, InetAddress address, int port) {
118            return (action == this.action &&
119                    port >= this.portStart &&
120                    port <= this.portEnd);
121        }
122    }
123
124    // rule to match address[/prefix] port[-end]
125    private static class AddressPortRangeRule extends PortRangeRule {
126        private final byte[] addressAsBytes;
127        private final int prefixByteCount;
128        private final byte mask;
129        AddressPortRangeRule(Action action, InetAddress address,
130                             int prefix, int port, int end)
131        {
132            super(action, port, end);
133            this.addressAsBytes = address.getAddress();
134            this.prefixByteCount = prefix >> 3;
135            this.mask = (byte)(0xff << (8 - (prefix % 8)));
136        }
137        @Override
138        public boolean match(Action action, InetAddress address, int port) {
139            if (action != action())
140                return false;
141            byte[] candidate = address.getAddress();
142            // same address type?
143            if (candidate.length != addressAsBytes.length)
144                return false;
145            // check bytes
146            for (int i=0; i<prefixByteCount; i++) {
147                if (candidate[i] != addressAsBytes[i])
148                    return false;
149            }
150            // check remaining bits
151            if ((prefixByteCount < addressAsBytes.length) &&
152                ((candidate[prefixByteCount] & mask) !=
153                 (addressAsBytes[prefixByteCount] & mask)))
154                    return false;
155            return super.match(action, address, port);
156        }
157    }
158
159    // parses port:[-end]
160    private static int[] parsePortRange(String s) {
161        int pos = s.indexOf('-');
162        try {
163            int[] result = new int[2];
164            if (pos < 0) {
165                boolean all = s.equals("*");
166                result[0] = all ? 0 : Integer.parseInt(s);
167                result[1] = all ? MAX_PORT : result[0];
168            } else {
169                String low = s.substring(0, pos);
170                if (low.length() == 0) low = "*";
171                String high = s.substring(pos+1);
172                if (high.length() == 0) high = "*";
173                result[0] = low.equals("*") ? 0 : Integer.parseInt(low);
174                result[1] = high.equals("*") ? MAX_PORT : Integer.parseInt(high);
175            }
176            return result;
177        } catch (NumberFormatException e) {
178            return new int[0];
179        }
180    }
181
182    private static void fail(String msg, Object... args) {
183        Formatter f = new Formatter();
184        f.format(msg, args);
185        throw new RuntimeException(f.out().toString());
186    }
187
188    // loads rules from the given file
189    // Each non-blank/non-comment line must have the format:
190    // ("bind" | "connect") 1*LWSP-char (hostname | ipaddress["/" prefix])
191    //     1*LWSP-char ("*" | port) [ "-" ("*" | port) ]
192    private static List<Rule> loadRulesFromFile(String file)
193        throws IOException
194    {
195        Scanner scanner = new Scanner(new File(file));
196        try {
197            List<Rule> result = new ArrayList<>();
198            while (scanner.hasNextLine()) {
199                String line = scanner.nextLine().trim();
200
201                // skip blank lines and comments
202                if (line.length() == 0 || line.charAt(0) == '#')
203                    continue;
204
205                // must have 3 fields
206                String[] s = line.split("\\s+");
207                if (s.length != 3) {
208                    fail("Malformed line '%s'", line);
209                    continue;
210                }
211
212                // first field is the action ("bind" or "connect")
213                Action action = null;
214                for (Action a: Action.values()) {
215                    if (s[0].equalsIgnoreCase(a.name())) {
216                        action = a;
217                        break;
218                    }
219                }
220                if (action == null) {
221                    fail("Action '%s' not recognized", s[0]);
222                    continue;
223                }
224
225                // * port[-end]
226                int[] ports = parsePortRange(s[2]);
227                if (ports.length == 0) {
228                    fail("Malformed port range '%s'", s[2]);
229                    continue;
230                }
231
232                // match all addresses
233                if (s[1].equals("*")) {
234                    result.add(new PortRangeRule(action, ports[0], ports[1]));
235                    continue;
236                }
237
238                // hostname | ipaddress[/prefix]
239                int pos = s[1].indexOf('/');
240                try {
241                    if (pos < 0) {
242                        // hostname or ipaddress (no prefix)
243                        InetAddress[] addresses = InetAddress.getAllByName(s[1]);
244                        for (InetAddress address: addresses) {
245                            int prefix =
246                                (address instanceof Inet4Address) ? 32 : 128;
247                            result.add(new AddressPortRangeRule(action, address,
248                                prefix, ports[0], ports[1]));
249                        }
250                    } else {
251                        // ipaddress/prefix
252                        InetAddress address = InetAddress
253                            .getByName(s[1].substring(0, pos));
254                        int prefix = -1;
255                        try {
256                            prefix = Integer.parseInt(s[1], pos + 1,
257                                s[1].length(), 10);
258                            if (address instanceof Inet4Address) {
259                                // must be 1-31
260                                if (prefix < 0 || prefix > 32) prefix = -1;
261                            } else {
262                                // must be 1-128
263                                if (prefix < 0 || prefix > 128) prefix = -1;
264                            }
265                        } catch (NumberFormatException e) {
266                        }
267
268                        if (prefix > 0) {
269                            result.add(new AddressPortRangeRule(action,
270                                        address, prefix, ports[0], ports[1]));
271                        } else {
272                            fail("Malformed prefix '%s'", s[1]);
273                            continue;
274                        }
275                    }
276                } catch (UnknownHostException uhe) {
277                    fail("Unknown host or malformed IP address '%s'", s[1]);
278                    continue;
279                }
280            }
281            return result;
282        } finally {
283            scanner.close();
284        }
285    }
286
287    // converts unbound TCP socket to a SDP socket if it matches the rules
288    private void convertTcpToSdpIfMatch(FileDescriptor fdObj,
289                                               Action action,
290                                               InetAddress address,
291                                               int port)
292        throws IOException
293    {
294        boolean matched = false;
295        for (Rule rule: rules) {
296            if (rule.match(action, address, port)) {
297                SdpSupport.convertSocket(fdObj);
298                matched = true;
299                break;
300            }
301        }
302        if (log != null) {
303            String addr = (address instanceof Inet4Address) ?
304                address.getHostAddress() : "[" + address.getHostAddress() + "]";
305            if (matched) {
306                log.format("%s to %s:%d (socket converted to SDP protocol)\n", action, addr, port);
307            } else {
308                log.format("%s to %s:%d (no match)\n", action, addr, port);
309            }
310        }
311    }
312
313    @Override
314    public void implBeforeTcpBind(FileDescriptor fdObj,
315                              InetAddress address,
316                              int port)
317        throws IOException
318    {
319        if (enabled)
320            convertTcpToSdpIfMatch(fdObj, Action.BIND, address, port);
321    }
322
323    @Override
324    public void implBeforeTcpConnect(FileDescriptor fdObj,
325                                InetAddress address,
326                                int port)
327        throws IOException
328    {
329        if (enabled)
330            convertTcpToSdpIfMatch(fdObj, Action.CONNECT, address, port);
331    }
332}
333