Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
^\.lintr$
^air\.toml$
^LICENSE\.md$
^\.github$
1 change: 1 addition & 0 deletions .github/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.html
50 changes: 50 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:

name: R-CMD-check.yaml

permissions: read-all

jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}

name: ${{ matrix.config.os }} (${{ matrix.config.r }})

strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes

steps:
- uses: actions/checkout@v6

- uses: r-lib/actions/setup-pandoc@v2

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check

- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Type: Package
Package: serverHeaders
Title: Server Header Summary
Version: 0.1.15
Version: 0.1.16
Authors@R:
person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre"))
Description: Checks and summarises the server headers for a given URL.
Expand Down
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# serverHeaders 0.1.16

## Bug fixes and minor improvements

* Every `header_summary()` method now has offline tests, and live tests skip when offline or on CI.
* R CMD check runs on GitHub Actions for pushes to main and pull requests.

# serverHeaders 0.1.15

## Bug fixes and minor improvements
Expand Down
4 changes: 4 additions & 0 deletions tests/testthat/helper-header_summary.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Summarise a header value as get_response_headers() would pass it
summarise_header = function(value, header) {
header_summary(structure(value, class = header))
}
20 changes: 10 additions & 10 deletions tests/testthat/test-check.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
test_that("Testing check", {
x = check("jumpingrivers.com")
expect_true(any(grepl(200, x$status_codes)))
expect_equal(ncol(x$headers), 6)
expect_gt(nrow(x$headers), 18)

x = check("https://report-uri.com/")
expect_equal(x$status_codes, 200)
expect_equal(ncol(x$headers), 6)
expect_gt(nrow(x$headers), 18)
test_that("check() returns status codes and a header summary", {
skip_if_offline()
skip_on_ci()
x = suppressMessages(check("jumpingrivers.com"))
expect_equal(x$status_codes[length(x$status_codes)], 200)
expect_named(
x$headers,
c("header", "status", "message", "value", "documentation", "primary_header")
)
expect_true(all(c("ssl", "ssl-redirection", "strict-transport-security") %in% x$headers$header))
})
17 changes: 12 additions & 5 deletions tests/testthat/test-check_server_headers.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
test_that("Is redirected works", {
res = httr::GET("neverssl.com")
expect_false(is_redirected(res))
res = httr::GET("google.com")
expect_true(is_redirected(res))
expect_true(is.data.frame(check("http://httpforever.com/")$headers))
response = function(status, version = "HTTP/1.1") {
list(all_headers = purrr::map2(status, version, \(s, v) list(status = s, version = v)))
}
expect_false(is_redirected(response(200)))
expect_true(is_redirected(response(c(301, 200))))
expect_true(is_redirected(response(200, "HTTP/2")))
})

test_that("check() copes with a site that has no https", {
skip_if_offline()
skip_on_ci()
expect_true(is.data.frame(suppressMessages(check("http://httpforever.com/"))$headers))
})

fake_response = function(type, body) {
Expand Down
9 changes: 6 additions & 3 deletions tests/testthat/test-check_ssl_functionality.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ test_that("SSL errors report the real error and keep the value", {
})

test_that("Sites without an SSL certificate get a WARN", {
skip_if_offline()
# neverssl.com never serves https
ssl = suppressMessages(check_ssl("neverssl.com"))
# What curl reports for a site that doesn't serve https
local_mocked_bindings(
HEAD = function(...) stop("SSL connect error"),
.package = "httr"
)
ssl = suppressMessages(check_ssl("no-https.example.com"))
expect_equal(ssl$status, "WARN")
expect_equal(ssl$message, "SSL incorrect")
expect_false(is.na(ssl$value))
Expand Down
12 changes: 7 additions & 5 deletions tests/testthat/test-check_status_code.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
test_that("Test Status codes", {
status_codes = check_status_code("jumpingrivers.com")
expect_true(any(grepl(200, status_codes)))
status_codes = check_status_code("jumpingrivers.com/bad-page")
expect_true(any(grepl(404, status_codes)))
test_that("Status codes follow each redirect", {
skip_if_offline()
skip_on_ci()
status_codes = suppressMessages(check_status_code("jumpingrivers.com"))
expect_equal(status_codes[length(status_codes)], 200)
status_codes = suppressMessages(check_status_code("jumpingrivers.com/bad-page"))
expect_equal(status_codes[length(status_codes)], 404)
})
80 changes: 80 additions & 0 deletions tests/testthat/test-header_summary.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,83 @@ test_that("Header values are compared case-insensitively", {
expect_equal(xss("1;mode=block")$status, "OK")
expect_equal(xss("1")$status, "WARN")
})

test_that("Unknown headers are reported but not judged", {
out = summarise_header("abc", "x-made-up")
expect_equal(out$header, "x-made-up")
expect_equal(out$status, "UNKNOWN")
expect_equal(out$value, "abc")
})

test_that("The scheme must be https", {
expect_equal(summarise_header("https", "scheme")$status, "OK")
expect_equal(summarise_header("http", "scheme")$status, "WARN")
})

test_that("Content-Type needs a charset", {
expect_equal(summarise_header("text/html; charset=utf-8", "content-type")$status, "OK")
expect_equal(summarise_header("text/html", "content-type")$status, "WARN")
})

test_that("Policy headers are noted when present", {
policies = c(
"report-to",
"nel",
"content-security-policy",
"content-security-policy-report-only",
"permissions-policy"
)
for (policy in policies) {
out = summarise_header("some-policy", policy)
expect_equal(out$status, "OK", label = policy)
expect_equal(out$value, "some-policy", label = policy)
}
expect_equal(summarise_header("*", "access-control-allow-origin")$status, "NOTE")
expect_match(summarise_header("nginx", "server")$message, "nginx")
})

test_that("X-Frame-Options allows deny or sameorigin only", {
expect_equal(summarise_header("DENY", "x-frame-options")$status, "OK")
expect_equal(summarise_header("sameorigin", "x-frame-options")$status, "OK")
allow = summarise_header("allow-from https://example.com", "x-frame-options")
expect_equal(allow$status, "WARN")
expect_match(allow$message, "Other domains")
expect_match(summarise_header("yes", "x-frame-options")$message, "not found")
})

test_that("X-Permitted-Cross-Domain-Policies needs a known value", {
expect_equal(summarise_header("None", "x-permitted-cross-domain-policies")$status, "OK")
expect_equal(summarise_header("everything", "x-permitted-cross-domain-policies")$status, "WARN")
})

test_that("Headers that leak or are deprecated get a WARN", {
expect_equal(summarise_header("PHP/8.2", "x-powered-by")$status, "WARN")
expect_match(summarise_header("pin-sha256=x", "public-key-pins")$message, "deprecated")
expect_match(summarise_header("max-age=86400", "expect-ct")$message, "deprecated")
})

test_that("Cookies must all use the Secure flag", {
expect_equal(summarise_header(logical(0), "cookies")$message, "No cookies detected")
expect_equal(summarise_header(c(TRUE, TRUE), "cookies")$status, "OK")
expect_equal(summarise_header(c(TRUE, FALSE), "cookies")$status, "WARN")
})

test_that("Redirection is reported", {
expect_equal(summarise_header(TRUE, "redirection")$status, "OK")
expect_equal(summarise_header(FALSE, "redirection")$status, "WARN")
})

test_that("Subresource Integrity is checked on script tags", {
sri = function(value) summarise_header(value, "subresource-integrity")
expect_equal(sri(list())$status, "OK")

hashed = c(
src = "https://cdn.example.com/a.js",
integrity = "sha384-x",
crossorigin = "anonymous"
)
expect_equal(sri(list(hashed))$status, "OK")

no_crossorigin = c(src = "https://cdn.example.com/b.js", integrity = "sha384-y")
expect_equal(sri(list(hashed, no_crossorigin))$status, "WARN")
})
Loading