Compare commits

..
Author SHA1 Message Date
clawbot c7696b4c11 Core Rule Set: Ghost's remaining fields pass with words that name commands (closes #359)
check / check (push) Waiting to run
Ghost 6.76.0 was sent the fields not yet recorded with python3,
powershell, ls -la and .htaccess. Fixes in ghost.conf:

- eight rules that refused words naming commands and files, 932235 among
  them, for staff profiles, setup, invitations, tiers, offers,
  recommendations, integrations, webhooks, feature images, comments, the
  CSV import's labels, ActivityPub, and source and the utm_ fields;
- 932235 for the existing plain-text fields and code injection;
- filter skips six more rules, for title:~'powershell & python3';
- pathname skips 930120, 932120 and 932260;
- invitation addresses, email_segment, a tag's canonical_url, and
  ActivityPub's handles, actorUri and domain;
- a tag's code injection skips what a post's does.

Model: opus-5-5
2026-11-12 09:14:50 +00:00
clawbot 101cc0aba5 Core Rule Set: a Ghost post's or page's canonical URL passes when it names a command or a file (closes #374)
check / check (push) Waiting to run
While bodies are read, 932120 refused the canonical URL
`https://example.com/blog/powershell/` and 930120 refused
`https://example.com/blog/.htaccess/`. The new exclusions 900383 and 900384
switch both rules off for `posts.N.canonical_url` under
`/ghost/api/admin/posts/` and `pages.N.canonical_url` under
`/ghost/api/admin/pages/`, as `url` does, and only when the query has no field
of that name. The recorded requests gain the saves the Ghost 6.76.0 admin
sends when those addresses are set in a post's and a page's settings.

Model: opus-5-5
2026-11-12 09:13:25 +01:00
clawbot c4bf76c966 rproxy test: send the body over its limit without Connection: close (closes #371)
check / check (push) Waiting to run
visit's client sends Connection: close, and Go's server then closes the
connection after the 413 without reading the rest of the body. The
visitor, still sending, could see the reset before the answer. The test
now sends the request on its own connection without that header, so Go's
server reads the rest before it closes.

Model: opus-5-5
2026-11-12 07:44:01 +01:00
5 changed files with 183 additions and 55 deletions

No files matched your search

+18 -7
View File
@@ -1762,8 +1762,10 @@ These query fields skip some rules:
the Admin API; the Admin API;
- `url`, the address the editor looks up for an embed or a bookmark, skips - `url`, the address the editor looks up for an embed or a bookmark, skips
930120 and 932120; 930120 and 932120;
- `pathname`, `source` and the `utm_` fields, which filter the analytics, skip - `source` and the `utm_` fields, which filter the analytics, skip those eight
those eight rules under `/ghost/api/admin/stats/`; rules under `/ghost/api/admin/stats/`;
- `pathname`, the page the analytics show, skips 930120, 932120 and 932260
there;
- `email_segment`, the members a post is sent to as a newsletter, skips 932120 - `email_segment`, the members a post is sent to as a newsletter, skips 932120
on the Admin API's posts; on the Admin API's posts;
- `query`, a search for an account in ActivityPub, skips those eight rules; - `query`, a search for an account in ActivityPub, skips those eight rules;
@@ -1808,17 +1810,25 @@ sent as `username` when signing in, and as `email` in these requests:
- inviting a staff user; - inviting a staff user;
- accepting an invitation. - accepting an invitation.
In ActivityPub, an account's handle skips 930120 and 932120. The domain of the In ActivityPub, an account's handle and its address skip 930120 and 932120. The
site's handle skips 930120, 932120 and 932260. Each field skips its rules only domain of the site's handle skips 930120, 932120 and 932260. Each field skips
on its own path: its rules only on its own path:
- `sourceHandle`, an account that moves here, at - `sourceHandle`, an account that moves here, when it is added at
`/.ghost/activitypub/v1/aliases`; `/.ghost/activitypub/v1/aliases`;
- `actorUri`, that account's address, when the admin removes it there;
- `targetHandle`, the account this one moves to, at - `targetHandle`, the account this one moves to, at
`/.ghost/activitypub/v1/migration`; `/.ghost/activitypub/v1/migration`;
- `domain`, saved at `/.ghost/activitypub/v1/domain` and checked at - `domain`, saved at `/.ghost/activitypub/v1/domain` and checked at
`/.ghost/activitypub/v1/domain/validate`. `/.ghost/activitypub/v1/domain/validate`.
The `canonical_url` of a post, a page or a tag skips 930120 and 932120, as `url`
does. Each skips them only on its own path:
- a post's under `/ghost/api/admin/posts/`;
- a page's under `/ghost/api/admin/pages/`;
- a tag's under `/ghost/api/admin/tags/`.
The articles carry HTML on purpose. These fields on the Admin API hold them: The articles carry HTML on purpose. These fields on the Admin API hold them:
- `lexical`, which the editor saves, and `mobiledoc` and `html`, which - `lexical`, which the editor saves, and `mobiledoc` and `html`, which
@@ -1864,7 +1874,8 @@ field above. Script injection is too, outside the articles and code injection.
`; wget http://example.com/x.sh` is still refused in these fields: `; wget http://example.com/x.sh` is still refused in these fields:
- the addresses, the handles and the domain; - the addresses, the handles and the domain;
- `search`, `url`, `email_segment` and `email`. - the `canonical_url` of posts, pages and tags;
- `search`, `url`, `pathname`, `email_segment` and `email`.
The other fields above skip 932235, which refuses that command but also refuses The other fields above skip 932235, which refuses that command but also refuses
`powershell & python3`. `powershell & python3`.
+63 -21
View File
@@ -57,25 +57,35 @@ SecRule REQUEST_FILENAME "@streq /ghost/api/admin/oembed/" \
ctl:ruleRemoveTargetById=930120;ARGS:url,\ ctl:ruleRemoveTargetById=930120;ARGS:url,\
ctl:ruleRemoveTargetById=932120;ARGS:url" ctl:ruleRemoveTargetById=932120;ARGS:url"
# The query fields of the analytics pages skip the eight rules that 900353 # The query fields that filter the analytics skip the eight rules that 900353
# lists, on /ghost/api/admin/stats/: # lists, on /ghost/api/admin/stats/:
# 930120: pathname=/.htaccess
# 932120: source=powershell.example.com # 932120: source=powershell.example.com
# 932235, 932260: utm_campaign=powershell & python3 # 932235, 932260: utm_campaign=powershell & python3
# 932250: utm_term=ls -la # 932250: utm_term=ls -la
# These fields filter the figures by a page, the site a visit came from, or a # These fields filter the figures by the site a visit came from, or a
# campaign: pathname, source, utm_source, utm_medium, utm_campaign, # campaign: source, utm_source, utm_medium, utm_campaign, utm_content and
# utm_content and utm_term. # utm_term.
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/stats/" \ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/stats/" \
"id:900367,phase:1,pass,nolog,\ "id:900367,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=930120;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932120;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932125;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932230;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932235;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932235;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932250;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932260;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/" ctl:ruleRemoveTargetById=932380;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/"
# pathname, the page the analytics show, skips three rules on
# /ghost/api/admin/stats/, which refused the path of a post and of tag pages:
# 930120: pathname=/tag/proc/
# 932120: pathname=/python3-and-powershell/
# 932260: pathname=/tag/python3/
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/stats/" \
"id:900385,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:pathname,\
ctl:ruleRemoveTargetById=932120;ARGS:pathname,\
ctl:ruleRemoveTargetById=932260;ARGS:pathname"
# email_segment, the members a post is sent to as a newsletter, skips 932120 # email_segment, the members a post is sent to as a newsletter, skips 932120
# on the Admin API's posts: email_segment=label:powershell-users. # on the Admin API's posts: email_segment=label:powershell-users.
@@ -299,22 +309,27 @@ SecRule REQUEST_FILENAME "@streq /ghost/api/admin/authentication/invitation/" \
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.invitation\.\d+\.email$/,\ ctl:ruleRemoveTargetById=932120;ARGS:/^json\.invitation\.\d+\.email$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.invitation\.\d+\.email$/" ctl:ruleRemoveTargetById=932260;ARGS:/^json\.invitation\.\d+\.email$/"
# In ActivityPub, an account's handle skips 930120 and 932120, which refused # In ActivityPub, an account's handle and its address skip 930120 and 932120.
# @erin.htaccess@example.com and @powershell@example.com. A handle starts with # They refused the handles @erin.htaccess@example.com and
# an @, so 932260 does not refuse @python3.dev@example.com. The domain of the # @powershell@example.com, and the addresses
# site's handle skips the same three rules as an address, which refused # https://example.com/users/erin.htaccess and
# https://example.com/users/powershell. A handle starts with an @, so 932260
# does not refuse @python3.dev@example.com. The domain of the site's handle
# skips the same three rules as an email address, which refused
# erin.htaccess.example.com, powershell.example.com and python3.example.com. # erin.htaccess.example.com, powershell.example.com and python3.example.com.
# Each of these fields skips them only on its own path: # Each of these fields skips them only on its own path:
# sourceHandle, an account that moves here, at /.ghost/activitypub/v1/aliases # sourceHandle, an account that moves here, added at
# /.ghost/activitypub/v1/aliases
# actorUri, that account's address, when the admin removes it there
# targetHandle, the account this one moves to, at # targetHandle, the account this one moves to, at
# /.ghost/activitypub/v1/migration # /.ghost/activitypub/v1/migration
# domain, saved at /.ghost/activitypub/v1/domain and checked at # domain, saved at /.ghost/activitypub/v1/domain and checked at
# /.ghost/activitypub/v1/domain/validate # /.ghost/activitypub/v1/domain/validate
SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/aliases" \ SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/aliases" \
"id:900379,phase:1,pass,nolog,chain" "id:900379,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.sourcehandle$/ "@eq 0" \ SecRule &ARGS_GET:/^json\.(?:sourcehandle|actoruri)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.sourcehandle$/,\ "ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:sourcehandle|actoruri)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.sourcehandle$/" ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:sourcehandle|actoruri)$/"
SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/migration" \ SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/migration" \
"id:900380,phase:1,pass,nolog,chain" "id:900380,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.targethandle$/ "@eq 0" \ SecRule &ARGS_GET:/^json\.targethandle$/ "@eq 0" \
@@ -413,6 +428,23 @@ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/ "@eq 0" \ SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=941110;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/" "ctl:ruleRemoveTargetById=941110;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/"
# The canonical URL of a post or a page skips the two rules that url skips:
# 930120: https://example.com/blog/.htaccess/
# 932120: https://example.com/blog/powershell/
# Each of these fields skips them only on its own path:
# posts.<n>.canonical_url, under /ghost/api/admin/posts/
# pages.<n>.canonical_url, under /ghost/api/admin/pages/
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/posts/" \
"id:900383,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.posts\.\d+\.canonical_url$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.posts\.\d+\.canonical_url$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.posts\.\d+\.canonical_url$/"
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/pages/" \
"id:900384,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.pages\.\d+\.canonical_url$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.pages\.\d+\.canonical_url$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.pages\.\d+\.canonical_url$/"
# Saving a staff user's profile sends the field profile_image, whose name holds # Saving a staff user's profile sends the field profile_image, whose name holds
# .profile, a file the list of system files names (930120). # .profile, a file the list of system files names (930120).
SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \ SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
@@ -440,6 +472,16 @@ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/tags/" \
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\ ctl:ruleRemoveTargetById=941160;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
ctl:ruleRemoveTargetById=941270;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/" ctl:ruleRemoveTargetById=941270;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/"
# A tag's canonical URL, tags.<n>.canonical_url under /ghost/api/admin/tags/,
# skips the two rules that url skips:
# 930120: https://example.com/tag/.htaccess/
# 932120: https://example.com/tag/powershell/
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/tags/" \
"id:900382,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.tags\.\d+\.canonical_url$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.tags\.\d+\.canonical_url$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.tags\.\d+\.canonical_url$/"
# The passwords, which skip each rule that random passwords set off: # The passwords, which skip each rule that random passwords set off:
# password, signing in at /ghost/api/admin/session/ # password, signing in at /ghost/api/admin/session/
# setup.<n>.password, creating the first staff user # setup.<n>.password, creating the first staff user
+90 -22
View File
@@ -280,6 +280,24 @@ var ghostDomainFields = []ghostField{
{http.MethodPost, ghostDomain + "/validate", `{"domain":"VALUE"}`}, {http.MethodPost, ghostDomain + "/validate", `{"domain":"VALUE"}`},
} }
// ghostURLFields hold the canonical URL of a post, a page or a tag, or the
// address of an account removed from those that move here in ActivityPub.
// They skip 930120 and 932120, as url does.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostURLFields = []ghostField{
{http.MethodPut, ghostPost, `{"posts":[{"canonical_url":"VALUE"}]}`},
{http.MethodPost, ghostPosts, `{"posts":[{"canonical_url":"VALUE"}]}`},
{http.MethodPut, ghostPage, `{"pages":[{"canonical_url":"VALUE"}]}`},
{http.MethodPost, ghostTags, `{"tags":[{"canonical_url":"VALUE"}]}`},
{
http.MethodPut,
ghostTags + "6af45d4ed281b00001a599a8/",
`{"tags":[{"canonical_url":"VALUE"}]}`,
},
{http.MethodDelete, ghostAliases, `{"actorUri":"VALUE"}`},
}
// ghostPostFields hold a post or a page, skip the rules of 900355, and take a // ghostPostFields hold a post or a page, skip the rules of 900355, and take a
// <script> element by 900366. // <script> element by 900366.
// //
@@ -367,6 +385,19 @@ const (
ghostInvitedEmail = ghostInvitation + "?email=" ghostInvitedEmail = ghostInvitation + "?email="
) )
// ghostAnalyticsFilters are source and the utm_ fields, which filter the
// analytics, each ending in "=".
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostAnalyticsFilters = []string{
ghostSource,
ghostStats + "&utm_source=",
ghostStats + "&utm_medium=",
ghostCampaign,
ghostStats + "&utm_content=",
ghostStats + "&utm_term=",
}
// ghostQueryFields are the query fields that skip some rules. // ghostQueryFields are the query fields that skip some rules.
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
@@ -379,13 +410,16 @@ var ghostQueryFields = []string{
// ghostCommandNames joins two command names, as an ordinary title can. // ghostCommandNames joins two command names, as an ordinary title can.
const ghostCommandNames = "powershell & python3" const ghostCommandNames = "powershell & python3"
// ghostURLs are addresses 930120 and 932120 refused in url. // ghostURLs are addresses 930120 and 932120 refused in url and in
// ghostURLFields.
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
var ghostURLs = []string{ var ghostURLs = []string{
"https://github.com/example/site/blob/main/.htaccess", "https://github.com/example/site/blob/main/.htaccess",
"https://example.com/blog/powershell-tips?ref=ghost", "https://example.com/blog/powershell-tips?ref=ghost",
"https://example.com/docs/python3/", "https://example.com/docs/python3/",
"https://example.com/blog/powershell/",
"https://example.com/blog/.htaccess/",
} }
// ghostAddresses are addresses 930120, 932120 and 932260 refused. // ghostAddresses are addresses 930120, 932120 and 932260 refused.
@@ -495,6 +529,10 @@ func TestGhostOrdinaryValuesPassInTheExcludedFields(t *testing.T) {
for _, address := range ghostURLs { for _, address := range ghostURLs {
wantGhostQuery(t, ghostEmbed+url.QueryEscape(address), false) wantGhostQuery(t, ghostEmbed+url.QueryEscape(address), false)
for _, f := range ghostURLFields {
wantGhostField(t, f, address, false)
}
} }
for _, address := range ghostAddresses { for _, address := range ghostAddresses {
@@ -578,8 +616,8 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
for _, attack := range ghostAttacks { for _, attack := range ghostAttacks {
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields, for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostHandleFields, ghostDomainFields, ghostArticleFields, ghostHandleFields, ghostDomainFields, ghostURLFields,
ghostCodeFields) { ghostArticleFields, ghostCodeFields) {
wantGhostField(t, f, attack, true) wantGhostField(t, f, attack, true)
} }
@@ -591,7 +629,7 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
// A script injection stays refused in every field that does not take a // A script injection stays refused in every field that does not take a
// <script> element. // <script> element.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields, for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostHandleFields, ghostDomainFields, ghostHandleFields, ghostDomainFields, ghostURLFields,
ghostSnippetAndNewsletterFooterFields) { ghostSnippetAndNewsletterFooterFields) {
wantGhostField(t, f, scriptTag, true) wantGhostField(t, f, scriptTag, true)
} }
@@ -601,16 +639,17 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
} }
// A command after a separator stays refused wherever 932235 does not // A command after a separator stays refused wherever 932235 does not
// misfire: in the addresses, handles, the domain, search, url, // misfire: in the addresses, handles, the domain, ghostURLFields, search,
// email_segment and email. The other fields skip 932235, which refuses // url, pathname, email_segment and email. The other fields skip 932235,
// "powershell & python3" as it refuses this. // which refuses "powershell & python3" as it refuses this.
for _, f := range slices.Concat(ghostAddressFields, ghostHandleFields, for _, f := range slices.Concat(ghostAddressFields, ghostHandleFields,
ghostDomainFields) { ghostDomainFields, ghostURLFields) {
wantGhostField(t, f, commandAfterASeparator, true) wantGhostField(t, f, commandAfterASeparator, true)
} }
for _, target := range []string{ for _, target := range []string{
ghostSearch, ghostEmbed, ghostSegment, ghostInvitedEmail, ghostSearch, ghostEmbed, ghostPathname, ghostSegment,
ghostInvitedEmail,
} { } {
wantGhostQuery( wantGhostQuery(
t, t,
@@ -649,10 +688,6 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
func TestGhostTwoCommandNamesPass(t *testing.T) { func TestGhostTwoCommandNamesPass(t *testing.T) {
t.Parallel() t.Parallel()
for _, f := range slices.Concat(ghostTextFields, ghostCodeFields) {
wantGhostField(t, f, ghostCommandNames, false)
}
// The site's title, saved through settings. // The site's title, saved through settings.
siteTitle := ghostField{ siteTitle := ghostField{
http.MethodPut, http.MethodPut,
@@ -661,12 +696,26 @@ func TestGhostTwoCommandNamesPass(t *testing.T) {
} }
wantGhostField(t, siteTitle, ghostCommandNames, false) wantGhostField(t, siteTitle, ghostCommandNames, false)
// filter as the admin's post picker writes it, after a person types a // Values that join two command names, as a person types them.
// title that joins two command names.
for _, typed := range []string{ for _, typed := range []string{
ghostCommandNames, "python3 & powershell", "powershell & ls -la", ghostCommandNames, "python3 & powershell", "powershell & ls -la",
"ls -la & python3", "ls -la & python3",
} { } {
for _, f := range slices.Concat(ghostTextFields, ghostCodeFields) {
wantGhostField(t, f, typed, false)
}
wantGhostImportLabel(t, ghostImport, typed, false)
for _, target := range slices.Concat(
ghostAnalyticsFilters,
[]string{ghostAccountSearch},
) {
wantGhostQuery(t, target+url.QueryEscape(typed), false)
}
// filter as the admin's post picker writes it, after a person types
// such a title.
filter := "status:published+title:~'" + typed + "'" filter := "status:published+title:~'" + typed + "'"
wantGhostQuery(t, ghostAdminFilter+url.QueryEscape(filter), false) wantGhostQuery(t, ghostAdminFilter+url.QueryEscape(filter), false)
} }
@@ -682,16 +731,21 @@ func TestGhostOrdinaryQueriesPass(t *testing.T) {
value, _ := url.QueryUnescape(encoded) value, _ := url.QueryUnescape(encoded)
text := url.QueryEscape("Notes on " + value + " & tidy files") text := url.QueryEscape("Notes on " + value + " & tidy files")
wantGhostQuery( for _, target := range ghostAnalyticsFilters {
t, wantGhostQuery(t, target+encoded, false)
ghostPathname+url.QueryEscape("/"+value+"/"), }
false,
)
wantGhostQuery(t, ghostSource+encoded, false)
wantGhostQuery(t, ghostCampaign+text, false) wantGhostQuery(t, ghostCampaign+text, false)
wantGhostQuery(t, ghostAccountSearch+text, false) wantGhostQuery(t, ghostAccountSearch+text, false)
} }
// pathname, the page the analytics show: a post and two tag pages.
for _, path := range []string{
"/tag/proc/", "/python3-and-powershell/", "/tag/python3/",
} {
wantGhostQuery(t, ghostPathname+url.QueryEscape(path), false)
}
wantGhostQuery( wantGhostQuery(
t, t,
ghostCampaign+url.QueryEscape(ghostCommandNames), ghostCampaign+url.QueryEscape(ghostCommandNames),
@@ -772,6 +826,8 @@ func TestGhostExclusionsAreNarrow(t *testing.T) {
setupEmail = `{"setup":[{"email":"python3.dev@example.com"}]}` setupEmail = `{"setup":[{"email":"python3.dev@example.com"}]}`
passwordResetEmail = `{"password_reset":[{"email":"python3.dev@example.com"}]}` passwordResetEmail = `{"password_reset":[{"email":"python3.dev@example.com"}]}`
userEmail = `{"users":[{"email":"python3.dev@example.com"}]}` userEmail = `{"users":[{"email":"python3.dev@example.com"}]}`
postURL = `{"posts":[{"canonical_url":"https://example.com/powershell/"}]}`
pageURL = `{"pages":[{"canonical_url":"https://example.com/powershell/"}]}`
) )
for _, refused := range []struct{ target, body string }{ for _, refused := range []struct{ target, body string }{
@@ -807,6 +863,12 @@ func TestGhostExclusionsAreNarrow(t *testing.T) {
{ghostPasswordReset + "?json.password_reset.0.email=x", passwordResetEmail}, {ghostPasswordReset + "?json.password_reset.0.email=x", passwordResetEmail},
{ghostPosts, userEmail}, {ghostPosts, userEmail},
{ghostUser + "?json.users.0.email=x", userEmail}, {ghostUser + "?json.users.0.email=x", userEmail},
{"/api/posts/1/", postURL},
{ghostMembers, postURL},
{ghostPage, postURL},
{ghostPost + "?json.posts.0.canonical_url=x", postURL},
{ghostPost, pageURL},
{ghostPage + "?json.pages.0.canonical_url=x", pageURL},
{ghostSession, `{"username":"x","pass":"' OR '1'='1"}`}, {ghostSession, `{"username":"x","pass":"' OR '1'='1"}`},
{"/ghost/api/admin/users/me/", `{"password":"' OR '1'='1"}`}, {"/ghost/api/admin/users/me/", `{"password":"' OR '1'='1"}`},
{"/ghost/api/admin/posts/1/", `{"users":[{"profile_image":null}]}`}, {"/ghost/api/admin/posts/1/", `{"users":[{"profile_image":null}]}`},
@@ -897,7 +959,7 @@ func TestGhostQueryExclusionsAreNarrow(t *testing.T) {
} }
// TestGhostStaffCommentAndActivityPubExclusionsAreNarrow checks the body // TestGhostStaffCommentAndActivityPubExclusionsAreNarrow checks the body
// fields that 900370 to 900381 name, as TestGhostExclusionsAreNarrow checks // fields that 900370 to 900382 name, as TestGhostExclusionsAreNarrow checks
// the others: each keeps its rules on another path, and when the query has // the others: each keeps its rules on another path, and when the query has
// a field of the same name. // a field of the same name.
func TestGhostStaffCommentAndActivityPubExclusionsAreNarrow(t *testing.T) { func TestGhostStaffCommentAndActivityPubExclusionsAreNarrow(t *testing.T) {
@@ -912,9 +974,11 @@ func TestGhostStaffCommentAndActivityPubExclusionsAreNarrow(t *testing.T) {
invited = `{"invites":[{"email":"python3.dev@example.com"}]}` invited = `{"invites":[{"email":"python3.dev@example.com"}]}`
accepted = `{"invitation":[{"email":"python3.dev@example.com"}]}` accepted = `{"invitation":[{"email":"python3.dev@example.com"}]}`
sourceHandle = `{"sourceHandle":"@powershell@example.com"}` sourceHandle = `{"sourceHandle":"@powershell@example.com"}`
actorURI = `{"actorUri":"https://example.com/users/powershell"}`
targetHandle = `{"targetHandle":"@powershell@example.com"}` targetHandle = `{"targetHandle":"@powershell@example.com"}`
domain = `{"domain":"python3.example.com"}` domain = `{"domain":"python3.example.com"}`
tagCode = `{"tags":[{"codeinjection_head":"<meta name=x content=y>"}]}` tagCode = `{"tags":[{"codeinjection_head":"<meta name=x content=y>"}]}`
tagURL = `{"tags":[{"canonical_url":"https://example.com/tag/powershell/"}]}`
) )
for _, refused := range []struct{ target, body string }{ for _, refused := range []struct{ target, body string }{
@@ -935,12 +999,16 @@ func TestGhostStaffCommentAndActivityPubExclusionsAreNarrow(t *testing.T) {
{ghostInvitation + "?json.invitation.0.email=x", accepted}, {ghostInvitation + "?json.invitation.0.email=x", accepted},
{ghostMigration, sourceHandle}, {ghostMigration, sourceHandle},
{ghostAliases + "?json.sourceHandle=x", sourceHandle}, {ghostAliases + "?json.sourceHandle=x", sourceHandle},
{ghostMigration, actorURI},
{ghostAliases + "?json.actorUri=x", actorURI},
{ghostAliases, targetHandle}, {ghostAliases, targetHandle},
{ghostMigration + "?json.targetHandle=x", targetHandle}, {ghostMigration + "?json.targetHandle=x", targetHandle},
{ghostAccount, domain}, {ghostAccount, domain},
{ghostDomain + "?json.domain=x", domain}, {ghostDomain + "?json.domain=x", domain},
{ghostPosts, tagCode}, {ghostPosts, tagCode},
{ghostTags + "?json.tags.0.codeinjection_head=x", tagCode}, {ghostTags + "?json.tags.0.codeinjection_head=x", tagCode},
{ghostPosts, tagURL},
{ghostTags + "?json.tags.0.canonical_url=x", tagURL},
} { } {
r := request{http.MethodPut, refused.target, []string{jsonBody}} r := request{http.MethodPut, refused.target, []string{jsonBody}}
+3 -1
View File
@@ -533,6 +533,8 @@
{"method": "GET", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""} {"method": "GET", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3980", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated {"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3980", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3984", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated {"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3984", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af5659a6dde5d0001d3d72e/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 713", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AoIVlIxucaQas8uV_V0tI2mNyxYEoOjBC.ajmnN2F%2BNe0IFunZWICXaJjQpUSV75M6lrkP5I0svVs"], "body": "{\"posts\":[{\"title\":\"Getting started with PowerShell\",\"slug\":\"getting-started-with-powershell\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"First published on the old blog.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"canonical_url\":\"https://example.com/blog/powershell/\",\"id\":\"6af5659a6dde5d0001d3d72e\",\"updated_at\":\"2026-11-12T07:07:12.000Z\"}]}"}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af565b96dde5d0001d3d735/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 701", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AoIVlIxucaQas8uV_V0tI2mNyxYEoOjBC.ajmnN2F%2BNe0IFunZWICXaJjQpUSV75M6lrkP5I0svVs"], "body": "{\"pages\":[{\"title\":\"Rewrite rules in .htaccess\",\"slug\":\"rewrite-rules-in-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"First published on the old blog.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"canonical_url\":\"https://example.com/blog/.htaccess/\",\"id\":\"6af565b96dde5d0001d3d735\",\"updated_at\":\"2026-11-12T07:07:42.000Z\"}]}"}
{"method": "GET", "target": "/ghost/assets/pages-route-Bx4g0rd3.js", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""} {"method": "GET", "target": "/ghost/assets/pages-route-Bx4g0rd3.js", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""} {"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""} {"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
@@ -776,7 +778,7 @@
{"method": "POST", "target": "/ghost/api/admin/integrations/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 55", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\"}]}"} {"method": "POST", "target": "/ghost/api/admin/integrations/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 55", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\"}]}"}
{"method": "PUT", "target": "/ghost/api/admin/integrations/6af5443aa509340001efcfc6/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\",\"description\":\"Runs ls -la and tidies .htaccess files\",\"icon_image\":null}]}"} {"method": "PUT", "target": "/ghost/api/admin/integrations/6af5443aa509340001efcfc6/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\",\"description\":\"Runs ls -la and tidies .htaccess files\",\"icon_image\":null}]}"}
{"method": "POST", "target": "/ghost/api/admin/webhooks/", "headers": ["Host: example.com", "Content-Length: 202", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"webhooks\":[{\"event\":\"post.published\",\"target_url\":\"https://example.com/hooks/python3\",\"name\":\"python3 & powershell hook\",\"secret\":null,\"api_version\":\"v6\",\"integration_id\":\"6af5443aa509340001efcfc6\"}]}"} {"method": "POST", "target": "/ghost/api/admin/webhooks/", "headers": ["Host: example.com", "Content-Length: 202", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"webhooks\":[{\"event\":\"post.published\",\"target_url\":\"https://example.com/hooks/python3\",\"name\":\"python3 & powershell hook\",\"secret\":null,\"api_version\":\"v6\",\"integration_id\":\"6af5443aa509340001efcfc6\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/tags/?include=count.posts", "headers": ["Host: example.com", "Content-Length: 699", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"tags\":[{\"name\":\"Shell\",\"slug\":\"shell\",\"description\":null,\"feature_image\":null,\"meta_title\":\"python3 & powershell tips\",\"meta_description\":\"Run ls -la and tidy .htaccess files with python3.\",\"canonical_url\":\"https://example.com/tag/python3/\",\"twitter_image\":null,\"twitter_title\":\"powershell & python3\",\"twitter_description\":\"ls -la, .htaccess and more\",\"og_image\":null,\"og_title\":\"python3 & powershell\",\"og_description\":\"ls -la and .htaccess tips\",\"codeinjection_head\":\"<meta name=\\\"google-site-verification\\\" content=\\\"x\\\"><link rel=\\\"preconnect\\\" href=\\\"https://fonts.example.com\\\">\",\"codeinjection_foot\":\"<style>.tag-python3 { color: #333; }</style>\",\"accent_color\":null,\"visibility\":\"public\"}]}"} {"method": "POST", "target": "/ghost/api/admin/tags/?include=count.posts", "headers": ["Host: example.com", "Content-Length: 702", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AWHEHmHzgM_62oH0uUMxh6TgpQCaauWmB.h%2FuJNd7nfwPSaFGmMarZNVguG8U1ZIIJsaLFE4%2BqXMA"], "body": "{\"tags\":[{\"name\":\"Shell\",\"slug\":\"shell\",\"description\":null,\"feature_image\":null,\"meta_title\":\"python3 & powershell tips\",\"meta_description\":\"Run ls -la and tidy .htaccess files with python3.\",\"canonical_url\":\"https://example.com/tag/powershell/\",\"twitter_image\":null,\"twitter_title\":\"powershell & python3\",\"twitter_description\":\"ls -la, .htaccess and more\",\"og_image\":null,\"og_title\":\"python3 & powershell\",\"og_description\":\"ls -la and .htaccess tips\",\"codeinjection_head\":\"<meta name=\\\"google-site-verification\\\" content=\\\"x\\\"><link rel=\\\"preconnect\\\" href=\\\"https://fonts.example.com\\\">\",\"codeinjection_foot\":\"<style>.tag-python3 { color: #333; }</style>\",\"accent_color\":null,\"visibility\":\"public\"}]}"}
{"method": "PUT", "target": "/ghost/api/admin/posts/6af5443ba509340001efcfd1/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 315", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"posts\":[{\"updated_at\":\"2026-11-12T04:44:43.000Z\",\"title\":\"Shell notes\",\"feature_image\":\"https://images.example.com/terminal.png\",\"feature_image_alt\":\"A terminal running ls -la on .htaccess with python3\",\"feature_image_caption\":\"python3 &amp; powershell, photo by <a href=\\\"https://example.com/ls-la\\\">Erin</a>\"}]}"} {"method": "PUT", "target": "/ghost/api/admin/posts/6af5443ba509340001efcfd1/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 315", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"posts\":[{\"updated_at\":\"2026-11-12T04:44:43.000Z\",\"title\":\"Shell notes\",\"feature_image\":\"https://images.example.com/terminal.png\",\"feature_image_alt\":\"A terminal running ls -la on .htaccess with python3\",\"feature_image_caption\":\"python3 &amp; powershell, photo by <a href=\\\"https://example.com/ls-la\\\">Erin</a>\"}]}"}
{"method": "POST", "target": "/ghost/api/admin/members/upload/", "headers": ["Host: example.com", "Content-Length: 990", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: multipart/form-data; boundary=----WebKitFormBoundaryq3Lm8vXc2TzR7wKd", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"membersfile\"; filename=\"members.csv\"\r\nContent-Type: text/csv\r\n\r\nemail,name,note,labels\r\npython3.dev@example.com,Grace python3,Asked about ls -la,powershell\r\nerin.htaccess@example.com,Erin,Tidies .htaccess files,python3\r\n\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npython3 import\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npowershell & .htaccess\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[email]\"\r\n\r\nemail\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[name]\"\r\n\r\nname\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[note]\"\r\n\r\nnote\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[labels]\"\r\n\r\nlabels\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd--\r\n"} {"method": "POST", "target": "/ghost/api/admin/members/upload/", "headers": ["Host: example.com", "Content-Length: 990", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: multipart/form-data; boundary=----WebKitFormBoundaryq3Lm8vXc2TzR7wKd", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"membersfile\"; filename=\"members.csv\"\r\nContent-Type: text/csv\r\n\r\nemail,name,note,labels\r\npython3.dev@example.com,Grace python3,Asked about ls -la,powershell\r\nerin.htaccess@example.com,Erin,Tidies .htaccess files,python3\r\n\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npython3 import\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npowershell & .htaccess\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[email]\"\r\n\r\nemail\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[name]\"\r\n\r\nname\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[note]\"\r\n\r\nnote\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[labels]\"\r\n\r\nlabels\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd--\r\n"}
{"method": "GET", "target": "/ghost/api/admin/stats/top-content/?date_from=2026-10-13&date_to=2026-11-12&timezone=Etc%2FUTC&member_status=all&pathname=%2Fpython3-and-powershell%2F", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""} {"method": "GET", "target": "/ghost/api/admin/stats/top-content/?date_from=2026-10-13&date_to=2026-11-12&timezone=Etc%2FUTC&member_status=all&pathname=%2Fpython3-and-powershell%2F", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
+9 -4
View File
@@ -93,10 +93,15 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
}, },
) )
got := visit(t, newRequest(t, http.MethodPost, addr, "/", // The visitor sends no Connection: close, so Go's server reads the rest
bytes.NewReader(make([]byte, tc.size)))) // of the body before it closes the connection. Closed with the rest
if got.status != http.StatusRequestEntityTooLarge { // unread, the connection is reset while the visitor still sends.
t.Errorf("answered %+v, want 413", got) conn := dial(t, addr)
send(t, conn, post+announcedLength(tc.size)+
strings.Repeat("x", tc.size))
if status := readStatus(t, conn); status != http.StatusRequestEntityTooLarge {
t.Errorf("status %d, want 413", status)
} }
if asked := standIn.question(t); len(asked.body) != tc.asked { if asked := standIn.question(t); len(asked.body) != tc.asked {