diff --git a/server/errors.go b/server/errors.go index 6d217352..28aa4be6 100644 --- a/server/errors.go +++ b/server/errors.go @@ -51,6 +51,7 @@ var ( errHTTPBadRequestJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"} errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"} errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"} + errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: Push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"} diff --git a/server/server.go b/server/server.go index deb94b54..78d7fd9b 100644 --- a/server/server.go +++ b/server/server.go @@ -287,6 +287,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == userStatsPath { return s.handleUserStats(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath { + return s.handleMatrixDiscovery(w) } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleStatic)(w, r, v) } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) { @@ -428,6 +430,10 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) return nil } +func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error { + return handleMatrixDiscovery(w) +} + func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*message, error) { t, err := s.topicFromPath(r.URL.Path) if err != nil { @@ -498,22 +504,12 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { - pushKey := r.Header.Get("X-Matrix-Pushkey") - if pushKey == "" { - return errHTTPBadRequestMatrixMessageInvalid - } - response := &matrixResponse{ - Rejected: make([]string, 0), - } _, err := s.handlePublishWithoutResponse(r, v) if err != nil { - response.Rejected = append(response.Rejected, pushKey) + pushKey := r.Header.Get(matrixPushkeyHeader) + return writeMatrixError(w, pushKey, err) } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - return err - } - return nil + return writeMatrixSuccess(w) } func (s *Server) sendToFirebase(v *visitor, m *message) { @@ -1316,22 +1312,6 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { } } -type matrixMessage struct { - Notification *matrixNotification `json:"notification"` -} - -type matrixNotification struct { - Devices []*matrixDevice `json:"devices"` -} - -type matrixDevice struct { - PushKey string `json:"pushkey"` -} - -type matrixResponse struct { - Rejected []string `json:"rejected"` -} - func (s *Server) transformMatrixJSON(next handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { if s.config.BaseURL == "" { @@ -1350,35 +1330,24 @@ func (s *Server) transformMatrixJSON(next handleFunc) handleFunc { } pushKey := m.Notification.Devices[0].PushKey if !strings.HasPrefix(pushKey, s.config.BaseURL+"/") { - return matrixError(w, pushKey, errHTTPBadRequestMatrixMessageInvalid) + return writeMatrixError(w, pushKey, errHTTPBadRequestMatrixPushkeyBaseURLMismatch) } u, err := url.Parse(pushKey) if err != nil { - return matrixError(w, pushKey, errHTTPBadRequestMatrixMessageInvalid) + return writeMatrixError(w, pushKey, errHTTPBadRequestMatrixMessageInvalid) } r.URL.Path = u.Path r.URL.RawQuery = u.RawQuery r.RequestURI = u.RequestURI() r.Body = io.NopCloser(bytes.NewReader(body.PeekedBytes)) - r.Header.Set("X-Matrix-Pushkey", pushKey) + r.Header.Set(matrixPushkeyHeader, pushKey) if err := next(w, r, v); err != nil { - return matrixError(w, pushKey, errHTTPBadRequestMatrixMessageInvalid) + return writeMatrixError(w, pushKey, errHTTPBadRequestMatrixMessageInvalid) } return nil } } -func matrixError(w http.ResponseWriter, pushKey string, err error) error { - log.Debug("Matrix message with push key %s rejected: %s", pushKey, err.Error()) - response := &matrixResponse{ - Rejected: []string{pushKey}, - } - if err := json.NewEncoder(w).Encode(response); err != nil { - return err - } - return nil -} - func (s *Server) authWrite(next handleFunc) handleFunc { return s.withAuth(next, auth.PermissionWrite) } diff --git a/server/server_matrix.go b/server/server_matrix.go new file mode 100644 index 00000000..c8b3eca4 --- /dev/null +++ b/server/server_matrix.go @@ -0,0 +1,56 @@ +package server + +import ( + "encoding/json" + "heckel.io/ntfy/log" + "io" + "net/http" +) + +const ( + matrixPushkeyHeader = "X-Matrix-Pushkey" +) + +type matrixMessage struct { + Notification *matrixNotification `json:"notification"` +} + +type matrixNotification struct { + Devices []*matrixDevice `json:"devices"` +} + +type matrixDevice struct { + PushKey string `json:"pushkey"` +} + +type matrixResponse struct { + Rejected []string `json:"rejected"` +} + +func handleMatrixDiscovery(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(w, `{"unifiedpush":{"gateway":"matrix"}}`+"\n") + return err +} + +func writeMatrixError(w http.ResponseWriter, pushKey string, err error) error { + log.Debug("Matrix message with push key %s rejected: %s", pushKey, err.Error()) + response := &matrixResponse{ + Rejected: []string{pushKey}, + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + return err + } + return nil +} + +func writeMatrixSuccess(w http.ResponseWriter) error { + response := &matrixResponse{ + Rejected: make([]string, 0), + } + if err := json.NewEncoder(w).Encode(response); err != nil { + return err + } + return nil +} diff --git a/server/server_test.go b/server/server_test.go index 32f4fc2d..343d502b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -916,6 +916,48 @@ func TestServer_PublishUnifiedPushText(t *testing.T) { require.Equal(t, "this is a unifiedpush text message", m.Message) } +func TestServer_MatrixGateway_Discovery(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"unifiedpush":{"gateway":"matrix"}}`+"\n", response.Body.String()) +} + +func TestServer_MatrixGateway_Push_Success(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String()) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, notification, m.Message) +} + +func TestServer_MatrixGateway_Push_Failure_InvalidPushkey(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + notification := `{"notification":{"devices":[{"pushkey":"http://wrong-base-url.com/mytopic?up=1"}]}}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":["http://wrong-base-url.com/mytopic?up=1"]}`+"\n", response.Body.String()) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, "", response.Body.String()) // Empty! +} + +func TestServer_MatrixGateway_Push_Failure_EverythingIsWrong(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + notification := `{"message":"this is not really a Matrix message"}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 400, response.Code) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 40019, err.Code) + require.Equal(t, 400, err.HTTPCode) +} + func TestServer_PublishActions_AndPoll(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", "my message", map[string]string{