diff --git a/server/actions.go b/action/action.go similarity index 96% rename from server/actions.go rename to action/action.go index f741c6a6..6aa2a1ee 100644 --- a/server/actions.go +++ b/action/action.go @@ -1,4 +1,6 @@ -package server +// Package action parses the "action buttons" that can be attached to a notification, in both the +// JSON and the human-readable "simple" format described at https://ntfy.sh/docs/publish/#action-buttons. +package action import ( "encoding/json" @@ -37,10 +39,10 @@ type actionParser struct { pos int } -// parseActions parses the actions string as described in https://ntfy.sh/docs/publish/#action-buttons. +// Parse parses the actions string as described in https://ntfy.sh/docs/publish/#action-buttons. // It supports both a JSON representation (if the string begins with "[", see parseActionsFromJSON), // and the "simple" format, which is more human-readable, but harder to parse (see parseActionsFromSimple). -func parseActions(s string) (actions []*model.Action, err error) { +func Parse(s string) (actions []*model.Action, err error) { // Parse JSON or simple format s = strings.TrimSpace(s) if strings.HasPrefix(s, "[") { diff --git a/server/actions_test.go b/action/action_test.go similarity index 69% rename from server/actions_test.go rename to action/action_test.go index 1de7783e..68b4527d 100644 --- a/server/actions_test.go +++ b/action/action_test.go @@ -1,4 +1,4 @@ -package server +package action import ( "testing" @@ -7,12 +7,12 @@ import ( ) func TestParseActions(t *testing.T) { - actions, err := parseActions("[]") + actions, err := Parse("[]") require.Nil(t, err) require.Empty(t, actions) // Basic test - actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open; view, Show portal, https://door.lan") + actions, err = Parse("action=http, label=Open door, url=https://door.lan/open; view, Show portal, https://door.lan") require.Nil(t, err) require.Equal(t, 2, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -23,7 +23,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "https://door.lan", actions[1].URL) // JSON - actions, err = parseActions(`[{"action":"http","label":"Open door","url":"https://door.lan/open"}, {"action":"view","label":"Show portal","url":"https://door.lan"}]`) + actions, err = Parse(`[{"action":"http","label":"Open door","url":"https://door.lan/open"}, {"action":"view","label":"Show portal","url":"https://door.lan"}]`) require.Nil(t, err) require.Equal(t, 2, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -34,7 +34,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "https://door.lan", actions[1].URL) // Other params - actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open, body=this is a body, method=PUT") + actions, err = Parse("action=http, label=Open door, url=https://door.lan/open, body=this is a body, method=PUT") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -44,7 +44,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "this is a body", actions[0].Body) // Extras with underscores - actions, err = parseActions("action=broadcast, label=Do a thing, extras.command=some command, extras.some_param=a parameter") + actions, err = Parse("action=broadcast, label=Do a thing, extras.command=some command, extras.some_param=a parameter") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "broadcast", actions[0].Action) @@ -54,7 +54,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "a parameter", actions[0].Extras["some_param"]) // Broadcast action with intent - actions, err = parseActions("action=broadcast, label=Do a thing, intent=io.heckel.ntfy.TEST_INTENT") + actions, err = Parse("action=broadcast, label=Do a thing, intent=io.heckel.ntfy.TEST_INTENT") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "broadcast", actions[0].Action) @@ -62,7 +62,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "io.heckel.ntfy.TEST_INTENT", actions[0].Intent) // Headers with dashes - actions, err = parseActions("action=http, label=Send request, url=http://example.com, method=GET, headers.Content-Type=application/json, headers.Authorization=Basic sdasffsf") + actions, err = Parse("action=http, label=Send request, url=http://example.com, method=GET, headers.Content-Type=application/json, headers.Authorization=Basic sdasffsf") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -72,7 +72,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"]) // Quotes - actions, err = parseActions(`action=http, "Look ma, \"quotes\"; and semicolons", url=http://example.com`) + actions, err = Parse(`action=http, "Look ma, \"quotes\"; and semicolons", url=http://example.com`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -80,7 +80,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `http://example.com`, actions[0].URL) // Single quotes - actions, err = parseActions(`action=http, '"quotes" and \'single quotes\'', url=http://example.com`) + actions, err = Parse(`action=http, '"quotes" and \'single quotes\'', url=http://example.com`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -88,7 +88,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `http://example.com`, actions[0].URL) // Single quotes (JSON) - actions, err = parseActions(`action=http, Post it, url=http://example.com, body='{"temperature": 65}'`) + actions, err = Parse(`action=http, Post it, url=http://example.com, body='{"temperature": 65}'`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -97,7 +97,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `{"temperature": 65}`, actions[0].Body) // Out of order - actions, err = parseActions(`label="Out of order!" , action="http", url=http://example.com`) + actions, err = Parse(`label="Out of order!" , action="http", url=http://example.com`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -105,7 +105,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `http://example.com`, actions[0].URL) // Spaces - actions, err = parseActions(`action = http, label = 'this is a label', url = "http://google.com"`) + actions, err = Parse(`action = http, label = 'this is a label', url = "http://google.com"`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -113,7 +113,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `http://google.com`, actions[0].URL) // Non-ASCII - actions, err = parseActions(`action = http, 'Кохайтеся а не воюйте, 💙🫤', url = "http://google.com"`) + actions, err = Parse(`action = http, 'Кохайтеся а не воюйте, 💙🫤', url = "http://google.com"`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -121,7 +121,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, `http://google.com`, actions[0].URL) // Multiple actions, awkward spacing - actions, err = parseActions(`http , 'Make love, not war 💙🫤' , https://ntfy.sh ; view, " yo ", https://x.org, clear=true`) + actions, err = Parse(`http , 'Make love, not war 💙🫤' , https://ntfy.sh ; view, " yo ", https://x.org, clear=true`) require.Nil(t, err) require.Equal(t, 2, len(actions)) require.Equal(t, "http", actions[0].Action) @@ -134,7 +134,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, true, actions[1].Clear) // Copy action (simple format) - actions, err = parseActions("copy, Copy code, 1234") + actions, err = Parse("copy, Copy code, 1234") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "copy", actions[0].Action) @@ -142,7 +142,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "1234", actions[0].Value) // Copy action (JSON) - actions, err = parseActions(`[{"action":"copy","label":"Copy OTP","value":"567890"}]`) + actions, err = Parse(`[{"action":"copy","label":"Copy OTP","value":"567890"}]`) require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "copy", actions[0].Action) @@ -150,7 +150,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, "567890", actions[0].Value) // Copy action with clear - actions, err = parseActions("copy, Copy code, 1234, clear=true") + actions, err = Parse("copy, Copy code, 1234, clear=true") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "copy", actions[0].Action) @@ -159,7 +159,7 @@ func TestParseActions(t *testing.T) { require.Equal(t, true, actions[0].Clear) // Copy action with explicit value key - actions, err = parseActions("action=copy, label=Copy token, clear=true, value=abc-123-def") + actions, err = Parse("action=copy, label=Copy token, clear=true, value=abc-123-def") require.Nil(t, err) require.Equal(t, 1, len(actions)) require.Equal(t, "copy", actions[0].Action) @@ -168,56 +168,56 @@ func TestParseActions(t *testing.T) { require.True(t, actions[0].Clear) // Copy action without value (error) - _, err = parseActions("copy, Copy code") + _, err = Parse("copy, Copy code") require.EqualError(t, err, "parameter 'value' is required for action 'copy'") // Invalid syntax - _, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`) + _, err = Parse(`label="Out of order!" x, action="http", url=http://example.com`) require.EqualError(t, err, "unexpected character 'x' at position 22") - _, err = parseActions(`label="", action="http", url=http://example.com`) + _, err = Parse(`label="", action="http", url=http://example.com`) require.EqualError(t, err, "parameter 'label' is required") - _, err = parseActions(`label=, action="http", url=http://example.com`) + _, err = Parse(`label=, action="http", url=http://example.com`) require.EqualError(t, err, "parameter 'label' is required") - _, err = parseActions(`label="xx", action="http", url=http://example.com, what is this anyway`) + _, err = Parse(`label="xx", action="http", url=http://example.com, what is this anyway`) require.EqualError(t, err, "term 'what is this anyway' unknown") - _, err = parseActions(`fdsfdsf`) + _, err = Parse(`fdsfdsf`) require.EqualError(t, err, "parameter 'action' cannot be 'fdsfdsf', valid values are 'view', 'broadcast', 'http' and 'copy'") - _, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`) + _, err = Parse(`aaa=a, "bbb, 'ccc, ddd, eee "`) require.EqualError(t, err, "key 'aaa' unknown") - _, err = parseActions(`action=http, label="omg the end quote is missing`) + _, err = Parse(`action=http, label="omg the end quote is missing`) require.EqualError(t, err, "unexpected end of input, quote started at position 20") - _, err = parseActions(`;;;;`) + _, err = Parse(`;;;;`) require.EqualError(t, err, "only 3 actions allowed") - _, err = parseActions(`,,,,,,;;`) + _, err = Parse(`,,,,,,;;`) require.EqualError(t, err, "term '' unknown") - _, err = parseActions(`''";,;"`) + _, err = Parse(`''";,;"`) require.EqualError(t, err, "unexpected character '\"' at position 2") - _, err = parseActions(`action=http, label=a label, body=somebody`) + _, err = Parse(`action=http, label=a label, body=somebody`) require.EqualError(t, err, "parameter 'url' is required for action 'http'") - _, err = parseActions(`action=http, label=a label, url=http://ntfy.sh, method=HEAD, body=somebody`) + _, err = Parse(`action=http, label=a label, url=http://ntfy.sh, method=HEAD, body=somebody`) require.EqualError(t, err, "parameter 'body' cannot be set if method is HEAD") - _, err = parseActions(`[ invalid json ]`) + _, err = Parse(`[ invalid json ]`) require.EqualError(t, err, "JSON error: invalid character 'i' looking for beginning of value") - _, err = parseActions(`[ { "some": "object" } ]`) + _, err = Parse(`[ { "some": "object" } ]`) require.EqualError(t, err, "parameter 'action' cannot be '', valid values are 'view', 'broadcast', 'http' and 'copy'") - _, err = parseActions("\x00\x01\xFFx\xFE") + _, err = Parse("\x00\x01\xFFx\xFE") require.EqualError(t, err, "invalid utf-8 string") - _, err = parseActions(`http, label, http://x.org, clear=x`) + _, err = Parse(`http, label, http://x.org, clear=x`) require.EqualError(t, err, "parameter 'clear' cannot be 'x', only boolean values are allowed (true/yes/1/false/no/0)") } diff --git a/server/server.go b/server/server.go index 5934c485..f58fadd1 100644 --- a/server/server.go +++ b/server/server.go @@ -29,6 +29,7 @@ import ( "github.com/gorilla/websocket" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/errgroup" + "heckel.io/ntfy/v2/action" "heckel.io/ntfy/v2/attachment" "heckel.io/ntfy/v2/db" "heckel.io/ntfy/v2/db/pg" @@ -1195,7 +1196,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *model.Message) (cache bo } actionsStr := readParam(r, "x-actions", "actions", "action") if actionsStr != "" { - m.Actions, e = parseActions(actionsStr) + m.Actions, e = action.Parse(actionsStr) if e != nil { return false, false, "", "", "", false, "", errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) }