File: server_matrix.go

package info (click to toggle)
ntfy 2.11.0-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 19,364 kB
  • sloc: javascript: 16,782; makefile: 282; sh: 105; php: 21; python: 19
file content (172 lines) | stat: -rw-r--r-- 7,062 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package server

import (
	"bytes"
	"encoding/json"
	"fmt"
	"heckel.io/ntfy/v2/util"
	"io"
	"net/http"
	"strings"
	"time"
)

// Matrix Push Gateway / UnifiedPush / ntfy integration:
//
// ntfy implements a Matrix Push Gateway (as defined in https://spec.matrix.org/v1.2/push-gateway-api/),
// in combination with UnifiedPush as the Provider Push Protocol (as defined in https://unifiedpush.org/developers/gateway/).
//
// In the picture below, ntfy is the Push Gateway (mostly in this file), as well as the Push Provider (ntfy's
// main functionality). UnifiedPush is the Provider Push Protocol, as implemented by the ntfy server and the
// ntfy Android app.
//
//                                    +--------------------+  +-------------------+
//                  Matrix HTTP      |                    |  |                   |
//             Notification Protocol |   App Developer    |  |   Device Vendor   |
//                                   |                    |  |                   |
//           +-------------------+   | +----------------+ |  | +---------------+ |
//           |                   |   | |                | |  | |               | |
//           | Matrix homeserver +----->  Push Gateway  +------> Push Provider | |
//           |                   |   | |                | |  | |               | |
//           +-^-----------------+   | +----------------+ |  | +----+----------+ |
//             |                     |                    |  |      |            |
//    Matrix   |                     |                    |  |      |            |
// Client/Server API  +              |                    |  |      |            |
//             |      |              +--------------------+  +-------------------+
//             |   +--+-+                                           |
//             |   |    <-------------------------------------------+
//             +---+    |
//                 |    |          Provider Push Protocol
//                 +----+
//
//         Mobile Device or Client
//

// matrixRequest represents a Matrix message, as it is sent to a Push Gateway (as per
// this spec: https://spec.matrix.org/v1.2/push-gateway-api/).
//
// From the message, we only require the "pushkey", as it represents our target topic URL.
// A message may look like this (excerpt):
//
//	{
//	  "notification": {
//	    "devices": [
//	       {
//	          "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1",
//	          ...
//	       }
//	    ]
//	  }
//	}
type matrixRequest struct {
	Notification *struct {
		Devices []*struct {
			PushKey string `json:"pushkey"`
		} `json:"devices"`
	} `json:"notification"`
}

// matrixResponse represents the response to a Matrix push gateway message, as defined
// in the spec (https://spec.matrix.org/v1.2/push-gateway-api/).
type matrixResponse struct {
	Rejected []string `json:"rejected"`
}

const (
	// matrixRejectPushKeyForUnifiedPushTopicWithoutRateVisitorAfter is the time after which a Matrix response
	// will return an HTTP 200 with the push key (i.e. "rejected":["<pushkey>"]}), if no rate visitor has been set on
	// the topic. Rejecting the push key will instruct the Matrix server to invalidate the pushkey and stop sending
	// messages to it. This must be longer than topicExpungeAfter. See https://spec.matrix.org/v1.6/push-gateway-api/
	matrixRejectPushKeyForUnifiedPushTopicWithoutRateVisitorAfter = 12 * time.Hour
)

// errMatrixPushkeyRejected represents an error when handing Matrix gateway messages
//
// If the push key is set, the app server will remove it and will never send messages using the same
// push key again, until the user repairs it.
type errMatrixPushkeyRejected struct {
	rejectedPushKey   string
	configuredBaseURL string
}

func (e errMatrixPushkeyRejected) Error() string {
	return fmt.Sprintf("push key must be prefixed with base URL, received push key: %s, configured base URL: %s", e.rejectedPushKey, e.configuredBaseURL)
}

// newRequestFromMatrixJSON reads the request body as a Matrix JSON message, parses the "pushkey", and creates a new
// HTTP request that looks like a normal ntfy request from it.
//
// It basically converts a Matrix push gatewqy request:
//
//	POST /_matrix/push/v1/notify HTTP/1.1
//	{ "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
//
// to a ntfy request, looking like this:
//
//	POST /upDAHJKFFDFD?up=1 HTTP/1.1
//	{ "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int) (*http.Request, error) {
	if baseURL == "" {
		return nil, errHTTPInternalErrorMissingBaseURL
	}
	body, err := util.Peek(r.Body, messageLimit)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()
	if body.LimitReached {
		return nil, errHTTPEntityTooLargeMatrixRequest
	}
	var m matrixRequest
	if err := json.Unmarshal(body.PeekedBytes, &m); err != nil {
		return nil, errHTTPBadRequestMatrixMessageInvalid
	} else if m.Notification == nil || len(m.Notification.Devices) == 0 || m.Notification.Devices[0].PushKey == "" {
		return nil, errHTTPBadRequestMatrixMessageInvalid
	}
	pushKey := m.Notification.Devices[0].PushKey // We ignore other devices for now, see discussion in #316
	if !strings.HasPrefix(pushKey, baseURL+"/") {
		return nil, &errMatrixPushkeyRejected{rejectedPushKey: pushKey, configuredBaseURL: baseURL}
	}
	newRequest, err := http.NewRequest(http.MethodPost, pushKey, io.NopCloser(bytes.NewReader(body.PeekedBytes)))
	if err != nil {
		return nil, err
	}
	newRequest.RemoteAddr = r.RemoteAddr // Not strictly necessary, since visitor was already extracted
	if r.Header.Get("X-Forwarded-For") != "" {
		newRequest.Header.Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
	}
	newRequest = withContext(newRequest, map[contextKey]any{
		contextMatrixPushKey: pushKey,
	})
	return newRequest, nil
}

// writeMatrixDiscoveryResponse writes the UnifiedPush Matrix Gateway Discovery response to the given http.ResponseWriter,
// as per the spec (https://unifiedpush.org/developers/gateway/).
func writeMatrixDiscoveryResponse(w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json")
	_, err := io.WriteString(w, `{"unifiedpush":{"gateway":"matrix"}}`+"\n")
	return err
}

// writeMatrixSuccess writes a successful matrixResponse (no rejected push key) to the given http.ResponseWriter
func writeMatrixSuccess(w http.ResponseWriter) error {
	return writeMatrixResponse(w, "")
}

// writeMatrixResponse writes a matrixResponse to the given http.ResponseWriter, as defined in
// the spec (https://spec.matrix.org/v1.2/push-gateway-api/)
func writeMatrixResponse(w http.ResponseWriter, rejectedPushKey string) error {
	rejected := make([]string, 0)
	if rejectedPushKey != "" {
		rejected = append(rejected, rejectedPushKey)
	}
	response := &matrixResponse{
		Rejected: rejected,
	}
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(response); err != nil {
		return err
	}
	return nil
}