ตรวจสอบใบรับรอง SSL ด้วย Python


85

ฉันจำเป็นต้องเขียนสคริปต์ที่เชื่อมต่อกับไซต์จำนวนมากบนอินทราเน็ตขององค์กรของเราผ่าน HTTPS และตรวจสอบว่าใบรับรอง SSL นั้นถูกต้อง พวกเขายังไม่หมดอายุพวกเขาจะออกให้สำหรับที่อยู่ที่ถูกต้อง ฯลฯ เราใช้ผู้ออกใบรับรองภายในองค์กรของเราเองสำหรับไซต์เหล่านี้ดังนั้นเราจึงมีคีย์สาธารณะของ CA เพื่อตรวจสอบใบรับรองกับ

โดยค่าเริ่มต้น Python จะยอมรับและใช้ใบรับรอง SSL เมื่อใช้ HTTPS ดังนั้นแม้ว่าใบรับรองจะไม่ถูกต้อง แต่ไลบรารี Python เช่น urllib2 และ Twisted จะใช้ใบรับรองอย่างมีความสุข

มีไลบรารีดีๆสักแห่งที่ให้ฉันเชื่อมต่อกับไซต์ผ่าน HTTPS และยืนยันใบรับรองด้วยวิธีนี้หรือไม่

ฉันจะตรวจสอบใบรับรองใน Python ได้อย่างไร


10
ความคิดเห็นของคุณเกี่ยวกับ Twisted ไม่ถูกต้อง: Twisted ใช้ pyopenssl ไม่ใช่การสนับสนุน SSL ในตัวของ Python แม้ว่าจะไม่ตรวจสอบใบรับรอง HTTPS ตามค่าเริ่มต้นในไคลเอนต์ HTTP แต่คุณสามารถใช้อาร์กิวเมนต์ "contextFactory" เพื่อ getPage และ downloadPage เพื่อสร้างโรงงานบริบทที่ตรวจสอบความถูกต้อง ในทางตรงกันข้ามกับความรู้ของฉันไม่มีทางที่โมดูล "ssl" ในตัวจะมั่นใจได้ว่าจะทำการตรวจสอบใบรับรอง
Glyph

4
ด้วยโมดูล SSL ใน Python 2.6 และใหม่กว่าคุณสามารถเขียนโปรแกรมตรวจสอบใบรับรองของคุณเองได้ ไม่เหมาะสม แต่ทำได้
Heikki Toivonen

3
สถานการณ์เปลี่ยนไปโดยค่าเริ่มต้น Python จะตรวจสอบใบรับรอง ฉันได้เพิ่มคำตอบใหม่ด้านล่าง
Dr. Jan-Philip Gehrcke

สถานการณ์ก็เปลี่ยนไปสำหรับ Twisted (ค่อนข้างก่อนที่จะทำกับ Python ในความเป็นจริง); หากคุณใช้treqหรือtwisted.web.client.Agentตั้งแต่เวอร์ชัน 14.0 Twisted จะยืนยันใบรับรองตามค่าเริ่มต้น
Glyph

คำตอบ:


19

ตั้งแต่รุ่น 2.7.9 / 3.4.3 บน Python โดยค่าเริ่มต้นจะพยายามดำเนินการตรวจสอบใบรับรอง

สิ่งนี้ได้รับการเสนอใน PEP 467 ซึ่งควรค่าแก่การอ่าน: https://www.python.org/dev/peps/pep-0476/

การเปลี่ยนแปลงมีผลต่อโมดูล stdlib ที่เกี่ยวข้องทั้งหมด (urllib / urllib2, http, httplib)

เอกสารที่เกี่ยวข้อง:

https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection

ขณะนี้คลาสนี้ทำการตรวจสอบใบรับรองและชื่อโฮสต์ที่จำเป็นทั้งหมดตามค่าเริ่มต้น หากต้องการเปลี่ยนกลับไปเป็นพฤติกรรมก่อนหน้าที่ไม่ได้รับการตรวจสอบ ssl._create_unverified_context () สามารถส่งผ่านไปยังพารามิเตอร์บริบท

https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection

เปลี่ยนแปลงในเวอร์ชัน 3.4.3: ขณะนี้คลาสนี้ทำการตรวจสอบใบรับรองและชื่อโฮสต์ที่จำเป็นทั้งหมดตามค่าเริ่มต้น หากต้องการเปลี่ยนกลับไปเป็นพฤติกรรมก่อนหน้าที่ไม่ได้รับการตรวจสอบ ssl._create_unverified_context () สามารถส่งผ่านไปยังพารามิเตอร์บริบท

โปรดทราบว่าการตรวจสอบในตัวใหม่จะขึ้นอยู่กับฐานข้อมูลใบรับรองที่ระบบให้มา ในทางตรงกันข้ามแพ็คเกจคำขอจะจัดส่งชุดใบรับรองของตัวเอง ข้อดีและข้อเสียของทั้งสองวิธีที่จะกล่าวถึงในฐานข้อมูลความน่าเชื่อถือส่วน PEP 476


โซลูชันใด ๆ เพื่อให้แน่ใจว่ามีการตรวจสอบใบรับรองสำหรับ python เวอร์ชันก่อนหน้าหรือไม่ ไม่สามารถอัปเกรดเวอร์ชันของ python ได้เสมอไป
vaab

ไม่ได้ตรวจสอบใบรับรองที่ถูกเพิกถอน เช่น revoked.badssl.com
Raz

จำเป็นต้องใช้HTTPSConnectionชั้นเรียนหรือไม่? ฉันใช้SSLSocket. ฉันจะตรวจสอบความถูกต้องได้SSLSocketอย่างไร? ฉันต้องตรวจสอบอย่างชัดเจนโดยใช้pyopensslตามที่อธิบายไว้ที่นี่หรือไม่?
anir

31

ฉันได้เพิ่มการแจกจ่ายไปยัง Python Package Index ซึ่งทำให้match_hostname()ฟังก์ชันจากsslแพ็คเกจPython 3.2 พร้อมใช้งานใน Python เวอร์ชันก่อนหน้า

http://pypi.python.org/pypi/backports.ssl_match_hostname/

คุณสามารถติดตั้งได้ด้วย:

pip install backports.ssl_match_hostname

หรือคุณสามารถทำให้การพึ่งพาที่ระบุไว้ในโครงการของsetup.pyคุณ ไม่ว่าจะด้วยวิธีใดก็สามารถใช้ได้ดังนี้:

from backports.ssl_match_hostname import match_hostname, CertificateError
...
sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3,
                      cert_reqs=ssl.CERT_REQUIRED, ca_certs=...)
try:
    match_hostname(sslsock.getpeercert(), hostname)
except CertificateError, ce:
    ...

1
ฉันขาดอะไรไป ... โปรดกรอกข้อมูลในช่องว่างด้านบนหรือให้ตัวอย่างที่สมบูรณ์ (สำหรับไซต์เช่น Google)
smholloway

ตัวอย่างจะมีลักษณะแตกต่างกันไปขึ้นอยู่กับไลบรารีที่คุณใช้เพื่อเข้าถึง Google เนื่องจากไลบรารีที่แตกต่างกันวางซ็อกเก็ต SSL ต่างกันและเป็นซ็อกเก็ต SSL ที่ต้องการgetpeercert()วิธีการที่เรียกว่าเอาต์พุตจึงสามารถส่งผ่านไปยังmatch_hostname()ได้
Brandon Rhodes

12
ฉันอายแทน Python ที่ใคร ๆ ก็ต้องใช้สิ่งนี้ ไลบรารี SSL HTTPS ในตัวของ Python ที่ไม่ตรวจสอบใบรับรองนอกกรอบโดยค่าเริ่มต้นถือเป็นเรื่องบ้าบออย่างสมบูรณ์และเป็นเรื่องที่น่าเจ็บปวดที่ต้องจินตนาการว่าระบบที่ไม่ปลอดภัยจำนวนเท่าใดในตอนนี้
Glenn Maynard


26

คุณสามารถใช้ Twisted เพื่อตรวจสอบใบรับรอง API หลักคือCertificateOptionsซึ่งสามารถให้เป็นcontextFactoryอาร์กิวเมนต์สำหรับฟังก์ชันต่างๆเช่นlistenSSLและstartTLS STARTTLS

น่าเสียดายที่ทั้ง Python และ Twisted ไม่ได้มาพร้อมกับใบรับรอง CA จำนวนมากที่จำเป็นสำหรับการตรวจสอบความถูกต้องของ HTTPS หรือตรรกะการตรวจสอบ HTTPS เนื่องจากข้อ จำกัด ใน PyOpenSSLคุณจึงไม่สามารถดำเนินการได้อย่างถูกต้องทั้งหมด แต่เนื่องจากใบรับรองเกือบทั้งหมดมีชื่อเรื่อง CommonName คุณจึงสามารถเข้าใกล้ได้มากพอ

นี่คือการใช้งานตัวอย่างไร้เดียงสาของไคลเอนต์ Twisted HTTPS ที่ยืนยันซึ่งละเว้นอักขระตัวแทนและส่วนขยาย subjectAltName และใช้ใบรับรองผู้ออกใบรับรองที่มีอยู่ในแพ็คเกจ 'ca-certificate' ใน Ubuntu ส่วนใหญ่ ลองใช้กับไซต์ใบรับรองที่คุณชื่นชอบและไม่ถูกต้อง :)

import os
import glob
from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, OP_NO_SSLv2
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
from twisted.python.urlpath import URLPath
from twisted.internet.ssl import ContextFactory
from twisted.internet import reactor
from twisted.web.client import getPage
certificateAuthorityMap = {}
for certFileName in glob.glob("/etc/ssl/certs/*.pem"):
    # There might be some dead symlinks in there, so let's make sure it's real.
    if os.path.exists(certFileName):
        data = open(certFileName).read()
        x509 = load_certificate(FILETYPE_PEM, data)
        digest = x509.digest('sha1')
        # Now, de-duplicate in case the same cert has multiple names.
        certificateAuthorityMap[digest] = x509
class HTTPSVerifyingContextFactory(ContextFactory):
    def __init__(self, hostname):
        self.hostname = hostname
    isClient = True
    def getContext(self):
        ctx = Context(TLSv1_METHOD)
        store = ctx.get_cert_store()
        for value in certificateAuthorityMap.values():
            store.add_cert(value)
        ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
        ctx.set_options(OP_NO_SSLv2)
        return ctx
    def verifyHostname(self, connection, x509, errno, depth, preverifyOK):
        if preverifyOK:
            if self.hostname != x509.get_subject().commonName:
                return False
        return preverifyOK
def secureGet(url):
    return getPage(url, HTTPSVerifyingContextFactory(URLPath.fromString(url).netloc))
def done(result):
    print 'Done!', len(result)
secureGet("https://google.com/").addCallback(done)
reactor.run()

คุณสามารถทำให้มันไม่ปิดกั้นได้หรือไม่?
ฌอนไรลีย์

ขอบคุณ; ตอนนี้ฉันมีข้อสังเกตว่าฉันได้อ่านและเข้าใจสิ่งนี้แล้ว: ตรวจสอบว่าการโทรกลับควรส่งคืน True เมื่อไม่มีข้อผิดพลาดและเป็นเท็จเมื่อมี โดยทั่วไปรหัสของคุณจะส่งกลับข้อผิดพลาดเมื่อ commonName ไม่ใช่ localhost ฉันไม่แน่ใจว่านั่นเป็นสิ่งที่คุณตั้งใจไว้หรือเปล่าแม้ว่าในบางกรณีมันจะสมเหตุสมผล ฉันเพิ่งคิดว่าฉันจะแสดงความคิดเห็นเกี่ยวกับเรื่องนี้เพื่อประโยชน์ของผู้อ่านคำตอบนี้ในอนาคต
Eli Courtwright

"self.hostname" ในกรณีนั้นไม่ใช่ "localhost"; หมายเหตุURLPath(url).netloc: นั่นหมายถึงส่วนโฮสต์ของ URL ที่ส่งไปยัง secureGet กล่าวอีกนัยหนึ่งก็คือการตรวจสอบว่า commonName ของเรื่องนั้นเหมือนกับชื่อที่ผู้โทรร้องขอ
Glyph

ฉันใช้รหัสทดสอบรุ่นนี้และใช้ Firefox, wget และ Chrome เพื่อเข้าสู่เซิร์ฟเวอร์ HTTPS ทดสอบ ในการทดสอบของฉันทำงานฉันพบว่ามีการเรียกการยืนยันชื่อโฮสต์กลับมา 3-4 ครั้งทุกครั้งที่เชื่อมต่อ ทำไมมันไม่วิ่งแค่ครั้งเดียว
themaestro

2
URLPath (blah) .netloc เป็น localhost เสมอ: URLPath .__ init__ ใช้ส่วนประกอบ URL แต่ละรายการคุณกำลังส่ง URL ทั้งหมดเป็น "แบบแผน" และรับค่า netloc เริ่มต้นของ 'localhost' เพื่อใช้ร่วมกับมัน คุณอาจต้องการใช้ URLPath.fromString (url) .netloc ขออภัยที่ทำให้การตรวจสอบใน VerifyHostName ย้อนกลับ: มันเริ่มปฏิเสธhttps://www.google.com/เนื่องจากหนึ่งในหัวเรื่องคือ 'www.google.com' ทำให้ฟังก์ชันส่งคืน False อาจหมายถึงการส่งคืน True (ยอมรับ) หากชื่อตรงกันและ False ถ้าไม่?
mzz

25

PycURLทำสิ่งนี้ได้อย่างสวยงาม

ด้านล่างนี้เป็นตัวอย่างสั้น ๆ มันจะโยนpycurl.errorถ้ามีบางอย่างที่คาวซึ่งคุณจะได้รับทูเปิลพร้อมรหัสข้อผิดพลาดและข้อความที่มนุษย์อ่านได้

import pycurl

curl = pycurl.Curl()
curl.setopt(pycurl.CAINFO, "myFineCA.crt")
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.URL, "https://internal.stuff/")

curl.perform()

คุณอาจต้องการกำหนดค่าตัวเลือกเพิ่มเติมเช่นตำแหน่งที่เก็บผลลัพธ์เป็นต้น แต่ไม่จำเป็นต้องถ่วงตัวอย่างด้วยสิ่งที่ไม่จำเป็น

ตัวอย่างข้อยกเว้นที่อาจเกิดขึ้น:

(60, 'Peer certificate cannot be authenticated with known CA certificates')
(51, "common name 'CN=something.else.stuff,O=Example Corp,C=SE' does not match 'internal.stuff'")

ลิงก์บางลิงก์ที่ฉันพบว่ามีประโยชน์คือ libcurl-docs สำหรับ setopt และ getinfo


15

หรือทำให้ชีวิตของคุณง่ายขึ้นโดยใช้ไลบรารีคำขอ :

import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)

อีกสองสามคำเกี่ยวกับการใช้งาน


10
certอาร์กิวเมนต์เป็นใบรับรองฝั่งไคลเอ็นต์ไม่ใบรับรองเซิร์ฟเวอร์เพื่อตรวจสอบกับ คุณต้องการใช้verifyอาร์กิวเมนต์
Paŭlo Ebermann

2
ขอให้ตรวจสอบโดยค่าเริ่มต้น ไม่จำเป็นต้องใช้verifyอาร์กิวเมนต์ยกเว้นว่ามีความชัดเจนมากขึ้นหรือปิดใช้งานการยืนยัน
Dr.Jan-Philip Gehrcke

1
ไม่ใช่โมดูลภายใน คุณต้องเรียกใช้คำขอติดตั้ง pip
Robert Townley

14

นี่คือตัวอย่างสคริปต์ที่แสดงการตรวจสอบใบรับรอง:

import httplib
import re
import socket
import sys
import urllib2
import ssl

class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
    def __init__(self, host, cert, reason):
        httplib.HTTPException.__init__(self)
        self.host = host
        self.cert = cert
        self.reason = reason

    def __str__(self):
        return ('Host %s returned an invalid certificate (%s) %s\n' %
                (self.host, self.reason, self.cert))

class CertValidatingHTTPSConnection(httplib.HTTPConnection):
    default_port = httplib.HTTPS_PORT

    def __init__(self, host, port=None, key_file=None, cert_file=None,
                             ca_certs=None, strict=None, **kwargs):
        httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs)
        self.key_file = key_file
        self.cert_file = cert_file
        self.ca_certs = ca_certs
        if self.ca_certs:
            self.cert_reqs = ssl.CERT_REQUIRED
        else:
            self.cert_reqs = ssl.CERT_NONE

    def _GetValidHostsForCert(self, cert):
        if 'subjectAltName' in cert:
            return [x[1] for x in cert['subjectAltName']
                         if x[0].lower() == 'dns']
        else:
            return [x[0][1] for x in cert['subject']
                            if x[0][0].lower() == 'commonname']

    def _ValidateCertificateHostname(self, cert, hostname):
        hosts = self._GetValidHostsForCert(cert)
        for host in hosts:
            host_re = host.replace('.', '\.').replace('*', '[^.]*')
            if re.search('^%s$' % (host_re,), hostname, re.I):
                return True
        return False

    def connect(self):
        sock = socket.create_connection((self.host, self.port))
        self.sock = ssl.wrap_socket(sock, keyfile=self.key_file,
                                          certfile=self.cert_file,
                                          cert_reqs=self.cert_reqs,
                                          ca_certs=self.ca_certs)
        if self.cert_reqs & ssl.CERT_REQUIRED:
            cert = self.sock.getpeercert()
            hostname = self.host.split(':', 0)[0]
            if not self._ValidateCertificateHostname(cert, hostname):
                raise InvalidCertificateException(hostname, cert,
                                                  'hostname mismatch')


class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
    def __init__(self, **kwargs):
        urllib2.AbstractHTTPHandler.__init__(self)
        self._connection_args = kwargs

    def https_open(self, req):
        def http_class_wrapper(host, **kwargs):
            full_kwargs = dict(self._connection_args)
            full_kwargs.update(kwargs)
            return CertValidatingHTTPSConnection(host, **full_kwargs)

        try:
            return self.do_open(http_class_wrapper, req)
        except urllib2.URLError, e:
            if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
                raise InvalidCertificateException(req.host, '',
                                                  e.reason.args[1])
            raise

    https_request = urllib2.HTTPSHandler.do_request_

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print "usage: python %s CA_CERT URL" % sys.argv[0]
        exit(2)

    handler = VerifiedHTTPSHandler(ca_certs = sys.argv[1])
    opener = urllib2.build_opener(handler)
    print opener.open(sys.argv[2]).read()

@tonfa: จับดี; ฉันลงเอยด้วยการเพิ่มการตรวจสอบชื่อโฮสต์ด้วยและฉันได้แก้ไขคำตอบของฉันเพื่อรวมรหัสที่ฉันใช้
Eli Courtwright

ฉันไม่สามารถเข้าถึงลิงก์เดิมได้ (เช่น 'หน้านี้') มันย้ายไปแล้ว?
Matt Ball

@Matt: ฉันเดาว่าอย่างนั้น แต่ FWIW ลิงก์ต้นฉบับไม่จำเป็นเนื่องจากโปรแกรมทดสอบของฉันเป็นตัวอย่างการทำงานที่สมบูรณ์และมีอยู่ในตัว ฉันเชื่อมโยงไปยังหน้าที่ช่วยฉันเขียนโค้ดนั้นเนื่องจากดูเหมือนว่าเป็นสิ่งที่ดีในการระบุแหล่งที่มา แต่เนื่องจากไม่มีอยู่แล้วฉันจะแก้ไขโพสต์ของฉันเพื่อลบลิงก์ขอบคุณที่ชี้ให้เห็น
Eli Courtwright

นี้ไม่ได้ทำงานกับขนย้ายวัสดุอื่น ๆ CertValidatingHTTPSConnection.connectเช่นรถขนพร็อกซี่เพราะการเชื่อมต่อซ็อกเก็ตด้วยตนเองใน ดูรายละเอียดคำขอดึงนี้ (และการแก้ไข)
schlamar

2
นี่คือวิธีการทำความสะอาดและใช้งานได้กับbackports.ssl_match_hostname.
schlamar

8

M2Cryptoสามารถทำการตรวจสอบ คุณยังสามารถใช้M2Crypto กับ Twisted ได้หากต้องการ ไคลเอ็นต์เดสก์ท็อปของแชนด์เลอร์ใช้ Twisted สำหรับระบบเครือข่ายและ M2Crypto สำหรับ SSLรวมถึงการตรวจสอบใบรับรอง

จากความคิดเห็นของ Glyphs ดูเหมือนว่า M2Crypto จะตรวจสอบใบรับรองโดยค่าเริ่มต้นได้ดีกว่าสิ่งที่คุณสามารถทำได้กับ pyOpenSSL ในปัจจุบันเนื่องจาก M2Crypto ตรวจสอบฟิลด์ subjectAltName ด้วย

ฉันยังเขียนบล็อกเกี่ยวกับวิธีรับใบรับรองที่ Mozilla Firefox มาพร้อมกับใน Python และใช้งานได้กับโซลูชัน Python SSL


4

Jython DOES ดำเนินการตรวจสอบใบรับรองตามค่าเริ่มต้นดังนั้นการใช้โมดูลไลบรารีมาตรฐานเช่น HTTplib.HTTPSConnection และอื่น ๆ โดย jython จะตรวจสอบใบรับรองและให้ข้อยกเว้นสำหรับความล้มเหลวเช่นข้อมูลประจำตัวที่ไม่ตรงกันใบรับรองที่หมดอายุเป็นต้น

ในความเป็นจริงคุณต้องทำงานพิเศษเพื่อให้ jython ทำงานเหมือน cpython กล่าวคือรับ jython เพื่อไม่ยืนยันใบรับรอง

ฉันได้เขียนบล็อกโพสต์เกี่ยวกับวิธีปิดใช้งานการตรวจสอบใบรับรองบน ​​jython เนื่องจากมีประโยชน์ในขั้นตอนการทดสอบ ฯลฯ

การติดตั้งผู้ให้บริการความปลอดภัยที่เชื่อถือได้ทั้งหมดบน java และ jython
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

รหัสต่อไปนี้ช่วยให้คุณได้รับประโยชน์จากการตรวจสอบความถูกต้องของ SSL ทั้งหมด (เช่นความถูกต้องของวันที่, ห่วงโซ่ใบรับรอง CA ... ) ยกเว้นขั้นตอนการตรวจสอบที่เสียบได้เช่นเพื่อยืนยันชื่อโฮสต์หรือทำขั้นตอนการตรวจสอบใบรับรองเพิ่มเติมอื่น ๆ

from httplib import HTTPSConnection
import ssl


def create_custom_HTTPSConnection(host):

    def verify_cert(cert, host):
        # Write your code here
        # You can certainly base yourself on ssl.match_hostname
        # Raise ssl.CertificateError if verification fails
        print 'Host:', host
        print 'Peer cert:', cert

    class CustomHTTPSConnection(HTTPSConnection, object):
        def connect(self):
            super(CustomHTTPSConnection, self).connect()
            cert = self.sock.getpeercert()
            verify_cert(cert, host)

    context = ssl.create_default_context()
    context.check_hostname = False
    return CustomHTTPSConnection(host=host, context=context)


if __name__ == '__main__':
    # try expired.badssl.com or self-signed.badssl.com !
    conn = create_custom_HTTPSConnection('badssl.com')
    conn.request('GET', '/')
    conn.getresponse().read()


-1

ฉันประสบปัญหาเดียวกัน แต่ต้องการลดการอ้างอิงของบุคคลที่สามให้น้อยที่สุด (เนื่องจากสคริปต์แบบครั้งเดียวนี้จะถูกเรียกใช้โดยผู้ใช้จำนวนมาก) วิธีการแก้ปัญหาของฉันคือการห่อโทรและตรวจสอบให้แน่ใจว่ารหัสทางออกคือcurl 0ทำงานอย่างมีเสน่ห์


ฉันจะบอกว่าstackoverflow.com/a/1921551/1228491 การใช้ pycurl เป็นทางออกที่ดีกว่ามาก
Marian
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.