curl/curl
github.com/curl/curl ↗ · 4000 files · 14.1 MB · scan capped
| | | | \| | / | | | | |) | | | (| || | <| | \|\/|| \\| README Curl is a command line tool for transferring data specified with URL syntax.
Overview
This is the curl project at 8.23.0-DEV: a C89-portable transfer library (libcurl, `lib/`, ~393 files) plus the `curl` command-line tool (`src/`, ~96 files) that is a thin client of that library. libcurl speaks ~25 URI schemes (HTTP/1-2-3, FTP(S), SMTP/IMAP/POP3, SCP/SFTP, LDAP, MQTT, RTSP, TFTP, DICT, Gopher, Telnet, WS(S), file) through a scheme registry of `struct Curl_scheme` records, and layers transport concerns (happy-eyeballs, SOCKS, HTTP proxy, HAProxy, TLS, QUIC, HTTP/2) as a stackable chain of connection filters rather than protocol-specific socket code. What is distinctive is the amount of pluggability compiled in: six TLS backends, two QUIC backends, two SSH backends, three DNS resolver strategies, ~50 `CURL_DISABLE_*` build knobs, and a ~2,100-case integration test suite driven by in-tree protocol servers. The audience is everyone from distro maintainers embedding libcurl to contributors adding a protocol or a command-line option.
System map
Two build artefacts share one repo: `lib/` builds libcurl, `src/` builds the `curl` tool on top of the public headers in `include/curl/`. The tool owns argv parsing, output files, globbing and retries; libcurl owns URL parsing, connection reuse, the multi state machine, and the filter chain that actually moves bytes. Backends live in `lib/vtls` (TLS), `lib/vquic` (QUIC/HTTP3), `lib/vssh` (SCP/SFTP), `lib/vauth` (SASL/NTLM/Kerberos), `lib/vdns` (resolvers + DoH), with `lib/curlx/` as the portability/utility layer shared by both. There is no database — persistence is flat files (cookie jar, HSTS, alt-svc, TLS session cache) plus whatever the user redirects output to. Two parallel build systems (autotools via `configure.ac`/`Makefile.am` and CMake via a 93KB `CMakeLists.txt`) are both first-class and CI-verified against each other.
- The CLI process entrypoint is `main()` in src/tool_main.c, which runs `tool_init_stderr()`, `main_checkfds()` (reopens fds 0/1/2 on /dev/null so sockets can never land on stdin/stdout/stderr), ignores SIGPIPE, then calls `globalconf_init()` → `operate(argc, argv)` → `globalconf_free()`.
- `operate()` is the tool's real driver: it forces `LC_NUMERIC=C`, reads `.curlrc` via `parseconfig()` unless argv[1] is `-q`/`--disable`, calls `parse_args()`, creates one `CURLSH` share handle, then hands off to `run_all_transfers()`.
- The public API surface is 12 headers under include/curl/ — `curl.h` (options/CURLcode), `multi.h`, `easy.h`, `urlapi.h`, `websockets.h`, `header.h`, `options.h`, `mprintf.h`, `system.h`, `curlver.h` — and `curlver.h` pins this tree at LIBCURL_VERSION "8.23.0-DEV" / LIBCURL_VERSION_NUM 0x081700.
- Every URI scheme libcurl knows is a `const struct Curl_scheme` (name, `run` vtable pointer, protocol bit, family bit, PROTOPT_* flags, default port) defined in lib/protocol.c and externed in lib/protocol.h; a scheme compiled out (e.g. `CURL_DISABLE_FTP`) keeps its record but gets a `ZERO_NULL` `run` pointer.
- Scheme lookup avoids a linear table scan: `Curl_getn_scheme()` dispatches on name length into `two_letter_scheme()` … `seven_letter_scheme()`, each a hand-written switch on the first character, and rejects any name shorter than 2 or longer than 7 characters.
- Transport is a chain of connection filters: `struct Curl_cftype` supplies do_connect/do_send/do_recv/do_shutdown/adjust_pollset/query, and `struct Curl_cfilter` links instances via `->next`, so TLS writes into whatever is below it without knowing whether that is a socket, a proxy tunnel, or HTTP/2.
- Concrete filter types are registered as `struct Curl_cftype Curl_cft_*` across the tree: tcp/udp/unix/tcp_accept (lib/cf-socket.c), ip_happy, socks_proxy, http_proxy, h1/h2 proxy, haproxy, http_connect, nghttp2, http3 (both ngtcp2 and quiche), capsule, dns, and recvbuf.
- The runtime boundary between easy and multi is one-way: the multi interface is the engine and `curl_easy_perform()` is a wrapper that creates (and caches on `data->multi_easy`) a 1-handle multi and pumps it.
- Each transfer is a 16-state machine (`MSTATE_INIT` … `MSTATE_MSGSENT`) declared in lib/multihandle.h and driven by `multi_runsingle()` in lib/multi.c, which is 4,262 lines and the single busiest file in the library.
- TLS is selected from a compile-ordered array `available_backends[]` (wolfSSL, GnuTLS, mbedTLS, OpenSSL, Schannel, Rustls); `CURL_SSL_BACKEND` in the environment can pick among them at runtime when multiple are built in.
- The storage layer is plain files, not a database: cookie jars via `cookie_output()`/`Curl_flush_cookies()`, HSTS via `Curl_hsts_save()`, Alt-Svc via `Curl_altsvc_save()`, and TLS session blobs via `tool_ssls_save()` called from `operate()`.
- Build/test commands live in the top Makefile.am: `make check` = test + examples + check-docs; `make test`, `test-full`, `test-nonflaky`, `test-torture`, `test-event`, `test-ci`, `pytest`; plus `make checksrc`, `make badwords`, `make lint`, `make tidy`, and `make ca-bundle`.
Feature catalog
User-facing surface splits in two. The `curl` tool exposes ~298 documented command-line options plus higher-level conveniences: URL globbing, parallel transfers, retry policies, ETag compare/store, `--write-out` templating (including JSON), config files, user-defined `--variable` expansion, `--libcurl` source generation, and xattr metadata. libcurl exposes ~91 documented API functions and ~422 documented options, covering the easy, multi, share, URL, header, mime, and WebSocket APIs. Protocol coverage is the headline feature: HTTP/1.1, HTTP/2, HTTP/3, FTP(S), SFTP/SCP, SMTP/IMAP/POP3, LDAP(S), MQTT, RTSP, TFTP, DICT, Gopher, Telnet, WS(S), and local `file`. Authentication and proxying are each their own subsystem with many interchangeable mechanisms.
- The command-line grammar is a single alphabetically-sorted `aliases[]` table of `struct LongShort` (long name, ARG_* type bits such as ARG_BOOL/ARG_FILE/ARG_STRG/ARG_SECS plus ARG_NO/ARG_TLS/ARG_CLEAR modifiers, short letter, and a `C_*` id); `findlongopt()`/`findshortopt()` resolve into it.
- Every command-line option has a matching curldown page — 298 files under docs/cmdline-opts/ — and the underscore-prefixed ones (`_OPTIONS.md`, `_GLOBBING.md`, `_VARIABLES.md`, `_EXITCODES.md`) are the manpage's prose sections.
- `--parallel` switches the tool from `serial_transfers()` to `parallel_transfers()`, which drives a real `CURLM` with `CURLMOPT_MAX_HOST_CONNECTIONS`, a notify callback, and `curl_multi_poll()`/`curl_multi_perform()`; concurrency is capped by `MAX_PARALLEL`/`MAX_PARALLEL_HOST` with `PARALLEL_DEFAULT` as the default.
- URL globbing (`http://host/[1-100]`, `{a,b}`) is implemented in src/tool_urlglob.c and feeds `glob_match_url()`, which also resolves `#1`..`#9` back-references when composing output filenames.
- `--write-out` is table-driven: a `writeoutvar variables[]` array maps names like `http_code`, `time_total`, `num_retries`, `conn_id`, `filename_effective` to a CURLINFO id and a writer function, with `%{json}`, `%{header_json}`, `%header{}`, `%time{}` and `%output{}` handled as special forms.
- `--libcurl <file>` makes the tool emit an equivalent C program: `easysrc_init()`/`easysrc_perform()`/`dumpeasysrc()` are wired into `operate()` and the serial loop, and the `my_setopt*` macros in src/tool_setopt.c double as source emitters.
- `--variable` supports environment expansion with functions: src/var.c reads `getenv(name)` directly (rather than `curl_getenv()`) specifically so that a blank-but-set variable is distinguishable.
- IPFS/IPNS URLs are a tool-level feature that reads `IPFS_GATEWAY`, then falls back to `IPFS_PATH`, then `$HOME/.ipfs/gateway`, rewriting `ipfs://` into an HTTP gateway request.
- Transfer resilience is a tool feature, not a library one: `retrycheck()` retries on timeouts/resolve failures, on HTTP 408/429/500/502/503/504/522/524, on FTP 4xx, on `--retry-connrefused`, and on `--retry-all-errors`, truncating the output file before each new attempt.
- HTTP/3 ships with two interchangeable QUIC backends — ngtcp2 (`lib/vquic/cf-ngtcp2.c`) and quiche (`lib/vquic/cf-quiche.c`) — each registering a `Curl_cft_http3` filter, plus an experimental HTTP/3 proxy path via `cf-ngtcp2-proxy.c` and CONNECT-UDP capsules.
- WebSocket support is a public API (`curl_ws_recv()`, `curl_ws_send()`, `curl_ws_meta()`) declared in include/curl/websockets.h and implemented in lib/ws.c with frame-level fragmentation handling.
- Authentication mechanisms are pluggable per-mechanism files under lib/vauth/: cleartext, CRAM-MD5, DIGEST (native and SSPI), NTLM (native and SSPI), Kerberos 5 (GSSAPI and SSPI), SPNEGO (GSSAPI and SSPI), OAuth2, and GSASL.
Data flow
Input boundaries are argv, `.curlrc`/`-K` config files, environment variables, `.netrc`, stdin uploads, and the network. argv is normalised into a linked list of `struct OperationConfig` (one per `--next`), which `config2setopts.c` translates wholesale into `curl_easy_setopt()` calls; from there the state owner is `struct Curl_easy` (`data->set` for user intent, `data->state`/`data->req` for per-request state). The URL becomes a `CURLU` via lib/urlapi.c, resolves to a `struct Curl_scheme`, and is matched against the connection pool before a filter chain is built. Bytes in from the network flow up through `Curl_xfer_write_resp()` → `Curl_client_write()` → an ordered writer stack → `CURLOPT_WRITEFUNCTION`; bytes out flow down through the mirror-image client reader chain. Output boundaries are the write callbacks (which the tool points at files or stdout), the header callback, the progress callback, and the file-backed caches flushed at cleanup.
- argv is consumed by `parse_args()`, which loops over argv, calls `getparameter()` per flag, treats bare arguments as `--url`, and on `PARAM_NEXT_OPERATION` allocates a fresh `OperationConfig` and links it onto `global->last` — so `--next` literally forks the config list.
- Config files are a second input boundary parsed by the same code path: `operate()` calls `parseconfig(NULL, CONFIG_MAX_LEVELS, &curlrc_path)` for `.curlrc` before argv, and `-K` re-enters it, with recursion bounded by `CONFIG_MAX_LEVELS`.
- The config→library translation is centralised in src/config2setopts.c, split into `ssh_setopts()`, `ssl_ca_setopts()`, `ssl_setopts()`, `cookie_setopts()`, `httpsig_setopts()`, `http_setopts()` and friends, all using `my_setopt_*` wrappers so the same call sites feed both libcurl and the `--libcurl` code generator.
- Library-side, options land in `struct Curl_easy` guarded by `magic == CURLEASY_MAGIC_NUMBER`; `lib/setopt.c` fans out into typed setters (`setopt_long_bool`, `setopt_long_net`, `setopt_long_ssl`, `setopt_long_proxy`, `setopt_long_http`) rather than one giant switch.
- URLs are parsed by `parseurl()` in lib/urlapi.c into a `CURLU`, with behaviour gated by flags such as `CURLU_GUESS_SCHEME`, `CURLU_DEFAULT_SCHEME`, `CURLU_NO_AUTHORITY`, `CURLU_DISALLOW_USER`, `CURLU_URLENCODE` and `CURLU_PATH_AS_IS`; the same parser backs the public `curl_url_*` API.
- The parsed scheme is only accepted for a connection if it passes `url_set_conn_scheme()`, which requires a non-NULL `run` vtable and membership in `data->set.allowed_protocols` (and `redir_protocols` when following a redirect) — this is the choke point that makes `--proto`/`--proto-redir` effective.
- Before dialling out, libcurl asks the connection pool for a reusable `connectdata` through `Curl_cpool_find()` with a match callback; reuse eligibility consults PROTOPT_SSL/PROTOPT_SSL_REUSE/PROTOPT_CONN_REUSE and compares origins and protocol families.
- Received bytes enter the client-writer chain via `Curl_client_write()`, which lazily builds the stack (`Curl_cwt_out` at CURL_CW_CLIENT, `cw_download` at CURL_CW_PROTOCOL, `cw_raw` at CURL_CW_RAW) and inserts each new writer first within its phase.
- Writer ordering is a fixed phase enum — RAW → TRANSFER_DECODE → PROTOCOL → BEFORE_DECODE → CONTENT_DECODE → CLIENT — so chunked decoding, size accounting, and gzip/brotli/zstd decompression always run in that order before the application callback.
- `cw_download_write()` is where `--max-filesize` is enforced and where excess body bytes beyond `req.maxdownload` cause `connclose()`; exceeding the limit returns `CURLE_FILESIZE_EXCEEDED` from inside the writer chain.
- Upload data flows through the mirror-image client-reader chain with phases CLIENT → CONTENT_ENCODE → PROTOCOL → TRANSFER_ENCODE → NET, entered via `Curl_client_read()`; HTTP's Expect: 100-continue is itself implemented as a reader (`cr_exp100_read`).
- On the tool side the terminal output boundary is `tool_cb_wrt.c`'s write callback writing into a `struct OutStruct` (stream, filename, `fopened`, `init` offset), with `setup_outfile()` deciding stdout vs. file vs. `--out-null` and opening in append mode when resuming.
Behavior traces
Three flows explain most of the codebase: tool startup, a single serial transfer, and the multi state machine that both serial and parallel modes ultimately run on. The tool never calls sockets directly — it builds `per_transfer` records, sets options, and calls `curl_easy_perform()` (serial) or feeds a `CURLM` (parallel). Inside libcurl, `curl_easy_perform()` is itself a multi loop, so every transfer walks `MSTATE_INIT` → … → `MSTATE_MSGSENT` in `multi_runsingle()`. Connection establishment is a re-entrant loop inside one filter (`cf_setup_connect`) that appends the next filter and restarts the sub-chain connect until the whole stack is up.
- STARTUP: `main()` → `tool_init_stderr()` → (Windows only) `win32_init()` → `main_checkfds()` → `signal(SIGPIPE, SIG_IGN)` → `memory_tracking_init()` (reads `CURL_MEMDEBUG`/`CURL_MEMLIMIT` on debug builds) → `globalconf_init()` → `operate()` → `globalconf_free()`.
- STARTUP (cont.): inside `operate()` the order is `setlocale(LC_NUMERIC,"C")` → `parseconfig()` for `.curlrc` → `parse_args()` → `easysrc_init()` if `--libcurl` → `curl_share_init()` → `share_setup()` → `tool_ssls_load()` → `get_args()` per OperationConfig → `run_all_transfers()` → `curl_share_cleanup()` → `dumpeasysrc()` → `varcleanup()`.
- PRIMARY ACTION (serial): `run_all_transfers()` → `serial_transfers()` → `create_transfer()` → `transfer_per_config()` → `cacertpaths()` → `single_transfer()` → `create_single()`, then per item `pre_transfer()` → `curl_easy_perform()` → `post_per_transfer()`; a retry sleeps `curlx_wait_ms(delay_ms)` and re-runs the same `per`.
- `create_single()` builds one `per_transfer` by walking `setup_input_file()` → `setup_url_pattern()` → `setup_etag_files()` → `curl_easy_init()` → `add_per_transfer()`, and only then applies output/header/option setup; failure paths must both `curl_easy_cleanup()` and close any opened etag stream.
- `pre_transfer()` is what turns `-T file` into `CURLOPT_INFILESIZE_LARGE`: it opens the upload file with `curlx_open(O_RDONLY|CURL_O_BINARY)`, `fstat`s it, and deliberately skips the size for non-`S_ISREG` inputs (devices, sockets, FIFOs).
- PRIMARY ACTION (parallel): `parallel_transfers()` → `curl_multi_init()` → set `CURLMOPT_QUICK_EXIT`, `CURLMOPT_NOTIFYFUNCTION=mnotify`, `CURLMOPT_MAX_HOST_CONNECTIONS` → `curl_multi_notify_enable(CURLMNOTIFY_INFO_READ)` → `add_parallel_transfers()` → loop { `curl_multi_poll(…,1000,…)`; `curl_multi_perform()`; `progress_meter()` } with completions harvested in `check_finished()`.
- `--fail-early` and fatal errors are implemented as a cooperative abort: `check_finished()` sets `s->wrapitup`, the main loop then sets `per->abort = TRUE` on every added transfer, and the progress callback turns that into `CURLE_ABORTED_BY_CALLBACK` which is rewritten into a clearer errorbuffer message.
- LIBRARY ENTRY: `curl_easy_perform()` → `CURL_EAPI_ENTER` guard → `easy_perform()`, which refuses a handle already in a user multi, detaches any connect-only connection, reuses or creates `data->multi_easy` via `Curl_multi_handle(16,1,3,7,3)`, copies MAXCONNECTS/QUICK_EXIT, `Curl_multi_add_handle()`, `sigpipe_init/apply`, then `easy_transfer()`.
- `easy_transfer()` is the blocking pump: `curl_multi_poll()` → `curl_multi_perform()` → when `still_running` hits 0, `curl_multi_info_read()` yields the CURLcode; a CURLMcode is mapped to `CURLE_OUT_OF_MEMORY` or `CURLE_BAD_FUNCTION_ARGUMENT`.
- STATE MACHINE: `multi_runsingle()` clears the handle's dirty bit, optionally runs `multi_schedule_pending()`, checks `multi_handle_timeout()`, then switches to `multistate_init/setup/connect/connecting/protoconnect/protoconnecting/do/doing/doing_more/did/ratelimiting/performing/done`, looping while any handler returns `CURLM_CALL_MULTI_PERFORM`.
- State transitions run entry hooks from a fixed `state_enter[MSTATE_LAST]` table in `mstate()` — only CONNECT, DID, DONE and COMPLETED have hooks — and every transition is traced via `CURL_TRC_M(data, "-> [%s]")`, which is what `--trace-config multi` surfaces.
- CONNECT: `cf_setup_connect()` is a re-entrant loop that connects the sub-chain, then appends filters in a fixed order — ip_happy (happy eyeballs) → SOCKS → HTTP proxy → HAProxy → origin filters (TLS/ALPN) — restarting at `connect_sub_chain` after each addition until `CF_SETUP_DONE`.
Testing signals
curl's testing is unusually broad and almost entirely in-tree. The centre of gravity is `tests/data/` — 2,119 XML-ish test-case files run by `tests/runtests.pl` against protocol servers curl itself ships (sws, socksd, tftpd, mqttd, rtspd, dnsd, plus Perl/Python FTP/TFTP/RTSP/DICT/telnet servers). Around that sit 80 C unit tests for library internals, 6 `tunit` tests for tool-only code, 273 `libtest` programs exercising the public API, and 31 pytest modules that test HTTP/2, HTTP/3, proxying, SSL and WebSockets against real nghttpx/caddy/vsftpd. Each run can be repeated in torture mode (fail every allocation in turn), under valgrind, or event-based. Coverage gaps are structural rather than accidental: platform-specific code (Windows/VMS/AmigaOS/MSDOS branches) and the rarer TLS/QUIC backend combinations can only be exercised by the matching CI job.
- The primary suite is 2,119 declarative test files in tests/data/, each an XML-like document with `<info><keywords>`, a `<reply>` server script, a `<client><command>` invocation, and a `<verify><protocol>` expectation — plus optional `<limits>` on allocation counts.
- `tests/runtests.pl` is the harness; its documented flags cover torture testing (`-t`), valgrind control, event-based runs, and keyword/feature selection such as `FTPS` or `!feat:proxy`, with per-test result flags (s/r/p/d/u/P/o/e/m/v/E) reported per run.
- Protocol servers are part of the repo, so tests need no external services: tests/server/ holds sws.c (HTTP), socksd.c, tftpd.c, mqttd.c, rtspd.c, dnsd.c and sockfilt.c, complemented by ftpserver.pl, http2-server.pl, http3-server.pl, sshserver.pl and dictserver.py.
- 80 unit tests under tests/unit/ exercise library internals directly (llist, hash, splay, strparse, urlapi, cookie parsing …) using the `UNITTEST_BEGIN_SIMPLE` macro and a `test_unitNNNN(const char *arg)` entrypoint.
- Tool-only code has its own unit directory, tests/tunit/, with 6 files named after the test number they back (tool1394, tool1604, tool1621, tool1622, tool1623, tool1720) — this is where `src/` helpers get direct coverage.
- 273 `libtest/libNNNN.c` programs plus a set of `cli_*.c` drivers (cli_h2_pausing, cli_hx_download, cli_ws_pingpong, cli_tls_session_reuse, cli_ev_download) test the public easy/multi/WebSocket APIs as real linked programs.
- The pytest suite in tests/http/ has 31 modules numbered by area — 01 basic, 02 download, 07 upload, 09 push, 10/13 proxy and proxy auth, 17 ssl use, 19 shutdown, 20 websockets, 22 HTTPSRR, 30-32 vsftpd, 40 socks, 50/51 scp+sftp, 60 h3 proxy — driven by `conftest.py` and a `testenv` package.
- Make targets select the mode: `tests/Makefile.am` maps `test`/`quiet-test`/`am-test`/`ci-test`/`full-test`/`nonflaky-test`/`torture-test`/`event-test` onto `$(TEST)` with different flag sets, and `ci-pytest` runs `pytest -n auto -v $(srcdir)/http`.
- Memory-behaviour is a first-class test axis: debug builds honour `CURL_MEMDEBUG` (log file) and `CURL_MEMLIMIT` (fail the Nth allocation) set up in `memory_tracking_init()`, with tests/memanalyze.pl and tests/memanalyzer.pm analysing the logs and test cases asserting allocation ceilings.
- Debug builds expose test-only hooks the suite depends on: `CURL_FORBID_REUSE`, `global->test_duphandle` (perform on a `curl_easy_duphandle()` copy), `global->test_event_based` (`curl_easy_perform_ev()` / libuv-driven `parallel_event()`), and `CURL_UPLOAD_SIZE`.
- Source-level checks run as their own CI workflow: `scripts/checksrc-all.pl` enforces the style guide, and the same job runs spellcheck, `scripts/cmakeopts.sh`, REUSE licence checks and other linters with `permissions: {}` and pinned action SHAs.
- Documentation is tested too: `make check` includes `check-docs` (docs/libcurl) and `examples` (docs/examples, 129 compilable C programs), with dedicated checkdocs.yml and checkurls.yml workflows.
Risk map
curl's risk profile is dominated by the fact that it parses hostile input (URLs, headers, server responses, redirect targets) in C, on every platform, across dozens of optional backends. The highest-attention paths are credential handling (.netrc, `-u`, redirects that must drop auth), filename derivation from server-controlled `Content-Disposition`, the redirect/protocol allowlist, and the TLS/QUIC backend glue. Structurally, the biggest fragility is combinatorial: ~50 `CURL_DISABLE_*` knobs times six TLS backends times two QUIC backends times platform `#ifdef`s means many buildable configurations that no single CI job exercises. Blast-radius hotspots are the few central files every protocol funnels through — lib/multi.c, lib/url.c, lib/sendf.c, lib/cfilters.c — where a regression is not protocol-local.
- Server-controlled filenames reach the filesystem: `parse_filename()` in tool_cb_hdr.c strips path components and CR/LF, but the reserved-name / invalid-character sanitizer `sanitize_file_name()` is compiled only `#if defined(_WIN32) || defined(MSDOS)`, so POSIX hardening rests entirely on those `strrchr('/')`/`strrchr('\\')` trims.
- `--output-dir` is applied by naive string concatenation (`curl_maprintf("%s/%s", config->output_dir, per->outfile)`) after the filename has been derived, so the safety of the combined path depends wholly on the earlier derivation step.
- The protocol allowlist is enforced in exactly one place — `url_set_conn_scheme()` checking `data->set.allowed_protocols` and, for redirects, `data->set.redir_protocols` — making it a single point of failure for `--proto`/`--proto-redir` and for redirect-to-`file://` style attacks.
- Credential-bearing code is spread across lib/creds.c, lib/netrc.c and lib/http.c's auth state machine (`Curl_http_output_auth`, `Curl_http_input_auth`, `http_perhapsrewind`, `Curl_http_auth_act`); the recent commit history shows this area actively hardened (e.g. keeping credentials out of `CURLINFO_REDIRECT_URL`).
- .netrc parsing is a hand-written lexer over an attacker-influenceable file with explicit hard caps — `MAX_NETRC_LINE` 16384, `MAX_NETRC_FILE` 128KB, `MAX_NETRC_TOKEN` 4096 — and a quoted-string state machine (`netrc_lexer_quoted`) that is worth re-reading before any change.
- Interactive password entry disables terminal echo by clearing the `ECHO` bit in `termios` and must restore it on every exit path; a failure here leaks a typed password to the screen and to scrollback.
- Several environment variables change behaviour without any command-line trace: `IPFS_GATEWAY`/`IPFS_PATH`/`HOME` in the tool, and `CURL_SSL_BACKEND` inside libcurl, which selects among compiled-in TLS backends at runtime.
- DoH creates additional easy handles that perform their own network transfers with their own option set (`ERROR_CHECK_SETOPT(CURLOPT_WRITEFUNCTION, doh_probe_write_cb)` etc.); any option not deliberately propagated to the probe is silently not applied to DNS traffic.
- Explicitly-experimental surface is documented as out of scope for security handling: the quiche QUIC backend and HTTP/3 proxy / CONNECT-UDP support are experimental, and docs/EXPERIMENTAL.md states vulnerabilities in experimental features are treated as ordinary bugs.
- Size is itself a risk in the hot files: lib/http.c is 5,112 lines, lib/multi.c 4,262, lib/ftp.c and lib/http2.c are next; `multi_runsingle()` alone dispatches thirteen states and is the path every protocol takes.
- The `CURL_DISABLE_*` matrix (documented across ~50 entries in docs/CURL-DISABLE.md, from `CURL_DISABLE_ALTSVC` to `CURL_DISABLE_NETRC`) means many code paths are conditionally compiled; `scripts/randdisable` exists precisely because the full matrix cannot be CI-covered.
- `Curl_fopen()` gets the durable-write pattern right (O_EXCL temp file, 0600, `fchmod` clone only when uid/gid match, then rename), so any new cache writer that bypasses it loses those properties — a coupling that is easy to miss.
Extension points
curl is extended by adding a record to a table, not by subclassing. New URI schemes are `struct Curl_scheme` records pointing at a `struct Curl_protocol` vtable; new transport behaviour is a `struct Curl_cftype` inserted into the filter chain; new response processing is a `struct Curl_cwtype` registered at a phase; new TLS/SSH/DNS backends are entries in a backend array guarded by a `USE_*` define. On the tool side, a new command-line option means one row in the alphabetical `aliases[]` table, one `C_*` enum value, one case in `getparameter()`, and one curldown page. Several of these tables are additionally mirrored by generated files or documentation that CI verifies.
- Adding a protocol means adding a `Curl_scheme_*` record in lib/protocol.c, externing it in lib/protocol.h, teaching the length-bucketed lookup functions about the new name, and supplying a `struct Curl_protocol` vtable; docs/internals/NEW-PROTOCOL.md documents the social and technical bar for that.
- The `struct Curl_protocol` vtable is the per-scheme hook set: setup_connection, connect, connecting, do_it, doing, done, proto_getsock/doing_pollset/domore_pollset/perform_pollset, disconnect, write_resp, write_resp_hd, connection_is_dead, attach, and follow.
- PROTOPT_* flags are the declarative knobs a new scheme uses to opt into behaviour — SSL, DUAL (two connections), NEEDSPWD, NOURLQUERY, WILDCARD, CREDSPERREQUEST, PROXY_AS_HTTP, CONN_REUSE, SSL_REUSE, NO_TRANSFER, HTTP_PROXY_TUNNEL — rather than special-casing the scheme in shared code.
- New transport layers plug in as `struct Curl_cftype` instances; `Curl_cf_setup_add()` / `cf_setup_create()` build the base filter and the `cf_setup_add_*` helpers decide insertion order, so a new filter is added by writing one more `cf_setup_add_*` step.
- Response-processing extensions register as `struct Curl_cwtype` with a `CURL_CW_*` phase; `cwriter_add()` inserts each writer first within its phase, so ordering is declared rather than coded.
- Content and transfer decodings are two explicit registries — `general_unencoders[]` and `transfer_unencoders[]` — resolved by name in `find_unencode_writer()`, which is where a new `Content-Encoding` would be added alongside deflate/gzip/brotli/zstd/identity.
- Upload-side extensions use the mirrored client-reader registry (`struct Curl_crtype` with `CURL_CR_*` phases); HTTP's Expect: 100-continue handling is implemented this way via `http_exp100_add_reader()`.
- A new TLS backend is a `struct Curl_ssl` added to `available_backends[]` behind a `USE_*` define; `curl_global_sslset()` and the `CURL_SSL_BACKEND` lookup both iterate that same array, so no other file needs to change to make it selectable.
- Resolver strategies are swappable at build time behind a common `lib/vdns/asyn.h` interface with three implementations — c-ares (`asyn-ares.c`), threaded (`asyn-thrdd.c`) and the shared base (`asyn-base.c`) — plus `cf-dns.c` exposing resolution as a connection filter.
- SSH has two interchangeable backends selected at build time (libssh2 and libssh) behind lib/vssh/vssh.h, following the same one-directory-per-backend-family convention as vtls and vquic.
- Adding a CLI option touches the alphabetically-sorted `aliases[]` table (the comment above it is explicit: "this array MUST be alphasorted based on the 'lname'"), the `C_*` id enum, a case in `getparameter()`, and the generated help in src/tool_listhelp.c.
- New `--write-out` variables are one row in `variables[]` mapping a name to a `VAR_*` id, a `CURLINFO_*` id, and a writer function (`writeString`/`writeLong`/`writeOffset`/`writeTime`), with NULL signalling a special form handled inline.
Contributor handoff
Treat this repo as two coupled codebases with one very strict style contract. Read the internals docs before the code — docs/internals/ explains the four architectural ideas (connection filters, client writers, client readers, the multi state machine) that the code assumes you already know. Style is machine-enforced: `scripts/checksrc.pl` runs in CI and by default in debug builds, and a style violation fails the build before your logic is ever reviewed. Anything user-visible needs a matching curldown doc and usually a test case, and several files are generated and must not be hand-edited. The safest edits are additive table rows; the riskiest are changes to lib/multi.c, lib/url.c, lib/sendf.c, or lib/cfilters.c, which every protocol depends on.
- Read first, in this order: docs/internals/README.md and the four core internals docs (CONNECTION-FILTERS.md, CLIENT-WRITERS.md, CLIENT-READERS.md, MID.md), then src/tool_main.c and src/tool_operate.c for the tool, then lib/easy.c and lib/multi.c for the library.
- Style is non-negotiable and automated: docs/internals/CODE_STYLE.md is enforced by `scripts/checksrc.pl`, invoked via `make checksrc`, run automatically after `./configure --enable-debug`, and gated in CI by `scripts/checksrc-all.pl`.
- `make lint` is the full pre-push check: it runs `scripts/badwords-all`, `make checksrc` across lib/src/tests/include/examples/projects, and `scripts/spacecheck.pl`.
- Do not hand-edit generated files: src/tool_hugehelp.c is produced by src/mkhelp.pl from the manpage during the build, and src/tool_listhelp.c is likewise generated option-help data.
- Docs are written in curldown (a markdown dialect with a YAML-ish front matter carrying `Title`, `Section`, `Added-in`, `See-also`), documented in docs/CURLDOWN.md and converted by scripts/managen; the checkdocs workflow validates them.
- Adding a test means creating tests/data/testNNNN in the XML-like format and adding it to the numerically-ordered `TESTCASES` list in tests/data/Makefile.am — the file is not picked up automatically.
- To run tests: `make test` (quiet) from the top, or drive the harness directly with `TFLAGS` — `cd tests && ./runtests.pl 1 to 200`, keyword selection like `FTPS` or `!feat:proxy`, `-t` for torture, and `make pytest` for the HTTP/2-3 python suite.
- Public ABI is a hard boundary: include/curl/curlver.h carries the version macros, docs/libcurl/ABI.md states the compatibility promise, and docs/libcurl/symbols-in-versions records when every symbol appeared — new symbols must be added there.
- Any new code must still compile with features disabled: wrap protocol/feature-specific code in the relevant `CURL_DISABLE_*` guard as documented in docs/CURL-DISABLE.md, and use `scripts/randdisable` to sanity-check random combinations.
- Both build systems must be updated together: file lists live in lib/Makefile.inc and src/Makefile.inc for autotools and in lib/CMakeLists.txt / src/CMakeLists.txt for CMake, and configure-vs-cmake.yml exists to catch divergence.
- Do not hand-write RELEASE-NOTES entries: it is maintained by scripts/release-notes.pl and the release flow in docs/RELEASE-PROCEDURE.md (the most recent commit on this branch is literally "RELEASE-NOTES: synced").
- Never file a suspected vulnerability as a normal PR or issue: SECURITY.md and docs/VULN-DISCLOSURE-POLICY.md require private reporting through the curl disclosure process.
Languages
| Language | Files | Share of source |
|---|---|---|
| C | 1043 | 92% |
| Markdown | 930 | docs/config |
| Python | 51 | 5% |
| Shell | 30 | 3% |
| YAML | 30 | docs/config |
| C++ | 2 | 0% |
| TOML | 2 | 0% |
| JSON | 1 | docs/config |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (393 files) src/ (96 files)
Repository structure
| Top-level path | Files |
|---|---|
tests/ | 2178 |
docs/ | 1070 |
lib/ | 393 |
src/ | 96 |
projects/ | 74 |
.github/ | 53 |
scripts/ | 40 |
CMake/ | 31 |
Docs found: READMEREADME.mddocs/ALTSVC.mddocs/BINDINGS.mddocs/BUG-BOUNTY.mddocs/BUGS.mddocs/CIPHERS-TLS12.mddocs/CIPHERS.mddocs/CODE_OF_CONDUCT.mddocs/CODE_REVIEW.md
CI & config entrypoints
README.md— Repository readme.github/workflows/appveyor-status.yml— GitHub Actions workflow.github/workflows/checkdocs.yml— GitHub Actions workflow.github/workflows/checksrc.yml— GitHub Actions workflow.github/workflows/checkurls.yml— GitHub Actions workflow.github/workflows/codeql.yml— GitHub Actions workflow.github/workflows/configure-vs-cmake.yml— GitHub Actions workflow.github/workflows/curl-for-win.yml— GitHub Actions workflow.github/workflows/distcheck.yml— GitHub Actions workflow.github/workflows/http3-linux.yml— GitHub Actions workflow
Structural health signals
Average score 10/10 across 0 analyzed source files; 0 hotspots surfaced.
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
c362f034692e2026-09-22 — RELEASE-NOTES: synced53be819ca8292026-09-21 — escape: use hex-pair lookup in curl_easy_escape7934b5400f3e2026-09-18 — capsule: reject a capsule that can never be buffered3aa67d5e49b92026-09-21 — digest: quote the digest-uri param as wellce359773039d2026-09-21 — getinfo: make sure CURLINFO_REDIRECT_URL does not contain creds