brandond / requests-negotiate-sspi

Windows SSPI-based Negotiate authentication support for Requests.
Apache License 2.0
130 stars 24 forks source link

Proxy Authentication Support ? #9

Open franklighter opened 7 years ago

franklighter commented 7 years ago
            if scheme.lower() in r.headers.get('WWW-Authenticate', '').lower():
                return self._retry_using_http_Negotiate_auth(r, scheme, kwargs)

I am searching SSPI authentication and glad to come across this package. May I wonder if there is any planned enhancement to extend WWW authentication to Proxy Authentication. Thank you.

brandond commented 7 years ago

I'm not really familiar with how this would work for proxy auth. From some brief research, I believe we'd need to work with the Proxy-Authorization and Proxy-Authenticate headers, but I don't have any infrastructure available to test this with. It could be as simple as just providing a method to use the proxy auth headers instead of the standard auth headers, or it could be completely different. The channel-binding stuff in might require special handling as well.

Do you have a particular proxy you're trying to use this with? Is there an easy way to set up a test environment?

cameronwallace commented 7 years ago

am having trouble finding the dependent sspi module... is it supported in 2.7?

brandond commented 7 years ago

@cameronwallace the sspi module comes from pywin32/pypiwin32; this module (currently) only works on Windows as SSPI is a Windows-specific API . There are other modules that support generic Kerberos implementations (see requests-kerberos, for example).

railocius commented 6 years ago

I'm also interested in NTLM proxy authentication. It is not much different than accessing a NTLM protected web-page. To test it, you can set up a NTLM proxy with authentication as described here: https://stackoverflow.com/questions/3868291/how-to-test-a-http-client-using-ntlm-authentication

You library is working fine for direct access, however when trying to use as a proxy maybe detail in the authentication does not work out.

If I want to access a secure HTTPS website through an NTLM auth proxy. I patched the code to handle 407 error messages (HTTP/1.1 407 Proxy Authentication Required) in addition to the 401, and also change the

auth_header 'Authorization' to 'Proxy-authorization'
auth_header_field 'WWW-Authenticate' to 'proxy-authenticate'

But, then I get the following error message:


Traceback (most recent call last):
  File "C:\a1\testMySSPI.py", line 14, in <module>
    r = requests.get('https://www.google.de/', proxies=proxy_dict, auth=HttpNegotiateAuth())
  File "C:\b2\Python27\lib\site-packages\requests\api.py", line 69, in get
    return request('get', url, params=params, **kwargs)
  File "C:\b2\Python27\lib\site-packages\requests\api.py", line 50, in request
    response = session.request(method=method, url=url, **kwargs)
  File "C:\b2\Python27\lib\site-packages\requests\sessions.py", line 465, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\b2\Python27\lib\site-packages\requests\sessions.py", line 573, in send
    r = adapter.send(request, **kwargs)
  File "C:\b2\Python27\lib\site-packages\requests\adapters.py", line 424, in send
    raise ConnectionError(e, request=request)
ConnectionError: HTTPSConnectionPool(host='www.google.de', port=443):
 Max retries exceeded with url: /
 (Caused by ProxyError('Cannot connect to proxy.', 
error('Tunnel connection failed: 407 Proxy Authentication Required',)))

Unencrypted http GET works over NTLM auth proxy, but I need the https CONNECT variant.

glaukon-ariston commented 5 years ago

@railocius The problem of accessing https sites lies with requests and urllib3 libraries. Try changing the following lines and see what happens:

  <path-to>\Python37\Lib\site-packages\requests\adapters.py:
    344          scheme = urlparse(request.url).scheme
    345  
    346:         #@Glaukon
    347          #is_proxied_http_request = (proxy and scheme != 'https')
    348          is_proxied_http_request = (proxy)
    349:         #@Glaukon
    350  
    351          using_socks_proxy = False

  <path-to>\Python37\Lib\site-packages\urllib3\poolmanager.py:
    411  
    412      def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None):
    413:         #@Glaukon
    414          #if scheme == "https":
    415          #    return super(ProxyManager, self).connection_from_host(
    416          #        host, port, scheme, pool_kwargs=pool_kwargs)
    417:         #@Glaukon
    418  
    419          return super(ProxyManager, self).connection_from_host(

EDIT: I have opened an issue #1520 for this in the urllib3 project.

glaukon-ariston commented 5 years ago

This is my work-in-progress modification of requests_negotiate_sspi.py to add proxy support. It works in my environment, but only for HTTP scheme due to urllib2 issue #1520. The gist of the changes is that I have reused the code from the Px project that deals with the NTLM authentication.

import base64
import hashlib
import logging
import socket
import struct

from requests.auth import AuthBase
from requests.exceptions import HTTPError

import pywintypes
import sspi
import sspicon
import win32security

try:
    from urllib.parse import urlparse
except ImportError:
    from urlparse import urlparse

########################################################################## PX
# Print if possible
def pprint(*objs):
    try:
        print(*objs)
    except:
        pass

# Dependencies
import os
import sys
import ctypes.wintypes
import multiprocessing
import threading
import time

#try:
#    import winkerberos
#except ImportError:
#    pprint("Requires module winkerberos")
#    sys.exit()

#try:
#    import ntlm_auth.ntlm
#except ImportError:
#    pprint("Requires module ntlm-auth")
#    sys.exit()

#try:
#    import keyring
#    import keyring.backends.Windows
#
#    keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring())
#except ImportError:
#    pprint("Requires module keyring")
#    sys.exit()

#
## https://github.com/genotrance/px/blob/master/px.py
#

# Windows version
#  6.1 = Windows 7
#  6.2 = Windows 8
#  6.3 = Windows 8.1
# 10.0 = Windows 10
WIN_VERSION = float(str(sys.getwindowsversion().major) + "." + str(sys.getwindowsversion().minor))

logging.basicConfig(level=logging.DEBUG)
_logger = logging.getLogger(__name__)

def dprint(msg):
    #sys.stdout.write(
    _logger.debug(
        #multiprocessing.current_process().name + ": " +
        #threading.current_thread().name + ": " + 
        sys._getframe(1).f_code.co_name + "@" + str(int(time.time())) + ": " + msg)

###
# NTLM support

def b64decode(val):
    try:
        return base64.decodebytes(val.encode("utf-8"))
    except AttributeError:
        return base64.decodestring(val)

def b64encode(val):
    try:
        return base64.encodebytes(val.encode("utf-8"))
    except AttributeError:
        return base64.encodestring(val)

class NtlmMessageGenerator:
    # use proxy server as parameter to use the one to which connecting was successful (doesn't need to be the first of the list)
    def __init__(self, proxy_type, proxy_server_address, delegate, authInfo):
        (username, domain, password) = authInfo if authInfo else (None, None, None)
        pwd = ""
        if username:
            pwd = keyring.get_password("Px", domain + "\\" + username)

        if proxy_type in ('NEGOTIATE', 'NTLM'):
            if not pwd:
                self.ctx = sspi.ClientAuth(proxy_type, os.environ.get("USERNAME"), scflags=0)
                self.get_response = self.get_response_sspi

                # Calling sspi.ClientAuth with scflags set requires you to specify all the flags, including defaults.
                # We just want to add ISC_REQ_DELEGATE.
                if delegate:
                    self.ctx.clientauth.scflags |= sspicon.ISC_REQ_DELEGATE

                #@#Glaukon[old code]
                #@# Channel Binding Hash (aka Extended Protection for Authentication)
                #@# If this is a SSL connection, we need to hash the peer certificate, prepend the RFC5929 channel binding type,
                #@# and stuff it into a SEC_CHANNEL_BINDINGS structure.
                #@# This should be sent along in the initial handshake or Kerberos auth will fail.
                #@if hasattr(response, 'peercert') and response.peercert is not None:
                #@    md = hashlib.sha256()
                #@    md.update(response.peercert)
                #@    appdata = 'tls-server-end-point:'.encode('ASCII')+md.digest()
                #@    cbtbuf = win32security.PySecBufferType(pkg_info['MaxToken'], sspicon.SECBUFFER_CHANNEL_BINDINGS)
                #@    cbtbuf.Buffer = struct.pack('LLLLLLLL{}s'.format(len(appdata)), 0, 0, 0, 0, 0, 0, len(appdata), 32, appdata)
                #@    sec_buffer.append(cbtbuf)
            else:
                self.ctx = ntlm_auth.ntlm.NtlmContext(username, pwd, domain, "", ntlm_compatibility=3)
                self.get_response = self.get_response_ntlm
        else:
            principal = None
            if pwd:
                if domain:
                    principal = (urlparse.quote(username) + "@" +
                        urlparse.quote(domain) + ":" + urlparse.quote(pwd))
                else:
                    principal = urlparse.quote(username) + ":" + urlparse.quote(pwd)

            _, self.ctx = winkerberos.authGSSClientInit("HTTP@" + proxy_server_address,
                principal=principal, gssflags=0, mech_oid=winkerberos.GSS_MECH_OID_SPNEGO)
            self.get_response = self.get_response_wkb

    def get_response_sspi(self, challenge=None):
        dprint("pywin32 SSPI")
        if challenge:
            challenge = b64decode(challenge)
        output_buffer = None
        try:
            error_msg, output_buffer = self.ctx.authorize(challenge)
        except pywintypes.error:
            traceback.print_exc(file=sys.stdout)
            return None

        response_msg = b64encode(output_buffer[0].Buffer)
        response_msg = response_msg.decode("utf-8").replace('\012', '')
        return response_msg

    def get_response_wkb(self, challenge=""):
        dprint("winkerberos SSPI")
        try:
            winkerberos.authGSSClientStep(self.ctx, challenge)
            auth_req = winkerberos.authGSSClientResponse(self.ctx)
        except winkerberos.GSSError:
            traceback.print_exc(file=sys.stdout)
            return None

        return auth_req

    def get_response_ntlm(self, challenge=""):
        dprint("ntlm-auth")
        if challenge:
            challenge = b64decode(challenge)
        response_msg = b64encode(self.ctx.step(challenge))
        response_msg = response_msg.decode("utf-8").replace('\012', '')
        return response_msg

########################################################################## PX

class HttpNegotiateAuth(AuthBase):
    _auth_info = None
    _service = 'HTTP'
    _host = None
    _delegate = False

    def __init__(self, username=None, password=None, domain=None, service=None, host=None, delegate=False):
        """Create a new Negotiate auth handler

           Args:
            username: Username.
            password: Password.
            domain: NT Domain name.
                Default: '.' for local account.
            service: Kerberos Service type for remote Service Principal Name.
                Default: 'HTTP'
            host: Host name for Service Principal Name.
                Default: Extracted from request URI
            delegate: Indicates that the user's credentials are to be delegated to the server.
                Default: False

            If username and password are not specified, the user's default credentials are used.
            This allows for single-sign-on to domain resources if the user is currently logged on
            with a domain account.
        """
        if domain is None:
            domain = '.'

        if username is not None and password is not None:
            self._auth_info = (username, domain, password)

        if service is not None:
            self._service = service

        if host is not None:
            self._host = host

        self._delegate = delegate

    def _update_host(self, url):
        if self._host is None:
            targeturl = urlparse(url)
            self._host = targeturl.hostname
            try:
                self._host = socket.getaddrinfo(self._host, None, 0, 0, 0, socket.AI_CANONNAME)[0][3]
            except socket.gaierror as e:
                _logger.info('Skipping canonicalization of name %s due to error: %s', self._host, e)

    def _prepare_new_request(self, response):
        # Consume content and release the original connection
        # to allow our new request to reuse the same one.
        response.content
        response.raw.release_conn()
        request = response.request.copy()

        # this is important for some web applications that store
        # authentication-related info in cookies
        if response.headers.get('set-cookie'):
            request.headers['Cookie'] = response.headers.get('set-cookie')
        return request

    def _retry_using_http_Negotiate_auth(self, response, scheme, authFields, args):
        if authFields[1] in response.request.headers:
            return response
        self._update_host(response.request.url)

        content_length = int(response.request.headers.get('Content-Length', '0'), base=10)
        if hasattr(response.request.body, 'seek'):
            if content_length > 0:
                response.request.body.seek(-content_length, 1)
            else:
                response.request.body.seek(0, 0)

        # Send initial challenge auth header
        dprint('Sending Initial Context Token')
        ntlm = NtlmMessageGenerator(scheme, None, self._delegate, self._auth_info)
        ntlm_resp = ntlm.get_response()
        if ntlm_resp:
            request = self._prepare_new_request(response)
            request.headers[authFields[1]] = "%s %s" % (scheme, ntlm_resp)
        else:
            return response

        # A streaming response breaks authentication.
        # This can be fixed by not streaming this request, which is safe
        # because the returned response3 will still have stream=True set if
        # specified in args. In addition, we expect this request to give us a
        # challenge and not the real content, so the content will be short
        # anyway.
        args_nostream = dict(args, stream=False)
        response2 = response.connection.send(request, **args_nostream)

        # Should get another 401 if we are doing challenge-response (NTLM)
        if response2.status_code not in (401, 407):
            if response2.status_code == 200:
                # Kerberos may have succeeded; if so, finalize our auth context
                token = response2.headers.get(authFields[0])
                if token:
                    try:
                        # Sometimes Windows seems to forget to prepend 'Negotiate' to the success response,
                        # and we get just a bare chunk of base64 token. Not sure why.
                        token = token.replace(scheme, '', 1).lstrip()
                        dprint('Kerberos Authentication')
                        ntlm_resp = ntlm.get_response(token)
                    except TypeError:
                        pass

            # Regardless of whether or not we finalized our auth context,
            # without a 401 we've got nothing to do. Update the history and return.
            response2.history.append(response)
            return response2

        # Extract challenge message from server
        challenge = [val[len(scheme)+1:] for val in response2.headers.get(authFields[0], '').split(', ') if scheme in val]
        if len(challenge) != 1:
            raise HTTPError('Did not get exactly one {} challenge from server.'.format(scheme))
        dprint('Got Challenge Token ({}) {}'.format(scheme, challenge[0]))

        # Perform next authorization step
        dprint('Sending Response')
        ntlm_resp = ntlm.get_response(challenge[0])
        if ntlm_resp:
            request = self._prepare_new_request(response2)
            request.headers[authFields[1]] = "%s %s" % (scheme, ntlm_resp)
        else:
            return response
        response3 = response2.connection.send(request, **args)

        # Update the history and return
        response3.history.append(response)
        response3.history.append(response2)

        return response3

    def _response_hook(self, r, **kwargs):
        if r.status_code in (401, 407):
            authFields = ('WWW-Authenticate', 'Authorization') if r.status_code == 401 else ('Proxy-Authenticate', 'Proxy-Authorization')
            auth = r.headers.get(authFields[0], '').upper()
            for scheme in ('NEGOTIATE', 'NTLM', 'KERBEROS'):
                if scheme in auth:
                    return self._retry_using_http_Negotiate_auth(r, scheme, authFields, kwargs)

    def __call__(self, r):
        r.headers['Connection'] = 'Keep-Alive'
        r.headers['Proxy-Connection'] = 'Keep-Alive'
        r.register_hook('response', self._response_hook)
        return r
andrenam commented 5 years ago

I can confirm that password-less proxy authentication works for HTTP with the changes provides by @glaukon-ariston. As he said, HTTPS fails.

@glaukon-ariston Is there any (dirty?) workaround for HTTPS? Do you have any idea how to get that working?

glaukon-ariston commented 5 years ago

@andrenam I have given up on any solution that relies on urllib3. Instead, I found out that pycurl is working fine with both HTTPS and HTTP over a proxy for SSPI authentication. Below is the code that I use to initialise pycurl. Hope it helps.

pip install pycurl
#
# Pycurl-vs-Requests
# https://github.com/0xyd/Pycurl-vs-Requests
#

import pycurl
import certifi
import io
import sys
import proxy

# Python 2.x vs 3.x support
try:
    import urllib.parse as urlparse
except ImportError:
    import urlparse

#==================================================================== CURL Init

def initPyCURL(proxy_servers):
    c = pycurl.Curl()

    if len(proxy_servers) > 0:
        c.setopt(c.PROXY, proxy_servers[0][0])
        c.setopt(c.PROXYPORT, proxy_servers[0][1])
        c.setopt(c.PROXYAUTH, c.HTTPAUTH_ANY)
        # http://curl.haxx.se/mail/tracker-2010-10/0019.html
        #c.setopt(c.PROXYAUTH, c.HTTPAUTH_NTLM)
        c.setopt(c.PROXYUSERPWD, ":")
        # tunnel to get to secure site.
        c.setopt(c.HTTPPROXYTUNNEL, True)

    #CURL_setopt(c.CURLOPT_POSTREDIR, 3);
    c.setopt(c.FOLLOWLOCATION, True)
    CURLOPT_ENCODING = getattr(c, 'ACCEPT_ENCODING', c.ENCODING)
    c.setopt(CURLOPT_ENCODING, '')
    c.setopt(c.FAILONERROR, True)
    #c.setopt(c.COOKIEJAR, 'cookie.txt')
    c.setopt(c.COOKIEFILE, '')

    c.setopt(c.CAINFO, certifi.where())
    c.setopt(c.SSL_VERIFYPEER, 0)
    c.setopt(c.SSL_VERIFYHOST, 0)
    #c.setopt(c.SSL_VERIFYPEER, 1)
    #c.setopt(c.SSL_VERIFYHOST, 2)

    # Define a callback function for logging.
    def log(debug_type, debug_msg):
        print("[%d] %s" % (debug_type, debug_msg.decode('utf-8', 'backslashreplace').strip()))
    c.setopt(c.VERBOSE, True)
    c.setopt(c.DEBUGFUNCTION, log)
    return c
#==================================================================== CURL Init

url = 'https://python.org/'

pycurl.global_init(pycurl.GLOBAL_WIN32)
pycurl.global_init(pycurl.GLOBAL_SSL)
sys.stderr.write("Using %s\n" % pycurl.version)

buffer = io.BytesIO()
proxy_str = proxy.winhttp_find_proxy_for_url(url, autodetect=True)
proxy_servers = proxy.parse_proxy(proxy_str)
c = initPyCURL(proxy_servers)
c.setopt(c.URL, url)
#c.setopt(c.WRITEFUNCTION, buffer.write)
c.setopt(c.WRITEDATA, buffer)
c.perform()
c.close()

body = buffer.getvalue()
# Body is a byte string.
# We have to know the encoding in order to print it to a text file
# such as standard output.
#print(body.decode('utf-8'))
print(len(body))
#
# proxy.py 
# Taken from the Px project
# https://github.com/genotrance/px/blob/master/px.py
import sys
import ctypes
import ctypes.wintypes

# Print if possible
def pprint(*objs):
    try:
        print(*objs)
    except:
        pass

# Windows version
#  6.1 = Windows 7
#  6.2 = Windows 8
#  6.3 = Windows 8.1
# 10.0 = Windows 10
WIN_VERSION = float(str(sys.getwindowsversion().major) + "." + str(sys.getwindowsversion().minor))

###
# Proxy detection

class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG(ctypes.Structure):
    _fields_ = [("fAutoDetect", ctypes.wintypes.BOOL), # "Automatically detect settings"
                ("lpszAutoConfigUrl", ctypes.wintypes.LPWSTR), # "Use automatic configuration script, Address"
                ("lpszProxy", ctypes.wintypes.LPWSTR), # "1.2.3.4:5" if "Use the same proxy server for all protocols",
                                                       # else advanced "ftp=1.2.3.4:5;http=1.2.3.4:5;https=1.2.3.4:5;socks=1.2.3.4:5"
                ("lpszProxyBypass", ctypes.wintypes.LPWSTR), # ";"-separated list, "Bypass proxy server for local addresses" adds "<local>"
               ]

class WINHTTP_AUTOPROXY_OPTIONS(ctypes.Structure):
    _fields_ = [("dwFlags", ctypes.wintypes.DWORD),
                ("dwAutoDetectFlags", ctypes.wintypes.DWORD),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPCWSTR),
                ("lpvReserved", ctypes.c_void_p),
                ("dwReserved", ctypes.wintypes.DWORD),
                ("fAutoLogonIfChallenged", ctypes.wintypes.BOOL), ]

class WINHTTP_PROXY_INFO(ctypes.Structure):
    _fields_ = [("dwAccessType", ctypes.wintypes.DWORD),
                ("lpszProxy", ctypes.wintypes.LPCWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPCWSTR), ]

# Parameters for WinHttpOpen, http://msdn.microsoft.com/en-us/library/aa384098(VS.85).aspx
WINHTTP_NO_PROXY_NAME = 0
WINHTTP_NO_PROXY_BYPASS = 0
WINHTTP_FLAG_ASYNC = 0x10000000

# dwFlags values
WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001
WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002

# dwAutoDetectFlags values
WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002

# dwAccessType values
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0
WINHTTP_ACCESS_TYPE_NO_PROXY = 1
WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3
WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4

# Error messages
WINHTTP_ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167

def winhttp_find_proxy_for_url(url, autodetect=False, pac_url=None, autologon=True):
    # Fix issue #51
    ACCESS_TYPE = WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
    if WIN_VERSION < 6.3:
        ACCESS_TYPE = WINHTTP_ACCESS_TYPE_DEFAULT_PROXY

    ctypes.windll.winhttp.WinHttpOpen.restype = ctypes.c_void_p
    hInternet = ctypes.windll.winhttp.WinHttpOpen(
        ctypes.wintypes.LPCWSTR("Px"),
        ACCESS_TYPE, WINHTTP_NO_PROXY_NAME,
        WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC)
    if not hInternet:
        dprint("WinHttpOpen failed: " + str(ctypes.GetLastError()))
        return ""

    autoproxy_options = WINHTTP_AUTOPROXY_OPTIONS()
    if pac_url:
        autoproxy_options.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL
        autoproxy_options.dwAutoDetectFlags = 0
        autoproxy_options.lpszAutoConfigUrl = pac_url
    elif autodetect:
        autoproxy_options.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT
        autoproxy_options.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
        autoproxy_options.lpszAutoConfigUrl = 0
    else:
        return ""
    autoproxy_options.fAutoLogonIfChallenged = autologon

    proxy_info = WINHTTP_PROXY_INFO()

    # Fix issue #43
    ctypes.windll.winhttp.WinHttpGetProxyForUrl.argtypes = [ctypes.c_void_p,
        ctypes.wintypes.LPCWSTR, ctypes.POINTER(WINHTTP_AUTOPROXY_OPTIONS),
        ctypes.POINTER(WINHTTP_PROXY_INFO)]
    ok = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hInternet, ctypes.wintypes.LPCWSTR(url),
            ctypes.byref(autoproxy_options), ctypes.byref(proxy_info))
    if not ok:
        error = ctypes.GetLastError()
        dprint("WinHttpGetProxyForUrl error %s" % error)
        if error == WINHTTP_ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT:
            dprint("Could not download PAC file, trying DIRECT instead")
            return "DIRECT"
        return ""

    if proxy_info.dwAccessType == WINHTTP_ACCESS_TYPE_NAMED_PROXY:
        # Note: proxy_info.lpszProxyBypass makes no sense here!
        if not proxy_info.lpszProxy:
            dprint('WinHttpGetProxyForUrl named proxy without name')
            return ""
        return proxy_info.lpszProxy.replace(" ", ",").replace(";", ",").replace(",DIRECT", "") # Note: We only see the first!
    if proxy_info.dwAccessType == WINHTTP_ACCESS_TYPE_NO_PROXY:
        return "DIRECT"

    # WinHttpCloseHandle()
    dprint("WinHttpGetProxyForUrl accesstype %s" % (proxy_info.dwAccessType,))
    return ""

def parse_proxy(proxystrs):
    if not proxystrs:
        return []

    servers = []
    for proxystr in [i.strip() for i in proxystrs.split(",")]:
        pserver = [i.strip() for i in proxystr.split(":")]
        if len(pserver) == 1 and pserver != 'DIRECT':
            pserver.append(80)
        elif len(pserver) == 2:
            try:
                pserver[1] = int(pserver[1])
            except ValueError:
                pprint("Bad proxy server port: " + pserver[1])
                sys.exit()
        else:
            pprint("Bad proxy server definition: " + proxystr)
            sys.exit()

        if tuple(pserver) not in servers:
            servers.append(tuple(pserver))

    return servers
andrenam commented 5 years ago

@glaukon-ariston Thank you :+1: I'll try it next week in our Windows corporate environment. Do I need some special binary or curl version with sspi on windows? Or just install pycurl via pip and I'm good to go?

glaukon-ariston commented 5 years ago

In my environment I could not do direct pip install pycurl, so I downloaded the appropriate pycurl's whl and installed it locally with pip install pycurl-7.43.1-cp37-cp37m-win_amd64.whl. Repeat the process with any dependent libraries if pip barks at you.

andrenam commented 5 years ago

@glaukon-ariston thanks, your code is working fine. :+1:
Only thing that didnt work was auto-discovery of the proxy: proxy_str = proxy.winhttp_find_proxy_for_url(url, autodetect=True) Setting this manually, the rest works fine.

Searching the web, I also found pywebcore. Do you know this - might be worth a try? I didnt get it running though, see https://github.com/benjimin/pywebcorp/issues/2

OmriBaso commented 3 years ago

@glaukon-ariston @andrenam Hey, have you guys tried maybe using Requests libary or python3.8 64bit for HTTPS? also - curl now supports sspi itself.. have you triedi it?

eissko commented 3 years ago

@J3wker which curl you exactly mean? can u pls send link to py module? thank you

OmriBaso commented 3 years ago

@J3wker which curl you exactly mean? can u pls send link to py module? thank you

I've checked it, it does not work bro... have you guys solved the issue with SSL using requests? maybe force authentication on every packet?

OmriBaso commented 3 years ago

@J3wker which curl you exactly mean? can u pls send link to py module? thank you

@andrenam @eissko Hey, I managed to get PyCurl to get this working !

I will be writing a library to replace requests using PyCurl in order to use SSPI proxies and etc. Follow me on github i will post in the weekend to come. right now using PyCurl is really a pain to be honest..

OmriBaso commented 3 years ago

As promised SSPI support to proxies (HTTP and HTTPs)- automatic PAC parsing and manual proxy detection / or DIRECT access to the internet:

here is my git repo: https://github.com/J3wker/besoCurl

Easy to use!

prusswan commented 3 months ago

@glaukon-ariston's examples were very useful, but even with urllib3/urllib3#1520 I could not find a solution to the HTTPS tunneling issue with requests-negotiate-sspi. Fortunately, I found this from request-kerberos which worked for my case (http proxy used for https scheme/endpoints, only empty challenge needed for a token)