Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test the HTTP server basics."""7from test_framework.test_framework import BitcoinTestFramework8from test_framework.netutil import NETWORK_ERRORS9from test_framework.util import (10assert_equal,11assert_raises,12mine_large_block,13str_to_b64str14)15from test_framework.wallet import MiniWallet17import concurrent.futures18import http.client19import re20import socket21import threading22import time23import urllib.parse25# Configuration option for some tests26RPCSERVERTIMEOUT = 227# Set in httpserver.h28MAX_HEADERS_SIZE = 819229MAX_BODY_SIZE = 32 * 1024 * 102431class BitcoinHTTPConnection:32def __init__(self, node):33self.url = urllib.parse.urlparse(node.url)34self.authpair = f'{self.url.username}:{self.url.password}'35self.headers = {"Authorization": f"Basic {str_to_b64str(self.authpair)}"}36self.reset_conn()38def reset_conn(self):39self.conn = http.client.HTTPConnection(self.url.hostname, self.url.port)40self.conn.connect()42def sock_closed(self):43if self.conn.sock is None:44return True45try:46self.conn.request('GET', '/')47self.conn.getresponse().read()48return False49except NETWORK_ERRORS:50return True52def close_sock(self):53self.conn.close()55def set_timeout(self, seconds):56self.conn.sock.settimeout(seconds)58def add_header(self, key, value):59self.headers.update({key: value})61def _request(self, method, path, data, connection_header, **kwargs):62headers = self.headers.copy()63if connection_header is not None:64headers["Connection"] = connection_header65self.conn.request(method, path, data, headers, **kwargs)66return self.conn.getresponse()68def post(self, path, data, connection_header=None, **kwargs):69return self._request('POST', path, data, connection_header, **kwargs)71def get(self, path, connection_header=None):72return self._request('GET', path, '', connection_header)74def send_raw(self, data):75self.conn.sock.sendall(data)77def post_raw(self, path, data):78data_bytes = data.encode("utf-8")79req = f"POST {path} HTTP/1.1\r\n"80req += f'Authorization: Basic {str_to_b64str(self.authpair)}\r\n'81req += f'Content-Length: {len(data_bytes)}\r\n\r\n'82self.send_raw(req.encode("ascii") + data_bytes)84def recv_raw(self):85'''86Blocking socket will wait until data is received and return up to 1024 bytes87'''88return self.conn.sock.recv(1024)90def expect_timeout(self, seconds):91# Wait for response, but expect a timeout disconnection92start = time.time()93response1 = self.recv_raw()94stop = time.time()95# Server disconnected with EOF96assert_equal(response1, b"")97# Server disconnected within an acceptable range of time:98# not immediately, and not too far over the configured duration.99# This allows for some jitter in the test between client and server.100duration = stop - start101assert duration <= seconds + 2, f"Server disconnected too slow: {duration} > {seconds}"102assert duration >= seconds - 1, f"Server disconnected too fast: {duration} < {seconds}"103# The connection is definitely closed.104assert self.sock_closed()106class HTTPBasicsTest (BitcoinTestFramework):107def set_test_params(self):108self.num_nodes = 1110def setup_network(self):111self.setup_nodes()112self.node = self.nodes[0]114def send_bad_and_tolerate_disconnect(self, conn, predicate_fn):115'''116Tolerate a race condition when sending a malformed request that should result117in the server disconnecting the client. The server *should* be sending an error118response as well but in some conditions on some platforms (Windows) the python119client might encounter the socket error before processing the response.120'''121with self.node.assert_debug_log([f"HTTPResponse (status code: {http.client.BAD_REQUEST}"]):122try:123response = predicate_fn()124assert_equal(response.status, http.client.BAD_REQUEST)125self.log.info(f"Client received expected {http.client.BAD_REQUEST} response before connection was terminated")126# Drain server response127response.read()128conn.set_timeout(2)129except NETWORK_ERRORS:130self.log.info(f"Client did not receive expected {http.client.BAD_REQUEST} response before connection was terminated")131assert conn.sock_closed()134def run_test(self):135# The test framework typically reuses a single persistent HTTP connection136# for all RPCs to a TestNode. Because we are setting -rpcservertimeout137# so low on this one node, its connection will quickly timeout and get dropped by138# the server. Negating this setting will force the AuthServiceProxy139# for this node to create a fresh new HTTP connection for every command140# called for the remainder of this test.141self.node.reuse_http_connections = False143self.check_default_connection()144self.check_socket_exclusivity()145self.check_keepalive_connection()146self.check_close_connection()147self.check_excessive_request_size()148self.check_pipelining(with_invalid_second_request=False)149self.check_pipelining(with_invalid_second_request=True)150self.check_chunked_transfer()151self.check_idle_timeout()152self.check_server_busy_idle_timeout()153self.check_auth_required()154self.check_wrong_credentials()155self.check_malformed_auth_headers()156self.check_disallowed_http_methods()157self.check_path_traversal()158self.check_request_smuggling_cl_te()159self.check_duplicate_content_length()160self.check_null_byte_in_uri()161self.check_invalid_http_version()162self.check_whitespace_in_headers()163self.check_connection_limit()164self.check_pipelined_data_is_throttled()165self.check_slow_read_throttle()168def check_default_connection(self):169self.log.info("Checking default HTTP/1.1 connection persistence")170conn = BitcoinHTTPConnection(self.node)171# Make request without explicit "Connection" header172response1 = conn.post('/', '{"method": "getbestblockhash"}').read()173assert b'"error":null' in response1174# Connection still open after request175assert not conn.sock_closed()176# Make second request without explicit "Connection" header177response2 = conn.post('/', '{"method": "getchaintips"}').read()178assert b'"error":null' in response2179# Connection still open after second request180assert not conn.sock_closed()181# Close182conn.close_sock()183assert conn.sock_closed()186def check_socket_exclusivity(self):187self.log.info("Checking that another process cannot bind the HTTP listen port")188url = urllib.parse.urlparse(self.node.url)189with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as competing_listener:190competing_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)191# Ill-configured sockets permit port reuse unless the original192# listener requested exclusive address use.193assert_raises(194OSError,195lambda: competing_listener.bind((url.hostname, url.port)))198def check_keepalive_connection(self):199self.log.info("Checking keep-alive connection persistence")200conn = BitcoinHTTPConnection(self.node)201# Make request with explicit "Connection: keep-alive" header202response1 = conn.post('/', '{"method": "getbestblockhash"}', connection_header='keep-alive').read()203assert b'"error":null' in response1204# Connection still open after request205assert not conn.sock_closed()206# Make second request with explicit "Connection" header207response2 = conn.post('/', '{"method": "getchaintips"}', connection_header='keep-alive').read()208assert b'"error":null' in response2209# Connection still open after second request210assert not conn.sock_closed()211# Close212conn.close_sock()213assert conn.sock_closed()216def check_close_connection(self):217self.log.info("Checking close connection after response")218conn = BitcoinHTTPConnection(self.node)219# Make request with explicit "Connection: close" header220response1 = conn.post('/', '{"method": "getbestblockhash"}', connection_header='close').read()221assert b'"error":null' in response1222# Connection closed after response223assert conn.sock_closed()226def check_excessive_request_size(self):227self.log.info("Checking excessive request size")229# Large URI plus up to 1000 bytes of default headers230# added by python's http.client still below total limit.231conn = BitcoinHTTPConnection(self.node)232response1 = conn.get(f'/{"x" * (MAX_HEADERS_SIZE - 1000)}')233assert_equal(response1.status, http.client.NOT_FOUND)235# Excessive URI size plus default headers breaks the limit.236conn = BitcoinHTTPConnection(self.node)237self.send_bad_and_tolerate_disconnect(conn, lambda: conn.get(f'/{"x" * MAX_HEADERS_SIZE}'))239# Compute how many short header lines need to be added to http.client240# default headers to make / break the total limit in a single request.241header_line_length = len("header_0000: foo\r\n")242headers_below_limit = (MAX_HEADERS_SIZE - 1000) // header_line_length243headers_above_limit = MAX_HEADERS_SIZE // header_line_length245# Many small header lines is ok246conn = BitcoinHTTPConnection(self.node)247for i in range(headers_below_limit):248conn.add_header(f"header_{i:04}", "foo")249response3 = conn.get('/x')250assert_equal(response3.status, http.client.NOT_FOUND)
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.