Docs and Matrix tests
This commit is contained in:
parent
0ff8e968ca
commit
18bd3c0e55
12 changed files with 172 additions and 140 deletions
|
@ -1,56 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ntfy.sh: EventSource Example</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<style>
|
||||
body { font-size: 1.2em; line-height: 130%; }
|
||||
#events { font-family: monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ntfy.sh: EventSource Example</h1>
|
||||
<p>
|
||||
This is an example showing how to use <a href="https://ntfy.sh">ntfy.sh</a> with
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a>.<br/>
|
||||
This example doesn't need a server. You can just save the HTML page and run it from anywhere.
|
||||
</p>
|
||||
<button id="publishButton">Send test notification</button>
|
||||
<p><b>Log:</b></p>
|
||||
<div id="events"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const publishURL = `https://ntfy.sh/example`;
|
||||
const subscribeURL = `https://ntfy.sh/example/sse`;
|
||||
const events = document.getElementById('events');
|
||||
const eventSource = new EventSource(subscribeURL);
|
||||
|
||||
// Publish button
|
||||
document.getElementById("publishButton").onclick = () => {
|
||||
fetch(publishURL, {
|
||||
method: 'POST', // works with PUT as well, though that sends an OPTIONS request too!
|
||||
body: `It is ${new Date().toString()}. This is a test.`
|
||||
})
|
||||
};
|
||||
|
||||
// Incoming events
|
||||
eventSource.onopen = () => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = `EventSource connected to ${subscribeURL}`;
|
||||
events.appendChild(event);
|
||||
};
|
||||
eventSource.onerror = (e) => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = `EventSource error: Failed to connect to ${subscribeURL}`;
|
||||
events.appendChild(event);
|
||||
};
|
||||
eventSource.onmessage = (e) => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = e.data;
|
||||
events.appendChild(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -75,9 +75,6 @@ var (
|
|||
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
|
||||
attachURLRegex = regexp.MustCompile(`^https?://`)
|
||||
|
||||
//go:embed "example.html"
|
||||
exampleSource string
|
||||
|
||||
//go:embed site
|
||||
webFs embed.FS
|
||||
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
||||
|
@ -283,8 +280,6 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
||||
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
|
||||
return s.ensureWebEnabled(s.handleExample)(w, r, v)
|
||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||
|
@ -357,11 +352,6 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||
_, err := io.WriteString(w, exampleSource)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||
appRoot := "/"
|
||||
if !s.config.WebRootIsApp {
|
||||
|
@ -435,7 +425,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
|
|||
}
|
||||
|
||||
func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error {
|
||||
return handleMatrixDiscovery(w)
|
||||
return writeMatrixDiscoveryResponse(w)
|
||||
}
|
||||
|
||||
func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*message, error) {
|
||||
|
|
|
@ -11,9 +11,36 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
matrixPushKeyHeader = "X-Matrix-Pushkey"
|
||||
)
|
||||
// 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/).
|
||||
|
@ -30,6 +57,7 @@ const (
|
|||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type matrixRequest struct {
|
||||
Notification *struct {
|
||||
Devices []*struct {
|
||||
|
@ -38,10 +66,13 @@ type matrixRequest struct {
|
|||
} `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"`
|
||||
}
|
||||
|
||||
// errMatrix represents an error when handing Matrix gateway messages
|
||||
type errMatrix struct {
|
||||
pushKey string
|
||||
err error
|
||||
|
@ -54,6 +85,12 @@ func (e errMatrix) Error() string {
|
|||
return fmt.Sprintf("message with push key %s rejected", e.pushKey)
|
||||
}
|
||||
|
||||
const (
|
||||
// matrixPushKeyHeader is a header that's used internally to pass the Matrix push key (from the matrixRequest)
|
||||
// along with the request. The push key is only used if an error occurs down the line.
|
||||
matrixPushKeyHeader = "X-Matrix-Pushkey"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
|
@ -82,7 +119,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
|
|||
} else if m.Notification == nil || len(m.Notification.Devices) == 0 || m.Notification.Devices[0].PushKey == "" {
|
||||
return nil, errHTTPBadRequestMatrixMessageInvalid
|
||||
}
|
||||
pushKey := m.Notification.Devices[0].PushKey
|
||||
pushKey := m.Notification.Devices[0].PushKey // We ignore other devices for now, see discussion in #316
|
||||
if !strings.HasPrefix(pushKey, baseURL+"/") {
|
||||
return nil, &errMatrix{pushKey: pushKey, err: errHTTPBadRequestMatrixPushkeyBaseURLMismatch}
|
||||
}
|
||||
|
@ -94,21 +131,27 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
|
|||
return newRequest, nil
|
||||
}
|
||||
|
||||
func handleMatrixDiscovery(w http.ResponseWriter) error {
|
||||
// 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
|
||||
}
|
||||
|
||||
// writeMatrixError logs and writes the errMatrix to the given http.ResponseWriter as a matrixResponse
|
||||
func writeMatrixError(w http.ResponseWriter, r *http.Request, v *visitor, err *errMatrix) error {
|
||||
log.Debug("%s Matrix gateway error: %s", logHTTPPrefix(v, r), err.Error())
|
||||
return writeMatrixResponse(w, err.pushKey)
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
|
|
21
server/server_matrix_test.go
Normal file
21
server/server_matrix_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatrix_NewRequestFromMatrixJSON_Success(t *testing.T) {
|
||||
baseURL := "https://ntfy.sh"
|
||||
maxLength := 4096
|
||||
body := `{"notification":{"content":{"body":"I'm floating in a most peculiar way.","msgtype":"m.text"},"counts":{"missed_calls":1,"unread":2},"devices":[{"app_id":"org.matrix.matrixConsole.ios","data":{},"pushkey":"https://ntfy.sh/upABCDEFGHI?up=1","pushkey_ts":12345678,"tweaks":{"sound":"bing"}}],"event_id":"$3957tyerfgewrf384","prio":"high","room_alias":"#exampleroom:matrix.org","room_id":"!slw48wfj34rtnrf:example.com","room_name":"Mission Control","sender":"@exampleuser:matrix.org","sender_display_name":"Major Tom","type":"m.room.message"}}`
|
||||
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", strings.NewReader(body))
|
||||
newRequest, err := newRequestFromMatrixJSON(r, baseURL, maxLength)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "POST", newRequest.Method)
|
||||
require.Equal(t, "https://ntfy.sh/upABCDEFGHI?up=1", newRequest.URL.String())
|
||||
require.Equal(t, "https://ntfy.sh/upABCDEFGHI?up=1", newRequest.Header.Get("X-Matrix-Pushkey"))
|
||||
require.Equal(t, body, readAll(t, newRequest.Body))
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -171,10 +172,6 @@ func TestServer_StaticSites(t *testing.T) {
|
|||
require.Equal(t, 301, rr.Code)
|
||||
|
||||
// Docs test removed, it was failing annoyingly.
|
||||
|
||||
rr = request(t, s, "GET", "/example.html", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Contains(t, rr.Body.String(), "</html>")
|
||||
}
|
||||
|
||||
func TestServer_WebEnabled(t *testing.T) {
|
||||
|
@ -185,9 +182,6 @@ func TestServer_WebEnabled(t *testing.T) {
|
|||
rr := request(t, s, "GET", "/", "", nil)
|
||||
require.Equal(t, 404, rr.Code)
|
||||
|
||||
rr = request(t, s, "GET", "/example.html", "", nil)
|
||||
require.Equal(t, 404, rr.Code)
|
||||
|
||||
rr = request(t, s, "GET", "/config.js", "", nil)
|
||||
require.Equal(t, 404, rr.Code)
|
||||
|
||||
|
@ -201,9 +195,6 @@ func TestServer_WebEnabled(t *testing.T) {
|
|||
rr = request(t, s2, "GET", "/", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
rr = request(t, s2, "GET", "/example.html", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
|
@ -1390,3 +1381,11 @@ func toHTTPError(t *testing.T, s string) *errHTTP {
|
|||
func basicAuth(s string) string {
|
||||
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(s)))
|
||||
}
|
||||
|
||||
func readAll(t *testing.T, rc io.ReadCloser) string {
|
||||
b, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package server
|
|||
import (
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -304,14 +303,6 @@ func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Reques
|
|||
return conf, backend
|
||||
}
|
||||
|
||||
func readAll(t *testing.T, rc io.ReadCloser) string {
|
||||
b, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState {
|
||||
ip, err := net.ResolveIPAddr("ip", remoteAddr)
|
||||
if err != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue