/** * HTTP API: WP_Http_Curl class * * @package WordPress * @subpackage HTTP * @since 4.4.0 */ /** * Core class used to integrate Curl as an HTTP transport. * * HTTP request method uses Curl extension to retrieve the url. * * Requires the Curl extension to be installed. * * @since 2.7.0 * @deprecated 6.4.0 Use WP_Http * @see WP_Http */ #[AllowDynamicProperties] class WP_Http_Curl { /** * Temporary header storage for during requests. * * @since 3.2.0 * @var string */ private $headers = ''; /** * Temporary body storage for during requests. * * @since 3.6.0 * @var string */ private $body = ''; /** * The maximum amount of data to receive from the remote server. * * @since 3.6.0 * @var int|false */ private $max_body_length = false; /** * The file resource used for streaming to file. * * @since 3.6.0 * @var resource|false */ private $stream_handle = false; /** * The total bytes written in the current request. * * @since 4.1.0 * @var int */ private $bytes_written_total = 0; /** * Send a HTTP request to a URI using cURL extension. * * @since 2.7.0 * * @param string $url The request URL. * @param string|array $args Optional. Override the defaults. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error */ public function request( $url, $args = array() ) { $defaults = array( 'method' => 'GET', 'timeout' => 5, 'redirection' => 5, 'httpversion' => '1.0', 'blocking' => true, 'headers' => array(), 'body' => null, 'cookies' => array(), 'decompress' => false, 'stream' => false, 'filename' => null, ); $parsed_args = wp_parse_args( $args, $defaults ); if ( isset( $parsed_args['headers']['User-Agent'] ) ) { $parsed_args['user-agent'] = $parsed_args['headers']['User-Agent']; unset( $parsed_args['headers']['User-Agent'] ); } elseif ( isset( $parsed_args['headers']['user-agent'] ) ) { $parsed_args['user-agent'] = $parsed_args['headers']['user-agent']; unset( $parsed_args['headers']['user-agent'] ); } // Construct Cookie: header if any cookies are set. WP_Http::buildCookieHeader( $parsed_args ); $handle = curl_init(); // cURL offers really easy proxy support. $proxy = new WP_HTTP_Proxy(); if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() ); curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() ); if ( $proxy->use_authentication() ) { curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() ); } } $is_local = isset( $parsed_args['local'] ) && $parsed_args['local']; $ssl_verify = isset( $parsed_args['sslverify'] ) && $parsed_args['sslverify']; if ( $is_local ) { /** This filter is documented in wp-includes/class-wp-http-streams.php */ $ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify, $url ); } elseif ( ! $is_local ) { /** This filter is documented in wp-includes/class-wp-http.php */ $ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify, $url ); } /* * CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT expect integers. Have to use ceil since. * a value of 0 will allow an unlimited timeout. */ $timeout = (int) ceil( $parsed_args['timeout'] ); curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, $timeout ); curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); curl_setopt( $handle, CURLOPT_URL, $url ); curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, ( true === $ssl_verify ) ? 2 : false ); curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, $ssl_verify ); if ( $ssl_verify ) { curl_setopt( $handle, CURLOPT_CAINFO, $parsed_args['sslcertificates'] ); } curl_setopt( $handle, CURLOPT_USERAGENT, $parsed_args['user-agent'] ); /* * The option doesn't work with safe mode or when open_basedir is set, and there's * a bug #17490 with redirected POST requests, so handle redirections outside Curl. */ curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false ); curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); switch ( $parsed_args['method'] ) { case 'HEAD': curl_setopt( $handle, CURLOPT_NOBODY, true ); break; case 'POST': curl_setopt( $handle, CURLOPT_POST, true ); curl_setopt( $handle, CURLOPT_POSTFIELDS, $parsed_args['body'] ); break; case 'PUT': curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'PUT' ); curl_setopt( $handle, CURLOPT_POSTFIELDS, $parsed_args['body'] ); break; default: curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $parsed_args['method'] ); if ( ! is_null( $parsed_args['body'] ) ) { curl_setopt( $handle, CURLOPT_POSTFIELDS, $parsed_args['body'] ); } break; } if ( true === $parsed_args['blocking'] ) { curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) ); curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) ); } curl_setopt( $handle, CURLOPT_HEADER, false ); if ( isset( $parsed_args['limit_response_size'] ) ) { $this->max_body_length = (int) $parsed_args['limit_response_size']; } else { $this->max_body_length = false; } // If streaming to a file open a file handle, and setup our curl streaming handler. if ( $parsed_args['stream'] ) { if ( ! WP_DEBUG ) { $this->stream_handle = @fopen( $parsed_args['filename'], 'w+' ); } else { $this->stream_handle = fopen( $parsed_args['filename'], 'w+' ); } if ( ! $this->stream_handle ) { return new WP_Error( 'http_request_failed', sprintf( /* translators: 1: fopen(), 2: File name. */ __( 'Could not open handle for %1$s to %2$s.' ), 'fopen()', $parsed_args['filename'] ) ); } } else { $this->stream_handle = false; } if ( ! empty( $parsed_args['headers'] ) ) { // cURL expects full header strings in each element. $headers = array(); foreach ( $parsed_args['headers'] as $name => $value ) { $headers[] = "{$name}: $value"; } curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers ); } if ( '1.0' === $parsed_args['httpversion'] ) { curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 ); } else { curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 ); } /** * Fires before the cURL request is executed. * * Cookies are not currently handled by the HTTP API. This action allows * plugins to handle cookies themselves. * * @since 2.8.0 * * @param resource $handle The cURL handle returned by curl_init() (passed by reference). * @param array $parsed_args The HTTP request arguments. * @param string $url The request URL. */ do_action_ref_array( 'http_api_curl', array( &$handle, $parsed_args, $url ) ); // We don't need to return the body, so don't. Just execute request and return. if ( ! $parsed_args['blocking'] ) { curl_exec( $handle ); $curl_error = curl_error( $handle ); if ( $curl_error ) { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return new WP_Error( 'http_request_failed', $curl_error ); } if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ), true ) ) { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) ); } if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return array( 'headers' => array(), 'body' => '', 'response' => array( 'code' => false, 'message' => false, ), 'cookies' => array(), ); } curl_exec( $handle ); $processed_headers = WP_Http::processHeaders( $this->headers, $url ); $body = $this->body; $bytes_written_total = $this->bytes_written_total; $this->headers = ''; $this->body = ''; $this->bytes_written_total = 0; $curl_error = curl_errno( $handle ); // If an error occurred, or, no response. if ( $curl_error || ( 0 === strlen( $body ) && empty( $processed_headers['headers'] ) ) ) { if ( CURLE_WRITE_ERROR /* 23 */ === $curl_error ) { if ( ! $this->max_body_length || $this->max_body_length !== $bytes_written_total ) { if ( $parsed_args['stream'] ) { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } fclose( $this->stream_handle ); return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) ); } else { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return new WP_Error( 'http_request_failed', curl_error( $handle ) ); } } } else { $curl_error = curl_error( $handle ); if ( $curl_error ) { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return new WP_Error( 'http_request_failed', $curl_error ); } } if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ), true ) ) { if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) ); } } if ( PHP_VERSION_ID < 80000 ) { // curl_close() has no effect as of PHP 8.0. curl_close( $handle ); } if ( $parsed_args['stream'] ) { fclose( $this->stream_handle ); } $response = array( 'headers' => $processed_headers['headers'], 'body' => null, 'response' => $processed_headers['response'], 'cookies' => $processed_headers['cookies'], 'filename' => $parsed_args['filename'], ); // Handle redirects. $redirect_response = WP_Http::handle_redirects( $url, $parsed_args, $response ); if ( false !== $redirect_response ) { return $redirect_response; } if ( true === $parsed_args['decompress'] && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) ) { $body = WP_Http_Encoding::decompress( $body ); } $response['body'] = $body; return $response; } /** * Grabs the headers of the cURL request. * * Each header is sent individually to this callback, and is appended to the `$header` property * for temporary storage. * * @since 3.2.0 * * @param resource $handle cURL handle. * @param string $headers cURL request headers. * @return int Length of the request headers. */ private function stream_headers( $handle, $headers ) { $this->headers .= $headers; return strlen( $headers ); } /** * Grabs the body of the cURL request. * * The contents of the document are passed in chunks, and are appended to the `$body` * property for temporary storage. Returning a length shorter than the length of * `$data` passed in will cause cURL to abort the request with `CURLE_WRITE_ERROR`. * * @since 3.6.0 * * @param resource $handle cURL handle. * @param string $data cURL request body. * @return int Total bytes of data written. */ private function stream_body( $handle, $data ) { $data_length = strlen( $data ); if ( $this->max_body_length && ( $this->bytes_written_total + $data_length ) > $this->max_body_length ) { $data_length = ( $this->max_body_length - $this->bytes_written_total ); $data = substr( $data, 0, $data_length ); } if ( $this->stream_handle ) { $bytes_written = fwrite( $this->stream_handle, $data ); } else { $this->body .= $data; $bytes_written = $data_length; } $this->bytes_written_total += $bytes_written; // Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR. return $bytes_written; } /** * Determines whether this class can be used for retrieving a URL. * * @since 2.7.0 * * @param array $args Optional. Array of request arguments. Default empty array. * @return bool False means this class can not be used, true means it can. */ public static function test( $args = array() ) { if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) { return false; } $is_ssl = isset( $args['ssl'] ) && $args['ssl']; if ( $is_ssl ) { $curl_version = curl_version(); // Check whether this cURL version support SSL requests. if ( ! ( CURL_VERSION_SSL & $curl_version['features'] ) ) { return false; } } /** * Filters whether cURL can be used as a transport for retrieving a URL. * * @since 2.7.0 * * @param bool $use_class Whether the class can be used. Default true. * @param array $args An array of request arguments. */ return apply_filters( 'use_curl_transport', true, $args ); } } Why SPV, Multisig, and Hardware Wallet Support Still Matter — A Practical Take on Desktop Bitcoin Wallets – Shweta Poddar Weddings Photography

Okay, so check this out—I’ve been juggling desktop wallets for years. Wow! Early impressions stick: lightweight clients felt liberating at first. Then reality hit me: trade-offs are everywhere. My instinct said “keep it simple,” but experience pushed me toward features that actually protect funds when things go sideways.

SPV wallets used to be the go-to for people who wanted speed and privacy without downloading the whole chain. Short and cool, right? Hmm… not so simple anymore. SPV (Simplified Payment Verification) trusts block headers and verifies merkle proofs for your transactions, which means much less disk space and faster sync. But it also opens subtle attack surfaces if you rely on untrusted or adversarial peers.

Here’s the thing. Seriously? SPV isn’t “bad”—it just makes different assumptions than a full node. On one hand, you get instant gratification: transactions show up quickly and you can manage lighter devices. On the other hand, you’re implicitly trusting network peers for accurate header and transaction data. Initially I thought that was acceptable for everyday use, but then I lost some peace of mind.

Let me be blunt: multisig changes the calculus. A wallet that supports multisig plus robust hardware wallet integration reduces dependence on any single software peer. It adds redundancy and real-world resilience. I’m biased, but multisig is the single most underrated safety upgrade for self-custody users.

Whoa!

Multisig isn’t just for paranoid whales. Medium users benefit too. Imagine a setup where your laptop, a mobile device, and a hardware key each hold a key share. If one device gets compromised, an attacker still needs the others. That is a very very important point—especially when you spend time on email, click strange links, or carry a work laptop that gets all sorts of tooling shoved onto it.

Okay—practicalities. There are two models I like: 2-of-3 personal multisig (easy, practical), and 2-of-3 where one key is a third-party watch-only cosigner used for recovery. Both give you safe spending without throttling your daily UX. But somethin’ to watch: multisig complicates backups and recovery procedures. If you lose a key, you’re not always dead—but you might be annoyed for days while you reconstruct access.

Here’s what bugs me about many wallet UIs: they hide the complexity until you need it. That moment is the worst possible time to learn. So build the habit early. Test your recovery. Seriously, test it. Recover to a new device before you actually need it. On one hand it’s a chore; on the other, it’s insurance that feels less theoretical when you’ve done it.

Hardware wallet support deserves its own shout-out. The physical device reduces exposure of private keys to hostile environments. A hardware wallet signs transactions offline and only exposes signed data. That barrier is huge. However—actually, wait—let me rephrase that: hardware is a tool, not a panacea. It protects keys at rest and in signing flows, but physical device rooting, compromised firmware, or supply-chain attacks can still matter.

So, what does a good desktop SPV wallet with multisig and hardware support look like? Short answer: flexible, transparent, and interoperable.

A desktop wallet interface showing multisig setup and hardware device connection

What to expect from a modern desktop wallet (and why Electrum still matters)

Check this out—I’ve used a bunch. Electrum stands out for its long track record, multisig friendliness, and wide hardware wallet compatibility. If you’re shopping, take a look at the electrum wallet for a sense of how these features can be combined without too much fluff. It isn’t flashy. It’s effective.

Electrum and similar wallets let you run in SPV mode but also offer options to connect to your own server or use trusted servers—trade-offs you can choose. They also support hardware signers like Ledger and Trezor, and they provide deterministic wallet formats that are relatively easy to back up. I’m not saying it’s perfect. I’m not 100% sure it’s right for every single person. But it’s tough to beat for advanced users who want control.

Consider these design principles when choosing a desktop wallet:

– Transparent verification: You should be able to inspect merkle proofs and headers if you want. Short wins. – Interoperable multisig formats: Use standards so you can move keys between software and hardware easily. – Hardware wallet integration: Not an afterthought. It should be tested and documented. – Recovery and backup workflows: Clear steps, tested by you. – Optional full-node connectivity: For those who want maximum assurance, plug in a node.

On average, people skip the last item. That’s okay for lower value holdings. But once balances rise, the need for a full-node-backed wallet increases. On one hand, running a full node is extra work; though actually, it gives you uncompromised verification and privacy gains that SPV can’t match.

One thing everyone misunderstands: privacy and SPV are not equals. SPV gives some privacy benefits because you don’t broadcast all your addresses to the entire network, but you still query peers for scripts and transactions, which leaks info. If privacy is a high priority, route your SPV client through Tor, or better, use an Electrum server you control (or a trusted one with a good privacy track record).

Many folks ask whether multisig and hardware wallets are too complex for everyday use. My counter: once configured, daily spending can be smooth—especially with a single-sig hot wallet for small amounts and a multisig cold storage for larger reserves. That’s pragmatic and human.

Pro tip: create a “spending policy” for yourself. Decide upfront how much to keep in a hot wallet, what needs multisig protection, and how often you’ll rotate keys. Write it down. Sound boring? Maybe. It prevents frantic decisions at 2 a.m.

FAQ: Quick answers to common questions

Is SPV safe enough for moderate holdings?

Yes—if paired with good practices: use trusted servers or your own Electrum server, route traffic through Tor when possible, and pair SPV with hardware-backed signing for private keys. For life-changing sums, consider a full node or multisig with geographically separated signers.

Does multisig complicate hardware wallet use?

Not really. Modern hardware wallets support multisig workflows; the UX is improving. You will need to coordinate descriptors/XPUBs correctly and keep clear backups, but the added protection is worth the extra steps.

Can I trust third-party Electrum servers?

Depends on your risk tolerance. Trusted servers ease setup, but they can lie about transactions or withhold history. For high assurance, run your own server or use servers run by organizations you trust—after vetting them. Also, use Tor to mask queries.

Final thoughts—I’m circling back to where I started. Initially I thought “lightweight is king,” and for a while that was true. But risk exposure accumulates. If you care about longevity and protection, layering SPV convenience with multisig and hardware-backed signing gives you both speed and defense. It’s a balancing act, not a binary choice.

I’ll leave you with this: test your backups today. Seriously. Recover to a blank device, walk through signing with your hardware device, and confirm you can spend. It sucks to do, but it’s a tiny cost for peace of mind. And hey, if you want a practical place to start exploring these workflows, take a look at the electrum wallet—it’s low drama and powerful when you need it.

Uncategorized

Leave a Comment

Your email address will not be published. Required fields are marked *