You are here

httpbl.module in http:BL 8

Same filename and directory in other branches
  1. 5 httpbl.module
  2. 6.2 httpbl.module
  3. 6 httpbl.module
  4. 7 httpbl.module

Implements Project Honeypot's http:BL for Drupal. It provides IP-based blacklisting through http:BL and allows linking to a honeypot.

8.x / 7.x / 6.x-2.x @author Bryan Lewellen (bryrock) <https://www.drupal.org/u/bryrock> @link http://drupal.org/project/httpbl @link http://httpbl.org/

6.x / 5.x @author Mark Janssen (praseodym) @link http://drupal.org/project/httpbl @link http://httpbl.org/

Contact: praseodym (at) gmail (dot) com

Additional D6 code support by David Norman (deekayen)

Feel free to improve this module, but please contact the author with any changes you make so they can be implemented into the 'official' version.

File

httpbl.module
View source
<?php

/**
 * @file
 * Implements Project Honeypot's http:BL for Drupal. It provides IP-based
 * blacklisting through http:BL and allows linking to a honeypot.
 *
 *
 * 8.x / 7.x / 6.x-2.x
 * @author Bryan Lewellen (bryrock) <https://www.drupal.org/u/bryrock>
 * @link http://drupal.org/project/httpbl
 * @link http://httpbl.org/
 *
 * 6.x / 5.x
 * @author Mark Janssen (praseodym)
 * @link http://drupal.org/project/httpbl
 * @link http://httpbl.org/
 *
 * Contact: praseodym (at) gmail (dot) com
 *
 * Additional D6 code support by David Norman (deekayen)
 *
 * Feel free to improve this module, but please contact the author with any
 * changes you make so they can be implemented into the 'official' version.
 *
 */
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\httpbl\HostQuery;
use Drupal\httpbl\Entity\Host;
use Drupal\httpbl\HttpblResponse;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_help().
 */
function httpbl_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.httpbl':
      $url = Url::fromUri('http://www.projecthoneypot.org/httpbl.php');
      $output = '';
      $output .= '<h3>' . t('About Http:BL') . '</h3>';
      $output .= '<p>' . t('The Http:BL module helps you block nuisance traffic from your site by implementing "http:BL," a blocking service that\'s provided externally by Project Honey Pot. You can also return the favor and make Project Honeypot better, by capturing and reporting nascent evil doers that are not yet in Project Honey Pot\'s database. For more in-depth information, visit the <a href="@httpbl" target=_blank>Project Honey Pot http:BL homepage</a>.', array(
        '@httpbl' => $url
          ->getUri(),
      )) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('<strong>Block all malicious traffic before it gets into your site (page request blocking)</strong>') . '</dt>';
      $output .= '<dd>' . t('Project Honey Pot tracks harvesters, comment spammers, and other suspicious visitors to websites. Http:BL allows website administrators to take advantage of the data generated by Project Honey Pot in order to keep suspicious and malicious web robots off their sites, by providing data back about the IP addresses of visitors to your website. Http:BL will grey-list or blacklist traffic, based on Project Honey Pot\'s assessment of risk.  Blacklisted traffic can optionally be Auto-Banned (added to banned IPs in the Core Ban module), banning them from future visits.') . '</dd>';
      $output .= '<dt>' . t('<strong>Session White-listing</strong>') . '</dt>';
      $output .= '<dd>' . t('Because sometimes innocent, real people get stuck with tarnished IPs, Http:BL gives people with grey-listed IPs the opportunity to white-list themselves on a per session basis, by responding to a simple challenge that spiders and spam-bots will usually fail.') . '</dd>';
      $output .= '<dt>' . t('<strong>Administer Evaluated Hosts (new in D8!)</strong>') . '</dt>';

      // @todo More later.
      $output .= '<dd>' . t('Now in D8, IP data is captured as evaluated Host entities, allowing them to be managed without direct database access.') . '</dd>';
      $output .= '<dt>' . t('<strong>Identify blocked traffic</strong>') . '</dt>';
      $output .= '<dd>' . t('Http:BL includes log messaging and Views reporting capability that will help you identify grey and blacklisted traffic, and review those IPs against Project Honeypot\'s database to see why they were banned.  In other words, if you\'re ever asked, "Why was this person blocked?" (a sensitive issue in some environments), you have resources available to help you answer the question, armed with factual details. If your site receives extremely heavy traffic, you can improve performance by keeping logging options set to a minimum level. You\'ll still have Views reporting access for revealing the blocked traffic. ') . '</dd>';
      $output .= '<dt>' . t('<strong>Block Comments only</strong>') . '</dt>';
      $output .= '<dd>' . t('Http:BL can be set for only checking and blocking comments only from malicious IPs.  Instead of refusing those IPs access to your site, just deny them access to commenting.  Any comments added by malicious IPs will be over-written with a message indicating the comment was blocked, for easy identification by admin and comment approvers.') . '</dd>';
      $output .= '<dt>' . t('<strong>Set up a Honey Pot (spammer trap)</strong>') . '</dt>';
      $output .= '<dd>' . t('You can set up honeypot traps (hidden links) that will report any IPs clicking them to Project Honeypot.  You give back to the project by helping to identify new and/or previously unidentified nuisance IPs that are not yet in the Project Honeypot database. Depending on your account settings (at Project Honeypot), you may receive notices letting you know that your site caught a new trouble-maker.  Stick a feather in your cap because you helped eliminate garbage not only from your site, but other\'s as well!') . '</dd>';
      $url = Url::fromUri('https://www.drupal.org/project/httpbl/issues/2992062');
      $output .= '<h3>' . t('Issues/Restrictions') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('<strong><a href="@httpbl" target=_blank>Issue #2992062</a>', array(
        '@httpbl' => $url
          ->getUri(),
      )) . '</strong></dt>';
      $output .= '<dd>' . t('Fatal errors have been reported when using this module for blocking page requests while the core extension Internal Page Cache (page_cache) is enabled.  Configuration for blocking page requests while using page_cache is still currently allowed (not prevented), but not recommended.</ br><strong>Recommended restrictions:</strong> Configure Http:BL to \'Block Comments only\' (see above) or uninstall Internal Page Cache if page request blocking is desired.<p>There are no reported conflicts with core extension Dynamic Internal Page Cache (dynamic_page_cache) when using Http:BL configured to block all page requests.</p>') . '</dd>';
      return $output;
    case 'httpbl.admin_config':
      $output = '<p>' . t('Configure http:BL settings for blocking page requests or comment submissions, to limit malicious traffic.') . '</p>';
      return $output;
  }
  return;
}

/**
 * Implements hook_comment_presave().
 *
 *  Checks and Blocks comment submissions from nuisance IPs.
 */
function httpbl_comment_presave($comment) {

  // Exit immediately if not configured for comment blocking only.
  if (\Drupal::state()
    ->get('httpbl.check') != HTTPBL_CHECK_COMMENTS) {
    return;
  }

  //Get the log service.
  $logTrapper = \Drupal::service('httpbl.logtrapper');

  // Check available storage options.  These are used while blocking comment to
  // inform comment reviewer of the extent of their options.
  if (\Drupal::state()
    ->get('httpbl.storage') == HTTPBL_DB_HH || \Drupal::state()
    ->get('httpbl.storage') == HTTPBL_DB_HH_DRUPAL) {

    // If auto-banning is enabled (using Httpbl with Ban module).
    if (\Drupal::state()
      ->get('httpbl.storage') == HTTPBL_DB_HH_DRUPAL) {
      $blacklist_and_ban = TRUE;
      $no_storage = FALSE;
      $blacklist_only = FALSE;
    }
    elseif (\Drupal::state()
      ->get('httpbl.storage') == HTTPBL_DB_HH) {
      $no_storage = FALSE;
      $blacklist_and_ban = FALSE;
      $blacklist_only = TRUE;
    }
  }
  else {
    $no_storage = TRUE;
    $blacklist_and_ban = FALSE;
    $blacklist_only = FALSE;
  }

  // Get request info needed below.
  $request = Drupal::request();
  $requestUri = $request
    ->getRequestUri();
  $ip = $request
    ->getClientIp();

  // Log this comment check.
  $logTrapper
    ->trapDebug('Checking @ip during comment pre-save.', [
    '@ip' => $ip,
  ]);

  //Get Evaluator service.
  $httpblEvaluator = \Drupal::service('httpbl.evaluator');

  // No Project Honeypot support for IPv6 addresses.
  // If this is not an IPv4, set to skip evaluation.
  $project_supported = TRUE;
  if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
    $project_supported = FALSE;
  }

  // Evaluation and comment processing begins.
  if (!isset($evaluated)) {

    // Evaluate this visitor IP.
    $evaluated = $httpblEvaluator
      ->evaluateVisitor($ip, $request, $project_supported);

    // If visitor was evaluated as not safe...
    if ($evaluated[1] > HTTPBL_LIST_SAFE) {

      // Comment user's IP found in Project Honeypot.
      // Let them know.
      drupal_set_message(t('Your comment was rejected because your IP address (@ip) is ranked "suspicious" by Project Honeypot.', [
        '@ip' => $ip,
      ]), 'error', FALSE);

      // Prepare to alter the comment subject and body.
      // Make link to the blocked host profile on Project Honeypot.
      $url = Url::fromUri('http://www.projecthoneypot.org/search_ip.php?ip=' . $ip);
      $url_options = [
        'attributes' => [
          'class' => [
            'httpbl-comment-blocked',
            'httpbl-comment-blocked-profile',
          ],
          'absolute' => TRUE,
        ],
      ];
      $url
        ->setOptions($url_options);
      $link = Link::fromTextAndUrl(t('Project Honeypot'), $url)
        ->toString();
      $honeypot_profile = 'http://www.projecthoneypot.org/search_ip.php?ip=' . $ip;

      // Force the comment to unpublished status.
      $comment
        ->setPublished(0);

      // Save original subject, then alter.
      $original_subject = $comment
        ->getSubject();
      $subject = t('(THIS COMMENT HAS BEEN Http:BL BLOCKED!)');
      $comment
        ->setSubject($subject);

      // Save original body, then alter
      $original_body = $comment
        ->get('comment_body')->value;

      // Add details for later comment review.  Why the comment was blocked,
      // with a link to the host's profile, if further research is desired.
      $blocked_body = t('<p><strong>!!! This comment was blocked and unpublished by the httpbl module!!!</strong></p>');
      $blocked_body .= t('<p><strong>Host IP (@ip) has been identified as a nuisance, by @Project.</strong></p>', [
        '@ip' => $ip,
        '@Project' => $link,
      ]);

      // Prepare a suggestion link for using httpbl to blacklist (and possibly
      // ban) a grey-listed host that makes repeated, comment annoyances.
      $blacklistUrl = Url::fromRoute('httpbl.host_add');
      $blacklistUrl_options = [
        'attributes' => [
          'class' => [
            'httpbl-comment-blocked',
            'httpbl-comment-blocked-config',
          ],
          'target' => '_blank',
        ],
      ];
      $blacklistUrl
        ->setOptions($blacklistUrl_options);
      $blacklistLink = Link::fromTextAndUrl(t('blacklisting'), $blacklistUrl)
        ->toString();

      // Prepare a suggestion link for using ban manager to ban IP.
      $banUrl = Url::fromRoute('ban.admin_page');
      $banUrl_options = [
        'attributes' => [
          'class' => [
            'httpbl-comment-blocked',
            'httpbl-comment-blocked-config',
          ],
          'target' => '_blank',
        ],
      ];
      $banUrl
        ->setOptions($banUrl_options);
      $banLink = Link::fromTextAndUrl(t('banning'), $banUrl)
        ->toString();

      // If this host is blacklisted...
      if ($evaluated[1] == HTTPBL_LIST_BLACK) {

        // Check for blacklisting & auto-banning
        if ($blacklist_and_ban) {

          // Find the host and get its ID and expiry date.
          $hosts = HostQuery::loadHostsByIp($ip);

          // Get the host expire time (from httpbl).
          foreach ($hosts as $host) {
            $hostId = $host->hid->value;
            $expires = \Drupal::service('date.formatter')
              ->formatTimeDiffUntil($host->expire->value);
          }

          //Get BanManager service.
          $banManager = \Drupal::service('ban.ip_manager');

          //Is this IP now successfully banned?
          if ($banManager
            ->isBanned($ip)) {

            // Advise comment reviewer that this visiting host is now
            // blacklisted and banned, for some (configured) amount of time.
            $blocked_body .= t('<p><strong>@ip has now been blacklisted and auto-banned from future site visits for @time.</strong></p>', [
              '@ip' => $ip,
              '@time' => $expires,
            ]);
          }
          else {

            // Prepare a suggestion link for using httpbl to update and auto-ban
            // a previously blacklisted host that was not previously banned, or
            // they can just ban it.
            $blacklistUrl2 = Url::fromUri('internal:/admin/httpbl/host/' . $hostId . '/edit');
            $blacklistUrl2_options = [
              'attributes' => [
                'class' => [
                  'httpbl-comment-blocked',
                  'httpbl-comment-blocked-config',
                ],
                'target' => '_blank',
              ],
            ];
            $blacklistUrl2
              ->setOptions($blacklistUrl2_options);
            $blacklistLink2 = Link::fromTextAndUrl(t('updating'), $blacklistUrl2)
              ->toString();

            // Advise comment reviewer that this visiting host is already
            // blacklisted, but was not previously banned.
            $blocked_body .= t('<p><strong>@ip is already blacklisted on this site, but is not yet banned. Consider @banning it or @updating it.</strong></p>', [
              '@ip' => $ip,
              '@banning' => $banLink,
              '@updating' => $blacklistLink2,
            ]);
          }

          // Advise commenter they are history!
          drupal_set_message(t('Your address (@ip) has been blacklisted for @time.', [
            '@ip' => $ip,
            '@time' => $expires,
          ]), 'error', FALSE);
        }

        // Check for blacklisting storage only (no auto-banning configured).
        if ($blacklist_only) {

          // Advise comment review that blacklisting commenters alone will not
          // prevent future visits from the same nuisance IP, but they can ban it.
          $blocked_body .= t('<p><strong>@ip is blacklisted on this site, but neither auto-banning or page-checking are configured to prevent future site visits. Consider @banning it.</strong></p>', [
            '@ip' => $ip,
            '@banning' => $banLink,
          ]);
        }

        // No blacklisting or banning storage options configured.
        // Advise comment reviewer of this situation.
        if ($no_storage) {
          $blocked_body .= t('<p><strong>@ip is blacklisted on this site, but NO http:BL options are configured to prevent future site visits.</strong></p>', [
            '@ip' => $ip,
          ]);
        }

        // Log this (blacklisted) blocked comment.
        $logTrapper
          ->trapWarning('Blocked comment from blacklisted @ip.', [
          '@ip' => $ip,
        ]);
      }
      elseif ($evaluated[1] == HTTPBL_LIST_GREY) {

        // Advise comment reviewer of this situation.
        $blocked_body .= t('<p><strong>@ip is currently grey-listed on this site.  If @ip is becoming a repeated nuisance, then consider @blacklisting it.  In the meantime, the commenter was offered a challenge for session white-listing, if they pass. </strong></p>', [
          '@ip' => $ip,
          '@blacklisting' => $blacklistLink,
        ]);

        // Since commenter is only greylisted, offer them a  chance (a link)
        // to take the whitelist challenge.
        $whitelistUrl = Url::fromRoute('httpbl.whitelist_challenge_form');
        $whitelistUrl_options = [
          'attributes' => [
            'class' => [
              'httpbl-whitelist-message',
            ],
            'target' => '_blank',
          ],
        ];
        $whitelistUrl
          ->setOptions($whitelistUrl_options);
        $whitelistLink = Link::fromTextAndUrl(t('challenge'), $whitelistUrl)
          ->toString();
        drupal_set_message(t('You can become white-listed for your current session by passing this @challenge.', [
          '@challenge' => $whitelistLink,
        ]), 'warning', FALSE);

        // Log this (greylisted) blocked comment.
        $logTrapper
          ->trapNotice('Blocked comment from grey-listed @ip.', [
          '@ip' => $ip,
        ]);
      }

      // Add in original comment content, in case it wasn't that bad, even
      // though the host IP comes up dirty.
      $blocked_body .= t('<p><strong>Original subject:&nbsp;</strong>' . $original_subject . '</p>');
      $blocked_body .= t('<p><strong>Original comment below:</strong></p>');
      $blocked_body .= $original_body;

      // Force filter on this comment to full html to be sure we see the profile
      // link with its attributes.
      $comment_body = [
        'summary' => '',
        'value' => $blocked_body,
        'format' => 'full_html',
      ];

      // Set the altered comment.
      $comment
        ->set('comment_body', $comment_body);

      // Update statistics if configured.
      if (\Drupal::state()
        ->get('httpbl.stats')) {
        \Drupal::state()
          ->set('httpbl.stat_comment', \Drupal::state()
          ->get('httpbl.stat_comment') + 1);
      }
    }
  }
}

/**
 * Implements hook_cron().
 *
 * Removes expired IPs from the 'httpbl' table. Also, if configured to auto ban
 * blacklisted IPs, expired blacklisted IPs in Httpbl will be removed from
 * Drupal's core 'ban_ip' table.
 */
function httpbl_cron() {

  // IMPORTANT! For the sake of running this with drush cron, need to compare
  // the actual integer values represented by the CONSTANT.  The CONSTANT use
  // is fine for cron auto-running and from UI, but running cron from command
  // line via drush, the CONSTANT values appear to be ignored, resulting in
  // incorrect results from the comparison logic.
  // Only continue if any level of host storage is enabled.
  if (\Drupal::state()
    ->get('httpbl.storage') > HTTPBL_DB_OFF || \Drupal::state()
    ->get('httpbl.storage') > 0) {

    // Make sure both managers are available or else we abort.
    if (\Drupal::hasService('ban.ip_manager') && \Drupal::hasService('httpbl.evaluator')) {
      $httpblManager = \Drupal::service('httpbl.evaluator');
      $banManager = \Drupal::service('ban.ip_manager');
      $logTrapper = \Drupal::service('httpbl.logtrapper');
    }
    else {

      // Log the failure.
      \Drupal::logger('httpbl')
        ->error('Httpbl cron expire failed! Critical services (httpbl.evaluator and/or ban.ip_manager) are not available.');
      return NULL;
    }

    // Count all expired Hosts in httpbl_host.
    $now = \Drupal::time()
      ->getRequestTime();
    $hostsCount = HostQuery::countExpiredHosts($now);

    // Log the count.
    $logTrapper
      ->trapInfo('@count evaluated hosts to be expired.', [
      '@count' => $hostsCount,
    ]);

    // Gather all expired Hosts.
    $expiredHosts = HostQuery::loadExpiredHosts($now);

    // Check if also auto-banning in Drupal's 'ban_ip' table, then remove
    // any of those first, based on httpbl expiry.
    if (\Drupal::state()
      ->get('httpbl.storage') == HTTPBL_DB_HH_DRUPAL || \Drupal::state()
      ->get('httpbl.storage') == 2) {
      foreach ($expiredHosts as $key => $host) {
        $host = Host::load($key);
        $host_ip = $host
          ->getHostIp();
        $status = $host
          ->getHostStatus();

        // Humanize the status codes for messages.
        $human = $httpblManager
          ->getHumanStatus($status);

        // Find expired IPs that have also been banned.
        $banned = $banManager
          ->isBanned($host_ip);

        // If found in ban_ip, un-ban them.
        if ($banned) {
          $banManager
            ->unBanIp($host_ip);
          $logTrapper
            ->trapDebug('Expired @human host @ip has been un-banned.', [
            '@human' => $human,
            '@ip' => $host_ip,
          ]);
        }
      }
    }

    // Now remove expired IPs from httpbl.
    foreach ($expiredHosts as $key => $host) {
      $host = Host::load($key);
      $host_ip = $host
        ->getHostIp();
      $status = $host
        ->getHostStatus();
      $human = $httpblManager
        ->getHumanStatus($status);
      $logTrapper
        ->trapDebug('@human @ip has expired.', [
        '@human' => $human,
        '@ip' => $host_ip,
      ]);

      // There could be a situation where auto-banning was once enabled, then
      // later disabled, which would leave httpbl expired and removed IPs still
      // being banned in core ban_ip table.  So check for those while removing
      // from httpbl, and issue a warning that they are still banned.
      $banned = $banManager
        ->isBanned($host_ip);
      $host
        ->delete();

      // Warning that expired IP still banned.
      if ($banned) {
        $logTrapper
          ->trapWarning('Expired @human @ip is still banned but removed from httpbl_host.', [
          '@human' => $human,
          '@ip' => $host_ip,
        ]);
      }
      else {

        // Simple info message that the IP has been expired.
        $logTrapper
          ->trapDebug('Expired @human host @ip has been deleted.', [
          '@human' => $human,
          '@ip' => $host_ip,
        ]);
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function httpbl_form_ban_ip_form_alter(&$form, FormStateInterface $form_state) {

  // Hide the the ability to add IPs to Ban module when httpbl is enabled.
  $httpblUrl = Url::fromRoute('entity.host.collection');
  $httpblLink = Link::fromTextAndUrl(t('Http:BL module'), $httpblUrl)
    ->toString();
  $httpblLink2 = Link::fromTextAndUrl(t('from Http:BL Evaluated Hosts'), $httpblUrl)
    ->toString();

  // Add an 'item' message with links to httpbl evaluated hosts admin.
  $form['httpbl_message'] = array(
    '#type' => 'item',
    '#markup' => t('<h3>The @httpbl is installed, enabling automated, expiry managed IPs (white-listing, grey-listing and blacklisting -- with auto-banning).  All IP banning should be handled @hosts.</h3>', [
      '@httpbl' => $httpblLink,
      '@hosts' => $httpblLink2,
    ]),
  );

  // Hide the textfield and submit button used for "manual" banning of IPs.
  $form['ip']['#type'] = 'hidden';
  $form['actions']['submit']['#type'] = 'hidden';
}

/**
 * Implements hook_page_bottom().
 */
function httpbl_page_bottom(array &$page_bottom) {

  // Check settings to see if page bottom honeypots enabled.
  if (\Drupal::state()
    ->get('httpbl.footer') ?: FALSE) {
    $link = \Drupal::state()
      ->get('httpbl.link') ?: NULL;
    $word = \Drupal::state()
      ->get('httpbl.word') ?: 'randomness';
    $honeypot = HttpblResponse::httpbl_honeylink($link, $word);
    if (!empty($honeypot)) {
      $page_bottom['httpbl'] = array(
        '#type' => 'markup',
        '#markup' => $honeypot,
      );
    }
  }
}

/**
 * Forms an associative array from a linear array.
 * (formerly known as drupal_map_assoc, but removed from D8)
 *
 * This function walks through the provided array and constructs an associative
 * array out of it. The keys of the resulting array will be the values of the
 * input array. The values will be the same as the keys unless a function is
 * specified, in which case the output of the function is used for the values
 * instead.
 *
 * @param $array
 *   A linear array.
 * @param $function
 *   A name of a function to apply to all values before output.
 *
 * @return
 *   An associative array.
 */
function httpbl_map_assoc($array, $function = NULL) {

  // array_combine() fails with empty arrays:
  // http://bugs.php.net/bug.php?id=34857.
  $array = !empty($array) ? array_combine($array, $array) : array();
  if (is_callable($function)) {
    $array = array_map($function, $array);
  }
  return $array;
}

Functions

Namesort descending Description
httpbl_comment_presave Implements hook_comment_presave().
httpbl_cron Implements hook_cron().
httpbl_form_ban_ip_form_alter Implements hook_form_FORM_ID_alter().
httpbl_help Implements hook_help().
httpbl_map_assoc Forms an associative array from a linear array. (formerly known as drupal_map_assoc, but removed from D8)
httpbl_page_bottom Implements hook_page_bottom().