Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions src/func_python/cloudevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
import logging
import os
import signal
import socket

import hypercorn.config
import hypercorn.asyncio

from cloudevents.http import from_http, CloudEvent
from cloudevents.conversion import to_structured, to_binary
from cloudevents.exceptions import MissingRequiredFields, InvalidRequiredFields

import func_python.sock

DEFAULT_LOG_LEVEL = logging.INFO
DEFAULT_LISTEN_ADDRESS = "[::]:8080"

logging.basicConfig(level=DEFAULT_LOG_LEVEL)

Expand Down Expand Up @@ -75,13 +76,7 @@ def serve(self):
"""serve serving this ASGIhandler, delegating implementation of
methods as necessary to the wrapped Function instance"""
cfg = hypercorn.config.Config()

la = os.getenv('LISTEN_ADDRESS', DEFAULT_LISTEN_ADDRESS)
[host, port] = la.rsplit(":", 1)
# fixup for IPv4-only machines
if not socket.has_ipv6 and host == '[::]':
la = "0.0.0.0:" + port
cfg.bind = [la]
cfg.bind = func_python.sock.bind()

logging.info(f"function starting on {cfg.bind}")
return asyncio.run(self._serve(cfg))
Expand Down
13 changes: 4 additions & 9 deletions src/func_python/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import logging
import os
import signal
import socket

import hypercorn.config
import hypercorn.asyncio

import func_python.sock

DEFAULT_LOG_LEVEL = logging.INFO
DEFAULT_LISTEN_ADDRESS = "[::]:8080"

logging.basicConfig(level=DEFAULT_LOG_LEVEL)

Expand Down Expand Up @@ -70,13 +71,7 @@ def serve(self):
"""serve serving this ASGIhandler, delegating implementation of
methods as necessary to the wrapped Function instance"""
cfg = hypercorn.config.Config()

la = os.getenv('LISTEN_ADDRESS', DEFAULT_LISTEN_ADDRESS)
[host, port] = la.rsplit(":", 1)
# fixup for IPv4-only machines
if not socket.has_ipv6 and host == '[::]':
la = "0.0.0.0:" + port
cfg.bind = [la]
cfg.bind = func_python.sock.bind()

logging.debug(f"function starting on {cfg.bind}")
return asyncio.run(self._serve(cfg))
Expand Down
73 changes: 73 additions & 0 deletions src/func_python/sock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import ipaddress
import logging
import os
import socket


DEFAULT_LISTEN_ADDRESS = '[::]:8080,0.0.0.0:8080'

def bind() -> list[str]:
"""
This function reads the 'LISTEN_ADDRESS' environment variable and binds sockets according to it's content.
This function gives us some more control over how sockets are created.
We creat them ourselves here, and forward them in the "fd://{fd}" format to the hypercorn server.
:return: Sequence of "bind" strings in format expected by the hypercorn server config.
"""

listen_addresses = os.getenv('LISTEN_ADDRESS', DEFAULT_LISTEN_ADDRESS).split(",")

fixup_ipv4_unspecified(listen_addresses)

ipv4_only = not socket.has_dualstack_ipv6()
result: list[str] = []

for address in listen_addresses:
if address.startswith("unix://") or address.startswith("fd://"):
logging.error(f'unsupported schema: <{address}>')
continue
sock: socket.socket

[host, port] = address.rsplit(":", 1)
if '[' in host:
if ipv4_only:
logging.warning(f'not binding <{address}> since IPv6 is not available')
continue
host = host.strip('[]')
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
try:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
except socket.error as e:
logging.warning(f"cannot set IPV6_V6ONLY: {e}")
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
sock.bind((host, int(port)))
result.append(f'fd://{sock.detach()}')
except socket.error as e:
logging.error(f"cannot bind socket <{address}>: {e}")

if len(result) <= 0:
raise Exception('failed to bind any sockets')

return result

def fixup_ipv4_unspecified(listen_addresses: list[str]) -> None:
"""
This function checks if the listen addresses contains unspecified IPv6 address but not unspecified IPv4 address.
If that's the case the function will insert appropriate unspecified IPv4 address into the list.
"""
ipv6_unspecified_port = None
ipv4_unspecified = False
for la in listen_addresses:
if la.startswith("unix://") or la.startswith("fd://"):
continue
[host,port] = la.rsplit(":", 1)
ip = ipaddress.ip_address(host.strip('[]'))
if ip.is_unspecified:
if isinstance(ip, ipaddress.IPv6Address):
ipv6_unspecified_port = port
if isinstance(ip, ipaddress.IPv4Address):
ipv4_unspecified = True
if ipv6_unspecified_port is not None and not ipv4_unspecified:
listen_addresses.append('0.0.0.0:' + ipv6_unspecified_port)
Loading