Skip to content

Commit

Permalink
Fix #152 - gracefully handle SSL connection abort
Browse files Browse the repository at this point in the history
  • Loading branch information
genotrance committed Jun 15, 2022
1 parent de4090d commit a0a2d57
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 89 deletions.
4 changes: 4 additions & 0 deletions HISTORY.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v0.7.2 - 2022-06-14
- Fixed #152 - handle connection errors in select loop gracefully
- Fixed #151 - handle libcurl 7.29 on Centos7

v0.7.1 - 2022-06-13
- Fixed #146 - px --install was broken when run in cmd.exe, also when
run as `python -m px`
Expand Down
24 changes: 16 additions & 8 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ if [ -f "/.dockerenv" ]; then
COMMAND="$3"
fi

MUSL=`ldd /bin/ls | grep musl`
if [ -z "$MUSL" ]; then
PXBIN="/px/px.dist-linux-glibc-x86_64/px.dist/px"
else
PXBIN="/px/px.dist-linux-musl-x86_64/px.dist/px"
fi

if [ "$DISTRO" = "alpine" ]; then

apk update && apk upgrade
Expand All @@ -40,7 +33,7 @@ if [ -f "/.dockerenv" ]; then
yum install -y ccache dbus-devel libffi-devel patchelf python36-cryptography upx
fi

elif [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
elif [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ] || [ "$DISTRO" = "linuxmint" ]; then

apt update -y && apt upgrade -y
apt install -y curl dbus gnome-keyring psmisc python3 python3-dev python3-pip
Expand All @@ -56,12 +49,27 @@ if [ -f "/.dockerenv" ]; then
zypper -n install gcc
fi

elif [ "$DISTRO" = "void" ]; then

xbps-install -Suy xbps
xbps-install -Sy curl dbus gcc gnome-keyring psmisc python3 python3-devel
python3 -m ensurepip

SHELL="sh"

else
echo "Unknown distro $DISTRO"
$SHELL
exit
fi

MUSL=`ldd /bin/ls | grep musl`
if [ -z "$MUSL" ]; then
PXBIN="/px/px.dist-linux-glibc-x86_64/px.dist/px"
else
PXBIN="/px/px.dist-linux-musl-x86_64/px.dist/px"
fi

dbus-run-session -- $SHELL -c 'echo "abc" | gnome-keyring-daemon --unlock'

cd /px
Expand Down
85 changes: 45 additions & 40 deletions px/libcurl/_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "curl.h"

import ctypes as ct
from xml.dom.minidom import Attr

from ._platform import CFUNC, defined
from ._dll import dll
Expand Down Expand Up @@ -524,45 +525,49 @@ class waitfd(ct.Structure):
(1, "sockfd"),
(1, "sockp"),))

# Name: curl_push_callback
#
# Desc: This callback gets called when a new stream is being pushed by the
# server. It approves or denies the new stream. It can also decide
# to completely fail the connection.
#
# Returns: CURL_PUSH_OK, CURL_PUSH_DENY or CURL_PUSH_ERROROUT

CURL_PUSH_OK = 0
CURL_PUSH_DENY = 1
CURL_PUSH_ERROROUT = 2 # added in 7.72.0

# forward declaration only
class pushheaders(ct.Structure): pass

pushheader_bynum = CFUNC(ct.c_char_p,
ct.POINTER(pushheaders),
ct.c_size_t)(
("curl_pushheader_bynum", dll), (
(1, "h"),
(1, "num"),))

pushheader_byname = CFUNC(ct.c_char_p,
ct.POINTER(pushheaders),
ct.c_char_p)(
("curl_pushheader_byname", dll), (
(1, "h"),
(1, "name"),))

# typedef int (*curl_push_callback)(CURL *parent,
# CURL *easy,
# size_t num_headers,
# ct.POINTER(curl_pushheaders) headers,
# void *userp);
push_callback = CFUNC(ct.c_int,
ct.POINTER(CURL), # parent
ct.POINTER(CURL), # easy
ct.c_size_t, # num_headers
ct.POINTER(pushheaders), # headers
ct.c_void_p) # userp
# libcurl < 7.44
try:
# Name: curl_push_callback
#
# Desc: This callback gets called when a new stream is being pushed by the
# server. It approves or denies the new stream. It can also decide
# to completely fail the connection.
#
# Returns: CURL_PUSH_OK, CURL_PUSH_DENY or CURL_PUSH_ERROROUT

CURL_PUSH_OK = 0
CURL_PUSH_DENY = 1
CURL_PUSH_ERROROUT = 2 # added in 7.72.0

# forward declaration only
class pushheaders(ct.Structure): pass

pushheader_bynum = CFUNC(ct.c_char_p,
ct.POINTER(pushheaders),
ct.c_size_t)(
("curl_pushheader_bynum", dll), (
(1, "h"),
(1, "num"),))

pushheader_byname = CFUNC(ct.c_char_p,
ct.POINTER(pushheaders),
ct.c_char_p)(
("curl_pushheader_byname", dll), (
(1, "h"),
(1, "name"),))

# typedef int (*curl_push_callback)(CURL *parent,
# CURL *easy,
# size_t num_headers,
# ct.POINTER(curl_pushheaders) headers,
# void *userp);
push_callback = CFUNC(ct.c_int,
ct.POINTER(CURL), # parent
ct.POINTER(CURL), # easy
ct.c_size_t, # num_headers
ct.POINTER(pushheaders), # headers
ct.c_void_p) # userp
except AttributeError:
pass

# eof
31 changes: 17 additions & 14 deletions px/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,17 @@ def handle_one_request(self):
try:
httpserver.BaseHTTPRequestHandler.handle_one_request(self)
except socket.error as error:
self.close_connection = True
easyhash = ""
if self.curl is not None:
easyhash = self.curl.easyhash + ": "
State.mcurl.stop(self.curl)
self.curl = None
if "forcibly closed" in str(error):
dprint("Connection closed by client")
dprint(easyhash + "Connection closed by client")
else:
traceback.print_exc(file=sys.stdout)
dprint("Socket error: %s" % error)
self.close_connection = True
State.mcurl.stop(self.curl)
self.curl = None
dprint(easyhash + "Socket error: %s" % error)

def address_string(self):
host, port = self.client_address[:2]
Expand All @@ -312,16 +315,16 @@ def log_message(self, format, *args):
dprint(format % args)

def do_curl(self):
dprint("Path = " + self.path)
if self.curl is None:
self.curl = mcurl.Curl(self.path, self.command, self.request_version, State.socktimeout)
else:
self.curl.reset(self.path, self.command, self.request_version, State.socktimeout)
self.curl.set_debug()

dprint(self.curl.easyhash + ": Path = " + self.path)
ipport = self.get_destination()
if ipport is None:
dprint("Configuring proxy settings")
dprint(self.curl.easyhash + ": Configuring proxy settings")
server = self.proxy_servers[0][0]
port = self.proxy_servers[0][1]
ret = self.curl.set_proxy(proxy = server, port = port)
Expand All @@ -336,11 +339,11 @@ def do_curl(self):
key = State.username
pwd = keyring.get_password("Px", key)
if len(key) == 0:
dprint("Using SSPI to login")
dprint(self.curl.easyhash + ": Using SSPI to login")
key = ":"
self.curl.set_auth(user = key, password = pwd, auth = State.auth)
else:
dprint("Skipping auth proxying")
dprint(self.curl.easyhash + ": Skipping auth proxying")

# Plain HTTP can be bridged directly
if not self.curl.is_connect():
Expand All @@ -353,10 +356,10 @@ def do_curl(self):
self.curl.set_useragent(State.useragent)

if not State.mcurl.do(self.curl):
dprint("Connection failed: " + self.curl.errstr)
dprint(self.curl.easyhash + ": Connection failed: " + self.curl.errstr)
self.send_error(self.curl.resp, self.curl.errstr)
elif self.curl.is_connect():
dprint("Connected")
dprint(self.curl.easyhash + ": SSL connected")
self.send_response(200, "Connection established")
self.send_header("Proxy-Agent", self.version_string())
self.end_headers()
Expand All @@ -377,7 +380,7 @@ def do_PAC(self):
self.send_response(200)

for key, value in headers.items():
dprint("Returning %s: %s" % (key, value))
dprint(self.curl.easyhash + ": Returning %s: %s" % (key, value))
self.send_header(key, value)

self.end_headers()
Expand Down Expand Up @@ -419,10 +422,10 @@ def get_destination(self):
servers, netloc, path = State.wproxy.find_proxy_for_url(
("https://" if "://" not in self.path else "") + self.path)
if servers[0] == wproxy.DIRECT:
dprint("Direct connection")
dprint(self.curl.easyhash + ": Direct connection")
return netloc
else:
dprint("Proxy = " + str(servers))
dprint(self.curl.easyhash + ": Proxy = " + str(servers))
self.proxy_servers = servers
return None

Expand Down
19 changes: 12 additions & 7 deletions px/mcurl.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ def _write_callback(buffer, size, nitems, userdata):
if curl.sentheaders:
try:
tsize = curl.client_wfile.write(bytes(buffer[:tsize]))
except (ConnectionError, BrokenPipeError) as exc:
dprint(curl.easyhash + ": Write error: " + str(exc))
except ConnectionError as exc:
dprint(curl.easyhash + ": Error writing to client: " + str(exc))
return 0
else:
dprint(curl.easyhash + ": Skipped %d bytes" % tsize)
Expand Down Expand Up @@ -160,8 +160,8 @@ def _header_callback(buffer, size, nitems, userdata):
return tsize
try:
return curl.client_wfile.write(data)
except (ConnectionError, BrokenPipeError) as exc:
dprint("Header write error: " + str(exc))
except ConnectionError as exc:
dprint(curl.easyhash + ": Error writing header to client: " + str(exc))
return 0

return 0
Expand Down Expand Up @@ -298,7 +298,7 @@ def set_proxy(self, proxy, port = 0, noproxy = None):
Set proxy options - returns False if this proxy server has auth failures
"""
if proxy in MCURL.failed:
dprint("Authentication issues with this proxy server")
dprint(self.easyhash + ": Authentication issues with this proxy server")
return False

self.proxy = proxy
Expand All @@ -324,7 +324,7 @@ def set_auth(self, user, password = None, auth = "ANY"):
if password is not None:
libcurl.easy_setopt(self.easy, libcurl.CURLOPT_PROXYPASSWORD, password.encode("utf-8"))
else:
dprint("Blank password for user")
dprint(self.easyhash + ": Blank password for user")
if auth is not None:
self.auth = auth

Expand Down Expand Up @@ -697,7 +697,12 @@ def select(self, curl: Curl, client_sock, idle = 30):
wdata = sdata
source = "client"

data = i.recv(4096)
try:
data = i.recv(4096)
except ConnectionError as exc:
# Fix #152 - handle connection errors gracefully
dprint(curl.easyhash + ": Read error from %s: " % source + str(exc))
data = ""
datalen = len(data)
if datalen != 0:
cl += datalen
Expand Down
2 changes: 1 addition & 1 deletion px/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"Px version"

__version__ = "0.7.1"
__version__ = "0.7.2"
Loading

0 comments on commit a0a2d57

Please sign in to comment.