Microsoft Office 365 IP Intelligence - V0.2

Problem this snippet solves:

Update to V0.1 - Microsoft Office 365 IP intelligence

Added a new method to check if a URL is part of Office365 IPI

How to use this snippet:

Refer to this Article: Intelligent Proxy Steering - Office365

Code :

/**
*** Name   : office365_ipi_extension
*** Author : Niels van Sluis, 
*** Modified by: Brett Smith @f5
*** Version: 0.2
*** Date   : 2018-02-25
**/
 
'use strict';
 
// Import the f5-nodejs module and others.
var f5 = require('f5-nodejs');
var parseString = require('xml2js').parseString;
var https = require('https');
var repeat = require('repeat');
var loki = require('lokijs');
var ipRangeCheck = require('ip-range-check');
 
// Create (in-memory) LokiJS database.
var db = new loki('db.json');
var products = db.addCollection('products');
 
// Create a new rpc server for listening to TCL iRule calls.
var ilx = new f5.ILXServer();
 
// URL to Microsoft Office 365 XML file.
var url = "https://support.content.office.net/en-us/static/O365IPAddresses.xml";
 
// Function to get XML file and convert to JSON object.
function xmlToJson(url, callback) {
    var req = https.get(url, function(res) {
        var xml = '';
 
        res.on('data', function(chunk) {
            xml += chunk;
        });
 
        res.on('error', function(e) {
            callback(e, null);
        }); 
 
        res.on('timeout', function(e) {
            callback(e, null);
        }); 
 
        res.on('end', function() {
            if(res.statusCode == 200) {
                parseString(xml, function(err, result) {
                    callback(null, result);
                });
            }
        });
    });
}
 
// Function that uses the data in the XML file to create a database
// that can be used to perform IP address and URL lookups.
function getOffice365() {
    xmlToJson(url, function(err,data) {
        if(err) {
            console.log("Error: xmlToJson failed");
            return;
        }
        
        // if xml happens to be empty due to an error, do not continue.
        if(!data) {
            console.log("Error: No data in XML file");
            return;
        }
        
        // Get date updated: 7/13/2017
        var productsUpdated = data.products.$.updated;
        
        // Only update if version changed
        var versionCheck = products.findObject({'name':'any'});
        if(versionCheck && versionCheck.version == productsUpdated) {
            console.log("Info: product version didn't changed; No update required");
            return;
        }
        
        var allIpAdresses = [];
        var allUrls = [];
 
        data.products.product.forEach(function (product) {
            var ipAddresses = [];
            var urls = [];
 
            product.addresslist.forEach(function (addresslist) {
                if(addresslist.$.type == "IPv4" || addresslist.$.type == "IPv6") {
                     if ( typeof addresslist.address !== 'undefined' && addresslist.address ) {
                        addresslist.address.forEach(function (address) {
                            ipAddresses.push(address);
                            allIpAdresses.push(address);
                        });
                    }
                } else if(addresslist.$.type == "URL") {
                    if ( typeof addresslist.address !== 'undefined' && addresslist.address ) {
                        addresslist.address.forEach(function (address) {
                            if(address.indexOf('*') !== -1) {
                                // Start Version 0.2 - Added by Brett Smith @f5.com
                                // remove the wilcard (*) from the URL
                                address = address.substr(address.indexOf('*')+1);
                                // End Version 0.2
                                urls.push(address);
                                allUrls.push(address);
                            } else {    
                                urls.push(address);
                                allUrls.push(address);
                            }
                        });
                    }
                }
            });
 
            var p = products.findObject({'name':product.$.name.toLowerCase()});
            if(!p) {
                products.insert({ name: product.$.name.toLowerCase(), ipAddresses: ipAddresses, urls: urls, version: productsUpdated });
            } else {
                p.ipAddresses = ipAddresses;
                p.urls = urls;
                p.version = productsUpdated;
            }
        });
        
        var p = products.findObject({'name':'any'});
        if(!p) {
            products.insert({ name: 'any', ipAddresses: allIpAdresses, urls: allUrls, version: productsUpdated });
        } else {
            p.ipAddresses = Array.from(new Set(allIpAdresses));
            p.urls = Array.from(new Set(allUrls));
            p.version = productsUpdated;
        }
        console.log("Info: update finished; " + products.data.length + " product records in database.");
    });
}
 
// refresh Microsoft Office 365 XML every hour
repeat(getOffice365).every(1, 'hour').start.now();
 
// Start Version 0.2 - Added by Brett Smith @f5.com
// ILX::call to check if an URL is part of Office365
ilx.addMethod('checkProductURL', function(objArgs, objResponse) {
    var productName = objArgs.params()[0];
    var hostName = objArgs.params()[1];
    
    var verdict = false;
    
    var req = products.findObject( { 'name':productName.toLowerCase()});
    if(Array.isArray(req.urls)) {
        for (let url of req.urls) {
            if(hostName.indexOf(url) !== -1) {
                console.log("match found:" + url);
                verdict = true;
                break;
            }
        }
    }

    // return verdict to Tcl iRule
    objResponse.reply(verdict);
});
// End Version 0.2

// ILX::call to check if an IP address is part of Office365
ilx.addMethod('checkProductIP', function(objArgs, objResponse) {
    var productName = objArgs.params()[0];
    var ipAddress = objArgs.params()[1];
    
    // fail-open = true, fail-close = false
    var verdict = true;
    
    var req = products.findObject( { 'name':productName.toLowerCase()});
    if(req) {
        verdict = ipRangeCheck(ipAddress, req.ipAddresses);
    }
 
    // return AuthnRequest to Tcl iRule
    objResponse.reply(verdict);
});
 
// Start listening for ILX::call and ILX::notify events.
ilx.listen();

Tested this on version:

13.0
Published Apr 20, 2018
Version 1.0