Skip to content

Commit

Permalink
fix: check content and accept types
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Meier <[email protected]>
  • Loading branch information
astromechza committed Oct 26, 2024
1 parent 9cda43b commit 058b86f
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 8 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
})

Expand Down
21 changes: 20 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"mime"
"net/http"
"sync"

Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion util.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 058b86f

Please sign in to comment.