source: trunk/src/allmydata/scripts/common_http.py

Last change on this file was f9a1eed, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-04-25T16:31:37Z

Make timeout optional, enable it only for integration tests.

  • Property mode set to 100644
File size: 3.4 KB
Line 
1"""
2Blocking HTTP client APIs.
3"""
4
5import os
6from io import BytesIO
7from http import client as http_client
8import urllib
9import allmydata # for __full_version__
10
11from allmydata.util.encodingutil import quote_output
12from allmydata.scripts.common import TahoeError
13from socket import error as socket_error
14
15# copied from twisted/web/client.py
16def parse_url(url, defaultPort=None):
17    url = url.strip()
18    parsed = urllib.parse.urlparse(url)
19    scheme = parsed[0]
20    path = urllib.parse.urlunparse(('','')+parsed[2:])
21    if defaultPort is None:
22        if scheme == 'https':
23            defaultPort = 443
24        else:
25            defaultPort = 80
26    host, port = parsed[1], defaultPort
27    if ':' in host:
28        host, port = host.split(':')
29        port = int(port)
30    if path == "":
31        path = "/"
32    return scheme, host, port, path
33
34class BadResponse(object):
35    def __init__(self, url, err):
36        self.status = -1
37        self.reason = "Error trying to connect to %s: %s" % (url, err)
38        self.error = err
39    def read(self, length=0):
40        return ""
41
42
43def do_http(method, url, body=b""):
44    if isinstance(body, bytes):
45        body = BytesIO(body)
46    elif isinstance(body, str):
47        raise TypeError("do_http body must be a bytestring, not unicode")
48    else:
49        # We must give a Content-Length header to twisted.web, otherwise it
50        # seems to get a zero-length file. I suspect that "chunked-encoding"
51        # may fix this.
52        assert body.tell
53        assert body.seek
54        assert body.read
55    scheme, host, port, path = parse_url(url)
56
57    # For testing purposes, allow setting a timeout on HTTP requests. If this
58    # ever become a user-facing feature, this should probably be a CLI option?
59    timeout = os.environ.get("__TAHOE_CLI_HTTP_TIMEOUT", None)
60    if timeout is not None:
61        timeout = float(timeout)
62
63    if scheme == "http":
64        c = http_client.HTTPConnection(host, port, timeout=timeout, blocksize=65536)
65    elif scheme == "https":
66        c = http_client.HTTPSConnection(host, port, timeout=timeout, blocksize=65536)
67    else:
68        raise ValueError("unknown scheme '%s', need http or https" % scheme)
69    c.putrequest(method, path)
70    c.putheader("Hostname", host)
71    c.putheader("User-Agent", allmydata.__full_version__ + " (tahoe-client)")
72    c.putheader("Accept", "text/plain, application/octet-stream")
73    c.putheader("Connection", "close")
74
75    old = body.tell()
76    body.seek(0, os.SEEK_END)
77    length = body.tell()
78    body.seek(old)
79    c.putheader("Content-Length", str(length))
80
81    try:
82        c.endheaders()
83    except socket_error as err:
84        return BadResponse(url, err)
85
86    while True:
87        data = body.read(65536)
88        if not data:
89            break
90        c.send(data)
91
92    return c.getresponse()
93
94
95def format_http_success(resp):
96    return quote_output(
97        "%s %s" % (resp.status, resp.reason),
98        quotemarks=False)
99
100def format_http_error(msg, resp):
101    return quote_output(
102        "%s: %s %s\n%r" % (msg, resp.status, resp.reason,
103                           resp.read()),
104        quotemarks=False)
105
106def check_http_error(resp, stderr):
107    if resp.status < 200 or resp.status >= 300:
108        print(format_http_error("Error during HTTP request", resp), file=stderr)
109        return 1
110
111
112class HTTPError(TahoeError):
113    def __init__(self, msg, resp):
114        TahoeError.__init__(self, format_http_error(msg, resp))
Note: See TracBrowser for help on using the repository browser.