diff --git a/README.md b/README.md index 02352a3..9ca4881 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ Transfer-Encoding: chunked ## FAQ: Why do the examples use HTTPS, do I need to use HTTPS? -The examples include an HTTP2 client, so the server is has a self-signed certificate so that it can present HTTP2 over HTTPS. -The client can still use HTTP1. +The examples include an HTTP2 client, so the server has a self-signed certificate so that it can present HTTP2 over HTTPS. +The client can still use HTTP1.1 as seen in the curl example above or the http1follower example. ## Dependencies diff --git a/client.go b/client.go index 4cd9f24..78aa788 100644 --- a/client.go +++ b/client.go @@ -121,7 +121,7 @@ func (b *SharedDoc) HttpPushPullChanges(ctx context.Context, url string, opts .. if err != nil { return fmt.Errorf("failed to setup request: %w", err) } - r.Header.Set("Content-Type", ContentType) + r.Header.Set("Content-Type", ContentTypeWithCharset) r.Header.Set("Accept", ContentType) // We don't need to send the body content if the server will reject it, so we can notify that expect-continue is supported. r.Header.Set("Expect", "100-continue") @@ -155,6 +155,10 @@ func (b *SharedDoc) HttpPushPullChanges(ctx context.Context, url string, opts .. } }() + if v := res.Header.Get("Content-Type"); v != "" && isNotSuitableContentType(v) { + return fmt.Errorf("http request returned a response with an unsuitable content type %s", v) + } + if _, err := b.consumeMessagesFromReader(ctx, o.state, res.Body, o.terminationCheck); err != nil { return err } diff --git a/client_test.go b/client_test.go index c2d3480..14fc091 100644 --- a/client_test.go +++ b/client_test.go @@ -139,7 +139,7 @@ func TestHttpPushPullChanges(t *testing.T) { assertEqual(t, request.URL.String(), "https://localhost") assertEqual(t, request.Header, map[string][]string{ "Accept": {ContentType}, - "Content-Type": {ContentType}, + "Content-Type": {ContentTypeWithCharset}, "Expect": {"100-continue"}, }) diff --git a/server.go b/server.go index f6ad22a..0974a33 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "mime" "net/http" "sync" @@ -50,12 +51,29 @@ func WithServerSyncState(state *automerge.SyncState) ServerOption { } } +func isNotSuitableContentType(in string) bool { + mt, p, err := mime.ParseMediaType(in) + log.Info(fmt.Sprintf("%v %v %v", mt, p, err)) + return err != nil || mt != ContentType || (p["charset"] != "" && p["charset"] != "utf-8") +} + func (b *SharedDoc) ServeChanges(rw http.ResponseWriter, req *http.Request, opts ...ServerOption) (finalErr error) { options := newServerOptions(opts...) if options.state == nil { options.state = automerge.NewSyncState(b.Doc()) } + // If there is an accept header, then ensure it's compatible. + if v := req.Header.Get("Accept"); v != "" && isNotSuitableContentType(v) { + rw.WriteHeader(http.StatusNotAcceptable) + return nil + } + // If there is a content-type header, then ensure it's what we expect + if v := req.Header.Get("Content-Type"); v != "" && isNotSuitableContentType(v) { + rw.WriteHeader(http.StatusUnsupportedMediaType) + return nil + } + ctx := req.Context() // Because the request body is relatively expensive to produce, the client may only want to produce it when the request has been accepted. @@ -65,7 +83,8 @@ func (b *SharedDoc) ServeChanges(rw http.ResponseWriter, req *http.Request, opts } log.InfoContext(ctx, "sending http sync response", slog.String("proto", req.Proto), slog.String("target", fmt.Sprintf("%s %s", req.Method, req.URL)), slog.Int("status", http.StatusOK)) - rw.Header().Set("Content-Type", ContentType) + rw.Header().Set("Content-Type", ContentTypeWithCharset) + rw.Header().Set("X-Content-Type-Options", "nosniff") rw.WriteHeader(http.StatusOK) // Flush the header, this should ensure the client can begin reacting to our sync messages while still producing the body content. if v, ok := rw.(http.Flusher); ok { diff --git a/server_test.go b/server_test.go index cfe1c72..b61d869 100644 --- a/server_test.go +++ b/server_test.go @@ -20,7 +20,8 @@ func TestServe_empty_request_body(t *testing.T) { assertErrorEqual(t, sd.ServeChanges(rw, req), "request closed with no messages received") assertEqual(t, rw.Result().StatusCode, http.StatusOK) assertEqual(t, rw.Result().Header, map[string][]string{ - "Content-Type": {ContentType}, + "Content-Type": {ContentTypeWithCharset}, + "X-Content-Type-Options": {"nosniff"}, }) assertEqual(t, rw.Body.String(), "{\"event\":\"sync\",\"data\":\"QgAAAQAAAA==\"}\n") } @@ -72,7 +73,8 @@ func TestServe_exchange(t *testing.T) { assertEqual(t, rw.Result().StatusCode, http.StatusOK) assertEqual(t, rw.Result().Header, map[string][]string{ - "Content-Type": {ContentType}, + "Content-Type": {ContentTypeWithCharset}, + "X-Content-Type-Options": {"nosniff"}, }) lines := strings.Split(strings.TrimSpace(rw.Body.String()), "\n") assertEqual(t, len(lines), 2) diff --git a/util.go b/util.go index bb1ef4e..05397ec 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,7 @@ package automergendjsonsync -const ContentType = "application/x-ndjson; charset=utf-8" +const ContentType = "application/x-ndjson" +const ContentTypeWithCharset = ContentType + "; charset=utf-8" const EventSync = "sync" type NdJson struct {