/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
/* <DESC>
 * Show how CURLOPT_OPENSOCKETFUNCTION can be used to block IP addresses.
 * </DESC>
 */
/* This is an advanced example that defines a whitelist or a blacklist to
 * filter IP addresses.
 */

#ifdef __AMIGA__
#include <stdio.h>
int main(void)
{
  printf("Platform not supported.\n");
  return 1;
}
#else

#ifdef _WIN32  /* Requires Windows Vista+ */
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#define strdup _strdup
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <curl/curl.h>

#ifndef TRUE
#define TRUE 1
#endif

#ifndef FALSE
#define FALSE 0
#endif

struct ip {
  /* The user-provided IP address or network (use CIDR) to filter */
  char *str;
  /* IP address family AF_INET (IPv4) or AF_INET6 (IPv6) */
  int family;
  /* IP in network byte format */
  union netaddr {
    struct in_addr ipv4;
#ifdef AF_INET6
    struct in6_addr ipv6;
#endif
  } netaddr;
  /* IP bits to match against.
   * This is equal to the CIDR notation or max bits if no CIDR.
   * For example if ip->str is 127.0.0.0/8 then ip->maskbits is 8.
   */
  int maskbits;
  struct ip *next;
};

enum connection_filter_t {
  CONNECTION_FILTER_BLACKLIST,
  CONNECTION_FILTER_WHITELIST
};

struct connection_filter {
  struct ip *list;
  enum connection_filter_t type;
  int verbose;
#ifdef AF_INET6
  /* If the address being filtered is an IPv4-mapped IPv6 address then it is
   * checked against IPv4 list entries as well, unless ipv6_v6only is set TRUE.
   */
  int ipv6_v6only;
#endif
};

static struct ip *ip_list_append(struct ip *list, const char *data)
{
  struct ip *ip, *last;
  char *cidr;

  ip = calloc(1, sizeof(*ip));
  if(!ip)
    return NULL;

  if(strchr(data, ':')) {
#ifdef AF_INET6
    ip->family = AF_INET6;
#else
    free(ip);
    return NULL;
#endif
  }
  else
    ip->family = AF_INET;

  ip->str = strdup(data);
  if(!ip->str) {
    free(ip);
    return NULL;
  }

  /* determine the number of bits that this IP matches against */
  cidr = strchr(ip->str, '/');
  if(cidr) {
    ip->maskbits = atoi(cidr + 1);
    if(ip->maskbits <= 0 ||
#ifdef AF_INET6
       (ip->family == AF_INET6 && ip->maskbits > 128) ||
#endif
       (ip->family == AF_INET && ip->maskbits > 32)) {
      free(ip->str);
      free(ip);
      return NULL;
    }
    /* ignore the CIDR notation when converting ip->str to ip->netaddr */
    *cidr = '\0';
  }
  else if(ip->family == AF_INET)
    ip->maskbits = 32;
#ifdef AF_INET6
  else if(ip->family == AF_INET6)
    ip->maskbits = 128;
#endif

  if(inet_pton(ip->family, ip->str, &ip->netaddr) != 1) {
    free(ip->str);
    free(ip);
    return NULL;
  }

  if(cidr)
    *cidr = '/';

  if(!list)
    return ip;
  for(last = list; last->next; last = last->next)
    ;
  last->next = ip;
  return list;
}

static void ip_list_free_all(struct ip *list)
{
  struct ip *next;
  while(list) {
    next = list->next;
    free(list->str);
    free(list);
    list = next;
  }
}

static void free_connection_filter(struct connection_filter *filter)
{
  if(filter) {
    ip_list_free_all(filter->list);
    free(filter);
  }
}

static int ip_match(struct ip *ip, void *netaddr)
{
  int bytes, tailbits;
  const unsigned char *x, *y;

  x = (const unsigned char *)&ip->netaddr;
  y = (const unsigned char *)netaddr;

  for(bytes = ip->maskbits / 8; bytes; --bytes) {
    if(*x++ != *y++)
      return FALSE;
  }

  tailbits = ip->maskbits % 8;
  if(tailbits) {
    unsigned char tailmask = (unsigned char)((0xFF << (8 - tailbits)) & 0xFF);
    if((*x & tailmask) != (*y & tailmask))
      return FALSE;
  }

  return TRUE;
}

#ifdef AF_INET6
static int is_ipv4_mapped_ipv6_address(int family, void *netaddr)
{
  if(family == AF_INET6) {
    int i;
    const unsigned char *x = (const unsigned char *)netaddr;
    for(i = 0; i < 12; ++i) {
      if(x[i])
        break;
    }
    /* support formats ::x.x.x.x (deprecated) and ::ffff:x.x.x.x */
    if((i == 12 && (x[i] || x[i + 1] || x[i + 2] || x[i + 3])) ||
       (i == 10 && (x[i] == 0xFF && x[i + 1] == 0xFF)))
      return TRUE;
  }

  return FALSE;
}
#endif /* AF_INET6 */

static curl_socket_t opensocket(void *clientp, curlsocktype purpose,
                                struct curl_sockaddr *address)
{
  /* filter the address */
  if(purpose == CURLSOCKTYPE_IPCXN) {
    void *cinaddr = NULL;

    if(address->family == AF_INET)
      cinaddr = &((struct sockaddr_in *)(void *)&address->addr)->sin_addr;
#ifdef AF_INET6
    else if(address->family == AF_INET6)
      cinaddr = &((struct sockaddr_in6 *)(void *)&address->addr)->sin6_addr;
#endif

    if(cinaddr) {
      struct ip *ip;
      struct connection_filter *filter = (struct connection_filter *)clientp;
#ifdef AF_INET6
      int mapped = !filter->ipv6_v6only &&
                   is_ipv4_mapped_ipv6_address(address->family, cinaddr);
#endif

      for(ip = filter->list; ip; ip = ip->next) {
        if(ip->family == address->family && ip_match(ip, cinaddr))
          break;
#ifdef AF_INET6
        if(mapped && ip->family == AF_INET && address->family == AF_INET6 &&
           ip_match(ip, (unsigned char *)cinaddr + 12))
          break;
#endif
      }

      if(ip && filter->type == CONNECTION_FILTER_BLACKLIST) {
        if(filter->verbose) {
          char buf[128] = { 0 };
          inet_ntop(address->family, cinaddr, buf, sizeof(buf));
          fprintf(stderr, "* Rejecting IP %s due to blacklist entry %s.\n",
                  buf, ip->str);
        }
        return CURL_SOCKET_BAD;
      }
      else if(!ip && filter->type == CONNECTION_FILTER_WHITELIST) {
        if(filter->verbose) {
          char buf[128] = { 0 };
          inet_ntop(address->family, cinaddr, buf, sizeof(buf));
          fprintf(stderr,
                  "* Rejecting IP %s due to missing whitelist entry.\n", buf);
        }
        return CURL_SOCKET_BAD;
      }
    }
  }

  return socket(address->family, address->socktype, address->protocol);
}

int main(void)
{
  CURL *curl;
  CURLcode result;
  struct connection_filter *filter;

  filter = calloc(1, sizeof(*filter));
  if(!filter)
    return 1;

  result = curl_global_init(CURL_GLOBAL_ALL);
  if(result != CURLE_OK) {
    free(filter);
    return (int)result;
  }

  curl = curl_easy_init();
  if(!curl) {
    curl_global_cleanup();
    free(filter);
    return 1;
  }

  /* Set the target URL */
  curl_easy_setopt(curl, CURLOPT_URL, "http://localhost");

  /* Define an IP connection filter.
   * If an address has CIDR notation then it matches the network.
   * For example 74.6.143.25/24 matches 74.6.143.0 - 74.6.143.255.
   */
  filter->type = CONNECTION_FILTER_BLACKLIST;
  filter->list = ip_list_append(filter->list, "98.137.11.164");
  filter->list = ip_list_append(filter->list, "127.0.0.0/8");
#ifdef AF_INET6
  filter->list = ip_list_append(filter->list, "::1");
#endif

  /* Set the socket function which does the filtering */
  curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, opensocket);
  curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, filter);

  /* Verbose mode */
  filter->verbose = TRUE;
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

  /* Perform the request */
  result = curl_easy_perform(curl);

  /* Check for errors */
  if(result != CURLE_OK) {
    fprintf(stderr, "curl_easy_perform() failed: %s\n",
            curl_easy_strerror(result));
  }

  /* Clean up */
  curl_easy_cleanup(curl);
  free_connection_filter(filter);

  /* Clean up libcurl */
  curl_global_cleanup();

  return 0;
}
#endif
