FileTransferProxy.java 16 KB
Newer Older
1
/**
2
 * Copyright (C) 1999-2008 Jive Software. All rights reserved.
3
 *
4 5 6 7 8 9 10 11 12 13 14
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
15 16
 */

17
package org.jivesoftware.openfire.filetransfer.proxy;
18

19
import java.net.InetAddress;
20 21
import java.net.NetworkInterface;
import java.net.SocketException;
22
import java.net.UnknownHostException;
23
import java.util.*;
24

25 26
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
27 28 29 30 31 32
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.PacketException;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.RoutableChannelHandler;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.XMPPServer;
33 34
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.container.BasicModule;
35 36 37 38 39
import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider;
40
import org.jivesoftware.openfire.filetransfer.FileTransferManager;
Gaston Dombiak's avatar
Gaston Dombiak committed
41 42 43
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
44 45
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
46
import org.xmpp.forms.DataForm;
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;

/**
 * Manages the transfering of files between two remote entities on the jabber network.
 * This class acts independtly as a Jabber component from the rest of the server, according to
 * the Jabber <a href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 bytestreams protocol</a>.
 *
 * @author Alexander Wenckus
 */
public class FileTransferProxy extends BasicModule
        implements ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider,
        RoutableChannelHandler {
62 63

    private static final Logger Log = LoggerFactory.getLogger( FileTransferProxy.class);
64

65 66 67 68 69 70 71 72 73 74 75
    /**
     * The JiveProperty relating to whether or not the file treansfer proxy is enabled.
     */
    public static final String JIVEPROPERTY_PROXY_ENABLED = "xmpp.proxy.enabled";

    /**
     * The JiveProperty relating to the port the proxy is operating on. Changing this value requires a restart of the
     * proxy.
     */
    public static final String JIVEPROPERTY_PORT = "xmpp.proxy.port";

76 77 78 79 80
    /**
     * Name of the property that hardcodes the external IP that is being listened on.
     */
    public static final String PROPERTY_EXTERNALIP = "xmpp.proxy.externalip";

81 82 83 84 85 86 87 88 89 90
    /**
     * Whether or not the file transfer proxy is enabled by default.
     */
    public static final boolean DEFAULT_IS_PROXY_ENABLED = true;

    /**
     * The default port of the file transfer proxy
     */
    public static final int DEFAULT_PORT = 7777;

91 92 93 94 95 96
    private String proxyServiceName;

    private IQHandlerInfo info;
    private RoutingTable routingTable;
    private PacketRouter router;
    private ProxyConnectionManager connectionManager;
97

98
    // The address to operate on. Null for any address.
99
    private InetAddress bindInterface;
100 101 102 103 104


    public FileTransferProxy() {
        super("SOCKS5 file transfer proxy");

105 106
        info = new IQHandlerInfo("query", FileTransferManager.NAMESPACE_BYTESTREAMS);

107
        PropertyEventDispatcher.addListener(new FileTransferPropertyListener());
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
    }

    public boolean handleIQ(IQ packet) throws UnauthorizedException {
        Element childElement = packet.getChildElement();
        String namespace = null;

        // ignore errors
        if (packet.getType() == IQ.Type.error) {
            return true;
        }
        if (childElement != null) {
            namespace = childElement.getNamespaceURI();
        }

        if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
Gaston Dombiak's avatar
Gaston Dombiak committed
123 124 125
            IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(packet);
            router.route(reply);
            return true;
126 127
        }
        else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
Gaston Dombiak's avatar
Gaston Dombiak committed
128 129 130 131
            // a component
            IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(packet);
            router.route(reply);
            return true;
132
        }
133
        else if (FileTransferManager.NAMESPACE_BYTESTREAMS.equals(namespace)) {
134 135
            if (packet.getType() == IQ.Type.get) {
                IQ reply = IQ.createResultIQ(packet);
136 137 138 139
                Element newChild = reply.setChildElement("query", FileTransferManager.NAMESPACE_BYTESTREAMS);

                final String externalIP = JiveGlobals.getProperty( PROPERTY_EXTERNALIP );
                if ( externalIP != null && !externalIP.isEmpty() )
140
                {
141 142
                    // OF-512: Override the automatic detection with a specific address (useful for NATs, proxies, etc)
                    final Element response = newChild.addElement( "streamhost" );
143
                    response.addAttribute( "jid", getServiceDomain() );
144
                    response.addAttribute( "host", externalIP );
145 146
                    response.addAttribute( "port", String.valueOf( connectionManager.getProxyPort() ) );
                }
147 148 149 150 151 152 153 154 155 156 157
                else
                {
                    // Report all network addresses that we know that we're servicing.
                    for ( final InetAddress address : getAddresses() )
                    {
                        final Element response = newChild.addElement( "streamhost" );
                        response.addAttribute( "jid", getServiceDomain() );
                        response.addAttribute( "host", address.getHostAddress() );
                        response.addAttribute( "port", String.valueOf( connectionManager.getProxyPort() ) );
                    }
                }
158 159 160
                router.route(reply);
                return true;
            }
161
            else if (packet.getType() == IQ.Type.set) {
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
                String sid = childElement.attributeValue("sid");
                JID from = packet.getFrom();
                JID to = new JID(childElement.elementTextTrim("activate"));

                IQ reply = IQ.createResultIQ(packet);
                try {
                    connectionManager.activate(from, to, sid);
                }
                catch (IllegalArgumentException ie) {
                    Log.error("Error activating connection", ie);
                    reply.setType(IQ.Type.error);
                    reply.setError(new PacketError(PacketError.Condition.not_allowed));
                }

                router.route(reply);
                return true;
            }
        }
        return false;
    }

    public IQHandlerInfo getInfo() {
        return info;
    }

187
    @Override
188 189
    public void initialize( XMPPServer server )
    {
190 191 192 193 194 195
        super.initialize(server);

        proxyServiceName = JiveGlobals.getProperty("xmpp.proxy.service", "proxy");
        routingTable = server.getRoutingTable();
        router = server.getPacketRouter();

196 197 198 199 200 201 202 203
        connectionManager = new ProxyConnectionManager(getFileTransferManager(server));
    }

    /**
     * Returns the IP address(es) that the proxy connection manager is servicing.
     */
    private Set<InetAddress> getAddresses()
    {
204
        final String interfaceName = JiveGlobals.getXMLProperty( "network.interface" );
205 206 207 208 209

        final Set<InetAddress> result = new HashSet<>();

        // Let's see if we hardcoded a specific interface, then use its address.
        if ( interfaceName != null && !interfaceName.trim().isEmpty() )
210 211 212 213
        {
            try
            {
                bindInterface = InetAddress.getByName( interfaceName.trim() );
214 215
                result.add( bindInterface );
                return result;
216 217 218 219 220
            }
            catch ( UnknownHostException e )
            {
                Log.error( "Error binding to network.interface '{}'", interfaceName, e );
            }
221
        }
222

223 224
        // When there's no specific address configured, return all available (non-loopback) addresses.
        try
225
        {
226 227
            final Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while ( networkInterfaces.hasMoreElements() )
228
            {
229 230
                final NetworkInterface networkInterface = networkInterfaces.nextElement();
                if ( networkInterface.isLoopback() )
231
                {
232 233 234 235 236 237
                    continue;
                }
                final Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
                while ( inetAddresses.hasMoreElements() )
                {
                    result.add( inetAddresses.nextElement() );
238 239
                }
            }
240 241 242 243
        }
        catch ( SocketException e )
        {
            Log.error( "Error determining all addresses for this server", e );
244 245 246 247
        }
        return result;
    }

248 249
    private FileTransferManager getFileTransferManager(XMPPServer server) {
        return server.getFileTransferManager();
250 251
    }

252
    @Override
253
    public void start() {
254 255
        super.start();

256
        if (isEnabled()) {
257
            startProxy();
258 259
        }
        else {
260
            XMPPServer.getInstance().getIQDiscoItemsHandler().removeServerItemsProvider(this);
261
        }
262 263
    }

264
    private void startProxy() {
265
        connectionManager.processConnections(bindInterface, getProxyPort());
Gaston Dombiak's avatar
Gaston Dombiak committed
266
        routingTable.addComponentRoute(getAddress(), this);
267 268 269 270 271
        XMPPServer server = XMPPServer.getInstance();

        server.getIQDiscoItemsHandler().addServerItemsProvider(this);
    }

272
    @Override
273
    public void stop() {
274 275
        super.stop();

276 277
        XMPPServer.getInstance().getIQDiscoItemsHandler()
                .removeComponentItem(getAddress().toString());
Gaston Dombiak's avatar
Gaston Dombiak committed
278
        routingTable.removeComponentRoute(getAddress());
279 280 281
        connectionManager.disable();
    }

282
    @Override
283
    public void destroy() {
284 285 286 287 288
        super.destroy();

        connectionManager.shutdown();
    }

289
    public void enableFileTransferProxy(boolean isEnabled) {
290
        JiveGlobals.setProperty(FileTransferProxy.JIVEPROPERTY_PROXY_ENABLED,
291 292
                                Boolean.toString(isEnabled));
        setEnabled( isEnabled );
293 294 295
    }

    private void setEnabled(boolean isEnabled) {
296
        if (isEnabled) {
297
            startProxy();
298 299 300 301 302 303
        }
        else {
            stop();
        }
    }

304 305 306 307 308 309 310 311 312 313 314
    /**
     * Returns true if the file transfer proxy is currently enabled and false if it is not.
     *
     * @return Returns true if the file transfer proxy is currently enabled and false if it is not.
     */
    public boolean isProxyEnabled() {
        return connectionManager.isRunning() &&
                JiveGlobals.getBooleanProperty(JIVEPROPERTY_PROXY_ENABLED, DEFAULT_IS_PROXY_ENABLED);
    }

    private boolean isEnabled() {
315
        return JiveGlobals.getBooleanProperty(JIVEPROPERTY_PROXY_ENABLED, DEFAULT_IS_PROXY_ENABLED);
316 317
    }

318 319 320 321 322
    /**
     * Sets the port that the proxy operates on. This requires a restart of the file transfer proxy.
     *
     * @param port The port.
     */
323
    public void setProxyPort(int port) {
324
        JiveGlobals.setProperty(JIVEPROPERTY_PORT, Integer.toString(port));
325 326
    }

327 328 329 330 331
    /**
     * Returns the port that the file transfer proxy is opertating on.
     *
     * @return Returns the port that the file transfer proxy is opertating on.
     */
332
    public int getProxyPort() {
333
        return JiveGlobals.getIntProperty(JIVEPROPERTY_PORT, DEFAULT_PORT);
334 335 336 337 338 339 340 341 342 343
    }

    /**
     * Returns the fully-qualifed domain name of this chat service.
     * The domain is composed by the service name and the
     * name of the XMPP server where the service is running.
     *
     * @return the file transfer server domain (service name + host name).
     */
    public String getServiceDomain() {
344
        return proxyServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
345 346
    }

347
    @Override
348 349 350 351
    public JID getAddress() {
        return new JID(null, getServiceDomain(), null);
    }

352
    @Override
Gaston Dombiak's avatar
Gaston Dombiak committed
353
    public Iterator<DiscoServerItem> getItems() {
354
        if(!isEnabled()) {
355
            return Collections.emptyIterator();
356
        }
357

358
        final DiscoServerItem item = new DiscoServerItem(new JID(
359 360 361
                getServiceDomain()), "Socks 5 Bytestreams Proxy", null, null, this,
                                                         this);

362
        return Collections.singleton(item).iterator();
363 364
    }

365
    @Override
366 367 368 369 370 371 372
    public Iterator<Element> getIdentities(String name, String node, JID senderJID) {
        // Answer the identity of the proxy
        Element identity = DocumentHelper.createElement("identity");
        identity.addAttribute("category", "proxy");
        identity.addAttribute("name", "SOCKS5 Bytestreams Service");
        identity.addAttribute("type", "bytestreams");

373
        return Collections.singleton(identity).iterator();
374 375
    }

376
    @Override
377
    public Iterator<String> getFeatures(String name, String node, JID senderJID) {
378
        return Arrays.asList(FileTransferManager.NAMESPACE_BYTESTREAMS,
379
                             "http://jabber.org/protocol/disco#info").iterator();
380 381
    }

382
    @Override
383
    public DataForm getExtendedInfo(String name, String node, JID senderJID) {
384 385 386
        return null;
    }

387
    @Override
388 389 390 391
    public boolean hasInfo(String name, String node, JID senderJID) {
        return true;
    }

392
    @Override
393
    public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
394
        // A proxy server has no items
395
        return new ArrayList<DiscoItem>().iterator();
396 397
    }

398
    @Override
399 400 401 402
    public void process(Packet packet) throws UnauthorizedException, PacketException {
        // Check if the packet is a disco request or a packet with namespace iq:register
        if (packet instanceof IQ) {
            if (handleIQ((IQ) packet)) {
Gaston Dombiak's avatar
Gaston Dombiak committed
403
                // Do nothing
404 405 406 407 408 409 410 411 412
            }
            else {
                IQ reply = IQ.createResultIQ((IQ) packet);
                reply.setChildElement(((IQ) packet).getChildElement().createCopy());
                reply.setError(PacketError.Condition.feature_not_implemented);
                router.route(reply);
            }
        }
    }
413

414
    private class FileTransferPropertyListener implements PropertyEventListener {
415
        @Override
416 417 418 419 420 421 422 423 424 425 426 427
        public void propertySet(String property, Map params)
        {
            if ( isEnabled() )
            {
                // Restart when configuration changed.
                if (JIVEPROPERTY_PORT.equalsIgnoreCase( property ))
                {
                    setEnabled( false );
                    setEnabled( true );
                }
            }

428 429
            if(JIVEPROPERTY_PROXY_ENABLED.equalsIgnoreCase(property)) {
                Object value = params.get("value");
430
                boolean isEnabled = (value != null ? Boolean.parseBoolean(value.toString()) : DEFAULT_IS_PROXY_ENABLED);
431
                setEnabled(isEnabled);
432 433 434
            }
        }

435
        @Override
436 437
        public void propertyDeleted(String property, Map params) {
            if(JIVEPROPERTY_PROXY_ENABLED.equalsIgnoreCase(property)) {
438
                setEnabled(DEFAULT_IS_PROXY_ENABLED);
439
            }
440 441 442 443 444 445 446 447 448 449

            if ( isEnabled() )
            {
                // Restart when configuration changed.
                if (JIVEPROPERTY_PORT.equalsIgnoreCase( property ) )
                {
                    setEnabled( false );
                    setEnabled( true );
                }
            }
450 451
        }

452
        @Override
453 454 455
        public void xmlPropertySet(String property, Map params) {
        }

456
        @Override
457 458 459
        public void xmlPropertyDeleted(String property, Map params) {
        }
    }
460
}