NiceLeeのBlog 用爱发电 bilibili~

Python 一种基于SNI的HTTPS代理

2021-03-19
nIceLee

阅读:


听人说起这个idea,感觉挺有意思(虽然没有什么大的用处)

前言

  • 一般情况下,我们的HTTPS代理是给中间服务器发送一条CONNECT消息,然后该干嘛干嘛。
  • 有那么一种情况,我将要访问的站点域名的ip直接指向中间服务器,这时,中间服务器如何处理才能让我正常访问呢?
    • 读取Client Hello消息中的SNI
    • 根据SNI建立与目标站点的连接,并转发Client Hello消息
    • 做水管工,传输客户端与服务器的数据
    • 计划通√

示例代码

  • 本地测试访问www.baidu.com,在host里面添加了记录指向本地,即:
    127.0.0.1 www.baidu.com
  • 由于是本机测试,为了防止死循环建立,设置了一个DNS表,存放了百度的实际地址。
  • 结果如图:

sni_proxy.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import socket, threading
import sni_helper

TIME_OUT_ERR = socket.timeout
def recv(socket, bufferSize=1024):
    try:
        return socket.recv(bufferSize)
    except TIME_OUT_ERR: # 设置超时时间后, socket.timeout变成数值
        if not stop:
            return recv(socket, bufferSize)
            
def socket_handler(clientSock, addr):
    clientSock.settimeout(5)
    data = recv(clientSock)
    if not data:
        return
    sni = sni_helper.GetSniFromSslPlainText(data)
    if not data:
        print('sni not found')
        return
    print('Accept new connection from %s:%s...' % addr)
    #print('Establishing new connection to %s' % sni)
    
    try:
        serverSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        #serverSock.connect((sni, 443))
        serverSock.connect((getHost(sni), 443))
        serverSock.send(data)
        serverSock.settimeout(5)
        
        t1 = threading.Thread(target=clientToServer, args=(clientSock, serverSock, sni), name='thread-%s-toServer'%sni)
        t2 = threading.Thread(target=serverToClient, args=(clientSock, serverSock, sni), name='thread-%s-toClient'%sni)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
    except:
        pass
    finally:
        clientSock.close()
        serverSock.close()

def getHost(sni):
    host = hosts.get(sni, sni)
    return host
def clientToServer(clientSock, serverSock, sni):
    try:
        #data = clientSock.recv(1024)
        data = recv(clientSock)
        while data and not stop:
            serverSock.send(data)
            data = recv(clientSock)
    except:
        pass
    finally:
        print('%s clientToServer 结束'%sni)
        clientSock.close()
        serverSock.close()
def serverToClient(clientSock, serverSock, sni):
    try:
        data = recv(serverSock)
        while data and not stop:
            clientSock.send(data)
            data = recv(serverSock)
    except:
        pass
    finally:
        print('%s serverToClient 结束'%sni)
        clientSock.close()
        serverSock.close()
def startServer(port: int = 443, maxLink = 5):
    # 创建一个socket:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0', port))
    s.listen(maxLink)
    s.settimeout(5.0)
    print('Waiting for connection...')
    
    while not stop:
        try:
            sock, addr = s.accept()
            # 创建新线程来处理TCP连接:
            t = threading.Thread(target=socket_handler, args=(sock, addr), name='thread-dealSocket %s:%s'%addr)
            t.start()
        except socket.timeout as e:
            pass


stop = False
hosts = {
    "www.baidu.com":"14.215.177.38",
}
if __name__ == '__main__':
    threadServer = threading.Thread(target=startServer, args=(443, 5), name='thread-startServer')
    threadServer.start()

    try:
        input('输入任何按键以停止服务\r\n')
    finally:
        stop = True

sni_helper.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import struct

ProtocolVersionSize = 2
RandomSize = 32

def GetSniFromSslPlainText(sslPlainText: bytes):
    '''
    https://tools.ietf.org/html/rfc6101#section-5.2.1
    struct {
        ContentType type;
        ProtocolVersion version;
        uint16 length;
        opaque fragment[SSLPlaintext.length];
    } SSLPlaintext;
    '''
    ContentTypeOffset = 0
    ProtocolVersionOffset = ContentTypeOffset + 1
    LengthOffset = ProtocolVersionOffset + ProtocolVersionSize
    HandshakeOffset = LengthOffset + 2;
    # SSL v2's ContentType has 0x80 bit set.
    # We do not care about SSL v2 here because it does not support client hello extensions
    if len(sslPlainText) < HandshakeOffset or sslPlainText[ContentTypeOffset] != 0x16 : #Handshake
        print('not SSLv3')
        return None
        
    handshakeLength = struct.unpack_from('>H' ,sslPlainText, LengthOffset)[0]
    sslHandshake = sslPlainText[HandshakeOffset:]
    if handshakeLength != len(sslHandshake):
        print('handshakeLength is not right')
        return None
    return GetSniFromSslHandshake(sslHandshake);

def GetSniFromSslHandshake(sslHandshake: bytes):
    '''
    https://tools.ietf.org/html/rfc6101#section-5.6
    struct {
        HandshakeType msg_type;    /* handshake type */
        uint24 length;             /* bytes in message */
        select (HandshakeType) {
            ...
            case client_hello: ClientHello;
            ...
        } body;
    } Handshake;
    '''
    HandshakeTypeOffset = 0;
    ClientHelloLengthOffset = HandshakeTypeOffset + 1;
    ClientHelloOffset = ClientHelloLengthOffset + 3;
    if len(sslHandshake) < ClientHelloOffset or sslHandshake[HandshakeTypeOffset] != 0x01: #ClientHello
        print('not a ClientHello msg')
        return None
    #clientHelloLength = struct.unpack_from('>?????' ,sslHandshake, ClientHelloLengthOffset)[0]
    clientHelloLength = ReadUInt24BigEndian(sslHandshake, ClientHelloLengthOffset)
    clientHello = sslHandshake[ClientHelloOffset:]
    if clientHelloLength != len(clientHello):
        print('clientHelloLength is not right')
        return None

    return GetSniFromClientHello(clientHello);

    
def SkipBytes(data: bytes, numberOfBytesToSkip:int):
        if data and numberOfBytesToSkip < len(data):
            return data[numberOfBytesToSkip:]
        
    
def SkipOpaqueType1(data: bytes):
    '''
    Opaque type is of structure:
      - length (minimum number of bytes to hold the max value)
      - data (length bytes)
    We will only use opaque types which are of max size: 255 (length = 1) or 2^16-1 (length = 2).
    We will call them SkipOpaqueType`length`
    '''
    if data:
        length = data[0]
        totalBytes = 1 + length
        return SkipBytes(data, totalBytes)

def SkipOpaqueType2(data: bytes):
    if data:
        length = struct.unpack_from('>H' ,data, 0)[0]
        totalBytes = 2 + length
        valid = len(data) >= totalBytes;
        if valid:
            return data[totalBytes:]
            
def GetSniFromClientHello(clientHello: bytes):
    '''
    Basic structure: https://tools.ietf.org/html/rfc6101#section-5.6.1.2
    Extended structure: https://tools.ietf.org/html/rfc3546#section-2.1
    struct {
        ProtocolVersion client_version; 2x uint8
        Random random; 32 bytes
        SessionID session_id; opaque type
        CipherSuite cipher_suites<2..2^16-1>; opaque type
        CompressionMethod compression_methods<1..2^8-1>; opaque type
        Extension client_hello_extension_list<0..2^16-1>;
    } ClientHello;
    '''
    p = SkipBytes(clientHello, ProtocolVersionSize + RandomSize);
    # Skip SessionID (max size 32 => size fits in 1 byte)
    p = SkipOpaqueType1(p);
    # Skip cipher suites (max size 2^16-1 => size fits in 2 bytes)
    p = SkipOpaqueType2(p);
    # Skip compression methods (max size 2^8-1 => size fits in 1 byte)
    p = SkipOpaqueType1(p);
    # is invalid structure or no extensions?
    if not p:
        print('invalid structure or no extensions')
        return None

    # client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes)
    extensionListLength = struct.unpack_from('>H' ,p, 0)[0]
    p = SkipBytes(p, 2);
    if extensionListLength != len(p):
        print('extensionListLength is not right: (%d, %d)'%(extensionListLength, len(p)))
        return None
    while p:
        sni, p, invalid = GetSniFromExtension(p)
        if sni:
            return sni
        if invalid:
            return None
    print('sni not found util end')

def GetSniFromExtension(extension: bytes):
    '''
    https://tools.ietf.org/html/rfc3546#section-2.3
    struct {
        ExtensionType extension_type;
        opaque extension_data<0..2^16-1>;
    } Extension;
    '''
    ExtensionDataOffset = 2 # ushort sizeof(ExtensionType);
    if len(extension) < ExtensionDataOffset:
        return None, None, True
    extensionType = struct.unpack_from('>H' , extension, 0)[0]
    extensionData = extension[ExtensionDataOffset:]
    if extensionType == 0x00: #ExtensionType.ServerName
        sni = GetSniFromServerNameList(extensionData)
        return sni, None, sni == None;
    else:
        remainingBytes = SkipOpaqueType2(extensionData);
        return None, remainingBytes, remainingBytes == None

def GetSniFromServerNameList(serverNameListExtension: bytes):
    '''
    https://tools.ietf.org/html/rfc3546#section-3.1
    struct {
        ServerName server_name_list<1..2^16-1>
    } ServerNameList;
    ServerNameList is an opaque type (length of sufficient size for max data length is prepended)
    '''
    ServerNameListOffset = 2 #sizeof(ushort);
    if len(serverNameListExtension) < 2: # 
        print('invalid serverNameListExtension length')
        return None
    serverNameListLength = struct.unpack_from('>H' , serverNameListExtension, 0)[0]
    serverNameList = serverNameListExtension[ServerNameListOffset:]
    if serverNameListLength > len(serverNameList):
        print('serverNameListExtension length is not right')
        return None

    #remainingBytes = serverNameList.Slice(serverNameListLength);
    serverName = serverNameList[:serverNameListLength]

    return GetSniFromServerName(serverName);

def GetSniFromServerName(serverName: bytes):
    '''
    https://tools.ietf.org/html/rfc3546#section-3.1
    struct {
        NameType name_type;
        select (name_type) {
            case host_name: HostName;
        } name;
    } ServerName;
    ServerName is an opaque type (length of sufficient size for max data length is prepended)
    '''
    ServerNameLengthOffset = 0;
    NameTypeOffset = ServerNameLengthOffset + 2;
    HostNameStructOffset = NameTypeOffset + 1; # HostName = 0x00

    if len(serverName) < HostNameStructOffset:
        print('serverName len is not right')
        return None

    hostNameStructLength = struct.unpack_from('>H' , serverName, 0)[0]  -1
    nameType = serverName[NameTypeOffset]
    hostNameStruct = serverName[HostNameStructOffset:]
    if hostNameStructLength != len(hostNameStruct) or nameType != 0x00:
        print('nameType is not Hostname')
        return None
    return GetSniFromHostNameStruct(hostNameStruct);

    
def GetSniFromHostNameStruct(hostNameStruct: bytes):
    '''
    https://tools.ietf.org/html/rfc3546#section-3.1
    HostName is an opaque type (length of sufficient size for max data length is prepended)
    '''
    HostNameLengthOffset = 0
    HostNameOffset = HostNameLengthOffset + 2 #sizeof(ushort);

    hostNameLength = struct.unpack_from('>H' , hostNameStruct, 0)[0]
    hostName = hostNameStruct[HostNameOffset:]
    if hostNameLength != len(hostName):
        print('hostName len is not valid')
        return None
    return hostName.decode("utf-8")


def ReadUInt24BigEndian(data: bytes, offset: int):
    return (data[offset] << 16) | (data[offset + 1] << 8) | data[offset + 2]

内容
隐藏