|
@ -1,5 +1,4 @@ |
|
|
#!/usr/bin/env python |
|
|
#!/usr/bin/python33 |
|
|
|
|
|
|
|
|
# Python implementation of RFC 6555 / Happy Eyeballs: find the quickest IPv4/IPv6 connection |
|
|
# Python implementation of RFC 6555 / Happy Eyeballs: find the quickest IPv4/IPv6 connection |
|
|
# See https://tools.ietf.org/html/rfc6555 |
|
|
# See https://tools.ietf.org/html/rfc6555 |
|
|
# Method: Start parallel sessions using threads, and only wait for the quickest succesful socket connect |
|
|
# Method: Start parallel sessions using threads, and only wait for the quickest succesful socket connect |
|
@ -11,20 +10,20 @@ from happyeyeballs import happyeyeballs |
|
|
print happyeyeballs('newszilla.xs4all.nl', port=119) |
|
|
print happyeyeballs('newszilla.xs4all.nl', port=119) |
|
|
""" |
|
|
""" |
|
|
# or with more logging: |
|
|
# or with more logging: |
|
|
''' |
|
|
""" |
|
|
from happyeyeballs import happyeyeballs |
|
|
from happyeyeballs import happyeyeballs |
|
|
import logging |
|
|
import logging |
|
|
logger = logging.getLogger('') |
|
|
logger = logging.getLogger('') |
|
|
logger.setLevel(logging.DEBUG) |
|
|
logger.setLevel(logging.DEBUG) |
|
|
print happyeyeballs('newszilla.xs4all.nl', port=119) |
|
|
print happyeyeballs('newszilla.xs4all.nl', port=119) |
|
|
''' |
|
|
""" |
|
|
|
|
|
|
|
|
import socket |
|
|
import socket |
|
|
import ssl |
|
|
import ssl |
|
|
import queue |
|
|
|
|
|
import threading |
|
|
import threading |
|
|
import time |
|
|
import time |
|
|
import logging |
|
|
import logging |
|
|
|
|
|
import queue |
|
|
|
|
|
|
|
|
DEBUG = False |
|
|
DEBUG = False |
|
|
|
|
|
|
|
@ -39,7 +38,7 @@ def do_socket_connect(queue, ip, PORT, SSL, ipv4delay): |
|
|
if ip.find(':') >= 0: |
|
|
if ip.find(':') >= 0: |
|
|
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
|
|
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
|
|
if ip.find('.') >= 0: |
|
|
if ip.find('.') >= 0: |
|
|
time.sleep(ipv4delay) # IPv4 ... so a delay for IPv4 as we prefer IPv6. Note: ipv4delay could be 0 |
|
|
time.sleep(ipv4delay) # IPv4 ... so a delay for IPv4 as we prefer IPv6. Note: ipv4delay could be 0 |
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
|
|
|
|
|
|
s.settimeout(3) |
|
|
s.settimeout(3) |
|
@ -78,11 +77,11 @@ def happyeyeballs(HOST, **kwargs): |
|
|
try: |
|
|
try: |
|
|
preferipv6 = kwargs['preferipv6'] |
|
|
preferipv6 = kwargs['preferipv6'] |
|
|
except: |
|
|
except: |
|
|
preferipv6 = True # prefer IPv6, so give IPv6 connects a head start by delaying IPv4 |
|
|
preferipv6 = True # prefer IPv6, so give IPv6 connects a head start by delaying IPv4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Find out if a cached result is available, and recent enough: |
|
|
# Find out if a cached result is available, and recent enough: |
|
|
timecurrent = int(time.time()) # current time in seconds since epoch |
|
|
timecurrent = int(time.time()) # current time in seconds since epoch |
|
|
retentionseconds = 100 |
|
|
retentionseconds = 100 |
|
|
hostkey = (HOST, PORT, SSL, preferipv6) # Example key: (u'ssl.astraweb.com', 563, True, True) |
|
|
hostkey = (HOST, PORT, SSL, preferipv6) # Example key: (u'ssl.astraweb.com', 563, True, True) |
|
|
try: |
|
|
try: |
|
@ -102,7 +101,7 @@ def happyeyeballs(HOST, **kwargs): |
|
|
# we only arrive here if the entry has to be determined. So let's do that: |
|
|
# we only arrive here if the entry has to be determined. So let's do that: |
|
|
|
|
|
|
|
|
# We have to determine the (new) best IP address |
|
|
# We have to determine the (new) best IP address |
|
|
start = time.clock() |
|
|
start = time.perf_counter() |
|
|
if DEBUG: logging.debug("\n\n%s %s %s %s", HOST, PORT, SSL, preferipv6) |
|
|
if DEBUG: logging.debug("\n\n%s %s %s %s", HOST, PORT, SSL, preferipv6) |
|
|
|
|
|
|
|
|
ipv4delay = 0 |
|
|
ipv4delay = 0 |
|
@ -111,42 +110,42 @@ def happyeyeballs(HOST, **kwargs): |
|
|
socket.getaddrinfo(HOST, PORT, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME) |
|
|
socket.getaddrinfo(HOST, PORT, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_IP, socket.AI_CANONNAME) |
|
|
if DEBUG: logging.debug("IPv6 address found for %s", HOST) |
|
|
if DEBUG: logging.debug("IPv6 address found for %s", HOST) |
|
|
if preferipv6: |
|
|
if preferipv6: |
|
|
ipv4delay=0.1 # preferipv6, AND at least one IPv6 found, so give IPv4 (!) a delay so that IPv6 has a head start and is preferred |
|
|
ipv4delay=0.1 # preferipv6, AND at least one IPv6 found, so give IPv4 (!) a delay so that IPv6 has a head start and is preferred |
|
|
except: |
|
|
except: |
|
|
if DEBUG: logging.debug("No IPv6 address found for %s", HOST) |
|
|
if DEBUG: logging.debug("No IPv6 address found for %s", HOST) |
|
|
|
|
|
|
|
|
myqueue = queue.Queue() # queue used for threads giving back the results |
|
|
myqueue = queue.Queue() # queue used for threads giving back the results |
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
# Get all IP (IPv4 and IPv6) addresses: |
|
|
# Get all IP (IPv4 and IPv6) addresses: |
|
|
allinfo = socket.getaddrinfo(HOST, PORT, 0, 0, socket.IPPROTO_TCP) |
|
|
allinfo = socket.getaddrinfo(HOST, PORT, 0, 0, socket.IPPROTO_TCP) |
|
|
for info in allinfo: |
|
|
for info in allinfo: |
|
|
address = info[4][0] |
|
|
address = info[4][0] |
|
|
thisthread = threading.Thread(target=do_socket_connect, args=(myqueue, address, PORT, SSL, ipv4delay)) |
|
|
thisthread = threading.Thread(target=do_socket_connect, args=(myqueue, address, PORT, SSL, ipv4delay)) |
|
|
thisthread.daemon = True |
|
|
thisthread.daemon = True |
|
|
thisthread.start() |
|
|
thisthread.start() |
|
|
result = None # default return value, used if none of threads says True/"OK", so no connect on any IP address |
|
|
result = None # default return value, used if none of threads says True/"OK", so no connect on any IP address |
|
|
# start reading from the Queue for message from the threads: |
|
|
# start reading from the Queue for message from the threads: |
|
|
for i in range(len(allinfo)): |
|
|
for i in range(len(allinfo)): |
|
|
s = myqueue.get() # get a response |
|
|
s = myqueue.get() # get a response |
|
|
if s[1] == True: |
|
|
if s[1] == True: |
|
|
result = s[0] |
|
|
result = s[0] |
|
|
break # the first True/"OK" is enough, so break out of for loop |
|
|
break # the first True/"OK" is enough, so break out of for loop |
|
|
except: |
|
|
except: |
|
|
if DEBUG: logging.debug("something went wrong in the try block") |
|
|
if DEBUG: logging.debug("something went wrong in the try block") |
|
|
result = None |
|
|
result = None |
|
|
logging.info("Quickest IP address for %s (port %s, ssl %s, preferipv6 %s) is %s", HOST, PORT, SSL, preferipv6, result) |
|
|
logging.info("Quickest IP address for %s (port %s, ssl %s, preferipv6 %s) is %s", HOST, PORT, SSL, preferipv6, result) |
|
|
delay = int(1000 * (time.clock() - start)) |
|
|
delay = int(1000 * (time.perf_counter() - start)) |
|
|
logging.debug("Happy Eyeballs lookup and port connect took %s ms", delay) |
|
|
logging.debug("Happy Eyeballs lookup and port connect took %s ms", delay) |
|
|
|
|
|
|
|
|
# We're done. Store and return the result |
|
|
# We're done. Store and return the result |
|
|
if result: |
|
|
if result: |
|
|
happyeyeballs.happylist[hostkey] = ( result, timecurrent ) |
|
|
happyeyeballs.happylist[hostkey] = ( result, timecurrent ) |
|
|
if DEBUG: logging.debug("Determined new result for %s with result %s", (hostkey, happyeyeballs.happylist[hostkey]) ) |
|
|
if DEBUG: logging.debug("Determined new result for %s with result %s", hostkey, happyeyeballs.happylist[hostkey] ) |
|
|
return result |
|
|
return result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
happyeyeballs.happylist = {} # The cached results. This static variable must be after the def happyeyeballs() |
|
|
happyeyeballs.happylist = {} # The cached results. This static variable must be after the def happyeyeballs() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -156,23 +155,23 @@ if __name__ == '__main__': |
|
|
logger.setLevel(logging.INFO) |
|
|
logger.setLevel(logging.INFO) |
|
|
if DEBUG: logger.setLevel(logging.DEBUG) |
|
|
if DEBUG: logger.setLevel(logging.DEBUG) |
|
|
|
|
|
|
|
|
# plain HTTP/HTTPS sites: |
|
|
# plain HTTP/HTTPS sites: |
|
|
print((happyeyeballs('www.google.com'))) |
|
|
print((happyeyeballs('www.google.com'))) |
|
|
print((happyeyeballs('www.google.com', port=443, ssl=True))) |
|
|
print((happyeyeballs('www.google.com', port=443, ssl=True))) |
|
|
print((happyeyeballs('www.nu.nl'))) |
|
|
print((happyeyeballs('www.nu.nl'))) |
|
|
|
|
|
|
|
|
# newsservers: |
|
|
# newsservers: |
|
|
print((happyeyeballs('newszilla6.xs4all.nl', port=119))) |
|
|
print((happyeyeballs('newszilla6.xs4all.nl', port=119))) |
|
|
print((happyeyeballs('newszilla.xs4all.nl', port=119))) |
|
|
print((happyeyeballs('newszilla.xs4all.nl', port=119))) |
|
|
print((happyeyeballs('block.cheapnews.eu', port=119))) |
|
|
print((happyeyeballs('block.cheapnews.eu', port=119))) |
|
|
print((happyeyeballs('block.cheapnews.eu', port=443, ssl=True))) |
|
|
print((happyeyeballs('block.cheapnews.eu', port=443, ssl=True))) |
|
|
print((happyeyeballs('sslreader.eweka.nl', port=563, ssl=True))) |
|
|
print((happyeyeballs('sslreader.eweka.nl', port=563, ssl=True))) |
|
|
print((happyeyeballs('news.thundernews.com', port=119))) |
|
|
print((happyeyeballs('news.thundernews.com', port=119))) |
|
|
print((happyeyeballs('news.thundernews.com', port=119, preferipv6=False))) |
|
|
print((happyeyeballs('news.thundernews.com', port=119, preferipv6=False))) |
|
|
print((happyeyeballs('secure.eu.thundernews.com', port=563, ssl=True))) |
|
|
print((happyeyeballs('secure.eu.thundernews.com', port=563, ssl=True))) |
|
|
|
|
|
|
|
|
# Strange cases |
|
|
# Strange cases |
|
|
print((happyeyeballs('does.not.resolve', port=443, ssl=True))) |
|
|
print((happyeyeballs('does.not.resolve', port=443, ssl=True))) |
|
|
print((happyeyeballs('www.google.com', port=119))) |
|
|
print((happyeyeballs('www.google.com', port=119))) |
|
|
print((happyeyeballs('216.58.211.164'))) |
|
|
print((happyeyeballs('216.58.211.164'))) |
|
|
|
|
|
|
|
|