2022-01-16 05:17:46 +01:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2022-04-19 15:14:32 +02:00
|
|
|
"encoding/json"
|
|
|
|
"heckel.io/ntfy/util"
|
2022-01-16 05:17:46 +01:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2022-04-20 22:31:25 +02:00
|
|
|
const (
|
|
|
|
actionIDLength = 10
|
|
|
|
actionsMax = 3
|
|
|
|
)
|
|
|
|
|
2022-01-16 05:17:46 +01:00
|
|
|
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
|
|
|
|
value := strings.ToLower(readParam(r, names...))
|
|
|
|
if value == "" {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return value == "1" || value == "yes" || value == "true"
|
|
|
|
}
|
|
|
|
|
|
|
|
func readParam(r *http.Request, names ...string) string {
|
2022-02-26 05:25:04 +01:00
|
|
|
value := readHeaderParam(r, names...)
|
|
|
|
if value != "" {
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
return readQueryParam(r, names...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func readHeaderParam(r *http.Request, names ...string) string {
|
2022-01-16 05:17:46 +01:00
|
|
|
for _, name := range names {
|
|
|
|
value := r.Header.Get(name)
|
|
|
|
if value != "" {
|
|
|
|
return strings.TrimSpace(value)
|
|
|
|
}
|
|
|
|
}
|
2022-02-26 05:25:04 +01:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func readQueryParam(r *http.Request, names ...string) string {
|
2022-01-16 05:17:46 +01:00
|
|
|
for _, name := range names {
|
|
|
|
value := r.URL.Query().Get(strings.ToLower(name))
|
|
|
|
if value != "" {
|
|
|
|
return strings.TrimSpace(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
2022-04-19 15:14:32 +02:00
|
|
|
|
|
|
|
func parseActions(s string) (actions []*action, err error) {
|
2022-04-20 05:26:46 +02:00
|
|
|
// Parse JSON or simple format
|
2022-04-19 15:14:32 +02:00
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
if strings.HasPrefix(s, "[") {
|
|
|
|
actions, err = parseActionsFromJSON(s)
|
|
|
|
} else {
|
|
|
|
actions, err = parseActionsFromSimple(s)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-04-20 05:26:46 +02:00
|
|
|
|
2022-04-23 19:40:26 +02:00
|
|
|
// Add ID field, ensure correct uppercase/lowercase
|
2022-04-19 15:14:32 +02:00
|
|
|
for i := range actions {
|
|
|
|
actions[i].ID = util.RandomString(actionIDLength)
|
2022-04-23 19:40:26 +02:00
|
|
|
actions[i].Action = strings.ToLower(actions[i].Action)
|
|
|
|
actions[i].Method = strings.ToUpper(actions[i].Method)
|
2022-04-20 05:26:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate
|
2022-04-20 22:31:25 +02:00
|
|
|
if len(actions) > actionsMax {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "only %d actions allowed", actionsMax)
|
2022-04-20 22:31:25 +02:00
|
|
|
}
|
2022-04-20 05:26:46 +02:00
|
|
|
for _, action := range actions {
|
|
|
|
if !util.InStringList([]string{"view", "broadcast", "http"}, action.Action) {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action '%s' unknown", action.Action)
|
2022-04-20 05:26:46 +02:00
|
|
|
} else if action.Label == "" {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'label' is required")
|
2022-04-20 22:31:25 +02:00
|
|
|
} else if util.InStringList([]string{"view", "http"}, action.Action) && action.URL == "" {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'url' is required for action '%s'", action.Action)
|
|
|
|
} else if action.Action == "http" && util.InStringList([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
|
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'body' cannot be set if method is %s", action.Method)
|
2022-04-19 15:14:32 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-20 05:26:46 +02:00
|
|
|
|
2022-04-19 15:14:32 +02:00
|
|
|
return actions, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseActionsFromJSON(s string) ([]*action, error) {
|
|
|
|
actions := make([]*action, 0)
|
|
|
|
if err := json.Unmarshal([]byte(s), &actions); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return actions, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseActionsFromSimple(s string) ([]*action, error) {
|
|
|
|
actions := make([]*action, 0)
|
|
|
|
rawActions := util.SplitNoEmpty(s, ";")
|
|
|
|
for _, rawAction := range rawActions {
|
2022-04-21 15:58:28 +02:00
|
|
|
newAction := &action{
|
|
|
|
Headers: make(map[string]string),
|
|
|
|
Extras: make(map[string]string),
|
|
|
|
}
|
2022-04-19 15:14:32 +02:00
|
|
|
parts := util.SplitNoEmpty(rawAction, ",")
|
|
|
|
if len(parts) < 3 {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action requires at least keys 'action', 'label' and one parameter: %s", rawAction)
|
2022-04-19 15:14:32 +02:00
|
|
|
}
|
|
|
|
for i, part := range parts {
|
|
|
|
key, value := util.SplitKV(part, "=")
|
|
|
|
if key == "" && i == 0 {
|
|
|
|
newAction.Action = value
|
|
|
|
} else if key == "" && i == 1 {
|
|
|
|
newAction.Label = value
|
2022-04-20 22:31:25 +02:00
|
|
|
} else if key == "" && util.InStringList([]string{"view", "http"}, newAction.Action) && i == 2 {
|
|
|
|
newAction.URL = value
|
2022-04-21 15:58:28 +02:00
|
|
|
} else if strings.HasPrefix(key, "headers.") {
|
|
|
|
newAction.Headers[strings.TrimPrefix(key, "headers.")] = value
|
|
|
|
} else if strings.HasPrefix(key, "extras.") {
|
|
|
|
newAction.Extras[strings.TrimPrefix(key, "extras.")] = value
|
2022-04-19 15:14:32 +02:00
|
|
|
} else if key != "" {
|
|
|
|
switch strings.ToLower(key) {
|
|
|
|
case "action":
|
|
|
|
newAction.Action = value
|
|
|
|
case "label":
|
|
|
|
newAction.Label = value
|
2022-04-23 05:07:35 +02:00
|
|
|
case "clear":
|
|
|
|
lvalue := strings.ToLower(value)
|
2022-04-23 21:23:18 +02:00
|
|
|
if !util.InStringList([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
|
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "'clear=%s' not allowed", value)
|
|
|
|
}
|
2022-04-23 05:07:35 +02:00
|
|
|
newAction.Clear = lvalue == "true" || lvalue == "yes" || lvalue == "1"
|
2022-04-19 15:14:32 +02:00
|
|
|
case "url":
|
|
|
|
newAction.URL = value
|
|
|
|
case "method":
|
|
|
|
newAction.Method = value
|
|
|
|
case "body":
|
|
|
|
newAction.Body = value
|
|
|
|
default:
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "key '%s' unknown", key)
|
2022-04-19 15:14:32 +02:00
|
|
|
}
|
|
|
|
} else {
|
2022-04-23 19:40:26 +02:00
|
|
|
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "unknown term '%s'", part)
|
2022-04-19 15:14:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
actions = append(actions, newAction)
|
|
|
|
}
|
|
|
|
return actions, nil
|
|
|
|
}
|