Skip to content

Commit f52f4b9

Browse files
authored
Don't serialize redacted headers (#742)
This required quite a lot of refactoring to clarify what's actually going on. It should now be clearer that there is a redacted sentinel value which is used to nicely format, print, and str redacted values that might be exposed to the user (I don't fully recall why I added this but I suspect it's because we only want to apply cli formatting at the exact time when the value is displayed). This code now lives in `utils-redacted.R`. I also clarified the type of the headers object — the componets can either be an atomic vector or a weakref. And since redacted components now have their own type, we no longer need the `redact` attribute. Fixes #721
1 parent e8fea4e commit f52f4b9

20 files changed

+238
-97
lines changed

NAMESPACE

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ S3method("$",httr2_headers)
44
S3method("[",httr2_headers)
55
S3method("[[",httr2_headers)
66
S3method(close,httr2_response)
7-
S3method(format,httr2_redacted)
7+
S3method(format,httr2_redacted_sentinel)
88
S3method(print,httr2_cmd)
99
S3method(print,httr2_headers)
1010
S3method(print,httr2_json)
1111
S3method(print,httr2_oauth_client)
1212
S3method(print,httr2_obfuscated)
13+
S3method(print,httr2_redacted_sentinel)
1314
S3method(print,httr2_request)
1415
S3method(print,httr2_response)
1516
S3method(print,httr2_token)
1617
S3method(print,httr2_url)
1718
S3method(str,httr2_headers)
1819
S3method(str,httr2_obfuscated)
19-
S3method(str,httr2_redacted)
20+
S3method(str,httr2_redacted_sentinel)
2021
export("%>%")
2122
export(curl_help)
2223
export(curl_translate)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# httr2 (development version)
22

3+
* Redacted headers are no longer serialized to disk. This is important since it makes it harder to accidentally leak secrets to files on disk, but comes at a cost: you can longer perform such requests that have been saved and reloaded (#721).
34
* New `req_get_method()` and `req_get_body()` allow you to do some limited prediction of what a request _will_ do when it's performed (#718).
45
* Functions that capture interrutps (like `req_perform_parallel()` and friends) are now easier to escape if they're called inside a loop: you can press Ctrl + C twice to guarantee an exit (#1810).
56
* New `last_request_json()` and `last_response_json()` to conveniently see JSON bodies (#734).

R/headers.R

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1-
as_headers <- function(x, redact = character(), error_call = caller_env()) {
2-
if (is.character(x) || is.raw(x)) {
1+
as_headers <- function(
2+
x,
3+
redact = character(),
4+
lifespan,
5+
error_call = caller_env()
6+
) {
7+
if (is.list(x)) {
8+
new_headers(
9+
x,
10+
redact = redact,
11+
lifespan = lifespan,
12+
error_call = error_call
13+
)
14+
} else if (is.character(x) || is.raw(x)) {
315
parsed <- curl::parse_headers(x)
416
valid <- parsed[grepl(":", parsed, fixed = TRUE)]
517
halves <- parse_in_half(valid, ":")
618

719
headers <- set_names(trimws(halves$right), halves$left)
8-
new_headers(as.list(headers), redact = redact, error_call = error_call)
9-
} else if (is.list(x)) {
10-
new_headers(x, redact = redact, error_call = error_call)
20+
new_headers(
21+
as.list(headers),
22+
redact = redact,
23+
lifespan = lifespan,
24+
error_call = error_call
25+
)
1126
} else {
1227
cli::cli_abort(
1328
"{.arg headers} must be a list, character vector, or raw.",
@@ -16,15 +31,34 @@ as_headers <- function(x, redact = character(), error_call = caller_env()) {
1631
}
1732
}
1833

19-
new_headers <- function(x, redact = character(), error_call = caller_env()) {
34+
new_headers <- function(
35+
x,
36+
redact = character(),
37+
lifespan,
38+
error_call = caller_env()
39+
) {
2040
if (!is_list(x)) {
2141
cli::cli_abort("{.arg x} must be a list.", call = error_call)
2242
}
2343
if (length(x) > 0 && !is_named(x)) {
2444
cli::cli_abort("All elements of {.arg x} must be named.", call = error_call)
2545
}
46+
for (i in seq_along(x)) {
47+
if (!is.atomic(x[[i]]) && !is_weakref(x[[i]])) {
48+
cli::cli_abort(
49+
c(
50+
"Each element of {.arg x} must be an atomic vector or a weakref.",
51+
i = "{.arg x[[{i}]]} is {obj_type_friendly(x[[i]])}."
52+
),
53+
call = error_call
54+
)
55+
}
56+
}
2657

27-
structure(x, redact = redact, class = "httr2_headers")
58+
needs_redact <- !is_redacted(x) & (tolower(names(x)) %in% tolower(redact))
59+
x[needs_redact] <- lapply(x[needs_redact], \(x) new_weakref(lifespan, x))
60+
61+
structure(x, class = "httr2_headers")
2862
}
2963

3064
#' @export
@@ -36,64 +70,18 @@ print.httr2_headers <- function(x, ..., redact = TRUE) {
3670

3771
show_headers <- function(x, redact = TRUE) {
3872
if (length(x) > 0) {
39-
vals <- lapply(headers_redact(x, redact), format)
73+
vals <- lapply(headers_flatten(x, redact), format)
4074
cli::cat_line(cli::style_bold(names(x)), ": ", vals)
4175
}
4276
}
4377

4478
#' @export
4579
str.httr2_headers <- function(object, ..., no.list = FALSE) {
46-
object <- unclass(headers_redact(object))
80+
object <- unclass(headers_flatten(object))
4781
cat(" <httr2_headers>\n")
4882
utils::str(object, ..., no.list = TRUE)
4983
}
5084

51-
headers_redact <- function(x, redact = TRUE) {
52-
if (!redact) {
53-
x
54-
} else {
55-
to_redact <- attr(x, "redact")
56-
attr(x, "redact") <- NULL
57-
58-
list_redact(x, to_redact, case_sensitive = FALSE)
59-
}
60-
}
61-
62-
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
63-
headers_flatten <- function(x) {
64-
is_redacted <- map_lgl(x, is_redacted)
65-
x[!is_redacted] <- lapply(x[!is_redacted], paste, collapse = ",")
66-
x
67-
}
68-
69-
list_redact <- function(x, names, case_sensitive = TRUE) {
70-
if (case_sensitive) {
71-
i <- match(names, names(x))
72-
} else {
73-
i <- match(tolower(names), tolower(names(x)))
74-
}
75-
x[i] <- list(redacted())
76-
x
77-
}
78-
79-
redacted <- function() {
80-
structure(list(NULL), class = "httr2_redacted")
81-
}
82-
83-
#' @export
84-
format.httr2_redacted <- function(x, ...) {
85-
cli::col_grey("<REDACTED>")
86-
}
87-
#' @export
88-
str.httr2_redacted <- function(object, ...) {
89-
cat(" ", cli::col_grey("<REDACTED>"), "\n", sep = "")
90-
}
91-
92-
is_redacted <- function(x) {
93-
inherits(x, "httr2_redacted")
94-
}
95-
96-
9785
#' @export
9886
`[.httr2_headers` <- function(x, i, ...) {
9987
if (is.character(i)) {
@@ -116,3 +104,32 @@ is_redacted <- function(x) {
116104
i <- match(tolower(name), tolower(names(x)))
117105
x[[i]]
118106
}
107+
108+
is_redacted <- function(x) {
109+
map_lgl(x, is_weakref)
110+
}
111+
which_redacted <- function(x) {
112+
names(x)[is_redacted(x)]
113+
}
114+
115+
# Flatten headers object into a list suitable either for display (if redacted)
116+
# or passing to curl (if not redacted).
117+
headers_flatten <- function(x, redact = TRUE) {
118+
is_redacted <- is_redacted(x)
119+
120+
out <- vector("list", length(x))
121+
names(out) <- names(x)
122+
123+
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
124+
out[!is_redacted] <- lapply(x[!is_redacted], paste, collapse = ",")
125+
126+
if (redact) {
127+
out[is_redacted] <- list(redacted_sentinel())
128+
} else {
129+
out[is_redacted] <- lapply(x[is_redacted], wref_value)
130+
# need to strip serialized weakrefs that now yield NULL
131+
out <- compact(out)
132+
}
133+
134+
out
135+
}

R/req-dry-run.R

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,21 @@ req_dry_run <- function(
6363
handle <- req_handle(req)
6464
curl::handle_setopt(handle, url = req$url)
6565
resp <- curl::curl_echo(handle, progress = FALSE)
66+
headers <- new_headers(
67+
as.list(resp$headers),
68+
redact = which_redacted(req$headers),
69+
lifespan = current_env()
70+
)
6671

6772
if (!quiet) {
6873
cli::cat_line(resp$method, " ", resp$path, " HTTP/1.1")
6974

70-
headers <- new_headers(as.list(resp$headers), attr(req$headers, "redact"))
7175
if (testing_headers) {
7276
# curl::curl_echo() overrides
7377
headers$host <- NULL
7478
headers$`content-length` <- NULL
7579
}
76-
77-
show_headers(headers)
80+
show_headers(headers, redact = redact_headers)
7881
cli::cat_line()
7982
show_body(resp$body, headers$`content-type`, pretty_json = pretty_json)
8083
}
@@ -83,7 +86,7 @@ req_dry_run <- function(
8386
method = resp$method,
8487
path = resp$path,
8588
body = resp$body,
86-
headers = as.list(resp$headers)
89+
headers = headers_flatten(headers, redact = redact_headers)
8790
))
8891
}
8992

R/req-headers.R

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,8 @@ req_headers <- function(.req, ..., .redact = NULL) {
6767
check_header_values(...)
6868

6969
headers <- modify_list(.req$headers, ..., .ignore_case = TRUE)
70-
71-
redact <- union(.redact, "Authorization")
72-
redact <- redact[tolower(redact) %in% tolower(names(headers))]
73-
redact <- sort(union(redact, attr(.req$headers, "redact")))
74-
75-
.req$headers <- new_headers(headers, redact)
70+
redact <- c("Authorization", .redact, which_redacted(.req$headers))
71+
.req$headers <- new_headers(headers, redact, lifespan = .req$state)
7672

7773
.req
7874
}

R/req-perform.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,10 @@ req_handle <- function(req) {
238238

239239
handle <- curl::new_handle()
240240
curl::handle_setopt(handle, url = req$url)
241-
curl::handle_setheaders(handle, .list = headers_flatten(req$headers))
241+
curl::handle_setheaders(
242+
handle,
243+
.list = headers_flatten(req$headers, redact = FALSE)
244+
)
242245
curl::handle_setopt(handle, .list = req$options)
243246
if (length(req$fields) > 0) {
244247
curl::handle_setform(handle, .list = req$fields)

R/req-verbose.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ req_verbose <- function(
5858
} else if (header_resp && type == 1) {
5959
verbose_header("<- ", msg)
6060
} else if (header_req && type == 2) {
61-
to_redact <- attr(headers, "redact")
61+
to_redact <- which_redacted(headers)
6262
verbose_header("-> ", msg, redact_headers, to_redact = to_redact)
6363
} else if (body_resp && type == 3) {
6464
# handled in handle_resp()
@@ -86,7 +86,8 @@ verbose_header <- function(prefix, x, redact = TRUE, to_redact = NULL) {
8686

8787
for (line in lines) {
8888
if (grepl("^[-a-zA-z0-9]+:", line)) {
89-
header <- headers_redact(as_headers(line, to_redact), redact)
89+
headers <- as_headers(line, to_redact, lifespan = current_env())
90+
header <- headers_flatten(headers, redact)
9091
cli::cat_line(
9192
prefix,
9293
cli::style_bold(names(header)),

R/req.R

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ print.httr2_request <- function(x, ..., redact_headers = TRUE) {
2727
method <- toupper(req_get_method(x))
2828
cli::cli_text("{.strong {method}} {x$url}")
2929

30-
bullets_with_header(
31-
"Headers:",
32-
headers_flatten(headers_redact(x$headers, redact_headers))
33-
)
30+
bullets_with_header("Headers:", headers_flatten(x$headers, redact_headers))
3431
cli::cli_text("{.strong Body}: {req_body_info(x)}")
3532
bullets_with_header("Options:", x$options)
3633
bullets_with_header("Policies:", x$policies)

R/utils-redacted.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
redacted_sentinel <- function() {
2+
structure(list(NULL), class = "httr2_redacted_sentinel")
3+
}
4+
#' @export
5+
print.httr2_redacted_sentinel <- function(x, ...) {
6+
cat(format(x), "\n", sep = "")
7+
invisible(x)
8+
}
9+
#' @export
10+
format.httr2_redacted_sentinel <- function(x, ...) {
11+
unclass(cli::col_grey("<REDACTED>"))
12+
}
13+
#' @export
14+
str.httr2_redacted_sentinel <- function(object, ...) {
15+
cat(" ", cli::col_grey("<REDACTED>"), "\n", sep = "")
16+
}
17+
18+
list_redact <- function(x, names) {
19+
x[names(x) %in% names] <- list(redacted_sentinel())
20+
x
21+
}
22+
23+
is_redacted_sentinel <- function(x) {
24+
inherits(x, "httr2_redacted_sentinel")
25+
}

R/utils.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ bullets <- function(x) {
1616
format(x)
1717
}
1818
} else {
19-
if (is_redacted(x)) {
19+
if (is_redacted_sentinel(x)) {
2020
format(x)
2121
} else {
2222
paste0("<", class(x)[[1L]], ">")

0 commit comments

Comments
 (0)