1 | """ |
---|
2 | Blocking HTTP client APIs. |
---|
3 | """ |
---|
4 | |
---|
5 | import os |
---|
6 | from io import BytesIO |
---|
7 | from http import client as http_client |
---|
8 | import urllib |
---|
9 | import allmydata # for __full_version__ |
---|
10 | |
---|
11 | from allmydata.util.encodingutil import quote_output |
---|
12 | from allmydata.scripts.common import TahoeError |
---|
13 | from socket import error as socket_error |
---|
14 | |
---|
15 | # copied from twisted/web/client.py |
---|
16 | def 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 | |
---|
34 | class 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 | |
---|
43 | def 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 | |
---|
95 | def format_http_success(resp): |
---|
96 | return quote_output( |
---|
97 | "%s %s" % (resp.status, resp.reason), |
---|
98 | quotemarks=False) |
---|
99 | |
---|
100 | def 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 | |
---|
106 | def 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 | |
---|
112 | class HTTPError(TahoeError): |
---|
113 | def __init__(self, msg, resp): |
---|
114 | TahoeError.__init__(self, format_http_error(msg, resp)) |
---|